Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions .github/scripts/release-evidence-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,37 @@
"desktop/scripts/appimage-patchelf.py": "63e78ef74160bc48fbcdd882065f1d416db6d13c",
"desktop/scripts/linux-packaged-e2e.ts": "9ca92352668ff881c85caea44a9b4747bedc7e2d"
}
},
{
"kind": "ci",
"version": 1,
"files": {
".github/workflows/ci.yml": "562e63d6bd140944d8c6ba808010913463c75826",
".github/actions/setup-project-bun/action.yml": "cf28f6f9d4385d69784f4e3610914f6952ce789d",
".github/scripts/release-evidence.cjs": "c4cd8c762abbfc1c0674937535c5a36b9cf705fd",
".github/scripts/release-evidence.test.cjs": "de30ad37105cf0e7dd8c538efb5ee1cfafe02990",
"scripts/ci/select-test-lane.sh": "b25836cb55f962d1ffd9e68ed00f45f8b45becc8",
"scripts/ci/windows-sensitive-diff.sh": "03495f256c671aff57e750febffe19ca7face6a1",
"scripts/ci/run-bun-test-batches.sh": "f8aa18e80aff1c95c584cd2c879dfbb6806c11fb",
"scripts/ci/bun-crash-signatures.sh": "d34662ddd07864d97bed909135095db33fe42c9e",
"scripts/ci/bun-install-frozen.sh": "e6d7bc3aec044e12297c7c4c04015a33df0eb22b",
"scripts/ci/docker-smoke.ts": "a8424f93986a31625a0a236cdab5667db67b3405",
"scripts/ci/test-durations.tsv": "0c7b8eeac35a5f09414d1b77185733b5936c594f",
"scripts/ci/test-durations.windows.tsv": "5ee31b6bd6cada6ab7d806ea03dd0d8e46fe1d29",
"scripts/test.ts": "363bf072c4b689478fdbb76a61bf4eadbc054548",
"scripts/lib/test-runner-bun.ts": "8763806104a851dbdefeea8cffe8d7dd8d1c8da1",
"scripts/test-temp-lock.ts": "ee83fb254b728de119028f52b9894bf52d7aea15",
"scripts/ci/run-changed-tests.sh": "9a54d3a5db9849bcbe0dd766954a840185419fa5",
"scripts/keyring-smoke.ts": "cd3be07ad57796172696dfa053ea1fb129f29365",
"scripts/privacy-scan.ts": "86314e891b45e49739b94c76cddd9e4cc2d4340f",
"scripts/structure-ssot.ts": "40ba68401df9b77156afed66d6a11c61df8e9a41",
"scripts/generate-ocx-skill-surface.ts": "750ea21e5668aa512a83aa5a7f8cd7bc5cbd79db",
"desktop/scripts/prepare-sidecar.ts": "20f7eec61f73635ea681e25f13dd850b62bb41e2",
"desktop/scripts/build-widget.sh": "38e0eb9d713804b603aa75c32383aa18ef01d06c",
"desktop/scripts/verify-macos-runtime.sh": "43f1a31375c6f6fe7dcafb9d96c231b98d1f6ba4",
"desktop/scripts/verify-linux-sidecar.sh": "c502bb5c8ba8346c722a8f5274f2274bb325a934",
"desktop/scripts/appimage-patchelf.py": "63e78ef74160bc48fbcdd882065f1d416db6d13c",
"desktop/scripts/linux-packaged-e2e.ts": "9ca92352668ff881c85caea44a9b4747bedc7e2d"
}
}
]
2 changes: 1 addition & 1 deletion .github/scripts/release-evidence.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test('run identity rejects forged titles, wrong repository, branch, event and at
const workflow = {id:42};
const run = {id:7, workflow_id:42, repository:{id:1273824907}, head_repository:{id:1273824907}, event:'workflow_dispatch', head_branch:'dev', status:'completed', conclusion:'success', run_attempt:1, head_sha:SHA, display_title:`ocx-ci-v1 lane=all sha=${SHA}`};
assert.doesNotThrow(() => validateRun(run, workflow, 'ci', SHA));
for (const change of [{event:'push'}, {head_branch:'main'}, {conclusion:'failure'}, {run_attempt:0}, {repository:{id:2}}, {head_repository:{id:2}}, {display_title:`ocx-ci-v1 lane=release-gates sha=${SHA}`}, {head_sha:'b'.repeat(40)}]) {
for (const change of [{event:'push'}, {head_branch:'main'}, {conclusion:'failure'}, {run_attempt:0}, {repository:{id:2}}, {head_repository:{id:2}}, {display_title:`ocx-ci-v1 lane=release-gates sha=${SHA}`}, {display_title:`ocx-ci-v1 lane=windows-native-claim sha=${SHA}`}, {head_sha:'b'.repeat(40)}]) {
assert.throws(() => validateRun({...run, ...change}, workflow, 'ci', SHA));
}
});
Expand Down
51 changes: 48 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ on:
workflow_dispatch:
inputs:
lane:
description: "all (default), release-gates, or macos-control"
description: "all (default), release-gates, macos-control, or windows-native-claim"
type: choice
default: all
options: [all, release-gates, macos-control]
options: [all, release-gates, macos-control, windows-native-claim]

permissions:
contents: read
Expand Down Expand Up @@ -103,6 +103,17 @@ jobs:
runner: ${{ steps.pick.outputs.runner }}
label: ${{ steps.pick.outputs.label }}
steps:
# This job also runs when the diagnostic lane skips changes.
- name: Validate dispatch source
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
REF: ${{ github.ref }}
run: |
if [ "$EVENT_NAME" = workflow_dispatch ] && [ "$REF" != refs/heads/dev ]; then
echo '::error::manual cross-platform CI must run from dev'
exit 1
fi
- name: Pick runner
id: pick
env:
Expand Down Expand Up @@ -142,6 +153,7 @@ jobs:
# in the `on:` block above.
changes:
name: changes
if: github.event_name != 'workflow_dispatch' || github.event.inputs.lane != 'windows-native-claim'
runs-on: ubuntu-latest
timeout-minutes: 5
# The workflow grants only `contents: read`, and specifying any permission
Expand Down Expand Up @@ -987,6 +999,39 @@ jobs:
- name: CLI help smoke
run: bun run src/cli/index.ts help

# A diagnostic run deliberately bypasses changes and the full CI graph. Its
# title identifies it as a partial run, never release evidence.
windows-native-claim:
name: windows native-main claim diagnostic
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev' && github.event.inputs.lane == 'windows-native-claim'
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout workflow commit
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Setup project Bun
uses: ./.github/actions/setup-project-bun
with:
role: test-runner
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Isolate homes
shell: pwsh
run: |
$root = Join-Path $env:RUNNER_TEMP "ocx-claim-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
$ocxHome = Join-Path $root 'opencodex'
$codexHome = Join-Path $root 'codex'
New-Item -ItemType Directory -Path $ocxHome, $codexHome -ErrorAction Stop | Out-Null
"OPENCODEX_HOME=$ocxHome" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"CODEX_HOME=$codexHome" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Run native-main claim diagnostics
env:
OCX_TEST_WINDOWS_NATIVE_CLAIM_DIAGNOSTIC: '1'
run: bun test tests/codex-integration/main-quota-provenance.test.ts --timeout 60000

# Windows shards run only on a full manual dispatch. Pull requests retain
# fast Ubuntu checks, including when the Windows-sensitive filter is set.
platform-windows:
Expand Down Expand Up @@ -1683,7 +1728,7 @@ jobs:

ci:
name: ci
if: always()
if: always() && (github.event_name != 'workflow_dispatch' || github.event.inputs.lane != 'windows-native-claim')
# Every producer, including the ones that only feed other jobs. `needs` holds
# direct dependencies only, so a failing `select-windows-runner` would
# otherwise reach this gate as nothing at all while its dependents report
Expand Down
4 changes: 4 additions & 0 deletions structure/ops/cross-platform-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ manual dispatch from `dev`, not on PRs. A full `lane=all` dispatch provides evid
source SHA; a PR green or a narrower dispatch is not full-platform evidence. After a batch of
merges, maintainers run one full dispatch on `dev` rather than one per PR.

The manual `windows-native-claim` lane runs the instrumented native-main quota
provenance test on one hosted Windows runner, without starting the full matrix.
It is diagnostic evidence only: it cannot mark a commit green for release.

Two paths sit outside the `ci` filter on purpose and get narrow jobs instead of the full matrix.
A change under `.github/actions/` runs `setup-action` on Linux for PRs and on Linux, Windows and macOS for full dispatch: it runs the
composite Bun setup and requires the installed runtime to equal the version `package.json`
Expand Down
9 changes: 8 additions & 1 deletion tests/ci-workflows/ci-review-lanes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe("CI review lanes", () => {
expect(enabled("macos-control", "schedule", "", "false", "false", "false")).toBe(true);
expect(ci.jobs["select-windows-runner"]!.if).toBeUndefined();
expect(ci.jobs["platform-windows"]!.needs).toEqual(["changes", "select-windows-runner"]);
expect(ci.jobs.ci!.if).toBe("always()");
expect(ci.jobs.ci!.if).toBe("always() && (github.event_name != 'workflow_dispatch' || github.event.inputs.lane != 'windows-native-claim')");
expect(ci.jobs.ci!.needs).toEqual(expect.arrayContaining([
"changes", "select-windows-runner",
"test", "platform-macos", "gates", "storage-policy", "api-usage",
Expand Down Expand Up @@ -376,15 +376,22 @@ describe("Windows and nightly CI contracts", () => {
expect(windows?.run).toBe("bash scripts/ci/windows-sensitive-diff.sh");
expect(windows?.run).not.toContain("${{");
let checkouts = 0;
let diagnosticCheckouts = 0;
for (const [name, job] of Object.entries(ci.jobs)) {
if (name === "changes") continue;
for (const step of job.steps ?? []) {
if (!step.uses?.startsWith("actions/checkout@")) continue;
if (name === "windows-native-claim") {
expect(step.with?.ref, `${name} checkout`).toBe("${{ github.sha }}");
diagnosticCheckouts += 1;
continue;
}
expect(step.with?.ref, `${name} checkout`).toBe("${{ needs.changes.outputs.checkout_ref }}");
checkouts += 1;
}
}
expect(checkouts).toBe(18);
expect(diagnosticCheckouts).toBe(1);
});

test.skipIf(process.platform === "win32" || !Bun.which("jq"))("aggregate never requests Windows on a pull request and requests both diagnostic jobs nightly", async () => {
Expand Down
72 changes: 65 additions & 7 deletions tests/ci-workflows/ci-scope-reduction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { repoPath } from "../helpers/repo-root";
*/

const workflow = Bun.YAML.parse(readFileSync(repoPath(".github", "workflows", "ci.yml"), "utf8")) as {
on?: { push?: { branches?: string[] } };
on?: { push?: { branches?: string[] }; workflow_dispatch?: { inputs?: { lane?: { options?: string[] } } } };
jobs?: Record<string, {
name?: string;
if?: string;
Expand All @@ -34,6 +34,7 @@ const workflow = Bun.YAML.parse(readFileSync(repoPath(".github", "workflows", "c
steps?: Array<{
name?: string;
id?: string;
shell?: string;
run?: string;
env?: Record<string, string>;
uses?: string;
Expand Down Expand Up @@ -63,7 +64,7 @@ test("release history checks run with complete git history in gates", () => {
expect(checkout?.with?.["fetch-depth"]).toBe(0);
});

type SelectionInputs = { event_name: string; ci: string; native: string; desktop?: string; docker?: string };
type SelectionInputs = { event_name: string; ci: string; native: string; desktop?: string; docker?: string; lane?: string; ref?: string };

function term(source: string, inputs: SelectionInputs): string | boolean {
const text = source.trim();
Expand All @@ -76,6 +77,9 @@ function term(source: string, inputs: SelectionInputs): string | boolean {
const literal = /^'([^']*)'$/.exec(text);
if (literal) return literal[1]!;
if (text === "github.event_name") return inputs.event_name;
if (text === "github.event.inputs.lane") return inputs.lane ?? "";
if (text === "github.ref") return inputs.ref ?? "";
if (text === "always()") return true;
const output = /^needs\.changes\.outputs\.([a-z-]+)$/.exec(text);
if (output) {
if (output[1] === "ci") return inputs.ci;
Expand Down Expand Up @@ -324,6 +328,60 @@ describe("the dispatch trigger", () => {
});
});

test.skipIf(process.platform === "win32")("the unavoidable selector refuses non-dev manual dispatches", () => {
const selector = jobs["select-windows-runner"]!;
expect(selector.if).toBeUndefined();
expect(selector.needs).toBeUndefined();
const guard = selector.steps?.find(step => step.name === "Validate dispatch source");
expect(guard?.shell).toBe("bash");
expect(guard?.env).toEqual({ EVENT_NAME: "${{ github.event_name }}", REF: "${{ github.ref }}" });
expect(guard?.run).toBeDefined();
expect(guard?.run).not.toContain("${{");
for (const [event, ref, expected] of [
["workflow_dispatch", "refs/heads/main", 1],
["workflow_dispatch", "refs/heads/feature", 1],
["workflow_dispatch", "refs/heads/dev", 0],
["pull_request", "refs/pull/1/merge", 0],
] as const) {
const result = Bun.spawnSync(["/bin/bash", "-e", "-c", guard!.run!], { env: { EVENT_NAME: event, REF: ref } });
expect(result.exitCode, `${event} ${ref}`).toBe(expected);
}
});

test("Windows claim diagnostic is dev-only and bypasses the full graph", () => {
const lane = "windows-native-claim";
expect(workflow.on?.workflow_dispatch?.inputs?.lane?.options).toContain(lane);
const diagnostic = jobs[lane]!;
expect(diagnostic["runs-on"]).toBe("windows-latest");
expect(diagnostic.needs).toBeUndefined();
const base = { ci: "false", native: "false", event_name: "workflow_dispatch", ref: "refs/heads/dev", lane };
expect(evaluate(diagnostic.if!, base)).toBe(true);
for (const variation of [{ event_name: "pull_request" }, { event_name: "push" }, { ref: "refs/heads/main" }, { lane: "all" }, { lane: "release-gates" }, { lane: "macos-control" }, { lane: "" }]) {
expect(evaluate(diagnostic.if!, { ...base, ...variation })).toBe(false);
}
expect(evaluate(changes!.if!, base)).toBe(false);
expect(evaluate(jobs.ci!.if!, base)).toBe(false);
for (const lane of ["", "all", "release-gates", "macos-control"]) {
expect(evaluate(changes!.if!, { ...base, lane })).toBe(true);
expect(evaluate(jobs.ci!.if!, { ...base, lane })).toBe(true);
}
expect(evaluate(changes!.if!, { ...base, event_name: "pull_request" })).toBe(true);
expect(evaluate(jobs.ci!.if!, { ...base, event_name: "pull_request" })).toBe(true);
for (const [name, job] of Object.entries(jobs)) {
if ([lane, "select-windows-runner", "changes", "ci"].includes(name)) continue;
expect(Array.isArray(job.needs) ? job.needs : [job.needs], name).toContain("changes");
expect(job.if ?? "", name).not.toMatch(/\b(always|failure|cancelled)\s*\(/);
}
const steps = diagnostic.steps ?? [];
const checkout = steps.find(step => step.uses?.startsWith("actions/checkout@"));
expect(checkout?.with?.ref).toBe("${{ github.sha }}");
expect(checkout?.with?.["persist-credentials"]).toBe(false);
expect(steps.find(step => step.uses === "./.github/actions/setup-project-bun")?.with?.role).toBe("test-runner");
expect(steps.some(step => step.run?.includes("bun install --frozen-lockfile"))).toBe(true);
expect(steps.some(step => step.run?.includes("bun test tests/codex-integration/main-quota-provenance.test.ts"))).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
expect(steps.some(step => step.run?.includes("OPENCODEX_HOME") && step.run.includes("CODEX_HOME") && step.run.includes("GITHUB_ENV"))).toBe(true);
});

describe("Docker PR selection", () => {
test("selects only its Ubuntu smoke for matching inputs without widening event policy", () => {
const job = jobs["docker-smoke"]!;
Expand Down Expand Up @@ -513,9 +571,9 @@ describe("the aggregate gate", () => {
expect(step?.env?.CHANGES_CI).toBe("${{ needs.changes.outputs.ci }}");
});

test.skipIf(cannotRunGate)("gates every job by name, with the aggregate itself excepted", () => {
test.skipIf(cannotRunGate)("gates every ordinary job by name, excluding the standalone diagnostic", () => {
const { gatedJobs } = runGate(PR_WITHOUT_NATIVE, {});
expect(gatedJobs.sort()).toEqual(Object.keys(jobs).filter(name => name !== "ci").sort());
expect(gatedJobs.sort()).toEqual(Object.keys(jobs).filter(name => !["ci", "windows-native-claim"].includes(name)).sort());
});

test.skipIf(cannotRunGate)("treats an unselected native job as not-requested and stays green over skips", () => {
Expand All @@ -531,7 +589,7 @@ describe("the aggregate gate", () => {
// Results consistent with those expectations must pass the gate: skipped is
// the correct outcome for a job this event never asked for.
const consistent = Object.fromEntries(Object.keys(jobs)
.filter(name => name !== "ci")
.filter(name => !["ci", "windows-native-claim"].includes(name))
.map(name => [name, expectations[name] === "requested" ? "success" : "skipped"]));
const { bad } = runGate(PR_WITHOUT_NATIVE, consistent);
expect(bad).toBe("");
Expand Down Expand Up @@ -559,7 +617,7 @@ describe("the aggregate gate", () => {
expect([outOfScope.test, outOfScope["test-changed"]]).toEqual(["not-requested", "not-requested"]);
// Results consistent with the fast lane pass; a shard that ran anyway fails by name.
const consistent = Object.fromEntries(Object.keys(jobs)
.filter(name => name !== "ci")
.filter(name => !["ci", "windows-native-claim"].includes(name))
.map(name => [name, fast[name] === "requested" ? "success" : "skipped"]));
expect(runGate({ ...PR_WITHOUT_NATIVE, fast: "true" }, consistent).bad).toBe("");
expect(runGate({ ...PR_WITHOUT_NATIVE, fast: "true" }, { ...consistent, test: "success" }).bad).toContain("test was not requested");
Expand Down Expand Up @@ -627,7 +685,7 @@ describe("the aggregate gate", () => {
}
// Results consistent with the narrowed expectations pass; a smoke job that ran anyway fails by name.
const consistent = Object.fromEntries(Object.keys(jobs)
.filter(name => name !== "ci")
.filter(name => !["ci", "windows-native-claim"].includes(name))
.map(name => [name, expectations[name] === "requested" ? "success" : "skipped"]));
expect(runGate(narrow, consistent).bad).toBe("");
expect(runGate(narrow, { ...consistent, "docker-smoke": "success" }).bad).toContain("docker-smoke");
Expand Down
Loading
Loading