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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>` 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 <patch|minor|major> --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 <patch|minor|major> --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 <pkg>@<version>` only works within **72 h** of that version's publish; later it's npm-support territory. Pre-checks: `npm view <pkg> dist-tags` (never unpublish the `latest` target — re-tag first) and per-version downloads at `https://api.npmjs.org/versions/<pkg>/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/<authId>` 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 <pkg>@<version> --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 <pkg> 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.
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading