Skip to content

ci: harden the release pipeline and dependency installs - #307

Merged
raviships merged 9 commits into
mainfrom
ci/harden-release-pipeline
Oct 24, 2026
Merged

raviships merged 9 commits into
mainfrom
ci/harden-release-pipeline

Conversation

@raviships

@raviships raviships commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Hardens the SDK release pipeline and dependency installs against the 2026 npm worm pattern (steal a token, or run inside a job with CI OIDC, then publish malicious versions) and against retargeted GitHub Action tags (the tj-actions/changed-files incident class). Also bumps @edgestore/cli's smol-toml past two DoS advisories.

1. Every action is pinned to a full commit SHA (b6f766c0). A tag like changesets/action@v1 can be moved to new code by whoever controls that repo, and in release.yml that code runs with id-token: write, so it can publish @edgestore/*. Each pin is the commit the old tag or branch points to today, so behavior does not change:

Action Was Pinned to
actions/checkout @v7 3d3c42e5… # v7.0.1
actions/setup-node @v6 24997072… # v6.5.0
pnpm/action-setup @v6 0977fd99… # v6.0.10
changesets/action @v1 (branch) a45c4d59… # v1.9.0
actions/cache @v5 caa29612… # v5.1.0
actions/setup-go @v6 924ae3a1… # v6.5.0

2. Dependabot keeps the pins current (8f264bb1, 314f04cb). It covers the github-actions ecosystem only and runs monthly. It waits 7 days after an action release before proposing it, the same idea as pnpm's minimumReleaseAge. Minor and patch bumps come as one grouped PR. Major bumps (for example changesets/action v2, setup-node v7) each get their own PR, so a release-pipeline change is never buried in a batch.

3. Less to steal or poison in CI (7985dd2f, 668a63aa):

  • persist-credentials: false on checkouts in jobs that never push: CI checks, the CLI smoke matrix, storage tests, and canary. Third-party test and build code in those jobs can no longer read the GITHUB_TOKEN from .git/config. Jobs that push keep the default: release, maintenance, promote-next, and start-rc. (changesets/action also writes its own ~/.netrc from GITHUB_TOKEN, so it doesn't depend on checkout credentials.)
  • The release, maintenance, and canary jobs no longer restore the pnpm store cache. Caches on main are written by CI runs that execute third-party code. A publishing job shouldn't restore something those runs could have tampered with. Cost: a slower install on release runs.

4. pnpm install policies (6e3c24dd, pnpm-workspace.yaml):

  • trustPolicy: no-downgrade fails an install when a package version has weaker publish evidence than an earlier version (trusted publisher > provenance > none). This is what a hijacked maintainer token looks like. pnpm 11 checks both new resolutions and existing lockfile entries, so every pnpm install --frozen-lockfile in CI and release enforces it.
  • blockExoticSubdeps isn't set: pnpm 11 already defaults it to true, so only direct dependencies may come from git or tarball URLs.
  • trustPolicyIgnoreAfter: 43200 (30 days, in minutes): the trust check covers only releases from the last 30 days, which is where hijacked versions appear (they're usually caught and pulled within hours or days). Older legitimate releases with weaker evidence pass without a hand-maintained exclusion list. Today those are semver@5.7.2, semver@6.3.1 (2023 backports) and vite@5.4.21.
  • Removes the minimumReleaseAgeExclude entries for next@16.4.1 and its @next/* packages added in fix(deps): upgrade Next.js to 16.4.1 (October 14 security release) #304. That release is now well past the one-day gate.

5. @edgestore/cli: smol-toml 1.7.0 → 1.9.1 (394fdedc, patch changeset). 1.9.1 is the latest version, published 2026-10-09, so it passes the 1-day release-age gate. It fixes GHSA-7w5x-hrqm-74c2 (a malformed document hangs the parser, fixed in 1.7.1) and GHSA-r4xh-jqrq-34v2 (quadratic-time parse, fixed in 1.9.0). The CLI parses Codex's config.toml with it. The lockfile also deduped Astro's transitive copy to 1.9.1. The three deprecated: lines that appeared for @opentelemetry/* entries are pnpm refreshing registry metadata.

Release job split: evaluated, not done

The release job runs pnpm install, changeset version, and pnpm build (third-party build tools) in the same job that holds id-token: write. Any step in that job can request an OIDC token and exchange it for npm publish tokens for @edgestore/*. Two things already limit this: pnpm runs no dependency lifecycle scripts (allowBuilds is all false), and installs are lockfile-frozen behind the release-age gate and, now, the trust policy.

A real split would look like this:

build    (contents: read)        install, build, pnpm pack → upload tarballs
publish  (id-token: write)       download tarballs → npm publish *.tgz
                                 + recreate tags and GitHub Releases

I left the structure as it is because:

  • changesets/action does version-PR-or-publish in one step. Its publish mode also decides which packages are unpublished, creates git tags, and creates GitHub Releases. A split means replacing that with custom scripts in three jobs (release, maintenance, canary). That isn't a simple change, and it can't be tested before merge.
  • The gain is partial. A compromised build could still put malicious code into the tarballs it hands to the publish job, and those would be published with valid provenance. The split stops token theft and out-of-band publishing, not a poisoned build.
  • The npm trusted publisher config names the workflow file (release.yml), plus an environment only if one is set. Publishing works today with no environment: on the job, so none is configured. A job split inside release.yml keeps working with that config. Adding an environment needs no npm change unless the npm config names it.

A cheaper follow-up gets most of the benefit: split release into a version job without id-token, plus a publish job (needs: version, if: hasChangesets == 'false') that keeps changesets/action publishing and runs in a protected GitHub environment with required reviewers. Most pushes to main would then never hold id-token, and every publish would wait for Ravi's approval.

Evidence

  • SHA pins: each tag was resolved with gh api repos/<owner>/<repo>/git/ref/tags/<tag>, dereferencing annotated tags. All six match. I also resolved the old floating refs (v7, v6, v5, and the changesets/action v1 branch). Each points to the same pinned commit, so pinning changes no code that runs.
  • actionlint v1.7.12 (release binary, checksum verified, shellcheck not installed): .github/workflows/* passes on this branch and on main.
  • dependabot.yml validates against the SchemaStore dependabot-2.0 schema. All workflow YAML parses in strict mode.
  • trustPolicy: with neither exclusions nor an ignore window, pnpm install --lockfile-only fails with ERR_PNPM_TRUST_DOWNGRADE on semver@5.7.2, semver@6.3.1 and vite@5.4.21, so the check runs against the lockfile. With trustPolicyIgnoreAfter: 43200, and the Next.js release-age exclusions removed, it reports ✓ Lockfile passes supply-chain policies (1746 entries) and leaves the lockfile unchanged.
  • CLI: pnpm --filter @edgestore/cli test: 29 files, 390 tests passed. pnpm --filter @edgestore/cli typecheck and pnpm check:dependencies pass.
  • Full suite: no JS/TS changed outside the CLI dependency bump; CI on this PR runs format, lint, typecheck, build, tests, and agent-doc checks, plus the CLI pack smoke on Linux, macOS, and Windows.
  • Not exercised before merge: release.yml itself. The diff there only changes uses: refs to the same commits and removes cache: pnpm from three setup-node steps. The first push to main after merge runs it in version-PR mode. The first publish runs after the next Version Packages PR is merged.

Risk and rollback

Door: Two-way. Revert the PR to restore floating tags, caching, and the old pnpm settings. The CLI change ships in the next release as a patch. Because the @edgestore/* packages are versioned together (fixed), that release bumps all of them.

Blast radius:

  • CI and release workflows: the same action code runs, but release installs are a bit slower without the cache.
  • Every pnpm install (contributors and CI): it now fails if a dependency update brings a release from the last 30 days with weaker publish evidence. Check who published it first, because a real hijack looks exactly like this. If it's legitimate, add an exact-version trustPolicyExclude entry or wait until it's 30 days old. Don't relax the policy.
  • Dependabot will open its first github-actions PRs within a month: one grouped minor/patch PR, plus separate major PRs (setup-node v7, changesets/action v2, cache v6, setup-go v7). Review the changesets/action v2 PR carefully before merging.
  • @edgestore/cli users get a newer TOML parser. The parse API is unchanged.

By: Claude Opus 5.5 High on Claude Code

🤖 Generated with Claude Code

raviships and others added 7 commits October 23, 2026 16:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…talls

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
edgestore-docs Ready Ready Preview Oct 24, 2026 12:56am UTC
example-access-control Ready Ready Preview Oct 24, 2026 12:56am UTC
example-components Ready Ready Preview Oct 24, 2026 12:56am UTC

Request Review

@raviships
raviships marked this pull request as ready for review October 23, 2026 10:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T00:56:40.419409Z 1390f4d Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@raviships

Copy link
Copy Markdown
Collaborator Author

@codex review

The previous review on 314f04c has shown "running" for about an hour without finishing, so this asks for a new one. Nothing has changed since then.

By: Claude Opus 5.5 Extra High on Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 314f04cbe5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@raviships

Copy link
Copy Markdown
Collaborator Author

@codex review

Review round 2 (4212484): removed blockExoticSubdeps: true from pnpm-workspace.yaml, since pnpm 11 already defaults it to true. Nothing else changed.

By: Claude Opus 5.5 Extra High on Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 421248426e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

trustPolicyIgnoreAfter (30 days) applies the trust check to fresh
releases, where hijacked versions appear, so the semver/vite exclusions
are no longer needed. next@16.4.1 is now well past the one-day release
age gate, so its minimumReleaseAgeExclude entries are removed. Also trims
the comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@raviships

Copy link
Copy Markdown
Collaborator Author

@codex review

Review round 3 (1390f4d): replaced the trustPolicyExclude list with trustPolicyIgnoreAfter: 43200, so only releases from the last 30 days are checked. Also removed the expired minimumReleaseAgeExclude entries for Next.js 16.4.1 and shortened the config comments. The lockfile passes the supply-chain check and is unchanged.

By: Claude Opus 5.5 Extra High on Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 1390f4d359

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@raviships
raviships merged commit d67b890 into main Oct 24, 2026
10 checks passed
@raviships
raviships deleted the ci/harden-release-pipeline branch October 24, 2026 02:10
@github-actions github-actions Bot mentioned this pull request Oct 24, 2026

This branch was successfully deployed

3 active deployments
Preview – edgestore-docs — 1390f4d3 Deployed Oct 24, 2026 by vercel[bot]
Preview – example-components — 1390f4d3 Deployed Oct 24, 2026 by vercel[bot]
Preview – example-access-control — 1390f4d3 Deployed Oct 24, 2026 by vercel[bot]
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