ci: publish to npm and ClawHub on merge to main#103
Conversation
Extends release.yml with a `publish` job that fires after the
existing version-bump-and-tag flow. The job is gated by a
`production` GitHub Environment so each version still requires an
explicit human approval before tokens are reachable, and is
restricted to runs whose head commit is the bump commit so the
published artifact matches the GitHub Release tag.
npm publish runs with `--provenance --access public`. The job
declares `id-token: write` so npm can sign via Sigstore using the
GitHub OIDC token — even if NPM_TOKEN later leaks, consumers can
verify the package was built by this workflow on this commit.
ClawHub publish uses `clawhub login --token` + `clawhub package
publish ./openclaw`. CLAWHUB_TOKEN is passed only as job-step env
on the relevant steps; the runner's $HOME is ephemeral, GitHub
auto-masks `secrets.*` in logs, and the step does not echo or pipe
the token anywhere.
Token-leak defenses also include:
- environment-scoped secrets (forks/PRs cannot read them)
- workflow `permissions: contents: read` at top-level, only
`id-token: write` added on the publish job
- `npm pack --dry-run` regression guard in both ci.yml and the
publish job (scripts/pack-check.mjs) — refuses if a future PR
widens the `files` allowlist to include `.npmrc`, `.env`,
`.github/`, secrets/, etc.
- concurrency group `hivemind-publish` so two simultaneous
merges cannot race two publishes of the same version
One-time setup the repo owner must do before this lands:
- Settings → Environments → New environment `production`
+ required reviewer
+ (optional) restrict to `main` branch
- Add environment secrets `NPM_TOKEN` (granular, scoped to
@deeplake/hivemind) and `CLAWHUB_TOKEN`
- Settings → Actions → General → ensure id-token is allowed
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR introduces a security gate that prevents sensitive files (e.g., ChangesNPM Package Tarball Security Gate
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
|
Claude finished @kaghni's task in 3m 44s —— View job ReviewThree significant issues — left as inline comments:
Token-leak defenses, provenance setup, and |
Coverage ReportNo Generated for commit 89000e9. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
scripts/pack-check.mjs (1)
17-22: 💤 Low valueConsider adding error handling for
npm packfailures.If
npm packfails (e.g., invalidpackage.json) or returns unexpected JSON, line 21 will throw an unhandled exception with a cryptic error. Wrapping the execution and parse in a try-catch with a clear error message would improve debuggability.🛡️ Optional: Add error handling
-const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'inherit'], -}); -const entries = JSON.parse(raw)[0].files.map((f) => f.path); +let entries; +try { + const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); + const parsed = JSON.parse(raw); + if (!parsed?.[0]?.files) { + throw new Error('Unexpected npm pack output structure'); + } + entries = parsed[0].files.map((f) => f.path); +} catch (err) { + console.error('pack-check failed to run npm pack:', err.message); + process.exit(1); +}🤖 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/pack-check.mjs` around lines 17 - 22, Wrap the call to execFileSync and the subsequent JSON.parse (the logic that produces raw and entries) in a try-catch so failures from npm pack or malformed JSON are caught; in the catch, surface a clear error using the same runtime logger or throw a new Error that includes context like "npm pack --dry-run failed" and the original error message/stack, and ensure the script exits non-zero instead of letting the unhandled exception from JSON.parse (which happens before entries/ hits are computed) leak; specifically update the block that defines raw, entries and hits (references: execFileSync, raw, entries, FORBIDDEN) to perform the exec and parse inside the try and handle/report errors cleanly..github/workflows/release.yml (1)
209-210: 💤 Low valueConsider pinning the clawhub CLI version.
Installing without a version (
npm install -g clawhub) means future runs may use a different CLI version with potentially breaking changes. Pinning ensures reproducible publishes.♻️ Suggested fix
- name: Install ClawHub CLI - run: npm install -g clawhub + run: npm install -g clawhub@0🤖 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 209 - 210, The workflow step named "Install ClawHub CLI" currently runs `npm install -g clawhub` which can install different CLI versions over time; change this to pin a specific version (e.g., replace the command with `npm install -g clawhub@<VERSION>` or use an input/environment variable like CLAWHUB_VERSION to control the exact release) so the `Install ClawHub CLI` step always installs a reproducible, explicit CLI version.
🤖 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 180-198: The inline Pack-check step contains buggy regexes and
duplicates logic from scripts/pack-check.mjs; replace the entire inline node
script in the "Pack-check (refuse forbidden filenames in tarball)" step with a
call to the canonical script (e.g. run "npm run pack:check" or execute "node
scripts/pack-check.mjs") so the workflow reuses the existing pack:check
implementation and avoids the regex bug and duplication.
---
Nitpick comments:
In @.github/workflows/release.yml:
- Around line 209-210: The workflow step named "Install ClawHub CLI" currently
runs `npm install -g clawhub` which can install different CLI versions over
time; change this to pin a specific version (e.g., replace the command with `npm
install -g clawhub@<VERSION>` or use an input/environment variable like
CLAWHUB_VERSION to control the exact release) so the `Install ClawHub CLI` step
always installs a reproducible, explicit CLI version.
In `@scripts/pack-check.mjs`:
- Around line 17-22: Wrap the call to execFileSync and the subsequent JSON.parse
(the logic that produces raw and entries) in a try-catch so failures from npm
pack or malformed JSON are caught; in the catch, surface a clear error using the
same runtime logger or throw a new Error that includes context like "npm pack
--dry-run failed" and the original error message/stack, and ensure the script
exits non-zero instead of letting the unhandled exception from JSON.parse (which
happens before entries/ hits are computed) leak; specifically update the block
that defines raw, entries and hits (references: execFileSync, raw, entries,
FORBIDDEN) to perform the exec and parse inside the try and handle/report errors
cleanly.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a66cf427-0f0f-45bc-b5a7-b9520417d356
📒 Files selected for processing (4)
.github/workflows/ci.yml.github/workflows/release.ymlpackage.jsonscripts/pack-check.mjs
| - name: Pack-check (refuse forbidden filenames in tarball) | ||
| run: | | ||
| npm pack --dry-run --json > /tmp/pack.json | ||
| node -e " | ||
| const entries = JSON.parse(require('fs').readFileSync('/tmp/pack.json','utf8'))[0].files.map(f => f.path); | ||
| const forbidden = entries.filter(p => | ||
| /(^|\/)\.npmrc$/.test(p) || | ||
| /(^|\/)\.env(\$|\.)/.test(p) || | ||
| /(^|\/)secrets?(\/|\$)/.test(p) || | ||
| /(^|\/)\.github(\/|\$)/.test(p) || | ||
| /(^|\/)\.git(\/|\$)/.test(p) | ||
| ); | ||
| if (forbidden.length) { | ||
| console.error('Refusing to publish — forbidden filenames in tarball:'); | ||
| for (const f of forbidden) console.error(' ' + f); | ||
| process.exit(1); | ||
| } | ||
| console.log('Pack-check OK — ' + entries.length + ' files, no forbidden patterns'); | ||
| " |
There was a problem hiding this comment.
Critical: Inline pack-check has broken regex patterns and duplicates logic.
Two issues here:
-
Regex bug: The inline patterns use
\$(literal dollar sign) instead of$(end-of-string anchor). For example, line 187's/(^|\/)\.env(\$|\.)/.test(p)won't match.envfiles correctly because\$matches a literal$character, not end-of-string. Compare toscripts/pack-check.mjsline 11:/(^|\/)\.env($|\.)/which uses the correct$. -
Code duplication: This inline logic duplicates
scripts/pack-check.mjs, creating maintenance risk. If forbidden patterns are updated in the script, this inline version will diverge.
Simply call the existing script:
🐛 Proposed fix: Use the existing pack:check script
- name: Pack-check (refuse forbidden filenames in tarball)
- run: |
- npm pack --dry-run --json > /tmp/pack.json
- node -e "
- const entries = JSON.parse(require('fs').readFileSync('/tmp/pack.json','utf8'))[0].files.map(f => f.path);
- const forbidden = entries.filter(p =>
- /(^|\/)\.npmrc$/.test(p) ||
- /(^|\/)\.env(\$|\.)/.test(p) ||
- /(^|\/)secrets?(\/|\$)/.test(p) ||
- /(^|\/)\.github(\/|\$)/.test(p) ||
- /(^|\/)\.git(\/|\$)/.test(p)
- );
- if (forbidden.length) {
- console.error('Refusing to publish — forbidden filenames in tarball:');
- for (const f of forbidden) console.error(' ' + f);
- process.exit(1);
- }
- console.log('Pack-check OK — ' + entries.length + ' files, no forbidden patterns');
- "
+ run: npm run pack:check🤖 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 180 - 198, The inline Pack-check
step contains buggy regexes and duplicates logic from scripts/pack-check.mjs;
replace the entire inline node script in the "Pack-check (refuse forbidden
filenames in tarball)" step with a call to the canonical script (e.g. run "npm
run pack:check" or execute "node scripts/pack-check.mjs") so the workflow reuses
the existing pack:check implementation and avoids the regex bug and duplication.
| # forks and unprotected branches cannot reach them. | ||
| name: Publish to npm + ClawHub | ||
| needs: release | ||
| if: "${{ startsWith(github.event.head_commit.message, 'chore: bump version') }}" |
There was a problem hiding this comment.
This if: will never match — the publish job will never run.
The existing release job (line 17) pushes the bump commit using secrets.GITHUB_TOKEN, and per GitHub's loop-prevention rule pushes made with GITHUB_TOKEN do not trigger new workflow runs. So there is no second workflow run whose head_commit.message starts with chore: bump version — your PR description's "bump-commit run: GitHub Release ──► publish job (new)" arrow is the path that doesn't actually fire here.
Fix: gate publish on a job output from release instead, and have release set that output only when it actually created a Release (e.g. inside steps.check.outputs.exists == 'false'):
| if: "${{ startsWith(github.event.head_commit.message, 'chore: bump version') }}" | |
| if: needs.release.outputs.published == 'true' |
(also add outputs: to the release job declaration and set published: 'true' from the Create-Release step). Alternative: use a PAT or GitHub App token on the actions/checkout at line 17 so the bump push can re-trigger the workflow — but the job-output approach avoids needing a second token entirely.
| # Pull the latest main HEAD: the push event's SHA may point at | ||
| # the pre-bump commit, but the `release` job above just pushed | ||
| # the bump commit — that's the version we want to publish. | ||
| ref: main |
There was a problem hiding this comment.
Race window: ref: main plus the production environment gate means the reviewer approves a specific commit, but during the approval pause main can advance — and on resume this checkout pulls the new tip and publishes a version the reviewer never saw. Pin to the trigger SHA instead (which, on the bump-commit retrigger, is the bump commit you want anyway):
| ref: main | |
| ref: ${{ github.sha }} |
| - name: Pack-check (refuse forbidden filenames in tarball) | ||
| run: | | ||
| npm pack --dry-run --json > /tmp/pack.json | ||
| node -e " | ||
| const entries = JSON.parse(require('fs').readFileSync('/tmp/pack.json','utf8'))[0].files.map(f => f.path); | ||
| const forbidden = entries.filter(p => | ||
| /(^|\/)\.npmrc$/.test(p) || | ||
| /(^|\/)\.env(\$|\.)/.test(p) || | ||
| /(^|\/)secrets?(\/|\$)/.test(p) || | ||
| /(^|\/)\.github(\/|\$)/.test(p) || | ||
| /(^|\/)\.git(\/|\$)/.test(p) | ||
| ); | ||
| if (forbidden.length) { | ||
| console.error('Refusing to publish — forbidden filenames in tarball:'); | ||
| for (const f of forbidden) console.error(' ' + f); | ||
| process.exit(1); | ||
| } | ||
| console.log('Pack-check OK — ' + entries.length + ' files, no forbidden patterns'); | ||
| " |
There was a problem hiding this comment.
Pack-check is duplicated — this inline node block re-implements scripts/pack-check.mjs (already wired as npm run pack:check in ci.yml). Two copies of the forbidden-filename list will drift; a future PR adding a pattern to one and missing the other defeats the regression-guard. Call the script:
| - name: Pack-check (refuse forbidden filenames in tarball) | |
| run: | | |
| npm pack --dry-run --json > /tmp/pack.json | |
| node -e " | |
| const entries = JSON.parse(require('fs').readFileSync('/tmp/pack.json','utf8'))[0].files.map(f => f.path); | |
| const forbidden = entries.filter(p => | |
| /(^|\/)\.npmrc$/.test(p) || | |
| /(^|\/)\.env(\$|\.)/.test(p) || | |
| /(^|\/)secrets?(\/|\$)/.test(p) || | |
| /(^|\/)\.github(\/|\$)/.test(p) || | |
| /(^|\/)\.git(\/|\$)/.test(p) | |
| ); | |
| if (forbidden.length) { | |
| console.error('Refusing to publish — forbidden filenames in tarball:'); | |
| for (const f of forbidden) console.error(' ' + f); | |
| process.exit(1); | |
| } | |
| console.log('Pack-check OK — ' + entries.length + ' files, no forbidden patterns'); | |
| " | |
| - name: Pack-check (refuse forbidden filenames in tarball) | |
| run: npm run pack:check |
…check Fixes three review comments on PR #103: 1. publish job would never fire — pushes made by the release job use the default GITHUB_TOKEN, which GitHub's loop-prevention rule says does NOT retrigger workflows. The previous gate (head_commit.message starts with 'chore: bump version') therefore never matched on automated runs. Replace with a job output: release sets outputs.published='true' only when Create GitHub Release ran; publish gates on needs.release.outputs.published=='true' and runs in the same workflow. 2. ref: main on the publish checkout was racy — during the production environment-gate pause, main can advance, so an approval at commit A could resume and publish commit B. Capture the bump SHA via 'git rev-parse HEAD' in a new release-job step and pin the checkout to needs.release.outputs.sha. 3. Inline pack-check in the publish job duplicated scripts/pack-check.mjs and its regex used \$ (literal dollar) instead of $ (end anchor), breaking .env matching. Replaced with 'npm run pack:check' which reuses the canonical script wired into ci.yml.
Exercises the full publish pipeline without uploading: validates NPM_TOKEN via 'npm whoami', runs 'npm publish --dry-run --provenance' (catches 2FA-required classic tokens via EOTP before the real run), validates CLAWHUB_TOKEN via 'clawhub whoami', and runs 'clawhub package publish ./openclaw --dry-run'. Uses the same 'production' environment as the real publish job so a successful smoke-test also proves the environment + secrets are wired correctly. No bump, no Release, no public artifact.
Summary
publishjob torelease.ymlthat fires after the existing version-bump flow and publishes@deeplake/hivemindto npm and the openclaw bundle to ClawHubproductionGitHub Environment with required reviewer — each version still gets explicit per-publish approval (so this CD does not bypass the existing manual gate, just routes it through GitHub's UI)--provenance --access public(Sigstore via GitHub OIDC), so even ifNPM_TOKENlater leaks, consumers can verify the package was built by this workflow on this commitnpm pack --dry-runregression guard (scripts/pack-check.mjs, wired into bothci.ymland the publish job) that hard-fails if a PR widens thefilesallowlist to include.npmrc,.env,.github/,secrets/, etc.Token-leak defenses, layered
NPM_TOKENandCLAWHUB_TOKENare stored on theproductionenvironment, not the repo. PRs from forks and unprotected branches cannot read them; only the gated publish job can.contents: readat top, only the publish job addsid-token: write(needed for OIDC provenance). Nopull-requests, no broadercontents: write..npmrc—actions/setup-node@v4writes a temporary~/.npmrcreferencing${NODE_AUTH_TOKEN}; passed only on thenpm publishstep'senv:. The runner's$HOMEis discarded when the job ends.NPM_TOKENleaks.npm view @deeplake/hivemind --json | jq .dist.attestationswill show the signature.${{ secrets.* }}is***in logs. We don'tset -xorechoenv on token-bearing steps.hivemind-publishgroup prevents two merges from racing two publishes of the same version.scripts/pack-check.mjsruns in PR CI, so a brokenfilesarray trips on PR review, never on a release run where tokens are reachable.Required one-time setup before merging
The repo owner must do these in GitHub UI before the first publish run reaches the gate:
productionmainNPM_TOKEN— recommended: granular automation token, scope@deeplake/hivemindonly,Read and write, 90-day expiry. Created at https://www.npmjs.com/settings/~/tokens/granular-access-tokens.CLAWHUB_TOKEN— generate viaclawhub loginand pull the token, or via clawhub.ai web UI.id-token: writeis allowed (some org policies disable it by default).How it fires after merge
The publish job's
if:requiresstartsWith(head_commit.message, 'chore: bump version'), so it runs exactly once per released version.Test plan
npm view @deeplake/hivemind versionmatches the GitHub Release tagnpm view @deeplake/hivemind --json | jq .dist.attestationsshows a sigstore attestation (Provenance badge on npmjs.com)clawhub:hivemindis installable andverify-install.shpasses against itgh run view --log <run-id> | grep -E 'npm_[A-Za-z0-9_]{20,}|CLAWHUB_[A-Za-z0-9]{20,}'returns nothingConfidence
Confidence: 80%, because the npm side is grounded in real
package.json+release.yml+ standard provenance/Environments practice, and the ClawHub side is grounded inclawhub --helpoutput (clawhub login --token+clawhub package publish <folder>are real subcommands of the publishedclawhubv0.12.3 package).Untested:
clawhub package publish ./openclawauto-detects the package namehivemindfromopenclaw/openclaw.plugin.json'sidfield, or whether--name hivemindneeds to be passed explicitly--manual-override-reason "Automated CD"to be added — first real publish will surface this if soid-token: writeis allowed by the org's current Actions permissions (rules 3 above), since policy may differ per reporef: main, but worth visually confirming on the first real run before approving the gate@deeplake/hivemind) should be wired into the publish job before npm publish; currently the human reviewer at the gate is the E2E checkpointSummary by CodeRabbit