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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
matrix:
include:
${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v'))
&& fromJSON('[{"name":"ubuntu-latest","os":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]","run_tests":true,"run_quality":true,"run_typecheck":true,"shard_index":0,"shard_count":2},{"name":"ubuntu-latest shard 2/2","os":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]","run_tests":true,"run_quality":false,"run_typecheck":false,"shard_index":1,"shard_count":2},{"name":"macos-latest","os":"macos-latest","runner":"\"macos-latest\"","run_tests":true,"run_quality":false,"run_typecheck":true,"shard_index":0,"shard_count":1},{"name":"macos-quality","os":"macos-latest","runner":"\"macos-latest\"","run_tests":false,"run_quality":true,"run_typecheck":false,"shard_index":0,"shard_count":1},{"name":"windows-latest","os":"windows-latest","runner":"\"windows-latest\"","run_tests":true,"run_quality":false,"run_typecheck":true,"shard_index":0,"shard_count":2},{"name":"windows-latest shard 2/2","os":"windows-latest","runner":"\"windows-latest\"","run_tests":true,"run_quality":false,"run_typecheck":false,"shard_index":1,"shard_count":2},{"name":"windows-quality","os":"windows-latest","runner":"\"windows-latest\"","run_tests":false,"run_quality":true,"run_typecheck":false,"shard_index":0,"shard_count":1}]')
&& fromJSON('[{"name":"ubuntu-latest","os":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]","run_tests":true,"run_quality":true,"run_typecheck":true,"shard_index":0,"shard_count":2},{"name":"ubuntu-latest shard 2/2","os":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]","run_tests":true,"run_quality":false,"run_typecheck":false,"shard_index":1,"shard_count":2},{"name":"windows-latest","os":"windows-latest","runner":"\"windows-latest\"","run_tests":true,"run_quality":false,"run_typecheck":true,"shard_index":0,"shard_count":2},{"name":"windows-latest shard 2/2","os":"windows-latest","runner":"\"windows-latest\"","run_tests":true,"run_quality":false,"run_typecheck":false,"shard_index":1,"shard_count":2},{"name":"windows-quality","os":"windows-latest","runner":"\"windows-latest\"","run_tests":false,"run_quality":true,"run_typecheck":false,"shard_index":0,"shard_count":1}]')
|| fromJSON('[{"name":"ubuntu-latest","os":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]","run_tests":true,"run_quality":true,"run_typecheck":true,"shard_index":0,"shard_count":2},{"name":"ubuntu-latest shard 2/2","os":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]","run_tests":true,"run_quality":false,"run_typecheck":false,"shard_index":1,"shard_count":2}]') }}
steps:
- name: Checkout
Expand Down Expand Up @@ -175,7 +175,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')) && fromJSON('[{"platform":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]"},{"platform":"windows-latest","runner":"\"windows-latest\""},{"platform":"macos-latest","runner":"\"macos-latest\""}]') || fromJSON('[{"platform":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]"}]') }}
include: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')) && fromJSON('[{"platform":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]"},{"platform":"windows-latest","runner":"\"windows-latest\""}]') || fromJSON('[{"platform":"opencodex","runner":"[\"self-hosted\",\"Linux\",\"X64\",\"jan\"]"}]') }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ jobs:
fi
# shellcheck disable=SC2016 # $run/$passed/$name are jq bindings, not shell variables.
matrix_ok="$(gh run view "$ci_id" --json headSha,conclusion,jobs --jq \
'. as $run | [.jobs[] | select(.conclusion == "success") | .name] as $passed | ($run.conclusion == "success" and (["ubuntu-latest", "ubuntu-latest shard 2/2", "macos-latest", "macos-quality", "windows-latest", "windows-latest shard 2/2", "windows-quality", "npm-global opencodex", "npm-global macos-latest", "npm-global windows-latest", "Security audit", "Lint GitHub Actions"] | all(.[]; . as $name | $passed | index($name) != null)))')"
'. as $run | [.jobs[] | select(.conclusion == "success") | .name] as $passed | ($run.conclusion == "success" and (["ubuntu-latest", "ubuntu-latest shard 2/2", "windows-latest", "windows-latest shard 2/2", "windows-quality", "npm-global opencodex", "npm-global windows-latest", "Security audit", "Lint GitHub Actions"] | all(.[]; . as $name | $passed | index($name) != null)))')"
if [ "$matrix_ok" != "true" ]; then
echo "::error::CI run ${ci_id} lacks successful required platform jobs"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ jobs:
fi
# shellcheck disable=SC2016 # $run/$passed/$name are jq bindings, not shell variables.
matrix_ok="$(gh run view "$ci_id" --json headSha,conclusion,jobs --jq \
'. as $run | [.jobs[] | select(.conclusion == "success") | .name] as $passed | ($run.conclusion == "success" and (["ubuntu-latest", "ubuntu-latest shard 2/2", "macos-latest", "macos-quality", "windows-latest", "windows-latest shard 2/2", "windows-quality", "npm-global opencodex", "npm-global macos-latest", "npm-global windows-latest", "Security audit", "Lint GitHub Actions"] | all(.[]; . as $name | $passed | index($name) != null)))')"
'. as $run | [.jobs[] | select(.conclusion == "success") | .name] as $passed | ($run.conclusion == "success" and (["ubuntu-latest", "ubuntu-latest shard 2/2", "windows-latest", "windows-latest shard 2/2", "windows-quality", "npm-global opencodex", "npm-global windows-latest", "Security audit", "Lint GitHub Actions"] | all(.[]; . as $name | $passed | index($name) != null)))')"
if [ "$matrix_ok" != "true" ]; then
echo "::error::CI run ${ci_id} lacks successful required platform jobs"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ bun run setup:hooks

**Pre-push** runs `bun run prepush` — `typecheck`, `lint:gui`, `test`, `privacy:scan`,
and `doctor:gui:if-changed` — before every `git push`. The same checks run on
ubuntu-latest, macos-latest, and windows-latest in CI (CI additionally builds the GUI
ubuntu-latest and windows-latest in CI (CI additionally builds the GUI
and smoke-tests the CLI). Skip in an emergency with `git push --no-verify`.
3 changes: 0 additions & 3 deletions tests/ci-workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,10 @@
for (const job of [
"ubuntu-latest",
"ubuntu-latest shard 2/2",
"macos-latest",
"macos-quality",
"windows-latest",
"windows-latest shard 2/2",
"windows-quality",
"npm-global opencodex",
"npm-global macos-latest",
"npm-global windows-latest",
"Security audit",
"Lint GitHub Actions",
Expand Down Expand Up @@ -3146,103 +3143,103 @@
expect(text).not.toMatch(/ghcr\.io\/groeponline\/opencodex:v[0-9]/);
});

test("az-01 deploy health-gates gitSha for up to 60s and rolls back to a captured prior digest", async () => {
const text = await readText(".github/workflows/deploy.yml");
const workflow = Bun.YAML.parse(text) as {
jobs?: {
deploy?: {
steps?: Array<{
name?: string;
id?: string;
if?: string;
run?: string;
env?: Record<string, string>;
}>;
};
};
};
const steps = workflow.jobs?.deploy?.steps ?? [];
const health = steps.find((step) => step.id === "health");
const rollback = steps.find((step) => step.name === "Rollback on failure");
expect(health).toBeDefined();
expect(rollback).toBeDefined();

expect(health!.run ?? "").toContain("deadline=$((SECONDS + 60))");
expect(health!.run ?? "").toContain(
'b.get("gitSha") == os.environ["TAG_SHA"]',
);
expect(health!.run ?? "").toContain('b.get("service") == "opencodex"');
expect(health!.run ?? "").not.toContain("MainPID");
expect(health!.run ?? "").not.toContain('b.get("pid")');
expect(health!.run ?? "").toContain("--max-time");
expect(health!.run ?? "").toContain("gitSha-verified healthy within 60s");
expect(health!.run ?? "").toContain(
"<title>opencodex · proxy dashboard</title>",
);
const resolveHealthIndex = steps.findIndex(
(step) => step.name === "Resolve health URLs",
);
const deployIndex = steps.findIndex(
(step) =>
step.name === "Deploy digest-pinned container (in-place cutover)",
);
const resolveHealth = steps[resolveHealthIndex];
expect(resolveHealthIndex).toBeGreaterThanOrEqual(0);
expect(resolveHealthIndex).toBeLessThan(deployIndex);
expect(resolveHealth?.run ?? "").toContain(
"http://127.0.0.1:10100/healthz",
);
expect(resolveHealth?.run ?? "").toContain("tailscale ip -4");
expect(resolveHealth?.run ?? "").toContain(
"no discoverable Tailscale IPv4",
);
expect(resolveHealth?.run ?? "").toContain(
'urls="$urls http://${ts_ip}:10100/healthz"',
);
expect(resolveHealth?.run ?? "").toContain("OPENCODEX_BIND_IP=$ts_ip");
expect(resolveHealth?.run ?? "").not.toContain("tailscale_ipv4=");
expect(text).not.toContain("100.109.39.86");
expect(text).not.toContain("OPENCODEX_BIND_IP=127.0.0.1");

expect(rollback!.if).toBe(
"(failure() || cancelled()) && steps.prev.outcome == 'success' && steps.deploy.outputs.cutover_started == 'true'",
);
expect(rollback!.env?.PREV_IMAGE).toBe("${{ steps.prev.outputs.image }}");
expect(rollback!.env?.BUN_RUNTIME).toBe(
"${{ steps.prev.outputs.bun_runtime }}",
);
expect(rollback!.env?.UNIT_BACKUP).toBe(
"${{ steps.prev.outputs.unit_backup }}",
);
expect(rollback!.env?.PREV_HEALTH_URLS).toBe(
"${{ steps.prev.outputs.health_urls }}",
);
expect(rollback!.run ?? "").not.toContain("${{ steps.prev.outputs");
expect(rollback!.run ?? "").toContain("OPENCODEX_IMAGE=");
expect(rollback!.run ?? "").not.toContain("bun run build:gui");
expect(rollback!.run ?? "").not.toContain("git checkout --force");
expect(rollback!.run ?? "").not.toContain("git reset --hard");
expect(rollback!.run ?? "").toContain(
"sudo systemctl restart opencodex-proxy.service",
);
expect(rollback!.run ?? "").toContain('urls="$PREV_HEALTH_URLS"');
expect(rollback!.run ?? "").toContain(
"no pre-deploy healthy endpoint was captured for rollback verification",
);
expect(rollback!.run ?? "").not.toContain(
"${OCX_HEALTH_URLS:-http://127.0.0.1:10100/healthz}",
);
expect(rollback!.run ?? "").toContain(
"printf 'OPENCODEX_BIND_IP=%s\\n' \"$OPENCODEX_BIND_IP\"",
);
expect(rollback!.run ?? "").toContain("deadline=$((SECONDS + 30))");
expect(rollback!.run ?? "").not.toContain("MainPID");
expect(rollback!.run ?? "").toContain('b.get("service") == "opencodex"');
expect(rollback!.run ?? "").toContain("rolled back and healthy");
expect(rollback!.run ?? "").toContain(
"<title>opencodex · proxy dashboard</title>",
);
});

Check notice on line 3242 in tests/ci-workflows.test.ts

View check run for this annotation

codefactor.io / CodeFactor

tests/ci-workflows.test.ts#L3146-L3242

Complex Method

test("design-system contract only runs when design-system inputs or the GUI change, identically on push and PR", async () => {
const text = await readText(".github/workflows/design-system-contract.yml");
Expand Down Expand Up @@ -3616,219 +3613,219 @@
expect(helperSrc).not.toContain(".ocx-translation-state");
});

test("container image workflow builds on self-hosted opencodex and pushes only gated GHCR digests", async () => {
const text = await readText(".github/workflows/container.yml");
const workflow = Bun.YAML.parse(text) as {
on?: {
pull_request?: { branches?: string[] };
push?: { branches?: string[]; tags?: string[] };
workflow_dispatch?: {
inputs?: Record<string, { type?: string; required?: boolean }>;
};
};
permissions?: Record<string, string>;
jobs?: Record<
string,
{
if?: string;
"runs-on"?: string;
"timeout-minutes"?: number;
permissions?: Record<string, string>;
steps?: Array<{
name?: string;
id?: string;
uses?: string;
if?: string;
with?: Record<string, unknown>;
run?: string;
}>;
}
>;
};

expect([...(workflow.on?.pull_request?.branches ?? [])].sort()).toEqual([
"dev",
"main",
]);
expect([...(workflow.on?.push?.branches ?? [])]).toEqual(["dev"]);
expect(workflow.on?.push?.tags).toEqual(["v*.*.*"]);
expect(workflow.on).toHaveProperty("workflow_dispatch");
expect(workflow.on?.workflow_dispatch?.inputs?.expected_sha).toEqual({
description:
"Optional immutable release SHA; required by release.yml tag dispatches",
required: false,
type: "string",
});
expect(workflow.permissions).toEqual({ contents: "read" });

const jobs = Object.keys(workflow.jobs ?? {});
expect(jobs).toEqual(["image", "publish"]);
const image = workflow.jobs?.image;
const publish = workflow.jobs?.publish;
expect(image?.["runs-on"]).toEqual(["self-hosted", "Linux", "X64", "jan"]);
expect(publish?.["runs-on"]).toEqual([
"self-hosted",
"Linux",
"X64",
"jan",
]);
expect(image?.["timeout-minutes"]).toBe(20);
expect(publish?.["timeout-minutes"]).toBe(20);
expect(image?.permissions).toEqual({ contents: "read", packages: "none" });
expect(publish?.permissions).toEqual({
contents: "read",
packages: "write",
actions: "read",
});
expect(text).not.toMatch(/packages:\s*\$\{\{/);
expect(String(image?.if ?? "")).toContain("pull_request");
expect(String(image?.if ?? "")).toContain("refs/heads/dev");
expect(String(publish?.if ?? "")).toContain("refs/tags/v");
expect(String(publish?.if ?? "")).toContain("workflow_dispatch");
expect(String(publish?.if ?? "")).toContain("refs/heads/main");
// release.yml dispatches this workflow on the tag ref (GITHUB_TOKEN tag
// pushes never fire `push`). Publish must accept it; the non-push image
// job must not duplicate the build for that same dispatch.
expect(String(publish?.if ?? "")).toContain(
"github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))",
);
expect(String(image?.if ?? "")).toContain(
"!startsWith(github.ref, 'refs/tags/v')",
);
expect(text).toContain(
"cancel-in-progress: ${{ !((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')))) }}",
);

expect(text).not.toContain("chef-control");
expect(text).not.toContain("/home/joep");
expect(text).not.toContain("deploy.yml");
expect(text).not.toMatch(/uses:\s+\S+@(?:v\d+|main|master)\b/);
expect(text).not.toMatch(
/^\s*-\s+uses:\s+\S+@(?![0-9a-f]{40}(?=[ \t]*(?:#.*)?$))\S+/m,
);
expect(text).toContain(
"actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7",
);
expect(text).toContain(
"docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0",
);
expect(text).toContain(
"docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0",
);

const imageSteps = image?.steps ?? [];
const publishSteps = publish?.steps ?? [];
const imageCheckout = imageSteps.find((step) => step.name === "Checkout");
const publishCheckout = publishSteps.find(
(step) => step.name === "Checkout",
);
expect(imageCheckout?.with?.["persist-credentials"]).toBe(false);
expect(publishCheckout?.with?.["persist-credentials"]).toBe(false);
expect(publishCheckout?.with?.["fetch-depth"]).toBe(0);
const onMain = publishSteps.find(
(step) => step.name === "Verify commit is on main",
);
const requireCi = publishSteps.find(
(step) => step.name === "Require successful Cross-platform CI",
);
expect(onMain?.run ?? "").toContain("merge-base --is-ancestor");
expect(onMain?.run ?? "").toContain("origin/main");
expect(onMain?.run ?? "").toContain('SHA" != "$EXPECTED_SHA');
expect(onMain?.run ?? "").not.toContain("${{");
expect(requireCi?.run ?? "").toContain("gh run list --workflow ci.yml");
expect(requireCi?.run ?? "").not.toContain("${{");
expect(
imageSteps.find((step) => step.name === "Log in to GHCR"),
).toBeUndefined();
const login = publishSteps.find((step) => step.name === "Log in to GHCR");
const imageBuild = imageSteps.find((step) => step.id === "build");
const publishBuild = publishSteps.find((step) => step.id === "build");
const publishMeta = publishSteps.find((step) => step.id === "meta");
const imageSummary = imageSteps.find(
(step) => step.name === "Record digest",
);
const publishSummary = publishSteps.find(
(step) => step.name === "Record digest",
);
expect(login).toBeDefined();
expect(imageBuild).toBeDefined();
expect(publishBuild).toBeDefined();
expect(publishMeta).toBeDefined();
expect(imageSummary).toBeDefined();
expect(publishSummary).toBeDefined();
// Tag-ref dispatch is validated like a tag push: semver shape, and the
// version tags are derived from the tag, never from a moving branch.
expect(publishMeta!.run).toContain(
'[[ "$EVENT_NAME" == "workflow_dispatch" && "$REF" == refs/tags/* ]]',
);
expect(publishMeta!.run).toContain(
"git tag ${REF_NAME} does not match package.json version",
);

expect(login!.if).toBeUndefined();
expect(login!.uses).toBe(
"docker/login-action@dbcb813823bdd20940b903addbd779551569679f",
);
expect(imageBuild!.uses).toBe(
"docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a",
);
expect(publishBuild!.uses).toBe(
"docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a",
);
expect(imageBuild!.with?.push).toBe(false);
expect(imageBuild!.with?.load).toBe(true);
expect(publishBuild!.with?.push).toBe(true);
expect(publishBuild!.with?.load).toBe(false);
expect(imageBuild!.with?.provenance).toBe(false);
expect(publishBuild!.with?.provenance).toBe(false);
expect(imageBuild!.with?.sbom).toBe(false);
expect(publishBuild!.with?.sbom).toBe(false);
expect(String(imageBuild!.with?.["build-args"] ?? "")).toContain(
"VCS_REF=${{ github.sha }}",
);
expect(String(publishBuild!.with?.["build-args"] ?? "")).toContain(
"VCS_REF=${{ github.sha }}",
);
expect(String(publishBuild!.with?.["build-args"] ?? "")).toContain(
"VERSION=${{ steps.meta.outputs.version }}",
);

expect(publishMeta!.run ?? "").toContain(
"json.load(open('package.json'))['version']",
);
expect(publishMeta!.run ?? "").not.toContain("${{");
expect(publishMeta!.run ?? "").toContain(
"publish job ran outside the push gate",
);

const match = (publishMeta!.run ?? "").match(/=~ (\^\S+\$) \]\]/);
expect(match).not.toBeNull();
const tagShape = new RegExp(match![1]!);
const publishIf = String(publish?.if ?? "");
const shouldPublishJob = (event: string, ref: string) =>
(event === "push" &&
publishIf.includes("refs/tags/v") &&
ref.startsWith("refs/tags/v")) ||
(event === "workflow_dispatch" &&
(ref === "refs/heads/main" || ref.startsWith("refs/tags/v")));
const shouldPush = (event: string, ref: string) =>
shouldPublishJob(event, ref) &&
(((event === "push" || event === "workflow_dispatch") &&
tagShape.test(ref)) ||
(event === "workflow_dispatch" && ref === "refs/heads/main"));
expect(shouldPush("push", "refs/tags/v1.2.3")).toBe(true);
expect(shouldPush("push", "refs/tags/v1.2.3-preview.4")).toBe(true);
expect(shouldPush("workflow_dispatch", "refs/heads/main")).toBe(true);
expect(shouldPush("workflow_dispatch", "refs/tags/v1.2.3")).toBe(true);
expect(shouldPush("push", "refs/heads/dev")).toBe(false);
expect(shouldPush("pull_request", "refs/pull/1/merge")).toBe(false);
expect(shouldPush("workflow_dispatch", "refs/heads/dev")).toBe(false);
expect(shouldPush("push", "refs/tags/v1.2")).toBe(false);
expect(shouldPush("push", "refs/tags/vfoo")).toBe(false);

expect(imageSummary!.run ?? "").toContain("GITHUB_STEP_SUMMARY");
expect(publishSummary!.run ?? "").toContain("digest");
});

Check notice on line 3828 in tests/ci-workflows.test.ts

View check run for this annotation

codefactor.io / CodeFactor

tests/ci-workflows.test.ts#L3616-L3828

Complex Method

test("React Doctor workflow is SHA-pinned, engine-pinned, gating, and read-only", async () => {
const workflow = await readText(".github/workflows/react-doctor.yml");
Expand Down
Loading