11name : publish
2- run-name : " ${{ format('{0} {1}', inputs.channel || (inputs.pre_release && 'rc' || ' latest') , inputs.version || inputs.bump) }}"
2+ run-name : " ${{ format('{0} {1}', inputs.channel || ' latest', inputs.version || inputs.bump || 'auto' ) }}"
33
44on :
5- # Automated releases are intentionally disabled.
65 # Releases are manual only — trigger via workflow_dispatch.
7- #
8- # push:
9- # branches:
10- # - main
6+ # Both "latest" and "dev" channels are dispatched by hand. There is
7+ # no auto-publish on push.
118 workflow_dispatch :
129 inputs :
10+ channel :
11+ description : ' npm dist-tag channel — "latest" (public stable) or "dev" (internal)'
12+ required : true
13+ type : choice
14+ default : latest
15+ options :
16+ - latest
17+ - dev
1318 bump :
14- description : " Bump major, minor, or patch "
19+ description : " Bump major/minor/patch — for latest, bumps stable; for dev, resets dev cycle "
1520 required : false
1621 type : choice
1722 options :
2025 - minor
2126 - major
2227 version :
23- description : " Override version (optional, takes precedence over bump) "
28+ description : " Override version (X.Y.Z for latest, X.Y.Z-dev.N for dev). Wins over bump. "
2429 required : false
2530 type : string
26- channel :
27- description : " npm dist-tag channel (explicit; wins over pre_release)"
28- required : false
29- type : choice
30- options :
31- - " "
32- - latest
33- - rc
34- - beta
35- pre_release :
36- description : " Publish to rc channel (ignored when channel is set)"
37- required : false
38- type : boolean
39- default : false
4031
41- concurrency : ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
32+ concurrency : ${{ github.workflow }}-${{ github.ref }}-${{ inputs.channel }}-${{ inputs. version || inputs.bump }}
4233
4334# id-token:write is required for npm provenance (SLSA attestation).
4435# This workflow must run on GitHub-hosted runners (not Blacksmith) for
@@ -78,14 +69,21 @@ jobs:
7869 - name : Format check
7970 run : bun run format:check
8071
72+ # Fail fast on bad/missing NPM_TOKEN before any side effects
73+ # (version.ts writes to package.json, network calls to GH, etc.)
74+ # Surfaces auth issues in ~2s instead of mid-publish.
75+ - name : Verify npm auth
76+ run : npm whoami --registry=https://registry.npmjs.org/
77+ env :
78+ NODE_AUTH_TOKEN : ${{ secrets.NPM_TOKEN }}
79+
8180 - name : Resolve version
8281 id : version
8382 run : bun script/version.ts
8483 env :
84+ KILO_CHANNEL : ${{ inputs.channel }}
8585 KILO_BUMP : ${{ inputs.bump }}
8686 KILO_VERSION : ${{ inputs.version }}
87- KILO_CHANNEL : ${{ inputs.channel }}
88- KILO_PRE_RELEASE : ${{ inputs.pre_release && 'true' || 'false' }}
8987 GH_REPO : ${{ github.repository }}
9088 GH_TOKEN : ${{ github.token }}
9189
@@ -96,17 +94,71 @@ jobs:
9694 NPM_CONFIG_PROVENANCE : " true"
9795 KILO_CHANNEL : ${{ steps.version.outputs.channel }}
9896
99- - name : Commit version bump and tag
97+ # Smoke test: confirm the version actually appeared on the npm
98+ # registry. `npm publish` has been known to report success while
99+ # the registry's eventual-consistency layer drops the new version
100+ # (rare, but real). Catching this here means we fail BEFORE
101+ # creating tags / GH releases that would point at a non-existent
102+ # version. Retries 3x with 5s backoff to absorb normal replication
103+ # lag.
104+ - name : Verify publish landed on registry
100105 env :
101- TAG : ${{ steps.version.outputs.tag }}
102106 VERSION : ${{ steps.version.outputs.version }}
107+ run : |
108+ MAX_ATTEMPTS=3
109+ for i in $(seq 1 $MAX_ATTEMPTS); do
110+ PUBLISHED=$(npm view "@kilocode/openclaw-security-advisor@$VERSION" version 2>/dev/null || echo "")
111+ if [ "$PUBLISHED" = "$VERSION" ]; then
112+ echo "Verified: $VERSION is live on npm"
113+ exit 0
114+ fi
115+ if [ "$i" -lt "$MAX_ATTEMPTS" ]; then
116+ echo "Attempt $i/$MAX_ATTEMPTS: registry returned '$PUBLISHED', expected '$VERSION'. Retrying in 5s..."
117+ sleep 5
118+ else
119+ echo "Attempt $i/$MAX_ATTEMPTS: registry returned '$PUBLISHED', expected '$VERSION'."
120+ fi
121+ done
122+ echo "::error::npm publish reported success but $VERSION did not appear on the registry after $MAX_ATTEMPTS attempts"
123+ exit 1
124+
125+ - name : Configure git identity
103126 run : |
104127 git config user.name "github-actions[bot]"
105128 git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
129+
130+ # Stable releases get the version bump committed back to main so
131+ # package.json on main always reflects the latest published stable.
132+ # Dev releases skip this: dev publishes are ephemeral and would
133+ # otherwise pollute main with snapshot commits.
134+ - name : Commit version bump (stable only)
135+ if : steps.version.outputs.channel == 'latest'
136+ env :
137+ TAG : ${{ steps.version.outputs.tag }}
138+ run : |
106139 git add package.json
107140 git commit -m "release: $TAG"
141+ git push origin HEAD
142+
143+ # Tag is created for both channels so future `gh release list`
144+ # queries can find the highest dev version. For dev releases the
145+ # tag points at an orphan commit (the package.json bump made in
146+ # this CI runner) — pushing the tag carries the orphan commit too.
147+ - name : Tag release
148+ env :
149+ TAG : ${{ steps.version.outputs.tag }}
150+ CHANNEL : ${{ steps.version.outputs.channel }}
151+ run : |
152+ # For dev, we haven't committed the bump yet — do it now so the
153+ # tag points at the bumped tree. version.ts is guaranteed to
154+ # have modified package.json before this step runs, so this
155+ # commit always has changes (no --allow-empty needed).
156+ if [ "$CHANNEL" = "dev" ]; then
157+ git add package.json
158+ git commit -m "release: $TAG"
159+ fi
108160 git tag "$TAG"
109- git push origin HEAD --tags
161+ git push origin "$TAG"
110162
111163 - name : Create GitHub release
112164 env :
0 commit comments