-
Notifications
You must be signed in to change notification settings - Fork 107
361 lines (335 loc) · 16.8 KB
/
Copy pathrelease.yaml
File metadata and controls
361 lines (335 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
name: Release
on:
push:
branches: [main]
permissions:
contents: write
id-token: write
jobs:
release:
name: Auto-bump version and create release
runs-on: ubuntu-latest
concurrency:
# Serialize releases. The 2-commit release pattern (release commit
# with bundles tracked → cleanup commit that untracks + bumps the
# marketplace.json sha) is not safe to interleave: a second release
# firing while the first is between its two commits would either
# see a transiently-broken main or race on the push.
group: hivemind-release
cancel-in-progress: false
outputs:
# Set only when the Create GitHub Release step actually runs.
# The downstream `publish` job gates on this so it fires once
# per real release — and crucially fires in the SAME workflow
# run as the bump+release, because pushes made with the default
# GITHUB_TOKEN do not retrigger workflows.
published: ${{ steps.set_outputs.outputs.published }}
sha: ${{ steps.set_outputs.outputs.sha }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v6.0.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
# Pinned to 22 to match ci.yaml. Under Node 24+ the tree-sitter
# 0.21 prebuild ABI no longer matches and the install falls back
# to a from-source compile that fails (binding.gyp does not
# request the C++20 standard Node 24's V8 headers now require).
# The ensure-tree-sitter.mjs postinstall heal cannot fully
# recover once npm drops the optional dep, so tsc later fails
# with "Cannot find module 'tree-sitter'". Aligning with CI's
# Node version makes the prebuild match and avoids the issue
# entirely. Engines field allows >=22; bundles produced by
# Node 22 stay forward-compatible.
node-version: 22
- name: Check if version was already bumped in this push
id: check_bump
run: |
# Query CURRENT origin/main HEAD, not the event checkout. On a
# workflow re-run (e.g. manually triggered after a transient
# failure), the event-supplied SHA may be stale: another release
# may have advanced `main` between the original trigger and the
# re-run. Without fetching, the regex would consult an older
# commit message and the guard could miss an already-created
# release pair, double-bumping the version.
git fetch origin main --depth=1
COMMIT_MSG=$(git log -1 --pretty=%B origin/main)
# Either commit in the 2-commit release pair counts as "already bumped",
# so a re-run of this workflow doesn't double-bump. The chore commit is
# what GITHUB_TOKEN pushes back to main; the release commit precedes it.
if echo "$COMMIT_MSG" | grep -qE "^(chore: untrack bundles|release: v)"; then
echo "skip=true" >> "$GITHUB_OUTPUT"
# CRITICAL: on a re-run after the bump pair already landed, the
# event checkout is the STALE pre-bump SHA. Hard-reset to current
# origin/main so the unconditional steps below (Extract version,
# Check if release exists, Set outputs) read the post-bump state.
# Without this, a re-run can hand the publish job a stale SHA →
# publishes the wrong commit / wrong version. Caught by codex review.
git reset --hard origin/main
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Bump version + build
if: steps.check_bump.outputs.skip == 'false'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
npm version patch --no-git-tag-version
# --ignore-scripts prevents npm ci from firing the `prepare` hook
# (husky && npm run build), which would double-build and fail if
# tree-sitter bindings aren't ready yet. postinstall (the
# tree-sitter heal) is run explicitly below instead.
npm ci --ignore-scripts
node scripts/ensure-tree-sitter.mjs
# `npm run build` runs `prebuild` (scripts/sync-versions.mjs) → tsc →
# esbuild. sync-versions propagates the new version into the 6
# tracked manifests; esbuild bakes it into bundles via `define`.
npm run build
- name: Commit 1 — release commit with bundles force-tracked
if: steps.check_bump.outputs.skip == 'false'
id: release_commit
run: |
VERSION=$(node -p "require('./package.json').version")
# Stage in two steps to avoid sweeping in gitignored junk:
# 1) `git add -A` → tracked modifications + non-gitignored
# untracked (e.g. package.json version
# bump, package-lock.json refresh,
# sync-versions edits to 6 manifests).
# 2) `git add -f <dirs>` → force-stage ONLY the 7 bundle dirs that
# are gitignored. This is what marketplace.json's
# sha-pinned source resolves to.
# Earlier draft used `git add -fA` which is "force AND all" —
# overrides gitignore for EVERYTHING, sweeping in node_modules/
# (~16k files), dist/, and .husky/_/. Either OOMs the husky
# pre-commit tsc check or balloons the commit beyond reason.
git add -A
git add -f \
bundle \
harnesses/claude-code/bundle \
harnesses/codex/bundle \
harnesses/cursor/bundle \
harnesses/hermes/bundle \
mcp/bundle \
harnesses/pi/bundle
git commit -m "release: v$VERSION"
RELEASE_SHA=$(git rev-parse HEAD)
echo "release_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
echo "RELEASE_SHA=$RELEASE_SHA" >> "$GITHUB_ENV"
- name: Commit 2 — untrack bundles + point marketplace at release sha
if: steps.check_bump.outputs.skip == 'false'
run: |
# Remove the 7 bundle dirs from the index but keep the files in the
# working tree (publish job in the same workflow run still needs
# them on disk for npm publish).
git rm --cached -r \
bundle \
harnesses/claude-code/bundle \
harnesses/codex/bundle \
harnesses/cursor/bundle \
harnesses/hermes/bundle \
mcp/bundle \
harnesses/pi/bundle
# Update marketplace.json's git-subdir source.sha to the release
# commit. Users doing `claude plugin marketplace add activeloopai/hivemind`
# clone main (HEAD = this cleanup commit), read marketplace.json, see
# the sha → clone activeloopai/hivemind at that sha (which DOES have
# bundles tracked) → extract `harnesses/claude-code/` → install.
node -e "
const fs = require('fs');
const path = '.claude-plugin/marketplace.json';
const m = JSON.parse(fs.readFileSync(path, 'utf-8'));
m.plugins[0].source.sha = process.env.RELEASE_SHA;
fs.writeFileSync(path, JSON.stringify(m, null, 2) + '\n');
"
git add .claude-plugin/marketplace.json
git commit -m "chore: untrack bundles, point marketplace at $RELEASE_SHA"
git push
- name: Extract version
id: version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=v${VERSION}" >> "$GITHUB_OUTPUT"
- name: Check if release already exists
id: check
run: |
if gh release view "${{ steps.version.outputs.version }}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get merged PR title
if: steps.check.outputs.exists == 'false'
id: pr
run: |
PR_TITLE=$(gh pr list --state merged --base main --limit 1 --json title -q '.[0].title // ""')
echo "title=$PR_TITLE" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub Release
if: steps.check.outputs.exists == 'false'
uses: softprops/action-gh-release@v3.0.0
with:
tag_name: ${{ steps.version.outputs.version }}
name: "${{ steps.version.outputs.version }} — ${{ steps.pr.outputs.title }}"
generate_release_notes: true
- name: Set outputs for publish job
# Captures the local HEAD SHA after the bump+push (or the
# untouched HEAD on the manual-bump path). The publish job
# checks out this SHA explicitly, so an in-flight `main`
# advance during the environment-approval pause cannot
# redirect the publish to an unreviewed commit.
id: set_outputs
if: steps.check.outputs.exists == 'false'
run: |
echo "published=true" >> "$GITHUB_OUTPUT"
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
publish:
# Publish @deeplake/hivemind to npm and the openclaw bundle to ClawHub.
# Runs in the SAME workflow run as `release` (gated on its
# `published` output) because the bump-commit push uses the
# default GITHUB_TOKEN, which by GitHub's loop-prevention rule
# does not retrigger workflows. Pauses at the `production`
# environment gate for human approval before tokens are
# accessible. Tokens live as environment-scoped secrets, so PRs
# from forks and unprotected branches cannot reach them.
name: Publish to npm + ClawHub
needs: release
if: needs.release.outputs.published == 'true'
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
concurrency:
group: hivemind-publish
cancel-in-progress: false
steps:
- uses: actions/checkout@v6.0.2
with:
# Pin to the bump commit captured by the release job. Pinning
# to a SHA (not `main`) is required: the `production`
# environment gate can pause for hours, during which `main`
# may advance — checking out a moving ref would publish a
# commit the reviewer never approved.
ref: ${{ needs.release.outputs.sha }}
fetch-depth: 1
# Publish reads a pinned SHA and never pushes; don't leave
# GITHUB_TOKEN in .git/config.
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
# Same rationale as the release job above — keep both jobs on
# Node 22 so the publish-time install/build matches what
# actually went through CI and what the release-job rebuild
# produced. Drifting between jobs would re-introduce the
# tree-sitter ABI/build mismatch.
node-version: 22
registry-url: "https://registry.npmjs.org"
scope: "@deeplake"
cache: "npm"
- name: Load secrets from 1Password
id: op_secrets
uses: 1Password/load-secrets-action@v4.0.0
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
CLAWHUB_TOKEN: "op://GitHub Actions/hivemind/CLAWHUB_TOKEN"
- name: Install dependencies
run: npm ci
- name: Build bundles
# Must run BEFORE the quality gate. `npm run ci` includes vitest,
# and the bundle-scan tests under
# harnesses/claude-code/tests/skillify-session-start-injection.test.ts read
# harnesses/openclaw/dist/index.js + harnesses/openclaw/dist/skillify-worker.js
# directly. harnesses/openclaw/dist/ is gitignored — it only exists after
# `npm run build`. Without this step before the gate, vitest
# fails with ENOENT and the publish aborts. Same root cause as
# 64cac0b in ci.yml; that fix never propagated to this job.
run: npm run build
- name: Quality gate (typecheck + duplication + test)
# Belt-and-braces: ci.yml already gates merges to main, but a
# publish job that doesn't re-run the gate could ship if someone
# disables a CI status check.
run: npm run ci
- name: Pack-check (refuse forbidden filenames in tarball)
run: npm run pack:check
- name: Audit openclaw bundle against ClawHub static-scan rules
# Hard gate before ANY publish step. The local audit script
# (scripts/audit-openclaw-bundle.mjs) replicates ClawHub's
# per-file regex rules — env-harvesting, dangerous-exec,
# potential-exfiltration, etc. — so we catch a flagged bundle
# in CI instead of after ClawHub's post-publish moderation
# removes the package from the store. Filed in #169 after a
# silent takedown of 0.7.26.
#
# Ordering: this step runs BEFORE `Publish to npm` AND before
# the ClawHub publish (CodeRabbit on #170 caught the earlier
# ordering, which would have npm-published a flagged bundle
# and only blocked ClawHub — partial release, mismatched
# versions across channels). Now a critical finding fails the
# whole release path.
#
# `--criticals-only`: block on `critical` findings only; surface
# `warn` findings as advisory log output but do not fail. The
# `potential-exfiltration` warn for the openclaw skillify-worker
# bundle is irreducible without splitting the worker into multiple
# shipped files (it reads its config from disk AND queries Deeplake
# over fetch — fundamental to skill mining). ClawHub historically
# accepted similar warns when the previous source-split fix
# `b277e0b` landed. If warns later become takedown triggers, fix
# the irreducible cases at the source layer and drop this flag.
run: npm run audit:openclaw -- --criticals-only
- name: Publish to npm with Sigstore provenance
# --provenance uses the GitHub OIDC token (id-token: write above)
# to sign the artifact via Sigstore. Even if NPM_TOKEN later leaks,
# consumers can verify the package was built by THIS workflow on
# THIS commit, and npmjs.com displays a Provenance badge.
# Node 22 ships with npm 10.x; OIDC tokenless publishing requires
# npm 11+. Upgrade in-place before publish; the rest of the job
# (build, test) keeps the Node 22 tree-sitter ABI.
run: |
npm install -g npm@11
npm publish --provenance --access public
- name: Install ClawHub CLI
# Pin the CLI version: it runs immediately before `clawhub login --token`
# with the real CLAWHUB_TOKEN in env, so a floating `@latest` would let an
# unexpected publish read/exfiltrate the token. Bump deliberately.
run: npm install -g clawhub@0.18.0
- name: Authenticate ClawHub CLI
# `clawhub login --token` writes a credential file inside the
# runner's $HOME, which is ephemeral and discarded when the job
# ends. The token is scoped to this step only (not exported to all
# steps via export-env) to limit the blast radius if any build step
# is compromised.
env:
CLAWHUB_TOKEN: ${{ steps.op_secrets.outputs.CLAWHUB_TOKEN }}
run: clawhub login --token "$CLAWHUB_TOKEN" --no-browser
- name: Publish openclaw bundle to ClawHub
# The release job above already synced harnesses/openclaw/package.json and
# harnesses/openclaw/openclaw.plugin.json versions to the bumped root
# version, so this publishes the same X.Y.Z that npm just got.
#
# --family code-plugin: the existing `hivemind` package on ClawHub
# was originally registered with family=code-plugin (see
# `clawhub package inspect hivemind` → "Family: Code Plugin"). The
# CLI's auto-detection picks a different default (bundle-plugin) on
# this layout, and ClawHub's server rejects with
# `Package already exists as a code-plugin; family changes are not
# allowed` (convex/packages.ts:4305). Pinning the family explicitly
# matches what's already registered and lets the publish proceed.
run: clawhub package publish ./harnesses/openclaw --family code-plugin
- name: Summary
run: |
VERSION=$(node -p "require('./package.json').version")
{
echo "### Published"
echo ""
echo "- **npm**: \`@deeplake/hivemind@${VERSION}\` (with Sigstore provenance)"
echo "- **ClawHub**: \`clawhub:hivemind@${VERSION}\`"
} >> "$GITHUB_STEP_SUMMARY"