Skip to content

dev auto

dev auto #7

Workflow file for this run

name: publish
run-name: "${{ format('{0} {1}', inputs.channel || 'latest', inputs.version || inputs.bump || 'auto') }}"
on:
# Releases are manual only — trigger via workflow_dispatch.
# Both "latest" and "dev" channels are dispatched by hand. There is
# no auto-publish on push.
workflow_dispatch:
inputs:
channel:
description: 'npm dist-tag channel — "latest" (public stable) or "dev" (internal)'
required: true
type: choice
default: latest
options:
- latest
- dev
bump:
description: "Bump major/minor/patch — for latest, bumps stable; for dev, resets dev cycle"
required: false
type: choice
options:
- ""
- patch
- minor
- major
version:
description: "Override version (X.Y.Z for latest, X.Y.Z-dev.N for dev). Wins over bump."
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.channel }}-${{ inputs.version || inputs.bump }}
# id-token:write is required for npm provenance (SLSA attestation).
# This workflow must run on GitHub-hosted runners (not Blacksmith) for
# provenance to work — GitHub's OIDC token is only issued on their infra.
permissions:
id-token: write
contents: write
jobs:
publish:
name: Publish to npm
runs-on: ubuntu-24.04
if: github.repository == 'Kilo-Org/openclaw-security-advisor'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Typecheck
run: bun run typecheck
- name: Test
run: bun test
- name: Format check
run: bun run format:check
- name: Resolve version
id: version
run: bun script/version.ts
env:
KILO_CHANNEL: ${{ inputs.channel }}
KILO_BUMP: ${{ inputs.bump }}
KILO_VERSION: ${{ inputs.version }}
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
# ============================================================
# POINT OF NO RETURN
# ============================================================
# `npm publish` is irreversible. Once it succeeds, every step
# below this point MUST eventually succeed (with retries) or the
# workflow exits via the recovery handler with explicit manual
# recovery instructions.
#
# Atomicity story:
# - All pre-publish validation runs above (token, version, no
# existing tag/release per version.ts).
# - Publish runs once. If it fails, no git/GH side effects.
# - Verification of publish-landed is INFORMATIONAL ONLY. It uses
# the registry HTTP endpoint (faster than `npm view`) and never
# fails the workflow regardless of outcome.
# - Tag + release operations are bundled into a single step with
# internal retries. If anything fails after retries, the
# recovery handler prints the exact manual recovery commands.
# ============================================================
# Authentication for npm publish uses OIDC trusted publishing —
# no NODE_AUTH_TOKEN needed. npm CLI auto-detects the OIDC
# environment when id-token: write is set and no token is present.
# Configured on npmjs.com under package settings → Trusted Publishers.
# Requires npm CLI v11.5.1+ and Node 22.14.0+.
- name: Publish to npm
id: publish
run: bun script/publish.ts
env:
NPM_CONFIG_PROVENANCE: "true"
KILO_CHANNEL: ${{ steps.version.outputs.channel }}
# Informational verification of the publish landing on the npm
# registry. Uses curl against the registry HTTP endpoint (not
# `npm view`, which has 30-90s propagation lag). Polls 6 times at
# 10s intervals. ALWAYS exits 0 — this step never fails the
# workflow. The publish step itself is the source of truth for
# whether the publish actually happened.
- name: Verify publish landed (informational)
if: steps.publish.outcome == 'success'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Probing registry for @kilocode/openclaw-security-advisor@$VERSION..."
for i in 1 2 3 4 5 6; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"https://registry.npmjs.org/@kilocode/openclaw-security-advisor/$VERSION")
if [ "$STATUS" = "200" ]; then
echo "::notice::Verified $VERSION is live on the registry"
exit 0
fi
echo " Attempt $i/6: registry returned HTTP $STATUS, retrying in 10s..."
sleep 10
done
echo "::warning::Could not verify $VERSION on the registry after 60s of polling. The publish step itself reported success; verification is informational only and the workflow will continue to the tag/release steps."
exit 0
- name: Configure git identity
if: steps.publish.outcome == 'success'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Atomic git/GH operations bundled into ONE step:
# 1. Build the local commit (on main for stable, on detached HEAD for dev)
# 2. Build the local tag
# 3. Push refs in a SINGLE git push transaction
# - stable: `git push origin HEAD --follow-tags` (commit + tag in one call)
# - dev: `git push origin <tag>` (the orphan commit travels with the tag)
# 4. Create the GH release
#
# All network operations have internal 3x retries with 5s backoff.
# If anything fails after retries, the next step prints recovery
# instructions.
- name: Tag and release (post-publish)
id: tag_and_release
if: steps.publish.outcome == 'success'
env:
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
CHANNEL: ${{ steps.version.outputs.channel }}
PREVIEW: ${{ steps.version.outputs.preview }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Build local commit + tag.
# For stable: commit on main (will be pushed below).
# For dev: detach HEAD first so main stays clean — the orphan
# commit gets pushed via the tag itself.
if [ "$CHANNEL" = "latest" ]; then
git add package.json
git commit -m "release: $TAG"
git tag "$TAG" -m "Release $TAG"
else
git checkout --detach
git add package.json
git commit -m "release: $TAG"
git tag "$TAG" -m "Release $TAG"
fi
echo "::notice::Built local commit + tag $TAG ($(git rev-parse HEAD))"
# Push refs with retries. Single git push command per branch
# to keep the operation as atomic as git allows.
push_with_retry() {
local max=3
for i in $(seq 1 $max); do
if "$@"; then
return 0
fi
if [ "$i" -lt "$max" ]; then
echo " push attempt $i/$max failed, retrying in 5s..."
sleep 5
fi
done
return 1
}
if [ "$CHANNEL" = "latest" ]; then
push_with_retry git push origin HEAD --follow-tags
else
push_with_retry git push origin "$TAG"
fi
echo "::notice::Pushed $TAG to origin"
# Create the GH release (last step). Retried 3x with backoff.
PRERELEASE_FLAG=""
if [ "$PREVIEW" = "true" ]; then
PRERELEASE_FLAG="--prerelease"
fi
gh_release_create() {
gh release create "$TAG" \
--title "$TAG" \
--generate-notes \
$PRERELEASE_FLAG
}
for i in 1 2 3; do
if gh_release_create; then
echo "::notice::Created GitHub release $TAG"
exit 0
fi
if [ "$i" -lt 3 ]; then
echo " gh release create attempt $i/3 failed, retrying in 5s..."
sleep 5
fi
done
echo "::error::Failed to create GH release $TAG after 3 attempts"
exit 1
# Recovery handler: runs ONLY when npm publish succeeded but the
# post-publish tag/release operations failed (or were skipped due
# to a failure between them). Prints exact manual recovery
# commands so the operator can complete the release by hand.
- name: Print recovery instructions on partial failure
if: failure() && steps.publish.outcome == 'success' && steps.tag_and_release.outcome != 'success'
env:
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
CHANNEL: ${{ steps.version.outputs.channel }}
PREVIEW: ${{ steps.version.outputs.preview }}
run: |
cat >&2 <<MSG
============================================================
PARTIAL PUBLISH STATE
============================================================
npm publish for @kilocode/openclaw-security-advisor@$VERSION
SUCCEEDED, but the post-publish git/GitHub-release operations
FAILED.
State right now:
- npm: $VERSION is live (cannot be unpublished)
- git: tag $TAG MAY OR MAY NOT exist on origin (check below)
- GH: release $TAG MAY OR MAY NOT exist (check below)
To complete the release manually, run from your local checkout:
cd /path/to/openclaw-security-advisor
git fetch origin --tags
# First check what already exists:
git ls-remote --tags origin "$TAG"
gh release view "$TAG" --repo Kilo-Org/openclaw-security-advisor
MSG
if [ "$CHANNEL" = "latest" ]; then
cat >&2 <<'MSG'
# === STABLE channel recovery ===
# If the tag is missing, build the commit on main and push with the tag:
MSG
cat >&2 <<MSG
git checkout main
git pull
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json'));p.version='$VERSION';delete p.private;fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
git add package.json
git commit -m "release: $TAG"
git tag "$TAG" -m "Release $TAG"
git push origin main --follow-tags
MSG
else
cat >&2 <<'MSG'
# === DEV channel recovery ===
# If the tag is missing, build an orphan commit (does NOT touch main):
MSG
cat >&2 <<MSG
git checkout main
git pull
git checkout --detach
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json'));p.version='$VERSION';delete p.private;fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
git add package.json
git commit -m "release: $TAG"
git tag "$TAG" -m "Release $TAG"
git push origin "$TAG"
git checkout main # CRITICAL: get back to main from detached HEAD
MSG
fi
PRERELEASE_FLAG=""
if [ "$PREVIEW" = "true" ]; then
PRERELEASE_FLAG=" --prerelease"
fi
cat >&2 <<MSG
# If the GH release is missing, create it:
gh release create "$TAG" \\
--repo Kilo-Org/openclaw-security-advisor \\
--title "$TAG" \\
--generate-notes${PRERELEASE_FLAG}
============================================================
MSG
exit 1