Skip to content

ci: harden npm release publishing#1918

Open
Tantanz20020918 wants to merge 2 commits into
mainfrom
ci/npm-secure-publishing
Open

ci: harden npm release publishing#1918
Tantanz20020918 wants to merge 2 commits into
mainfrom
ci/npm-secure-publishing

Conversation

@Tantanz20020918

@Tantanz20020918 Tantanz20020918 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrate npm releases from a long-lived token to trusted publishing, and add fail-closed validation before release side effects.

Changes

  • Add package, lockfile, tag, and latest-main preflight checks
  • Publish through Node 22, pinned npm, OIDC, and the protected npm-production environment
  • Verify run-scoped release assets, checksums, and npm package integrity
  • Fail installation when checksums are missing or invalid
  • Add release workflow and negative-path regression tests

Test Plan

  • make unit-test passed
  • make script-test passed (184/184)
  • validate passed
  • local-eval passed (E2E 0/0; no CLI behavior changes, skillave N/A)
  • acceptance-reviewer passed (4/4 release scenarios, 58/58 assertions)
  • go vet ./..., gofmt -l ., go mod tidy, and incremental lint passed
  • manual verification: node scripts/release-preflight.js, bash -n scripts/tag-release.sh, and git diff --check

Related Issues

N/A

Summary by CodeRabbit

  • New Features

    • Added release preflight checks for stable, synchronized package versions and optional vX.Y.Z tag validation.
    • Added release asset verification for archive filename matrices and checksums.txt SHA-256 integrity.
    • Updated the release process to verify assets via workflow artifacts before GitHub Release and npm publishing.
    • Strengthened release tagging to validate branch/commit state and tag alignment.
  • Bug Fixes

    • Installation now hard-fails if checksums.txt is missing or if expected SHA-256 digests are missing/invalid.
  • Tests

    • Expanded script-test coverage and added workflow contract tests for both production and preview isolation.

@Tantanz20020918 Tantanz20020918 added size/L Large or sensitive change across domains or core paths domain/security Security: validation, injection prevention, vulnerability mitigation. labels Jul 16, 2026
@github-actions github-actions Bot added size/XL Architecture-level or global-impact change and removed domain/security Security: validation, injection prevention, vulnerability mitigation. size/L Large or sensitive change across domains or core paths labels Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release pipeline validates stable metadata, verifies generated assets, publishes npm artifacts idempotently, and creates or verifies matching GitHub Release assets. Tagging gains stricter repository checks, while installer checksum validation fails closed on missing or malformed data.

Changes

Release integrity and publishing

Layer / File(s) Summary
Release metadata and tag gates
package.json, scripts/release-preflight.js, scripts/tag-release.sh, scripts/release-preflight.test.js
Adds stable version and tag validation, clean main checks, exact commit matching, and local/remote tag verification.
Release asset verification
scripts/verify-release-assets.js, scripts/verify-release-assets.test.js
Validates archive names, checksum manifests, archive sets, file types, and SHA-256 digests through module and CLI tests.
Artifact-driven release workflow
.github/workflows/release.yml, scripts/release-workflow.test.js, Makefile
Builds and verifies run-scoped artifacts, performs idempotent npm publishing, verifies GitHub Release assets, serializes runs, and expands the test target.
Fail-closed installer checksums
scripts/install.js, scripts/install.test.js
Rejects missing checksum manifests and missing, malformed, or invalid SHA-256 expectations with security errors.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: feature

Suggested reviewers: liangshuo-1

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseTrigger
  participant Preflight
  participant GoReleaser
  participant AssetVerifier
  participant PublishRelease
  participant GitHubRelease
  participant NpmRegistry
  ReleaseTrigger->>Preflight: validate tag and package metadata
  Preflight->>GoReleaser: permit non-publishing build
  GoReleaser->>AssetVerifier: provide release-assets artifact
  AssetVerifier->>PublishRelease: pass verified artifact
  PublishRelease->>GitHubRelease: create or verify release and compare assets
  PublishRelease->>NpmRegistry: compare integrity and publish if needed
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly reflects the main change: hardening npm release publishing.
Description check ✅ Passed The description follows the required template and covers summary, changes, test plan, and related issues.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/npm-secure-publishing

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.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.96%. Comparing base (be1f362) to head (15e4175).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1918   +/-   ##
=======================================
  Coverage   74.96%   74.96%           
=======================================
  Files         892      892           
  Lines       94058    94058           
=======================================
  Hits        70506    70506           
  Misses      18139    18139           
  Partials     5413     5413           

☔ 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.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@15e41759869afaee7f3e045947c2766923df3dbc

🧩 Skill update

npx skills add larksuite/cli#ci/npm-secure-publishing -y -g

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR Quality Summary

CI did not complete successfully. Use the failed check links below to decide whether this PR needs a code change or a rerun.

Failed checks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
scripts/release-preflight.test.js (1)

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

Exercise each release gate behaviorally, not only through source-text ordering.

Add subprocess failure cases for a non-main branch, staged/unstaged metadata, and HEAD != FETCH_HEAD, asserting that no tag query, creation, or push occurs. The fake diff command will need configurable exit statuses.

🤖 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 `@scripts/release-preflight.test.js` around lines 326 - 355, Extend the release
preflight tests around the existing tag-release subprocess harness to execute
failure cases for a non-main branch, staged or unstaged package metadata, and
HEAD differing from FETCH_HEAD. Make the fake diff command support configurable
exit statuses, assert each gate rejects the release, and verify no tag query,
creation, or push command is reached in every case.
🤖 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 @.github/workflows/release.yml:
- Around line 78-83: Update the release artifact naming in the upload and
corresponding download steps to use only github.run_id, removing
github.run_attempt so reruns reference the same artifact. Apply this
consistently to the artifact references near the upload/download steps at the
indicated locations, and add overwrite: true to the upload configuration if the
producing job can rerun.

In `@scripts/release-preflight.js`:
- Line 8: Update the version validation around STABLE_VERSION_PATTERN and the
release:check flow to reject any major, minor, or patch component exceeding
Number.MAX_SAFE_INTEGER, matching node-semver behavior. Apply the same
semver-compatible bound check to package fields and the generated --tag vX.Y.Z
value while preserving acceptance of valid stable versions.

In `@scripts/tag-release.sh`:
- Line 15: Bind all Git operations in the release script, including
CURRENT_BRANCH and the commands in the affected preflight, tagging, and push
flow, to the repository root derived from SCRIPT_DIR rather than the caller’s
working directory. Preserve existing release behavior while ensuring absolute
invocations and non-root working directories operate on the script’s checkout.
Add a regression test that invokes the script from a non-root cwd and verifies
Git operations target that repository.

---

Nitpick comments:
In `@scripts/release-preflight.test.js`:
- Around line 326-355: Extend the release preflight tests around the existing
tag-release subprocess harness to execute failure cases for a non-main branch,
staged or unstaged package metadata, and HEAD differing from FETCH_HEAD. Make
the fake diff command support configurable exit statuses, assert each gate
rejects the release, and verify no tag query, creation, or push command is
reached in every case.
🪄 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: f45e910c-1f08-4585-b41d-4f892df52801

📥 Commits

Reviewing files that changed from the base of the PR and between 6558657 and 12b7f7a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • .github/workflows/release.yml
  • Makefile
  • package.json
  • scripts/install.js
  • scripts/install.test.js
  • scripts/release-preflight.js
  • scripts/release-preflight.test.js
  • scripts/release-workflow.test.js
  • scripts/tag-release.sh
  • scripts/verify-release-assets.js
  • scripts/verify-release-assets.test.js

Comment on lines +78 to +83
- name: Upload build artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-assets-${{ github.run_id }}-${{ github.run_attempt }}
path: release-assets/
if-no-files-found: error

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
workflow="$(fd -a '^release\.yml$' .github/workflows | head -n1)"
rg -n -C2 'github\.run_attempt|release-assets-' "$workflow"

Repository: larksuite/cli

Length of output: 1103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workflow=".github/workflows/release.yml"

echo "== workflow outline =="
ast-grep outline "$workflow" --view expanded || true

echo
echo "== relevant excerpt =="
sed -n '1,260p' "$workflow" | nl -ba | sed -n '1,260p'

echo
echo "== artifact references =="
rg -n 'release-assets-|upload-artifact|download-artifact|run_attempt|run_id' "$workflow"

Repository: larksuite/cli

Length of output: 283


🌐 Web query:

GitHub Actions rerun selected jobs successful upstream jobs do not rerun github.run_attempt increments artifact upload download rerun workflow jobs

💡 Result:

When you perform a partial re-run in GitHub Actions—by selecting either "re-run failed jobs" or specific jobs—the workflow execution behavior is as follows: Upstream/Successful Jobs: Jobs that previously completed successfully are not re-run [1]. They remain in their finished state, and the workflow preserves their outputs and results [1]. Only the selected failed jobs and their dependent jobs are re-executed [2][3]. Run Attempt Increment: Every time a re-run is triggered (regardless of whether you re-run all jobs, failed jobs, or specific jobs), it initiates a new attempt for the overall workflow run [4]. The github.run_attempt context variable increments by 1 for this new attempt [5][6]. Artifacts: GitHub Actions maintains a single set of artifacts for a workflow run [7][8]. When a re-run occurs, artifacts from the new attempt are associated with the run, and existing artifacts from previous attempts are overwritten if they share the same name [7][8]. Because a workflow run only ever holds one set of artifacts at a time, you cannot independently maintain artifacts from both the original run and a re-run if they have overlapping names [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workflow=".github/workflows/release.yml"

sed -n '1,240p' "$workflow" | cat -n | sed -n '1,240p'

Repository: larksuite/cli

Length of output: 9488


Keep the artifact name stable across reruns. github.run_attempt changes on reruns, so retrying a failed downstream job looks for release-assets-${{ github.run_id }}-${{ github.run_attempt }} even though the upload step wasn’t rerun. Switch the upload/download names at 81, 96, 124, and 179 to github.run_id; add overwrite: true on upload if the producer itself can rerun.

🤖 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 @.github/workflows/release.yml around lines 78 - 83, Update the release
artifact naming in the upload and corresponding download steps to use only
github.run_id, removing github.run_attempt so reruns reference the same
artifact. Apply this consistently to the artifact references near the
upload/download steps at the indicated locations, and add overwrite: true to the
upload configuration if the producing job can rerun.

const fs = require("node:fs");
const path = require("node:path");

const STABLE_VERSION_PATTERN = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 --hidden --glob '!node_modules/**' \
  'npm(@[0-9]| install .*npm)|"semver"\s*:' \
  package.json package-lock.json .github Makefile 2>/dev/null || true

curl -fsSL https://raw.githubusercontent.com/npm/node-semver/main/classes/semver.js |
  rg 'MAX_SAFE_INTEGER|Invalid (major|minor|patch) version'

Repository: larksuite/cli

Length of output: 905


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline scripts/release-preflight.js --view expanded || true
printf '\n---\n'
cat -n scripts/release-preflight.js | sed -n '1,220p'
printf '\n---\n'
rg -n "release-preflight|STABLE_VERSION_PATTERN|tag.slice\\(1\\)|version" .github scripts -g '!node_modules/**'

Repository: larksuite/cli

Length of output: 39460


Reject versions with numeric components above node-semver’s limit.
This regex still accepts values like 9007199254740992.0.0; npm/node-semver rejects components above Number.MAX_SAFE_INTEGER, so release:check can pass and the publish step fails later. Use a semver-compatible bound check for the package fields and --tag vX.Y.Z.

🤖 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 `@scripts/release-preflight.js` at line 8, Update the version validation around
STABLE_VERSION_PATTERN and the release:check flow to reject any major, minor, or
patch component exceeding Number.MAX_SAFE_INTEGER, matching node-semver
behavior. Apply the same semver-compatible bound check to package fields and the
generated --tag vX.Y.Z value while preserving acceptance of valid stable
versions.

Comment thread scripts/tag-release.sh
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists locally, skipping."
exit 0
CURRENT_BRANCH=$(git branch --show-current)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bind Git operations to the repository containing this script.

Preflight reads ${SCRIPT_DIR}/.., but Git uses the caller's working directory. An absolute invocation from another checkout could tag and push that checkout's origin; invocation from a subdirectory can also miss dirty root metadata.

Proposed fix
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+cd "${REPO_ROOT}"

Add a regression test that invokes the script with a non-root cwd.

Also applies to: 21-85

🤖 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 `@scripts/tag-release.sh` at line 15, Bind all Git operations in the release
script, including CURRENT_BRANCH and the commands in the affected preflight,
tagging, and push flow, to the repository root derived from SCRIPT_DIR rather
than the caller’s working directory. Preserve existing release behavior while
ensuring absolute invocations and non-root working directories operate on the
script’s checkout. Add a regression test that invokes the script from a non-root
cwd and verifies Git operations target that repository.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 @.github/workflows/release.yml:
- Around line 159-164: Update the existing release-state parsing in
.github/workflows/release.yml at lines 159-164 to validate both draft and
prerelease fields, rejecting the release when either state is true while
preserving the current error-and-exit behavior. Add a contract assertion in
scripts/release-workflow.test.js at lines 166-177 covering rejection of an
existing prerelease release.
🪄 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: 2ebb1004-d0d3-404d-8bba-cc086ce174f0

📥 Commits

Reviewing files that changed from the base of the PR and between 12b7f7a and 15e4175.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • scripts/release-workflow.test.js

Comment on lines +159 to +164
IS_DRAFT="$(node -e 'const r=require(process.argv[1]); if(typeof r.draft!=="'"'boolean"'"') process.exit(2); process.stdout.write(String(r.draft))' "$RELEASE_JSON")"
if [[ "$IS_DRAFT" == "true" ]]; then
echo "An existing mutable Draft Release for ${TAG} is not trusted; remove it and rerun." >&2
exit 1
fi
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject existing prerelease releases for stable versions.

The verifier requires stable X.Y.Z metadata, but an existing release with prerelease: true currently passes and can be paired with a stable npm publication.

  • .github/workflows/release.yml#L159-L164: parse and reject both draft and prerelease release states.
  • scripts/release-workflow.test.js#L166-L177: add a contract assertion covering prerelease rejection.
📍 Affects 2 files
  • .github/workflows/release.yml#L159-L164 (this comment)
  • scripts/release-workflow.test.js#L166-L177
🤖 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 @.github/workflows/release.yml around lines 159 - 164, Update the existing
release-state parsing in .github/workflows/release.yml at lines 159-164 to
validate both draft and prerelease fields, rejecting the release when either
state is true while preserving the current error-and-exit behavior. Add a
contract assertion in scripts/release-workflow.test.js at lines 166-177 covering
rejection of an existing prerelease release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant