Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5c5e167
Report staging PRs and deployment evidence to Factory without duplica…
daveosterjr Sep 30, 2026
bfcff04
Merge pull request #22 from DealMachine/codex/staging-release-intake
daveosterjr Sep 30, 2026
89bfbc2
Expose active phone filtering in public export Commands
daveosterjr Oct 1, 2026
14d99d0
Preserve list polling behavior already installed by Next
daveosterjr Oct 1, 2026
c4abae5
Merge pull request #23 from DealMachine/codex/feature2-sync-20261001
daveosterjr Oct 1, 2026
a1fd125
Automate CLI npm releases and Factory tracking (#24)
daveosterjr Oct 1, 2026
8df57f8
Add isolated CLI live smoke and package regression coverage
daveosterjr Oct 2, 2026
10503b6
Merge pull request #26 from DealMachine/codex/cli-regression-suites
daveosterjr Oct 6, 2026
59ab7eb
Add bulk prospect archive and reactivate commands
daveosterjr Oct 9, 2026
5db12f7
Isolate prospect command tests from stored login credentials
daveosterjr Oct 9, 2026
e045bd5
Merge pull request #27 from DealMachine/codex/feature2-sync-20261009-…
daveosterjr Oct 9, 2026
5aa8c85
Adopt E2E-first testing policy and prune redundant checks
daveosterjr Oct 10, 2026
afc9dbd
Merge pull request #29 from DealMachine/codex/test-policy-audit-20261010
daveosterjr Oct 10, 2026
94f82a1
Merge remote-tracking branch 'origin/staging' into codex/feature2-syn…
daveosterjr Oct 10, 2026
2c3968c
Merge pull request #31 from DealMachine/codex/feature2-sync-20261010-…
daveosterjr Oct 10, 2026
7456c11
Sync feature-2 prospect statuses and shared contracts
daveosterjr Oct 13, 2026
e6326df
Merge pull request #33 from DealMachine/codex/feature2-sync-20261013
daveosterjr Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/scripts/factory-production-intake.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Generated from Factory production intake. SHA256: e0a02d6fea6ce2bcfd4782b139c805cb1b737e39a3f92ea1ee56035edd7c1294
/** Runs in the owning repository with read-only GitHub access. No cloud or npm credentials. */
export async function collectProduction(repository, token, workflow, jobName, runId) {
const get = async (path) => {
const response = await fetch(`https://api.github.com/repos/${repository}/${path}`, { redirect: 'error', signal: AbortSignal.timeout(20000),
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' } });
if (!response.ok)
throw new Error(`GitHub production read failed (${response.status}).`);
return response.json();
};
const ownedRun = (r) => r && r.head_branch === 'master' && ['push', 'workflow_dispatch'].includes(r.event)
&& r.path === `.github/workflows/${workflow}` && r.head_repository?.full_name === repository;
const { workflow_runs: history } = await get(`actions/workflows/${workflow}/runs?branch=master&per_page=30`);
const run = runId ? await get(`actions/runs/${runId}`) : history.find(ownedRun);
if (!ownedRun(run))
return null;
const jobs = async (r) => (await get(`actions/runs/${r.id}/attempts/${r.run_attempt}/jobs?per_page=100`));
const deploymentJobs = (r, result) => result.jobs.filter((j) => j.name === jobName && j.head_sha === r.head_sha);
const current = await jobs(run);
if (current.total_count > 100)
throw new Error('Production workflow has more jobs than the collector supports.');
const matching = deploymentJobs(run, current);
if (matching.length > 1)
throw new Error('Production job name must be unique.');
const job = matching[0];
const state = run.status !== 'completed' ? 'running' : run.conclusion !== 'success' ? 'failed'
: job?.status === 'completed' && job.conclusion === 'success' ? 'deployed' : 'skipped';
const pullRequests = [];
let linkageComplete = false;
// Only an earlier successful deployment establishes the range of newly shipped commits.
// The first tracked release retains its build receipt but requires a person to review its ticket scope.
if (state === 'deployed') {
let previous;
// A rerun finishes at a different time than its original creation. Compare the latest
// completed receipts so a later rerun cannot become an earlier deployment boundary.
const earlier = history.filter((r) => ownedRun(r) && r.id !== run.id && r.updated_at < run.updated_at && r.status === 'completed' && r.conclusion === 'success')
.sort((a, b) => b.updated_at.localeCompare(a.updated_at)).slice(0, 10);
for (const candidate of earlier) {
const prior = await jobs(candidate);
const matches = deploymentJobs(candidate, prior);
if (prior.total_count <= 100 && matches.length === 1 && matches[0].status === 'completed' && matches[0].conclusion === 'success') {
previous = candidate;
break;
}
}
if (previous) {
const diff = await get(`compare/${previous.head_sha}...${run.head_sha}?per_page=100`);
if (['ahead', 'identical'].includes(diff.status) && diff.total_commits <= 100) {
linkageComplete = true;
const found = new Map();
for (const commit of diff.commits) {
const prs = await get(`commits/${commit.sha}/pulls?per_page=100`);
if (prs.length >= 100) {
linkageComplete = false;
break;
}
for (const pr of prs)
if (pr.merged_at && ['staging', 'master'].includes(pr.base?.ref) && pr.base?.repo?.full_name === repository && pr.merge_commit_sha && diff.commits.some((c) => c.sha === pr.merge_commit_sha))
found.set(pr.number, pr);
}
if (found.size > 50)
linkageComplete = false;
if (linkageComplete)
for (const pr of found.values()) {
const rows = await get(`pulls/${pr.number}/files?per_page=100`);
if (rows.length >= 100)
linkageComplete = false;
pullRequests.push({ number: pr.number, title: pr.title.slice(0, 300), body: (pr.body ?? '').slice(0, 20000), headRef: pr.head.ref,
mergeSha: pr.merge_commit_sha, files: rows.length < 100 ? rows.map((f) => f.filename) : null });
}
}
}
}
return { repository, workflow, runId: String(run.id), attempt: run.run_attempt, candidate: run.head_sha, state,
url: `https://github.com/${repository}/actions/runs/${run.id}`, createdAt: run.created_at, updatedAt: run.updated_at,
observedAt: new Date().toISOString(), job: job ? { name: job.name, status: job.status, conclusion: job.conclusion } : null, linkageComplete, pullRequests };
}

// Appended to the compiled production collector by scripts/sync-production-intake.mjs.
const endpoint = 'https://factory-rover-81024286635.us-west1.run.app/github/production-intake';
try {
let runIds = [process.env.FACTORY_RUN_ID].filter(Boolean);
if (!runIds.length) {
const history = await fetch(`https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/actions/workflows/${process.env.FACTORY_PRODUCTION_WORKFLOW}/runs?branch=master&per_page=30`, {
redirect: 'error', signal: AbortSignal.timeout(20000), headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Accept: 'application/vnd.github+json' } });
if (!history.ok) throw new Error(`GitHub production history failed (${history.status}).`);
runIds = (await history.json()).workflow_runs.filter(r => ['push', 'workflow_dispatch'].includes(r.event) && Date.now() - Date.parse(r.created_at) < 7 * 86400000)
.map(r => String(r.id)).reverse();
}
let failed = false;
for (const runId of runIds) try {
const payload = await collectProduction(process.env.GITHUB_REPOSITORY, process.env.GITHUB_TOKEN, process.env.FACTORY_PRODUCTION_WORKFLOW, process.env.FACTORY_PRODUCTION_JOB, runId);
if (!payload) continue;
const request = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
request.searchParams.set('audience', 'https://factory.dealmachine.com/production-intake');
const identity = await fetch(request, { headers: { Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` }, signal: AbortSignal.timeout(20000) });
if (!identity.ok) throw new Error(`GitHub identity request failed (${identity.status}).`);
const { value } = await identity.json();
if (!value) throw new Error('GitHub returned no intake identity.');
let body = JSON.stringify(payload);
if (Buffer.byteLength(body) > 500000) {
payload.pullRequests = []; payload.linkageComplete = false; body = JSON.stringify(payload);
}
const response = await fetch(endpoint, { method: 'POST', headers: { Authorization: `Bearer ${value}`, 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(60000) });
if (!response.ok) throw new Error(`Factory production intake failed (${response.status}); scheduled reconciliation will retry.`);
console.log(`Recorded production run ${payload.runId}, attempt ${payload.attempt}: ${payload.state}.`);
} catch (error) { failed = true; console.error(`Production run ${runId}: ${error.message}`); }
if (failed) process.exitCode = 1;
} catch (error) { console.error(error.message); process.exitCode = 1; }
98 changes: 98 additions & 0 deletions .github/scripts/factory-release-intake.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Generated from Factory staging intake. Source SHA256: 7ef6161ca1c9aca6af23c8742bb5db246697ea32f6edb611c4acc5d7a64711b9
/** Reads only GitHub metadata. The same collector runs in each owning repository and in recovery. */
export async function collectStaging(repository, token, deploymentWorkflow, recoveryCheck = null) {
const get = async (path) => {
const response = await fetch(`https://api.github.com/repos/${repository}/${path}`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' },
redirect: 'error', signal: AbortSignal.timeout(20_000),
});
if (!response.ok)
throw new Error(`GitHub staging read failed (${response.status}).`);
return response.json();
};
const [stage, production] = await Promise.all([get('branches/staging'), get('branches/master')]);
const stagingSha = stage.commit.sha;
const contains = async (ancestor, head) => {
if (ancestor === head)
return true;
const comparison = await get(`compare/${ancestor}...${head}`);
return ['ahead', 'identical'].includes(comparison.status);
};
const pullRequests = [];
const resolved = [];
for (let page = 1;; page++) {
const rows = await get(`pulls?state=closed&base=staging&sort=updated&direction=desc&per_page=100&page=${page}`);
for (const pr of rows) {
if (!pr.merged_at || !pr.merge_commit_sha || pr.base?.ref !== 'staging')
continue;
if (!await contains(pr.merge_commit_sha, stagingSha)) {
resolved.push({ number: pr.number, disposition: 'removed-from-staging' });
continue;
}
if (await contains(pr.merge_commit_sha, production.commit.sha)) {
resolved.push({ number: pr.number, disposition: 'on-master' });
continue;
}
pullRequests.push({ number: pr.number, title: pr.title, body: (pr.body ?? '').slice(0, 20000), headRef: pr.head.ref,
mergeSha: pr.merge_commit_sha, mergedAt: pr.merged_at });
}
if (rows.length < 100)
break;
if (page >= 20)
throw new Error('Staging PR history exceeds the reconciliation limit. An owner must partition the history.');
}
let deployment = null;
if (deploymentWorkflow) {
const { workflow_runs: runs } = await get(`actions/workflows/${deploymentWorkflow}/runs?branch=staging&per_page=30`);
const run = runs.find((r) => r.head_branch === 'staging' && r.head_sha === stagingSha && ['push', 'workflow_dispatch'].includes(r.event));
if (run)
deployment = { runId: String(run.id), headSha: run.head_sha, status: run.status, conclusion: run.conclusion,
url: `https://github.com/${repository}/actions/runs/${run.id}`, updatedAt: run.updated_at };
}
if (recoveryCheck) {
// This unique job belongs to the reviewed staging deployer. Recovery's GitHub App has
// Checks read, but no Actions access. Check suites independently bind the exact branch.
const { check_runs: checks } = await get(`commits/${stagingSha}/check-runs?per_page=100`);
for (const check of checks) {
if (check.name !== recoveryCheck || check.app?.slug !== 'github-actions' || check.head_sha !== stagingSha)
continue;
const suite = await get(`check-suites/${check.check_suite.id}`);
const run = new RegExp(`^https://github\\.com/${repository}/actions/runs/(\\d+)/job/\\d+$`).exec(check.details_url ?? '');
if (!run || suite.head_branch !== 'staging' || suite.head_sha !== stagingSha || suite.app?.slug !== 'github-actions')
continue;
if (!deployment || deployment.runId === run[1])
deployment = { runId: run[1], headSha: stagingSha, status: check.status, conclusion: check.conclusion,
url: `https://github.com/${repository}/actions/runs/${run[1]}`, updatedAt: check.completed_at ?? check.started_at ?? new Date().toISOString() };
break;
}
}
// A moving source cannot produce an internally inconsistent snapshot.
const [current, currentProduction] = await Promise.all([get('branches/staging'), get('branches/master')]);
if (current.commit.sha !== stagingSha || currentProduction.commit.sha !== production.commit.sha)
throw new Error('Source moved during reconciliation. Retry on the next run.');
return { repository, branch: 'staging', stagingSha, productionSha: production.commit.sha, observedAt: new Date().toISOString(), deployment, pullRequests, resolved };
}

// Appended to the compiled staging collector by scripts/sync-staging-intake.mjs.
const endpoint = 'https://factory-rover-81024286635.us-west1.run.app/github/staging-intake';
async function sendObservation(payload) {
const request = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
request.searchParams.set('audience', 'https://factory.dealmachine.com/staging-intake');
const identity = await fetch(request, { headers: { Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` }, signal: AbortSignal.timeout(20000) });
if (!identity.ok) throw new Error(`GitHub identity request failed (${identity.status}).`);
const { value } = await identity.json();
if (!value) throw new Error('GitHub did not return an intake identity.');
const body = JSON.stringify(payload);
if (Buffer.byteLength(body) > 500000) throw new Error('The staging observation exceeds the intake request limit.');
const result = await fetch(endpoint, { method: 'POST', headers: { Authorization: `Bearer ${value}`, 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(60000) });
if (!result.ok) throw new Error(`Factory staging intake failed (${result.status}); the scheduled reconciliation will retry.`);
}
try {
if (process.env.FACTORY_INTAKE_MODE === 'reconcile') await sendObservation({ reconcile: true });
else {
const observed = await collectStaging(process.env.GITHUB_REPOSITORY, process.env.GITHUB_TOKEN, process.env.FACTORY_DEPLOYMENT_WORKFLOW || null, process.env.FACTORY_DEPLOYMENT_CHECK || null);
for (let offset = 0; offset < observed.pullRequests.length || offset === 0; offset += 10)
await sendObservation({ ...observed, pullRequests: observed.pullRequests.slice(offset, offset + 10) });
console.log(`Recorded ${observed.pullRequests.length} unpromoted staging PRs.`);
}
} catch (error) { console.error(error.message); process.exitCode = 1; }
37 changes: 37 additions & 0 deletions .github/workflows/factory-production-intake.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Track production builds in Factory
on:
workflow_run:
workflows: ["Publish CLI to npm"]
types: [requested, in_progress, completed]
branches: [master]
schedule:
- cron: '3,13,23,33,43,53 * * * *'
workflow_dispatch:
permissions:
contents: read
pull-requests: read
actions: read
id-token: write
concurrency:
group: factory-production-intake
cancel-in-progress: false
jobs:
intake:
if: github.ref == 'refs/heads/master' && vars.FACTORY_PRODUCTION_INTAKE_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
ref: master
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Record production build and deployment evidence
env:
GITHUB_TOKEN: ${{ github.token }}
FACTORY_PRODUCTION_WORKFLOW: "publish-npm.yml"
FACTORY_PRODUCTION_JOB: "Publish production CLI packages"
FACTORY_RUN_ID: ${{ github.event.workflow_run.id || '' }}
run: node .github/scripts/factory-production-intake.mjs
35 changes: 35 additions & 0 deletions .github/workflows/factory-release-intake.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Track staging work in Factory

on:
schedule:
- cron: '7,17,27,37,47,57 * * * *'
workflow_dispatch:

permissions:
contents: read
pull-requests: read
actions: read
id-token: write

concurrency:
group: factory-staging-intake
cancel-in-progress: false

jobs:
intake:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
ref: master
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Reconcile staging PRs and deployment receipts
env:
GITHUB_TOKEN: ${{ github.token }}
FACTORY_INTAKE_MODE: observe
FACTORY_DEPLOYMENT_WORKFLOW: ''
run: node .github/scripts/factory-release-intake.mjs
54 changes: 35 additions & 19 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -1,42 +1,58 @@
name: Publish CLI to npm
on:
push:
branches: [master]
workflow_dispatch:
inputs:
version:
description: Exact version already committed on master
required: true
expected_sha:
description: Full reviewed source commit; reject a dispatch if the branch moved
type: string
channel:
description: npm distribution tag
required: true
default: next
type: choice
options: [next, latest]
required: false
permissions:
contents: read
concurrency:
group: cli-npm-release
cancel-in-progress: false
jobs:
publish:
if: github.ref == 'refs/heads/master'
name: Publish production CLI packages
if: github.ref == 'refs/heads/master' && vars.PRODUCTION_DEPLOY_ENABLED == 'true'
runs-on: ubuntu-latest
environment: npm
timeout-minutes: 30
environment:
name: npm
url: https://www.npmjs.com/package/dealmachine
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Verify the reviewed dispatch candidate
if: github.event_name == 'workflow_dispatch' && inputs.expected_sha != ''
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
shell: bash
run: |
[[ "$EXPECTED_SHA" =~ ^[a-f0-9]{40}$ ]]
test "$GITHUB_SHA" = "$EXPECTED_SHA"
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: https://registry.npmjs.org
- name: Verify requested release
env:
RELEASE_VERSION: ${{ inputs.version }}
run: node --input-type=module -e "import fs from 'node:fs'; import assert from 'node:assert/strict'; assert.equal(JSON.parse(fs.readFileSync('package.json')).version, process.env.RELEASE_VERSION);"
- name: Verify trusted publishing tooling
run: node --input-type=module -e "import { execFileSync } from 'node:child_process'; import assert from 'node:assert/strict'; const v = execFileSync('npm', ['--version'], { encoding:'utf8' }).trim().split('.').map(Number); assert(v[0] > 11 || (v[0] === 11 && (v[1] > 5 || (v[1] === 5 && v[2] >= 1))), 'npm 11.5.1 or later is required for trusted publishing');"
- run: npm ci
- name: Publish implementation and alias
env:
RELEASE_CHANNEL: ${{ inputs.channel }}
run: npm run release:publish -- --tag "$RELEASE_CHANNEL"
- name: Build, publish and verify implementation and alias
id: release
run: npm run release:auto:publish
- name: Retain release evidence and exact package archives
if: always()
uses: actions/upload-artifact@v4
with:
name: cli-release-${{ github.sha }}-${{ github.run_attempt }}
path: artifacts/
if-no-files-found: ignore
retention-days: 90
Loading
Loading