Skip to content

chore(ci): scope CI secrets to the release environment - #59028

Closed
thdxr wants to merge 2 commits into
devfrom
ci-environments
Closed

thdxr wants to merge 2 commits into
devfrom
ci-environments

Conversation

@thdxr

@thdxr thdxr commented Oct 25, 2026

Copy link
Copy Markdown
Member

Issue for this PR

Release-path hardening requested by Dax (security finding 319).

Type of change

  • Refactor / code improvement

What does this PR do?

This PR moves every CI job that reads a secret into one GitHub environment, release, which only allows dev and beta. Once the repo-level secrets are moved into that environment and deleted, no other branch can run a job that reads them.

  • Every job that reads a secret other than GITHUB_TOKEN, or needs id-token: write, and runs on push to dev/beta, on a schedule, or from workflow_dispatch now declares environment: release unconditionally. That covers npm publish, signing, AUR, Homebrew, WinGet, Discord and Cloudflare. No jobs are split.
  • In publish.yml, the conditional environment on version and on publish is replaced with plain environment: release.
  • Deploys from the production branch keep using the existing production environment (branch production, reviewer thdxr). Merging it into release would let any dev push read STRIPE_SECRET_KEY_PROD and assume the production AWS role, whose trust is bound to environment:production.
  • Least privilege: the top-level permissions in publish.yml drop to contents: read. Job-level grants are added only where needed: id-token: write for Azure OIDC and npm trusted publishing, packages: write for the GHCR push. The same treatment applies to deploy.yml, containers.yml, models-snapshot.yml, nix-hashes.yml, notify-discord.yml and publish-vscode.yml. models-snapshot and nix-hashes push with the GitHub App token, so they don't need contents: write on GITHUB_TOKEN.
  • .github/CODEOWNERS gains @anomalyco/ci-owners on /.github/, /script/, /packages/*/script/ and /github/. The existing app/desktop entries are kept.

Job → environment → secrets

Workflow / job Trigger Environment Secrets
publish / version push dev, beta; dispatch release OPENCODE_APP_SECRET, OPENCODE_API_KEY
publish / build-cli 〃 release OPENCODE_APP_SECRET
publish / sign-cli-macos 〃 release APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD
publish / build-node-app-archive, build-node-cli 〃 none none
publish / sign-cli-windows 〃 release (+ id-token) AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE, AZURE_TRUSTED_SIGNING_ENDPOINT
publish / build-electron 〃 release (+ id-token) Azure ×6, APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_API_KEY, APPLE_API_KEY_PATH, APPLE_API_ISSUER, SENTRY_AUTH_TOKEN, OPENCODE_APP_SECRET
publish / publish 〃 release (+ id-token, packages) OPENCODE_APP_SECRET, AUR_KEY, TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PASSWORD, WINGET_GITHUB_TOKEN, CLOUDFLARE_API_TOKEN; npm via OIDC
publish / notify-discord-v2 〃 release DISCORD_WEBHOOK
deploy / deploy push dev, beta, production; dispatch release (dev, beta) / production (production) (+ id-token) CLOUDFLARE_API_TOKEN, PLANETSCALE_SERVICE_TOKEN, PLANETSCALE_SERVICE_TOKEN_NAME, STRIPE_SECRET_KEY_DEV (release) / STRIPE_SECRET_KEY_PROD (production), SENTRY_AUTH_TOKEN; var AWS_DEPLOY_ROLE_ARN
deploy-files / deploy, deploy-www / deploy push dev, production release / production CLOUDFLARE_API_TOKEN
deploy-changelog / deploy push dev release CLOUDFLARE_API_TOKEN
deploy-posts / deploy, deploy-update / deploy push production production (unchanged) CLOUDFLARE_API_TOKEN
models-snapshot / refresh schedule, dispatch release OPENCODE_APP_SECRET
nix-hashes / update-hashes push dev, dispatch release OPENCODE_APP_SECRET
nix-hashes / compute-hash 〃 none none
docs-locale-sync / sync-locales (disabled) push dev release OPENCODE_APP_SECRET, OPENCODE_API_KEY
stats / stats schedule, dispatch release POSTHOG_KEY
publish-vscode / publish tag vscode-v* release (unchanged; policy already allows vscode-v*) VSCE_PAT, OPENVSX_TOKEN
notify-discord / notify release: released (tag ref) release DISCORD_WEBHOOK
containers / build push dev none GITHUB_TOKEN only (packages: write)
publish-github-action / publish tag github-v*, dispatch none GITHUB_TOKEN only
release-github-action / release push dev (github/**) none GITHUB_TOKEN only

PR-triggered workflows: secrets that stay repo-level

check, test, storybook, nix-eval (pull_request), plus pr-management, pr-standards (pull_request_target), plus compliance-close, close-issues, close-prs (schedule) use no repo-level secrets. They only use GITHUB_TOKEN, and the repo default token permission is read. Nothing high-risk has to stay at repo level, so every secret can move.

Settings changes needed (not applied by this PR)

1. Create in the release environment (secrets):
OPENCODE_APP_SECRET, OPENCODE_API_KEY, APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_API_KEY, APPLE_API_KEY_PATH, APPLE_API_ISSUER, AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE, AZURE_TRUSTED_SIGNING_ENDPOINT, SENTRY_AUTH_TOKEN, AUR_KEY, TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PASSWORD, WINGET_GITHUB_TOKEN, CLOUDFLARE_API_TOKEN, DISCORD_WEBHOOK, VSCE_PAT, OPENVSX_TOKEN, POSTHOG_KEY, PLANETSCALE_SERVICE_TOKEN, PLANETSCALE_SERVICE_TOKEN_NAME, STRIPE_SECRET_KEY_DEV
…and variable: AWS_DEPLOY_ROLE_ARN (the dev role ARN, copied from the dev environment).

  • OPENVSX_TOKEN doesn't exist at repo, org or environment level today, so the OpenVSX step of publish-vscode already fails.
  • AUR_KEY is currently an org secret. Move it, then remove this repo's access to the org secret.

2. Add to the production environment (secrets): CLOUDFLARE_API_TOKEN, PLANETSCALE_SERVICE_TOKEN, PLANETSCALE_SERVICE_TOKEN_NAME, STRIPE_SECRET_KEY_PROD, SENTRY_AUTH_TOKEN. It already has AWS_DEPLOY_ROLE_ARN. Prefer separate, narrower Cloudflare, PlanetScale and Sentry tokens from the release ones.

3. AWS: add repo:anomalyco/opencode:environment:release to the sub list of IAM role opencode-dev-github-actions-deploy (account 438095551154). It is currently environment:dev only. Remove environment:dev after this PR merges. Don't touch the production role; it stays environment:production.

4. Environment policies:

  • release: keep branches dev and beta and tag vscode-v*. Proposed: add tag v* only if notify-discord.yml is still wanted. It fires on release: released, whose ref is the tag, and v* tags are limited to ci-owners by ruleset. Otherwise delete notify-discord.yml; V2 announces from publish.yml. Don't add production to release, for the reason given above. The existing production environment already covers production deploys.
  • The beta, v1, copilot and staging - packages/docs environments have no branch policy, so any branch can use them. None of them holds secrets today. Delete them, or restrict beta to beta. The dev environment can be deleted once step 3 is done. Its ENTERPRISE_SALES_INBOX_EMAIL secret isn't referenced by any workflow, and neither is the copy in production.
  • The v1 branch is unprotected, and its own publish.yml (ci: gate v1 releases behind the release environment #58899) gates v1 releases on release. Since v1 was removed from the release policy, v1 versioned releases can't get secrets. That is the intended outcome. If v1 releases are still needed, the safe option is a separate v1-release environment with a required reviewer, plus a ruleset that restricts v1 to ci-owners.

5. npm trusted publishers: set environment release on the trusted publisher of every package script/publish.ts publishes: the @opencode/* packages schema, codemode, theme, ai, util, protocol, client, cli and its platform packages, cli-node and its platform packages, plugin, plugin-browser, core, simulation, server, sdk, ui. The opencode-ai packages are published from the v1 branch; see the v1 note above.

6. Azure federated credential: change the subject to repo:anomalyco/opencode:environment:release. Today's jobs run without an environment, so the token subject is repo:anomalyco/opencode:ref:refs/heads/dev (or beta). With an environment it becomes environment:release, so the old credential stops matching on the first run after merge.

7. Repo secrets no workflow on dev references (delete candidates): APPLE_ID, APPLE_PASSWORD, BRENDAN_GITHUB_TOKEN, DISCORD_ISSUES_WEBHOOK_URL, KEYCHAIN_PASSWORD, PORTER_APP_18525_975734319, SIGNPATH_API_KEY, SIGNPATH_ARTIFACT_CONFIGURATION_SLUG, SIGNPATH_ORGANIZATION_ID, SIGNPATH_PROJECT_SLUG, SIGNPATH_SIGNING_POLICY_SLUG.

Merge order

Every reference is a plain secrets.NAME. GitHub resolves environment secrets first, then repo, then org, so until a repo or org secret is deleted the jobs keep finding it. The PR is therefore safe to merge before secrets move, except for three things that must be ready first:

  1. Before merging: do step 3 (AWS trust on environment:release), add the AWS_DEPLOY_ROLE_ARN variable to release (step 1), and add the second Azure federated credential (step 6, keeping the old one). Do steps 5 and 2 too: npm OIDC fails without the environment claim, and production deploys need their secrets once the repo copies are gone. Without step 3, the next dev deploy can't assume the AWS role. Variables don't fall back the way secrets do: AWS_DEPLOY_ROLE_ARN exists only in the dev and production environments.
  2. Merge, then watch one dev push of publish, deploy and deploy-*.
  3. Copy the secrets into release and production (steps 1 and 2).
  4. Delete the repo-level copies and remove repo access to the org AUR_KEY. Only after this step is the goal met: until then, any branch can push a workflow that reads repo secrets directly.
  5. Clean up the old AWS subject, the old Azure credential and the unused environments and secrets (step 4 and step 7).

Known side effect, not fixed here: the GitHub App and rulesets

script/publish.ts pushes refs/tags/vX.Y.Z and HEAD:dev using the OpenCode GitHub App token (opencode-agent, app id 1549082). models-snapshot and nix-hashes also push to dev with it. publish-github-action and release-github-action push latest and github-v* tags with GITHUB_TOKEN.

  • The ruleset "Release tags: ci-owners only" (v*, vscode-v*, github-v*) bypasses only team ci-owners. The app is a bypass actor on "release tags (thdxr only)", but every matching ruleset must pass, so the release tag push is blocked. release-github-action creating github-v* tags is blocked too.
  • The dev ruleset requires a PR and status checks, and only OrganizationAdmin can bypass it. That blocks the app's sync release versions commit and the bot commits from models-snapshot and nix-hashes.

Proposal: add Integration 1549082 as a bypass actor on the "Release tags: ci-owners only" ruleset only. The app's private key lives only in the release environment, so after step 4 only dev/beta jobs can mint that token. For dev, either add the app as a bypass on the dev ruleset (the bot commits are generated files), or have those jobs open PRs. For the github-v* tags, move release-github-action and publish-github-action to the app token with environment: release.

How did you verify your code works?

  • actionlint 1.7 (-ignore runner-label) reports only the existing if: false warning in docs-locale-sync.yml.
  • A Bun YAML scan over every workflow confirmed that every job reading a non-GITHUB_TOKEN secret or needing id-token: write declares an environment.
  • The current settings were read from the GitHub API (environments, policies, rulesets, repo and org secret names) and from the AWS IAM trust policies.
  • Nothing ran end-to-end. The first dev push after merge is the real test.

Checklist

  • I have not included unrelated changes in this PR

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoping the secrets to environments and moving permissions down to the jobs both look right. The production/release split in deploy*.yml is sensible, and the GITHUB_TOKEN reductions hold up: the pushes use the App token, publish-vscode only uploads, and the GHCR push gets packages: write at job level. I only reviewed the YAML; the settings and merge-order steps can't be checked from here.

Public API: unchanged. Only CI workflows and CODEOWNERS change.

Concerns:

  1. Approval gate on every dev run. #58885 and #58898 added release as the place where versioned releases wait for approval, and their comments say dev previews skip that wait. This PR makes release unconditional for publish (version, build-cli, sign-cli-macos, publish), deploy, deploy-files, deploy-www, deploy-changelog and nix-hashes on every dev push. It does the same for the scheduled models-snapshot and stats jobs. If release still has required reviewers, all of those will wait for a human every time. The settings section doesn't say to remove the reviewer, and doing so would drop the manual gate on versioned releases. Either state that the reviewer goes away, or keep a separate approval-gated environment, used conditionally as before, for the versioned-release path (see inline).
  2. CODEOWNERS gaps (inline): packages/desktop/scripts/ and sdks/vscode/script/ also run in release with signing and publishing secrets, but /packages/*/script/ doesn't cover them.

if: github.repository == 'anomalyco/opencode'
# Versioned releases wait for approval in the release environment, which only allows dev.
environment: ${{ (inputs.bump || inputs.version) && 'release' || '' }}
environment: release

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be (inputs.bump || inputs.version) && 'release' || '', and #58885 described release as the step where versioned releases wait for approval. With release unconditional here and on build-cli, sign-cli-macos, publish, the deploy* jobs, nix-hashes, models-snapshot and stats, every dev push and every scheduled run will wait for a reviewer if one is still configured on release. Could the description say whether the required reviewer is being removed? If the manual gate on versioned releases should stay, you could use a second environment holding the same secrets, with the reviewer, and apply it only to version/publish when needs.version.outputs.release is set.

Comment thread .github/CODEOWNERS
# CI, release, and publish scripts
/.github/ @anomalyco/ci-owners
/script/ @anomalyco/ci-owners
/packages/*/script/ @anomalyco/ci-owners

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/packages/*/script/ doesn't match packages/desktop/scripts/, whose prepare.ts and publish.ts run in build-electron and publish with the Apple, Azure and Tauri signing secrets. It also doesn't match sdks/vscode/script/, which runs with VSCE_PAT. packages/desktop/scripts/ still falls under the existing packages/desktop/ rule (Hona and Brendonovich only). If this file is meant to gate every script that runs in release, those two paths need entries too.

@thdxr

thdxr commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

Removed publish-vscode.yml (we no longer publish the VS Code extension). Also deleted the repo-level VSCE_PAT secret and the vscode-v* tag from the release environment, so drop VSCE_PAT and OPENVSX_TOKEN from the secrets-to-move list. Production deploys are out of scope for this pass.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only deletes publish-vscode.yml. My two earlier threads stay open. The approval gate on release hasn't been addressed. Deleting the workflow takes care of the sdks/vscode/script/ half of the CODEOWNERS thread, but packages/desktop/scripts/ still isn't covered.

Public API: unchanged (CI only).

With the workflow gone, nothing publishes the VS Code extension anymore. sdks/vscode/script/release still pushes vscode-v* tags, but those tags no longer trigger anything, and sdks/vscode/script/publish is no longer called. If ending extension releases is intended, those scripts can go too. If not, the extension needs another way to publish. The description also still lists publish-vscode, VSCE_PAT/OPENVSX_TOKEN and the vscode-v* tag in the release policy, so the settings steps should be updated to match.

@thdxr

thdxr commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

Folded into #59046, which includes both commits from this PR (secrets scoped to the release environment, and the VS Code workflow removal) plus the build/publish split.

@thdxr thdxr closed this Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant