Skip to content

fix(ci): harden release scripts and workflow (P0 security)#6017

Open
killagu wants to merge 3 commits into
nextfrom
claude/release-p0-hardening
Open

fix(ci): harden release scripts and workflow (P0 security)#6017
killagu wants to merge 3 commits into
nextfrom
claude/release-p0-hardening

Conversation

@killagu

@killagu killagu commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #6016 (now merged). This is P0 of a release-security hardening design — deliberately scoped to low-risk, code-only changes. No change to the publish set or versions.

Why

A multi-agent security review of the release pipeline surfaced concrete weaknesses. These are the ones fixable without restructuring the workflow:

File Issue Fix
scripts/version.js execSync(\git commit -m "${msg}"`)` interpolates package names into a shell string → injection sink build commit + tag via execFileSync('git', [...]) argv; validate package names
scripts/publish.js typo'd package name would publish a brand-new bogus package reject names failing the npm name grammar before npm publish
scripts/publish.js a prerelease could be published to the latest dist-tag refuse prerelease → latest (last line of defence)
scripts/publish.js isPublished() silently swallowed all errors (network/5xx ⇒ "not published") distinguish a real 404 from indeterminate errors (warn)
scripts/publish.js NPM_CONFIG_LOGLEVEL: verbose on real publish may surface auth headers in logs verbose only on --dry-run
.github/workflows/release.yml branch was a free-form string → any ref releasable; reviewing the workflow ≠ what ships branchnext/master choice + a runtime guard (choice options aren't enforced for API-triggered dispatches) requiring dispatch-ref == release-branch

Deferred to P1 (intentionally not here)

  • npm publish --ignore-scriptsunsafe as-is: @eggjs/egg-bundler has prepublishOnly: npm run build. Belongs with the build/publish job split where dist/ is pre-built + digest-verified.
  • Build/publish job split, GitHub Environment approval gate, replacing the GIT_TOKEN PAT with a GitHub App, npm trusted-publisher pinning, and moving to a GitHub-Release-triggered model.

Verification

  • oxfmt --check + oxlint --type-aware clean; pre-commit hook passes.
  • version.js --dry-run and publish.js --dry-run still process all 79 packages, 0 failures; name validation does not false-trigger.
  • Negative test: the prerelease→latest guard correctly refuses and exits non-zero.
  • release.yml parses; guard step runs first.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Added release workflow safeguards to ensure releases run only from the intended branch.
    • Strengthened npm publish preflight validation with stricter package name checks.
    • Prevented prerelease versions from being published under the latest dist-tag.
    • Improved npm publish verification/error handling and reduced misleading output.
    • Adjusted npm logging to be less verbose during real publishes (while keeping dry-run verbosity).
  • Documentation

    • Added and linked a decision record documenting the hardened secure release pipeline approach.
    • Updated wiki index and changelog entries for the new security guidance.

Low-risk, code-only hardening of the release pipeline (stacked on the npm
publish fix). No change to the publish set or versions.

scripts/version.js:
- build the version commit and tag via `execFileSync('git', [...])` argv
  instead of an interpolated shell string — the commit message embeds package
  names, which was a shell-injection sink
- validate every package name against the npm name grammar before bumping

scripts/publish.js:
- reject any malformed package name before `npm publish` (a typo would publish
  a brand-new bogus package)
- refuse to publish a prerelease version to the `latest` dist-tag (last line of
  defence against a prerelease becoming the default install)
- isPublished(): distinguish a real 404 (quiet, expected) from network/registry
  errors (warn loudly) instead of silently swallowing every failure
- use verbose npm logging only on --dry-run (avoid surfacing auth headers in
  real publish logs)

scripts/utils.js:
- add shared isValidNpmPackageName / assertValidNpmPackageName helpers

.github/workflows/release.yml:
- constrain the `branch` input to a `next`/`master` choice, and add a guard
  step that re-validates the allowlist at runtime (choice options are not
  enforced for API-triggered dispatches) and requires the workflow to be
  dispatched from the very branch it releases (dispatch ref == release branch)

Verified: oxfmt + oxlint clean; version.js and publish.js --dry-run still
process all 79 packages with 0 failures; the prerelease->latest guard trips as
expected; release.yml parses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 28, 2026 15:35

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The release workflow now restricts branch selection and validates dispatch origin, while release scripts validate npm package names, harden version/tag execution, refine publish checks, and update the release decision documentation and wiki index/log.

Changes

Release Hardening

Layer / File(s) Summary
release.yml: branch input constraint and guard step
.github/workflows/release.yml
Changes branch from string to choice with next and master, and adds a guard step that fails when the dispatch ref does not match the requested branch.
npm name validation helpers in utils.js
scripts/utils.js
Adds NPM_NAME_RE and exports isValidNpmPackageName and assertValidNpmPackageName.
version.js: name validation and execFileSync for git
scripts/version.js
Imports execFileSync and assertValidNpmPackageName, validates each package name before version bump, and replaces shell-interpolated git commit/tag commands with argv-based execFileSync calls.
publish.js: preflight validation and publish guards
scripts/publish.js
Validates package names before publishing, blocks prerelease versions from the latest dist-tag, refines isPublished failures, and limits verbose npm logging to dry runs.
secure release pipeline decision record and wiki updates
wiki/decisions/secure-release-pipeline.md, wiki/index.md, wiki/log.md
Adds the secure release pipeline decision record, links it from the wiki index, and adds a log entry for the release hardening work.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • eggjs/egg#5810: Modifies the same release/publish flow files, including scripts/publish.js and the release workflow path.

Suggested reviewers

  • elrrrrrrr

🐇 I hopped by the release gate, ears up and bright,
next and master stayed in their lane just right.
Names got a proper sniff, no sneaky tags in sight,
And git rode argv with a safer little bite.
The burrow is tidy, the moon is looking new,
With a hop, a check, and a rabbit-approved queue.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: CI release workflow and scripts were hardened for security.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/release-p0-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: ba786ba
Status: ✅  Deploy successful!
Preview URL: https://9b797cc6.egg-v3.pages.dev
Branch Preview URL: https://claude-release-p0-hardening.egg-v3.pages.dev

View logs

@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.94%. Comparing base (3efb994) to head (ba786ba).
⚠️ Report is 1 commits behind head on next.

Additional details and impacted files
@@           Coverage Diff           @@
##             next    #6017   +/-   ##
=======================================
  Coverage   81.94%   81.94%           
=======================================
  Files         677      677           
  Lines       20652    20652           
  Branches     4100     4100           
=======================================
  Hits        16923    16923           
  Misses       3215     3215           
  Partials      514      514           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@socket-security

socket-security Bot commented Jun 28, 2026

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces several security and safety improvements to the release scripts. It adds validation for npm package names to prevent typos or malicious names, blocks publishing prerelease versions to the "latest" tag, handles non-404 errors gracefully when checking if a package is published, and switches from execSync to execFileSync for git commands to avoid shell injection. Additionally, it restricts verbose npm logging to dry runs to prevent leaking auth headers. The review feedback suggests explicitly setting NPM_CONFIG_LOGLEVEL to notice during a real publish to ensure any environment-inherited verbose settings do not leak credentials, and safely handling the caught error in isPublished by checking if err is an instance of Error before accessing err.message.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread scripts/publish.js Outdated
Comment on lines +130 to +132
// Verbose logging only on dry-run; on a real publish it can surface auth
// headers in CI logs.
env: { ...process.env, ...(isDryRun ? { NPM_CONFIG_LOGLEVEL: 'verbose' } : {}) },

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.

security-high high

To prevent potential credential leakage in CI logs, it is safer to explicitly override NPM_CONFIG_LOGLEVEL to a non-verbose level (such as notice) when not running a dry run. If the environment already has NPM_CONFIG_LOGLEVEL set to verbose, the current implementation would still inherit and use it, potentially exposing sensitive auth headers.

Suggested change
// Verbose logging only on dry-run; on a real publish it can surface auth
// headers in CI logs.
env: { ...process.env, ...(isDryRun ? { NPM_CONFIG_LOGLEVEL: 'verbose' } : {}) },
// Verbose logging only on dry-run; on a real publish it can surface auth
// headers in CI logs. Force 'notice' on real publish to prevent leaks.
env: { ...process.env, NPM_CONFIG_LOGLEVEL: isDryRun ? 'verbose' : 'notice' },

Comment thread scripts/publish.js Outdated
// warn loudly. We still return false, but npm's version immutability prevents
// an accidental overwrite if a publish is then attempted.
if (!/E404|404 Not Found/i.test(stderr)) {
console.warn(` ⚠️ could not verify ${name}@${version} on npm: ${stderr.split('\n')[0] || err.message}`);

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.

medium

When logging the error, accessing err.message directly can be unsafe if err is not an instance of Error (or is null/undefined). To prevent potential runtime errors and ensure a meaningful error message is always displayed, check if err is an Error instance before accessing message, or use String(err) as a fallback.

      const errMsg = err instanceof Error ? err.message : String(err);
      console.warn(`  ⚠️  could not verify ${name}@${version} on npm: ${stderr.split('\n')[0] || errMsg}`);
References
  1. When creating a new Error from a caught exception, check if the caught value is an Error instance before accessing its message property. Use String(err) as a fallback for non-Error values to ensure a meaningful error message.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: ba786ba
Status: ✅  Deploy successful!
Preview URL: https://0d53f1a0.egg-cci.pages.dev
Branch Preview URL: https://claude-release-p0-hardening.egg-cci.pages.dev

View logs

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/publish.js`:
- Around line 130-132: The publish env setup still inherits a caller-provided
NPM_CONFIG_LOGLEVEL during real publishes, so update the env construction in
publish.js to explicitly override or remove that key when isDryRun is false. Use
the existing isDryRun conditional around the env object to force a non-verbose
loglevel for real publishes, preventing any inherited verbose setting from
leaking into the publish command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71e2c5ce-67bc-407c-8b49-f576fc2385b6

📥 Commits

Reviewing files that changed from the base of the PR and between 3efb994 and b3d0d3c.

📒 Files selected for processing (4)
  • .github/workflows/release.yml
  • scripts/publish.js
  • scripts/utils.js
  • scripts/version.js

Comment thread scripts/publish.js Outdated
- publish.js: force NPM_CONFIG_LOGLEVEL=notice on real publishes instead of
  only conditionally adding verbose for dry-run, so an inherited
  NPM_CONFIG_LOGLEVEL=verbose can't leak auth headers into CI logs
  (CodeRabbit/Gemini)
- publish.js: guard err.message access for non-Error throws in isPublished()
- utils.js: add JSDoc to the new npm-name validation helpers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@killagu

killagu commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 5bc567b:

  • NPM_CONFIG_LOGLEVEL leak (CodeRabbit 🟠 / Gemini 🔴) — valid. The previous form only added verbose for dry-run, so an inherited NPM_CONFIG_LOGLEVEL=verbose would still pass through on a real publish. Now forced: NPM_CONFIG_LOGLEVEL: isDryRun ? 'verbose' : 'notice'.
  • Unsafe err.message in isPublished() (Gemini 🟡) — valid. Now err instanceof Error ? err.message : String(err).
  • Docstring coverage warning — added JSDoc to the new isValidNpmPackageName / assertValidNpmPackageName helpers.

Verified: oxfmt + oxlint clean; publish.js --dry-run still processes all 79 packages with 0 failures.

Decision page capturing the target hardened release pipeline (GitHub-Release-
triggered, Environment-gated, OIDC pinned, build/publish split, GitHub App token)
and the phased migration. P0 of this design landed in the accompanying script /
workflow hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 29, 2026 02:29

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
wiki/decisions/secure-release-pipeline.md (1)

90-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add language specifier to the diagram code block.

The ASCII architecture diagram uses a fenced code block without a language specifier, which triggers markdownlint MD040. Adding text preserves the diagram formatting while satisfying the linter.

-```
+```text
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wiki/decisions/secure-release-pipeline.md` at line 90, The architecture
diagram in the secure release pipeline decision document uses a fenced code
block without a language tag, causing markdownlint MD040. Update the diagram’s
fenced block to use a text specifier so the formatting is preserved while
satisfying the linter; locate the diagram block in the decision document and
adjust the fence accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@wiki/decisions/secure-release-pipeline.md`:
- Line 90: The architecture diagram in the secure release pipeline decision
document uses a fenced code block without a language tag, causing markdownlint
MD040. Update the diagram’s fenced block to use a text specifier so the
formatting is preserved while satisfying the linter; locate the diagram block in
the decision document and adjust the fence accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: adbcefb1-38e0-4dec-9e42-c868f7169f92

📥 Commits

Reviewing files that changed from the base of the PR and between 5bc567b and ba786ba.

📒 Files selected for processing (3)
  • wiki/decisions/secure-release-pipeline.md
  • wiki/index.md
  • wiki/log.md
✅ Files skipped from review due to trivial changes (1)
  • wiki/index.md

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.

2 participants