From c58d2912c943753db7d81bedb00c5c5899903287 Mon Sep 17 00:00:00 2001 From: Joep <20927987+MisterWanted@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:03:04 +0000 Subject: [PATCH] chore(ci): remove the retired dev lanes and the dormant promotion exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev branch is gone; its CI lanes (ci/container push triggers), the dead dev -> main promotion exception in enforce-target, and the stale dev wording go with it. Also restores security-audit gating on pull requests: it only triggered for PRs targeting dev, so it had stopped running on PRs entirely. πŸ€– Generated with Codebuff Co-Authored-By: Codebuff --- .github/scripts/enforce-pr-target.test.cjs | 31 ++++++-- .github/scripts/pr-quality.cjs | 55 ++++---------- .github/scripts/pr-quality.test.cjs | 81 ++++++--------------- .github/workflows/ci.yml | 2 +- .github/workflows/container.yml | 7 +- .github/workflows/enforce-issue-quality.yml | 2 +- .github/workflows/enforce-pr-target.yml | 11 +-- .github/workflows/pr-labeler.yml | 2 +- .github/workflows/security-audit.yml | 6 +- .github/workflows/stale-needs-info.yml | 2 +- AGENTS.md | 9 +-- CHANGELOG.md | 8 ++ MAINTAINERS.md | 6 +- structure/06_docs-and-release.md | 26 ++++--- tests/ci-workflows.test.ts | 27 ++++--- 15 files changed, 122 insertions(+), 153 deletions(-) diff --git a/.github/scripts/enforce-pr-target.test.cjs b/.github/scripts/enforce-pr-target.test.cjs index 5f2dde7a..f6dd416d 100644 --- a/.github/scripts/enforce-pr-target.test.cjs +++ b/.github/scripts/enforce-pr-target.test.cjs @@ -6,7 +6,10 @@ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); describe("enforce-pr-target workflow", () => { - const workflowPath = path.join(__dirname, "../workflows/enforce-pr-target.yml"); + const workflowPath = path.join( + __dirname, + "../workflows/enforce-pr-target.yml", + ); const workflow = fs.readFileSync(workflowPath, "utf8"); it("uses pull_request_target without checking out PR head code", () => { @@ -23,8 +26,13 @@ describe("enforce-pr-target workflow", () => { // "Resource not accessible by integration" when contents stays unset/read // (seen on #626). Assert the real permissions block, not comment text // that also mentions these scopes. - const permissionsBlock = workflow.match(/^permissions:\n((?:[ \t]+.+\n)+)/m); - assert.ok(permissionsBlock, "workflow must declare a top-level permissions block"); + const permissionsBlock = workflow.match( + /^permissions:\n((?:[ \t]+.+\n)+)/m, + ); + assert.ok( + permissionsBlock, + "workflow must declare a top-level permissions block", + ); const lines = permissionsBlock[1] .split("\n") .map((line) => line.trim()) @@ -50,10 +58,16 @@ describe("enforce-pr-target workflow", () => { it("checks out trusted default-branch scripts only (never PR head)", () => { assert.match(workflow, /actions\/checkout@[0-9a-f]{40}/); - assert.match(workflow, /ref:\s*\$\{\{\s*github\.event\.repository\.default_branch\s*\}\}/); + assert.match( + workflow, + /ref:\s*\$\{\{\s*github\.event\.repository\.default_branch\s*\}\}/, + ); assert.match(workflow, /sparse-checkout:\s*\.github\/scripts/); assert.match(workflow, /persist-credentials:\s*false/); - assert.doesNotMatch(workflow, /ref:\s*\$\{\{\s*github\.event\.pull_request\.head/); + assert.doesNotMatch( + workflow, + /ref:\s*\$\{\{\s*github\.event\.pull_request\.head/, + ); }); it("loads pr-quality via require from the checked-out scripts", () => { @@ -70,8 +84,11 @@ describe("enforce-pr-target workflow", () => { ); assert.ok(qualityCall, "must call collectPrQualityFailures"); assert.match(qualityCall[1], /stackedBase/); - assert.match(qualityCall[1], /headFromSameRepo/); - assert.match(qualityCall[1], /isSameGithubRepo/); + // The retired dev-promotion exception is gone: no same-repo `dev` head + // may ever be special-cased again, so these must stay absent. + assert.doesNotMatch(qualityCall[1], /headFromSameRepo/); + assert.doesNotMatch(qualityCall[1], /isSameGithubRepo/); + assert.doesNotMatch(qualityCall[1], /headRef/); }); it("strips stale WRONG BRANCH prefix on failure when base is corrected", () => { diff --git a/.github/scripts/pr-quality.cjs b/.github/scripts/pr-quality.cjs index 5c7fb3d4..5d33468c 100644 --- a/.github/scripts/pr-quality.cjs +++ b/.github/scripts/pr-quality.cjs @@ -35,14 +35,16 @@ function isWrongAncestry({ aheadMainMax = ANCESTRY_AHEAD_MAIN_MAX, }) { return ( - behindMain === 0 && - behindBase >= threshold && - aheadMain <= aheadMainMax + behindMain === 0 && behindBase >= threshold && aheadMain <= aheadMainMax ); } function authorHasPushPermission(permission) { - return permission === "admin" || permission === "maintain" || permission === "write"; + return ( + permission === "admin" || + permission === "maintain" || + permission === "write" + ); } /** @@ -103,7 +105,9 @@ function assessPrDescription(body) { const cleaned = clean(withoutTemplate); if (!cleaned) { // Unterminated comment tails removed as well β€” see stripHtmlComments in issue-quality.cjs. - const strippedComments = withoutTemplate.replace(/|$)/g, "").trim(); + const strippedComments = withoutTemplate + .replace(/|$)/g, "") + .trim(); if (!strippedComments) return { ok: false, reason: "empty" }; if (isPlaceholderOnlyValue(strippedComments)) { return { ok: false, reason: "placeholder" }; @@ -113,7 +117,9 @@ function assessPrDescription(body) { if (isPlaceholderOnlyValue(cleaned)) { return { ok: false, reason: "placeholder" }; } - if (hasSubstantialStructuredContent(cleaned, MIN_SECTION_LEN, MIN_RICH_SECTIONS)) { + if ( + hasSubstantialStructuredContent(cleaned, MIN_SECTION_LEN, MIN_RICH_SECTIONS) + ) { return { ok: true }; } if ( @@ -125,34 +131,6 @@ function assessPrDescription(body) { return { ok: false, reason: "thin" }; } -/** - * Fail-closed same-repository check. A fork can name its head `dev`; that is - * not a maintainer promotion. Prefer numeric GitHub repo ids; fall back to - * `full_name`, then owner/name. Missing fields never compare equal. - */ -function isSameGithubRepo(headRepo, baseRepo) { - if (!headRepo || !baseRepo || typeof headRepo !== "object" || typeof baseRepo !== "object") { - return false; - } - if (typeof headRepo.id === "number" && typeof baseRepo.id === "number") { - return headRepo.id === baseRepo.id; - } - const headFull = typeof headRepo.full_name === "string" ? headRepo.full_name : ""; - const baseFull = typeof baseRepo.full_name === "string" ? baseRepo.full_name : ""; - if (headFull && baseFull) { - return headFull === baseFull; - } - const headOwner = headRepo.owner && headRepo.owner.login; - const baseOwner = baseRepo.owner && baseRepo.owner.login; - return Boolean( - headOwner && - baseOwner && - headOwner === baseOwner && - headRepo.name && - headRepo.name === baseRepo.name, - ); -} - function collectPrQualityFailures({ baseRef, allowedBases, @@ -165,15 +143,9 @@ function collectPrQualityFailures({ ancestryLookupFailed = false, /** True when baseRef is another open PR's head (stacked child). */ stackedBase = false, - /** PR head ref. Promotion also requires same-repository head. */ - headRef, - /** True only when head and base resolve to the same GitHub repository. */ - headFromSameRepo = false, }) { const failures = []; - const promotionBase = - baseRef === "main" && headRef === "dev" && headFromSameRepo === true; - const wrongBase = !allowedBases.includes(baseRef) && !stackedBase && !promotionBase; + const wrongBase = !allowedBases.includes(baseRef) && !stackedBase; if (wrongBase) { failures.push({ code: "wrong_base" }); } else { @@ -204,7 +176,6 @@ module.exports = { ANCESTRY_BEHIND_THRESHOLD, ANCESTRY_AHEAD_MAIN_MAX, isWrongAncestry, - isSameGithubRepo, authorHasPushPermission, assessPrDescription, collectPrQualityFailures, diff --git a/.github/scripts/pr-quality.test.cjs b/.github/scripts/pr-quality.test.cjs index 8e280102..8ba50f93 100644 --- a/.github/scripts/pr-quality.test.cjs +++ b/.github/scripts/pr-quality.test.cjs @@ -8,7 +8,6 @@ const { authorHasPushPermission, assessPrDescription, collectPrQualityFailures, - isSameGithubRepo, } = require("./pr-quality.cjs"); describe("isWrongAncestry", () => { @@ -21,12 +20,21 @@ describe("isWrongAncestry", () => { it("uses threshold 20 by default", () => { assert.equal(ANCESTRY_BEHIND_THRESHOLD, 20); - assert.equal(isWrongAncestry({ behindMain: 0, behindBase: 20, aheadMain: 1 }), true); - assert.equal(isWrongAncestry({ behindMain: 0, behindBase: 19, aheadMain: 1 }), false); + assert.equal( + isWrongAncestry({ behindMain: 0, behindBase: 20, aheadMain: 1 }), + true, + ); + assert.equal( + isWrongAncestry({ behindMain: 0, behindBase: 19, aheadMain: 1 }), + false, + ); }); it("passes when head is behind main (not sitting on main tip)", () => { - assert.equal(isWrongAncestry({ behindMain: 1, behindBase: 44, aheadMain: 1 }), false); + assert.equal( + isWrongAncestry({ behindMain: 1, behindBase: 44, aheadMain: 1 }), + false, + ); }); it("passes stale dev-based branches that are many commits ahead of main", () => { @@ -53,7 +61,9 @@ describe("assessPrDescription", () => { assert.equal(assessPrDescription("").ok, false); assert.equal(assessPrDescription(" ").ok, false); assert.equal( - assessPrDescription("\n\n").reason, + assessPrDescription( + "\n\n", + ).reason, "empty", ); }); @@ -122,7 +132,8 @@ describe("collectPrQualityFailures", () => { const failures = collectPrQualityFailures({ baseRef: "main", allowedBases: allowed, - body: "## Summary\n" + "x".repeat(50) + "\n\n## Test plan\n" + "y".repeat(50), + body: + "## Summary\n" + "x".repeat(50) + "\n\n## Test plan\n" + "y".repeat(50), behindMain: 0, behindBase: 0, authorPermission: "read", @@ -279,11 +290,13 @@ describe("collectPrQualityFailures", () => { assert.ok(failures.some((f) => f.code === "wrong_base")); }); - it("does not flag wrong_base for same-repo maintainer promotion main + head dev", () => { + it("no longer special-cases a promotion-shaped main + head dev PR", () => { + // The dev β†’ main promotion exception was removed with the retired branch. + // With `main` outside this test's allow-list, a promotion-shaped PR is an + // ordinary wrong_base like any other non-allow-listed base β€” the head name + // never gets special treatment again. const failures = collectPrQualityFailures({ baseRef: "main", - headRef: "dev", - headFromSameRepo: true, allowedBases: allowed, body: [ "## Summary", @@ -297,15 +310,13 @@ describe("collectPrQualityFailures", () => { behindBase: 0, authorPermission: "write", }); - assert.ok(!failures.some((f) => f.code === "wrong_base")); + assert.ok(failures.some((f) => f.code === "wrong_base")); assert.ok(!failures.some((f) => f.code === "wrong_ancestry")); }); - it("still flags wrong_base for a fork head named dev targeting main", () => { + it("still flags wrong_base for a head named dev on a non-allow-listed base", () => { const failures = collectPrQualityFailures({ baseRef: "main", - headRef: "dev", - headFromSameRepo: false, allowedBases: allowed, body: [ "## Summary", @@ -325,7 +336,6 @@ describe("collectPrQualityFailures", () => { it("still flags wrong_base for main + head other", () => { const failures = collectPrQualityFailures({ baseRef: "main", - headRef: "feat/other", allowedBases: allowed, body: [ "## Summary", @@ -342,46 +352,3 @@ describe("collectPrQualityFailures", () => { assert.ok(failures.some((f) => f.code === "wrong_base")); }); }); - -describe("isSameGithubRepo", () => { - it("matches numeric ids and rejects a fork id", () => { - assert.equal(isSameGithubRepo({ id: 1 }, { id: 1 }), true); - assert.equal(isSameGithubRepo({ id: 1 }, { id: 2 }), false); - }); - - it("does not treat missing ids as equal", () => { - assert.equal(isSameGithubRepo({}, {}), false); - assert.equal(isSameGithubRepo(null, { id: 1 }), false); - }); - - it("falls back to full_name then owner/name", () => { - assert.equal( - isSameGithubRepo( - { full_name: "GroepOnline/opencodex" }, - { full_name: "GroepOnline/opencodex" }, - ), - true, - ); - assert.equal( - isSameGithubRepo( - { full_name: "fork/opencodex" }, - { full_name: "GroepOnline/opencodex" }, - ), - false, - ); - assert.equal( - isSameGithubRepo( - { name: "opencodex", owner: { login: "GroepOnline" } }, - { name: "opencodex", owner: { login: "GroepOnline" } }, - ), - true, - ); - assert.equal( - isSameGithubRepo( - { name: "opencodex", owner: { login: "contributor" } }, - { name: "opencodex", owner: { login: "GroepOnline" } }, - ), - false, - ); - }); -}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 457b6d31..72b42090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ on: # tests/ci-workflows.test.ts without anything noticing. - ".github/workflows/**" push: - branches: [main, preview, dev] + branches: [main, preview] paths: - "src/**" - "bin/**" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 8cbea1e7..2e306a4c 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -6,13 +6,12 @@ name: Container image # release.yml creates the tag with GITHUB_TOKEN, and tag pushes made with that # token never start a `push` run; release.yml therefore dispatches this workflow # on the exact tag so the GHCR image carries the release identity (SHA + version). -# Pull requests, push to dev, and off-main branch dispatch build-and-load. They do not push. +# Pull requests and off-main branch dispatch build-and-load. They do not push. # packages permissions stay static literals. actionlint rejects expressions there. on: pull_request: - branches: [main, dev] + branches: [main] push: - branches: [dev] tags: - "v*.*.*" workflow_dispatch: @@ -31,7 +30,7 @@ concurrency: jobs: image: - if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/v')) + if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/v')) runs-on: ubuntu-latest timeout-minutes: 20 permissions: diff --git a/.github/workflows/enforce-issue-quality.yml b/.github/workflows/enforce-issue-quality.yml index ee5a44c8..3e7dc75e 100644 --- a/.github/workflows/enforce-issue-quality.yml +++ b/.github/workflows/enforce-issue-quality.yml @@ -1,7 +1,7 @@ name: Enforce issue quality # Issue events always load this workflow from the repository DEFAULT branch -# (currently `main`), not from `dev`. Landing here on `dev` alone does not +# (currently `main`). Landing here on a non-default branch alone does not # change live issue-quality behavior until the change is also on that default # branch. on: diff --git a/.github/workflows/enforce-pr-target.yml b/.github/workflows/enforce-pr-target.yml index e38d02b2..884d6b44 100644 --- a/.github/workflows/enforce-pr-target.yml +++ b/.github/workflows/enforce-pr-target.yml @@ -38,15 +38,14 @@ jobs: with: script: | const path = require("path"); - const { collectPrQualityFailures, isSameGithubRepo } = require( + const { collectPrQualityFailures } = require( path.join(process.cwd(), ".github", "scripts", "pr-quality.cjs"), ); // PRs target `main`, the integration branch. `dev` is not an - // allowed feature-PR base. Maintainer promotion remains an - // explicit leftover exception in collectPrQualityFailures: base - // main + head dev on the same repository. Stacked children that - // target another open PR head are also exempt. + // allowed feature-PR base; the retired `dev` lane and its + // promotion exception were removed with the branch. Stacked + // children that target another open PR head are exempt. // (See tests/ci-workflows.test.ts β€” the allow-list is pinned there.) const DEFAULT_BASE = "main"; const ALLOWED_BASES = ["main"]; @@ -349,8 +348,6 @@ jobs: const failures = collectPrQualityFailures({ baseRef: pr.base.ref, - headRef: pr.head.ref, - headFromSameRepo: isSameGithubRepo(pr.head.repo, pr.base.repo), allowedBases: ALLOWED_BASES, body: pr.body, behindMain, diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index b028acdb..7c151945 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,7 +1,7 @@ name: PR Labeler # pull_request_target always loads this workflow from the repository DEFAULT -# branch (currently `main`), not from `dev`. Landing here on `dev` alone does +# branch (currently `main`). Landing here on a non-default branch alone does # not change live labeler behavior until the change is also on that default # branch β€” same promotion model as enforce-issue-quality.yml. on: diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index d3f9e123..828addef 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -8,12 +8,12 @@ name: Security & Config Audit # 4. Privacy scan (credential/token/email leakage in tracked files) # 5. TypeScript strict typecheck # -# Runs on every PR targeting dev, on push to dev/main, and on manual dispatch. +# Runs on every PR targeting main, on push to main, and on manual dispatch. # A finding blocks merge β€” the fix is always a code change, never a suppression. on: pull_request: - branches: [dev] + branches: [main] paths: - "src/**" - "gui/src/**" @@ -26,7 +26,7 @@ on: - "package.json" - "tsconfig.json" push: - branches: [dev, main] + branches: [main] paths: - "src/**" - "gui/src/**" diff --git a/.github/workflows/stale-needs-info.yml b/.github/workflows/stale-needs-info.yml index 67434f41..f7f819bb 100644 --- a/.github/workflows/stale-needs-info.yml +++ b/.github/workflows/stale-needs-info.yml @@ -1,7 +1,7 @@ name: Close stale needs-info issues # Scheduled workflows only run from the repository DEFAULT branch -# (currently `main`), not from `dev`. Landing here on `dev` alone does not +# (currently `main`). Landing here on a non-default branch alone does not # change live issue-stale behavior until the change is also on that default # branch. on: diff --git a/AGENTS.md b/AGENTS.md index 8c2761f0..8e9dbd0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,7 +120,7 @@ sha / branch / detached / dirty only β€” never the diff. - `main` β€” the single integration branch and the target for every pull request. - `dev` β€” retired on 2026-10-01. Its content had fully landed on `main` (#181) and the branch was deleted. The `dev` β†’ `main` promotion exception - in the target-branch check is dormant; do not recreate `dev` as an + and the `dev` CI lanes were removed with it; do not recreate `dev` as an integration or feature base. - `preview` β€” prerelease train (`x.y.z-preview.*` versions). @@ -139,8 +139,8 @@ commits in the description. The **`enforce-target`** CI check accepts **`main`** as the only integration base. The same-repository **`dev`** β†’ **`main`** promotion leftover exception -stays in the check but has been dormant since `dev` was retired. It rejects -empty, thin, or malformed descriptions; authors with repository push permission +was removed from the check when `dev` was retired. It rejects empty, thin, or +malformed descriptions; authors with repository push permission skip the leftover ancestry heuristic only. Required technical checks apply even when branch protection is not configured. External approval is advisory, never a blocker. @@ -164,8 +164,7 @@ reviewers (Codex, CodeRabbit). concrete failure mode, and suggest a fix. Avoid vague or purely stylistic commentary. - **Branch targeting:** flag any pull request that does not target `main` - (stacked children are the only live exception; the retired `dev` β†’ `main` - promotion exception is dormant). + (stacked children are the only exception). - **Security boundary (highest priority):** changes touching authentication, credential/token handling, OAuth flows, GitHub Actions workflows, release automation (`scripts/release.ts`, `.github/workflows/release.yml`), or diff --git a/CHANGELOG.md b/CHANGELOG.md index d00792aa..5b654a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,14 @@ All notable changes to the GroepOnline `opencodex` fork. Format follows ### Security +- `security-audit.yml` only triggered for pull requests targeting `dev`, so + since `main` became the integration line the audit never ran on any pull + request at all. It now gates pull requests targeting `main` again (push stays + on `main`), restoring fail-closed audit gating before merge. The retired + `dev` CI lanes (ci/container push triggers) and the dormant `dev` β†’ `main` + promotion exception in `enforce-target` were removed together with the + branch; the exception was also dead logic, since `main` is always in the + allow-list. - Dependency advisories published 2026-09-29 are resolved; `bun audit` now reports zero findings at every severity in both the root and `gui` workspaces. `brace-expansion` 5.0.9 -> 5.0.12 (GHSA-qhr7-859c-m2p7, **high**, DoS via diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 4e75753c..238648c5 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -21,9 +21,9 @@ see [The retired `dev2-go` line](#the-retired-dev2-go-line). - Pull requests target `main`. It is the only integration line. The target-branch check accepts `main` as the integration base. The - same-repository `dev` β†’ `main` promotion leftover exception remains in the - check but has been dormant since `dev` was retired on 2026-10-01; a feature - or fork head targeting `dev` is `wrong_base`. + same-repository `dev` β†’ `main` promotion leftover exception was removed + when `dev` was retired on 2026-10-01; a feature or fork head targeting `dev` + is `wrong_base`. - The **`enforce-target`** CI check rejects empty, thin, or malformed descriptions; authors with repository push permission skip the leftover ancestry heuristic only. Required technical checks must succeed on the exact diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md index 5b100c37..22062811 100644 --- a/structure/06_docs-and-release.md +++ b/structure/06_docs-and-release.md @@ -20,6 +20,7 @@ The workflow runs on `main` pushes touching `docs-site/**` or the workflow itsel `docs-site`, uploads the artifact, and deploys with GitHub Pages. [Decision Log] + - λͺ©μ κ³Ό μ˜λ„: Serve this fork's public documentation on a ChefGroep hostname, proxied by Cloudflare. - κΈ°μ‘΄ κ΅¬ν˜„ 및 μ œμ•½ 쑰건: Upstream Pages still owns `opencodex.me`. GitHub project Pages at `/opencodex` 404s root-relative `/_astro` assets. `ocx.chefgroep.online` is the live GUI tunnel, not docs. - κ²€ν† ν•œ μ£Όμš” λŒ€μ•ˆ: Keep `groeponline.github.io/opencodex` as canonical; steal `opencodex.me`; put docs on the ocx tunnel. @@ -37,12 +38,12 @@ bun run build ## GitHub workflow map -| Workflow | Trigger | Purpose | -| --- | --- | --- | -| `.github/workflows/ci.yml` | `pull_request`, `push` to `main`/`dev`/`preview`, or manual dispatch when runtime/package paths change | Cross-platform runtime/package quality gate on Linux, Windows, and macOS. The `test` job (Bun) runs typecheck, `bun test --isolate tests`, the privacy scan, release-helper syntax check, GUI lint/build, and `ocx help`; `npm-global-smoke` (Node only, **no setup-bun**) builds package assets, packs the tarball, installs it globally, and runs `ocx help` to prove the bundled-Bun launcher works without a separate Bun install. | -| `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. It requires the exact `GITHUB_SHA` to have a successful Cross-platform CI run before publish or dry-run. | -| `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. | -| `.github/workflows/service-lifecycle.yml` | `push` touching `src/service.ts`, `src/cli/index.ts`, or the workflow, or manual dispatch | Linux systemd smoke test: install, verify, `ocx stop` stops the service, uninstall. | +| Workflow | Trigger | Purpose | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.github/workflows/ci.yml` | `pull_request`, `push` to `main`/`preview`, or manual dispatch when runtime/package paths change | Cross-platform runtime/package quality gate on Linux, Windows, and macOS. The `test` job (Bun) runs typecheck, `bun test --isolate tests`, the privacy scan, release-helper syntax check, GUI lint/build, and `ocx help`; `npm-global-smoke` (Node only, **no setup-bun**) builds package assets, packs the tarball, installs it globally, and runs `ocx help` to prove the bundled-Bun launcher works without a separate Bun install. | +| `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. It requires the exact `GITHUB_SHA` to have a successful Cross-platform CI run before publish or dry-run. | +| `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. | +| `.github/workflows/service-lifecycle.yml` | `push` touching `src/service.ts`, `src/cli/index.ts`, or the workflow, or manual dispatch | Linux systemd smoke test: install, verify, `ocx stop` stops the service, uninstall. | Docs-only changes intentionally route through the docs workflow instead of the runtime CI gate. If a docs change also edits runtime/package/release files, run the relevant local runtime checks before @@ -70,6 +71,7 @@ repository settings remain the source of truth for actual account permissions an enforcement. [Decision Log] + - λͺ©μ κ³Ό μ˜λ„: Make project ownership and review authority discoverable without exposing credentials or treating a documentation file as an access-control mechanism. - κΈ°μ‘΄ κ΅¬ν˜„ 및 μ œμ•½ 쑰건: Contribution and security docs referred to maintainers generically, while the repository had no maintainer roster or CODEOWNERS policy. GitHub permissions can change independently of the source tree. - κ²€ν† ν•œ μ£Όμš” λŒ€μ•ˆ: Keep the roster only in GitHub settings; introduce a larger standalone governance charter; list raw GitHub permission levels in the repository. @@ -108,12 +110,12 @@ Release workflow dispatch. Docs publishing is separate from npm release publishi Every npm release version must map cleanly across four surfaces: -| Surface | Required state | -| --- | --- | -| `package.json` | `version` equals the release workflow `version` input. | -| npm registry | `@groeponline/opencodex@` does not exist before publish, then exists after publish with the requested dist-tag. | -| Git tag | `v` does not exist before publish, then points at the exact release commit. | -| GitHub Release | `v` does not exist before publish, then is created from the exact release commit. | +| Surface | Required state | +| -------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `package.json` | `version` equals the release workflow `version` input. | +| npm registry | `@groeponline/opencodex@` does not exist before publish, then exists after publish with the requested dist-tag. | +| Git tag | `v` does not exist before publish, then points at the exact release commit. | +| GitHub Release | `v` does not exist before publish, then is created from the exact release commit. | The release must fail before `npm publish` if npm, the Git tag, or the GitHub Release already has the requested version. This prevents partial releases where npm is published but GitHub Release creation diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index eb1c7826..2e393caf 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -185,7 +185,7 @@ describe("GitHub Actions hardening", () => { ["pull_request", "refs/heads/main", "image"], ["pull_request", "refs/tags/v1.5.0", "image"], ["pull_request_target", "refs/heads/main", undefined], - ["push", "refs/heads/dev", "image"], + ["push", "refs/heads/dev", undefined], ["push", "refs/heads/main", undefined], ["push", "refs/tags/v1.5.0", "publish"], ["workflow_dispatch", "refs/heads/feature", "image"], @@ -419,7 +419,6 @@ describe("GitHub Actions hardening", () => { }; }; expect([...(ci.on?.push?.branches ?? [])].sort()).toEqual([ - "dev", "main", "preview", ]); @@ -1226,9 +1225,17 @@ describe("GitHub Actions hardening", () => { // at once while every behavioural scenario below still passes. expect(script).toMatch(/const ALLOWED_BASES = \["main"\];/); expect(script).toMatch(/const DEFAULT_BASE = "main";/); - expect(script).toContain("headRef: pr.head.ref"); - expect(script).toContain( - "headFromSameRepo: isSameGithubRepo(pr.head.repo, pr.base.repo)", + // The retired dev-promotion exception is gone for good. It was also dead + // logic β€” promotionBase required base `main`, which the allow-list already + // accepts β€” so removal is behaviour-neutral by construction. These pins + // keep it from being silently reintroduced. + expect(script).not.toContain("isSameGithubRepo"); + expect(script).not.toContain("promotionBase"); + const quality = await readText(".github/scripts/pr-quality.cjs"); + expect(quality).not.toContain("isSameGithubRepo"); + expect(quality).not.toContain("promotionBase"); + expect(quality).toMatch( + /const wrongBase = !allowedBases\.includes\(baseRef\) && !stackedBase;/, ); // Every mutation targets the PR the event fired for. `pull_number` is the @@ -1631,7 +1638,10 @@ describe("GitHub Actions hardening", () => { expect(commentBody).not.toContain("dev2-go"); }); - test("a maintainer promotion PR from dev onto main is not wrong-base", async () => { + test("a head named dev onto main passes wrong-base only via the allow-list", async () => { + // The dev β†’ main promotion exception was removed with the branch. A head + // named `dev` gets no special treatment: this passes only because `main` + // is the allow-listed base, exactly like any other head. const result = await run({ pr: { base: { ref: "main" }, @@ -3569,10 +3579,9 @@ describe("GitHub Actions hardening", () => { }; expect([...(workflow.on?.pull_request?.branches ?? [])].sort()).toEqual([ - "dev", "main", ]); - expect([...(workflow.on?.push?.branches ?? [])]).toEqual(["dev"]); + expect([...(workflow.on?.push?.branches ?? [])]).toEqual([]); expect(workflow.on?.push?.tags).toEqual(["v*.*.*"]); expect(workflow.on).toHaveProperty("workflow_dispatch"); expect(workflow.on?.workflow_dispatch?.inputs?.expected_sha).toEqual({ @@ -3604,7 +3613,7 @@ describe("GitHub Actions hardening", () => { }); expect(text).not.toMatch(/packages:\s*\$\{\{/); expect(String(image?.if ?? "")).toContain("pull_request"); - expect(String(image?.if ?? "")).toContain("refs/heads/dev"); + expect(String(image?.if ?? "")).not.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");