From 69d5327a0a293bd15697d45eb329bdf1dd3c2291 Mon Sep 17 00:00:00 2001 From: chenkun Date: Mon, 5 Oct 2026 00:47:49 +0800 Subject: [PATCH] ci: add single 'gate' check context for the upcoming PR-gated main The 'protect main' ruleset will require one stable check name instead of enumerating matrix cells: 'gate' aggregates the test+e2e matrices (plus CodeQL's 'analyze' is required separately). Docs (AGENTS.md, CONTRIBUTING.md) switch to the PR-based workflow: direct pushes to main rejected for everyone, release commits land via release PRs. --- .github/workflows/ci.yml | 14 ++++++++++++++ AGENTS.md | 5 +++-- CONTRIBUTING.md | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b054a9..b433af0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,3 +72,17 @@ jobs: OPENCODE_E2E_V1_SPEC: opencode-ai@latest OPENCODE_E2E_V2_SPEC: "@opencode/cli@latest" run: node test/e2e/run.mjs --host ${{ matrix.host }} + + # Branch-ruleset gate: one stable check context that aggregates the whole + # matrix, so the "protect main" ruleset requires a single name instead of + # enumerating matrix cells (which would silently drift on matrix changes). + # Keep the job id `gate` stable, or update the ruleset in the same PR. + gate: + if: always() + needs: [test, e2e] + runs-on: ubuntu-latest + steps: + - run: | + echo "test: ${{ needs.test.result }}" + echo "e2e: ${{ needs.e2e.result }}" + [ "${{ needs.test.result }}" = success ] && [ "${{ needs.e2e.result }}" = success ] diff --git a/AGENTS.md b/AGENTS.md index f9eab7c..552a07c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,18 +33,19 @@ Behavioral facts verified against host source; getting these wrong is the histor - Plugin log wording is a **test contract**: the e2e harness and `test/index.test.ts` grep the log for phrases like `startup: N injected`, `reload: N updated/withdrawn`, `reconnecting MCP server(s)` — reword messages and tests together. The log rotates at ~256 KiB keeping one `.old` generation; when debugging a missing line, check both files. - Fix/feat/docs commits must **not** touch `package.json`'s `version` — on `main` it always mirrors the latest published npm version. The bump happens only in the release commit (see Release). Commits up to `v0.4.6` predate this policy (bump-at-commit-time, with `(vX.Y.Z)` suffixes in commit messages), which is why 0.4.5/0.4.6 exist in git history but were never published to npm. - CI (`.github/workflows/ci.yml`): unit tests on ubuntu+windows+macOS × node 22.18/24, e2e matrix os × host. Windows-specific branches (permission checks, `~\` expansion) are why Windows CI exists — don't remove it; macOS shares the POSIX branches with Linux and adds a second Unix userland. It also fires weekly (Mon 06:00 UTC) and on manual dispatch, when each e2e cell additionally runs against the `@latest` host specs — the pinned specs stay sticky, so a red weekly run means a new host release drifted; align the plugin, then bump the pins. All actions across the workflows are SHA-pinned (version in the trailing comment) and every workflow declares least-privilege `permissions:` — keep both when touching CI (OpenSSF Scorecard hygiene); `dependabot.yml` bumps the pins weekly (github-actions only — npm excluded so lockfile resolved hosts stay on npmmirror). `codeql.yml` adds CodeQL SAST (ubuntu, TS, no build). +- **All changes land on `main` via PR** — the `protect main` ruleset rejects direct pushes for everyone (no bypass actors, admins included). A merge requires the `gate` (ci.yml matrix aggregate — one stable context so the ruleset survives matrix changes; keep the job id stable or update the ruleset in the same PR) and `analyze` (CodeQL) checks green on an up-to-date branch. Squash and merge commits are both accepted. ## Release Releases are **CI-gated** by `.github/workflows/release.yml`: pushing a `v` tag replays the full CI matrix (unit + real-host e2e, ubuntu+windows+macOS — reused from `ci.yml` via `workflow_call`) and only then publishes to npm with provenance and creates the GitHub Release. There is deliberately **no** `NPM_TOKEN` anywhere — npm's trusted-publisher binding only accepts this workflow's OIDC identity, so local `npm publish` fails by design. -- Release flow (**bump-at-release-time**): pick the semver level from the changes accumulated on `main` since the last tag, run `npm version --no-git-tag-version` (bumps `package.json` and syncs `package-lock.json` in one step), commit as `chore: release vX.Y.Z` and push `main`. Then `v=$(node -p "require('./package.json').version") && git tag -m "v$v" "v$v" && git push origin "v$v"` — the `-m` is not optional on machines with `tag.gpgSign=true` (a bare `git tag` opens an editor and fails non-interactively). The guard job fails the run when tag ≠ package.json version or the version is already on npm. +- Release flow (**bump-at-release-time**): pick the semver level from the changes accumulated on `main` since the last tag, run `npm version --no-git-tag-version` (bumps `package.json` and syncs `package-lock.json` in one step), commit as `chore: release vX.Y.Z` on a release branch, open a PR and merge it (direct pushes to `main` are rejected — the ruleset has no bypass actors). Then `git pull` on `main` and `v=$(node -p "require('./package.json').version") && git tag -m "v$v" "v$v" && git push origin "v$v"` — the `-m` is not optional on machines with `tag.gpgSign=true` (a bare `git tag` opens an editor and fails non-interactively). The guard job fails the run when tag ≠ package.json version or the version is already on npm. - **Changelog is generated during `npm version`**: the `version` lifecycle script runs `scripts/changelog.ts`, which turns CHANGELOG.md's `[Unreleased]` into a dated section for the new version — curated entries carry over verbatim, conventional commits since the previous tag are appended as draft entries (`chore: release` excluded), link definitions refresh, the file is left staged. **Review/trim the draft before the release commit.** Re-running is a no-op once the section exists. The release guard additionally fails when CHANGELOG.md lacks the `## [X.Y.Z]` section, and the GitHub Release body is extracted from that section (no more `--generate-notes`). - The `npm publish` step must stay directly in `release.yml`: npm validates the *calling* workflow's filename against the trusted-publisher binding — a publish hidden behind `workflow_call` would mismatch (reusing `ci.yml` for the test matrix only is fine). - One-time bootstrap (**done** 2026-10-03 — the binding is live and the v0.4.4 rehearsal published through it; keep for forks/re-creation): trusted publishing can only be configured once the package exists on npm (npm/cli#8544), so the **first** publish is a one-time manual `npm publish` (that one version carries no provenance). Here the package-creating publish was a minimal `0.0.0-stage` placeholder (2 files, `"stub": true`), followed ~1 min later by the real manual `0.4.3`; the stub was **unpublished 2026-10-04** (see next bullet) and no longer exists. Then npmjs.com → package → Settings → Trusted publisher → GitHub Actions: org/user `bytesnail`, repo `opencode-secrets-env`, workflow filename `release.yml` (case-sensitive, `.yml` included; npm does not validate the fields until a publish runs), allowed action `npm publish`. - Removing a stray npm version (playbook verified 2026-10-04): `npm unpublish @` only works within **72 h** of that version's publish; later it's npm-support territory. Pre-checks: `npm view dist-tags` (never unpublish the `latest` target — re-tag first) and per-version downloads at `https://api.npmjs.org/versions//last-week` (zero = safe). Gotchas hit in practice: the maintainer machine's registry is npmmirror (read-only mirror) — **every write needs `--registry https://registry.npmjs.org`**; `npm login --registry https://registry.npmjs.org --auth-type=web` prints a URL and polls, no TTY needed; with 2FA on the account, writes fail EOTP and npm **redacts the `https://www.npmjs.com/auth/cli/` URL as `***` in non-TTY output and logs** (authId is treated as a secret), so the web-OTP flow looks unreachable headless — workaround: run under a pseudo-TTY, `script -qec "npm unpublish @ --registry https://registry.npmjs.org --browser=false" /tmp/log` in the background; npm's `otplease` then prints the real URL unredacted, polls the `doneUrl`, and auto-retries the unpublish once the maintainer completes browser auth (works with any account factor — passkey/security key/TOTP — no TOTP secret needed locally). Verify: `npm view versions` no longer lists it and the tarball URL 404s; the npmjs.com versions tab and npmmirror lag a few minutes. - Version policy: every release's semver level is a deliberate maintainer decision — `patch` for fixes, `minor` for features; going 1.0.0 is its own deliberate decision. Never bump the version outside a release commit. -- GitHub rulesets are configured (manage with `gh api repos/bytesnail/opencode-secrets-env/rulesets`): `v*` tags are immutable — creation restricted to repo admins, no update/deletion/force-push; `main` is protected against deletion and force-pushes. Recreate equivalent rules in a fork before relying on them. +- GitHub rulesets are configured (manage with `gh api repos/bytesnail/opencode-secrets-env/rulesets`): `v*` tags are immutable — creation restricted to repo admins, no update/deletion/force-push; `main` is PR-gated (see Conventions) plus deletion/force-push protection. Recreate equivalent rules in a fork before relying on them. - Optional hardening: restrict the binding to `npm stage publish` (every release then needs 2FA approval on npmjs.com), or put the publish job in a GitHub `environment` with required reviewers — the environment name must then also be entered in the trusted-publisher binding. - `npm pack --dry-run` audits the tarball — the `files` whitelist must never ship `test/`, `.github/` or `AGENTS.md`. - GitHub's repo **Packages** sidebar lists **GitHub Packages registry artifacts only** — an npmjs.com package never appears there, provenance or not (verified empty on this repo 2026-10-03). The link works the other way: provenance + the `repository` field make the npmjs package page point back to this repo/commit/workflow (live since v0.4.4). Populating the sidebar would need a mirror publish to `npm.pkg.github.com` — cosmetic only (GPR npm requires auth even for public installs); deliberately skipped. GitHub-side discoverability comes from the README npm badge and the Releases section. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ada8bae..07dcf8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,11 @@ enough for most PRs — CI does the rest. ## Pull requests +`main` is PR-gated for everyone, admins included — direct pushes are +rejected by the `protect main` ruleset. A merge needs the required checks +green (`gate`, the ci.yml matrix aggregate, plus `analyze` from CodeQL) on +an up-to-date branch; squash and merge commits are both accepted. + - One concern per PR; describe the user-visible behavior change. - Update both READMEs when user-facing behavior or options change. - Add or adjust unit tests for logic changes; e2e coverage for