From e107bb35d4d9a6a975c83fe24e075ef2c9d3f389 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 30 Apr 2026 20:44:25 -0400 Subject: [PATCH] feat(ci): add release-sdk.yml stopgap workflow for dev/next/latest CC publishes (#2925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a workflow_dispatch-only release path that publishes get-shit-done-cc to ONE chosen dist-tag per run (dev | next | latest), with the SDK bundled inside the CC tarball both as the existing loose sdk/dist/ tree and as a fresh sdk-bundle/gsd-sdk.tgz npm-installable artifact. Why: @gsd-build/sdk publishes from canary.yml and release.yml fail because the @gsd-build npm token is currently unavailable. CC users don't consume @gsd-build/sdk directly — bin/gsd-sdk.js resolves sdk/dist/cli.js from inside the installed CC package. This workflow ships only get-shit-done-cc (which we hold the token for) and bundles the SDK two ways so any future install path can pick whichever shape it needs. The new sdk-bundle/ directory is added to the CC files whitelist in-tree at build time only — never committed. Existing canary.yml and release.yml are intentionally untouched; restore them to primary use once the @gsd-build/sdk token is recovered. Per-tag version derivation when the version input is empty: - dev → -dev.N (next sequential, scanning v-dev.* tags) - next → -rc.N (matches release.yml convention) - latest → (clean, no suffix) Refuses to publish when the version already exists on npm or has an existing git tag (no accidental overwrites). Verifies the publish landed on the registry and the dist-tag resolves correctly before marking the run successful. --- .github/workflows/release-sdk.yml | 280 ++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .github/workflows/release-sdk.yml diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml new file mode 100644 index 000000000..1e52efa69 --- /dev/null +++ b/.github/workflows/release-sdk.yml @@ -0,0 +1,280 @@ +# Release SDK Bundle +# +# Stopgap workflow_dispatch publish path: builds get-shit-done-cc with the +# compiled SDK and the SDK .tgz bundled inside the CC tarball, then +# publishes the CC package to ONE chosen dist-tag (dev | next | latest) +# per run. +# +# Why this exists: @gsd-build/sdk publishes from canary.yml and release.yml +# fail because the @gsd-build npm token is currently unavailable. CC users +# do not consume @gsd-build/sdk directly — bin/gsd-sdk.js resolves +# sdk/dist/cli.js from inside the installed CC package, so the bundled +# copy is sufficient for full functionality. This workflow ships CC alone +# (no separate @gsd-build/sdk publish attempt) and additionally bakes a +# bundled gsd-sdk-.tgz at sdk-bundle/gsd-sdk.tgz inside the CC +# tarball as a recoverable npm-installable artifact. +# +# Existing canary.yml and release.yml are intentionally untouched. They +# remain the canonical two-package publish path; restore them to primary +# use once @gsd-build/sdk ownership is recovered. +# +# Tracking issue: #2925 + +name: Release SDK Bundle + +on: + workflow_dispatch: + inputs: + tag: + description: 'npm dist-tag to publish under' + required: true + type: choice + options: + - dev + - next + - latest + version: + description: 'Explicit version (e.g. 1.50.0-dev.3, 1.50.0-rc.2, 1.50.0). Empty = derive from package.json base + tag-appropriate suffix.' + required: false + type: string + ref: + description: 'Branch or ref to build from (default: the workflow-dispatch ref, typically dev)' + required: false + type: string + dry_run: + description: 'Dry run (skip npm publish, git tag, and push)' + required: false + type: boolean + default: false + +# Per dist-tag, no concurrent publishes for the same stream. Different streams +# can publish in parallel because they target different dist-tags. +concurrency: + group: release-sdk-${{ inputs.tag }} + cancel-in-progress: false + +env: + NODE_VERSION: 24 + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write # tag + push + id-token: write # provenance + environment: npm-publish + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ inputs.ref }} + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + + - name: Determine version + id: ver + env: + INPUT_TAG: ${{ inputs.tag }} + INPUT_OVERRIDE: ${{ inputs.version }} + run: | + set -e + RAW=$(node -p "require('./package.json').version") + BASE=$(echo "$RAW" | sed 's/-.*//') + if [ -n "$INPUT_OVERRIDE" ]; then + VERSION="$INPUT_OVERRIDE" + else + case "$INPUT_TAG" in + dev) + N=1 + while git tag -l "v${BASE}-dev.${N}" | grep -q .; do + N=$((N + 1)) + done + VERSION="${BASE}-dev.${N}" + ;; + next) + N=1 + while git tag -l "v${BASE}-rc.${N}" | grep -q .; do + N=$((N + 1)) + done + VERSION="${BASE}-rc.${N}" + ;; + latest) + VERSION="$BASE" + ;; + *) + echo "::error::Unknown tag '$INPUT_TAG' (expected dev|next|latest)" + exit 1 + ;; + esac + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$INPUT_TAG" >> "$GITHUB_OUTPUT" + echo "→ Will publish v${VERSION} to dist-tag '${INPUT_TAG}'" + + - name: Refuse if version already exists on npm + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + EXISTING=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || true) + if [ -n "$EXISTING" ]; then + echo "::error::get-shit-done-cc@${VERSION} is already published. Bump version or pass an explicit override input." + exit 1 + fi + + - name: Refuse if git tag already exists + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + if git rev-parse "v${VERSION}" >/dev/null 2>&1; then + echo "::error::git tag v${VERSION} already exists. Bump version or pass an explicit override input." + exit 1 + fi + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Bump in-tree version (not committed) + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + npm version "$VERSION" --no-git-tag-version + cd sdk && npm version "$VERSION" --no-git-tag-version + + - name: Install dependencies + run: npm ci + + - name: Run full test suite + run: npm test + + - name: Build SDK dist for tarball + run: npm run build:sdk + + - name: Verify CC tarball ships sdk/dist/cli.js (bug #2647 guard) + run: bash scripts/verify-tarball-sdk-dist.sh + + - name: Pack SDK as tarball and bundle into CC source tree + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + set -e + cd sdk + npm pack + # npm pack emits gsd-build-sdk-.tgz in the cwd + TARBALL="gsd-build-sdk-${VERSION}.tgz" + if [ ! -f "$TARBALL" ]; then + echo "::error::Expected $TARBALL but npm pack did not produce it. Listing sdk/:" + ls -la + exit 1 + fi + mkdir -p ../sdk-bundle + mv "$TARBALL" ../sdk-bundle/gsd-sdk.tgz + cd .. + ls -la sdk-bundle/ + + - name: Add sdk-bundle to CC files whitelist (in-tree, not committed) + run: | + node <<'NODE' + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + if (!Array.isArray(pkg.files)) { + console.error('::error::package.json files is not an array'); + process.exit(1); + } + if (!pkg.files.includes('sdk-bundle')) { + pkg.files.push('sdk-bundle'); + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + console.log('Added sdk-bundle/ to package.json files whitelist'); + } else { + console.log('sdk-bundle/ already in files whitelist'); + } + NODE + + - name: Verify CC tarball will contain sdk-bundle/gsd-sdk.tgz + run: | + set -e + TARBALL=$(npm pack --ignore-scripts 2>/dev/null | tail -1) + if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then + echo "::error::npm pack produced no tarball" + exit 1 + fi + echo "Inspecting $TARBALL for sdk-bundle/gsd-sdk.tgz:" + if ! tar -tzf "$TARBALL" | grep -q "package/sdk-bundle/gsd-sdk.tgz"; then + echo "::error::CC tarball is missing package/sdk-bundle/gsd-sdk.tgz" + tar -tzf "$TARBALL" | grep -E "sdk-bundle|sdk/dist" | head -20 + exit 1 + fi + echo "✅ CC tarball contains sdk-bundle/gsd-sdk.tgz" + rm -f "$TARBALL" + + - name: Dry-run publish validation + env: + TAG: ${{ steps.ver.outputs.tag }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --dry-run --tag "$TAG" + + - name: Tag and push + if: ${{ !inputs.dry_run }} + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + git tag "v${VERSION}" + git push origin "v${VERSION}" + + - name: Publish to npm (CC bundle, SDK included as both loose tree and .tgz) + if: ${{ !inputs.dry_run }} + env: + TAG: ${{ steps.ver.outputs.tag }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --provenance --access public --tag "$TAG" + + - name: Verify publish landed on registry + if: ${{ !inputs.dry_run }} + env: + VERSION: ${{ steps.ver.outputs.version }} + TAG: ${{ steps.ver.outputs.tag }} + run: | + PUBLISHED="NOT_FOUND" + for delay in 5 10 20 30 45; do + PUBLISHED=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || echo "NOT_FOUND") + if [ "$PUBLISHED" = "$VERSION" ]; then + break + fi + echo "Waiting ${delay}s for registry to catch up (saw: $PUBLISHED)..." + sleep "$delay" + done + if [ "$PUBLISHED" != "$VERSION" ]; then + echo "::error::Version $VERSION did not appear on the registry within timeout" + exit 1 + fi + TAG_VERSION=$(npm view get-shit-done-cc dist-tags."$TAG" 2>/dev/null || echo "NOT_FOUND") + if [ "$TAG_VERSION" != "$VERSION" ]; then + echo "::error::dist-tag '$TAG' resolves to '$TAG_VERSION', expected '$VERSION'" + exit 1 + fi + echo "✅ get-shit-done-cc@${VERSION} live on dist-tag '${TAG}'" + + - name: Summary + env: + VERSION: ${{ steps.ver.outputs.version }} + TAG: ${{ steps.ver.outputs.tag }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + echo "## Release SDK Bundle: v${VERSION} → @${TAG}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + if [ "$DRY_RUN" = "true" ]; then + echo "**DRY RUN** — npm publish, git tag, and push were skipped." >> "$GITHUB_STEP_SUMMARY" + else + echo "- Published \`get-shit-done-cc@${VERSION}\` to dist-tag \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- SDK bundled inside the CC tarball at:" >> "$GITHUB_STEP_SUMMARY" + echo " - \`sdk/dist/cli.js\` (loose tree, consumed by \`bin/gsd-sdk.js\` shim)" >> "$GITHUB_STEP_SUMMARY" + echo " - \`sdk-bundle/gsd-sdk.tgz\` (npm-installable artifact)" >> "$GITHUB_STEP_SUMMARY" + echo "- Git tag \`v${VERSION}\` pushed" >> "$GITHUB_STEP_SUMMARY" + echo "- Install: \`npm install -g get-shit-done-cc@${TAG}\`" >> "$GITHUB_STEP_SUMMARY" + fi