ci: fix two release-pipeline bugs the first tag exposed (#247) #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write # for GitHub Releases | |
| id-token: write # for npm provenance (if enabled) | |
| jobs: | |
| # ---------------------------------------------------------------------- | |
| # Validate — typecheck + tests + build all packages | |
| # ---------------------------------------------------------------------- | |
| validate: | |
| name: Validate before release | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| channel: ${{ steps.version.outputs.channel }} | |
| is_mandatory: ${{ steps.version.outputs.is_mandatory }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| # The release gate must validate in the same environment as CI. Without | |
| # this, `pnpm test` here runs against a weaker box than ci.yml uses, so a | |
| # green CI stops predicting a green release — which is exactly how the | |
| # first v0.3.0 tag failed: the deny-all-net fallback test spawns `bwrap`, | |
| # and this runner had no bubblewrap installed. | |
| # Kept byte-for-byte in step with the "Install sandbox tools" step in | |
| # ci.yml; change both together. | |
| - name: Install sandbox tools | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y bubblewrap slirp4netns curl | |
| sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true | |
| sudo sysctl -w net.ipv4.ip_unprivileged_port_start=53 || true | |
| - run: pnpm typecheck | |
| - run: pnpm lint | |
| - run: pnpm format:check | |
| - name: Test | |
| # Matches ci.yml: opts in the selective-allowlist integration test, which | |
| # self-skips when bwrap/slirp4netns are absent. | |
| env: | |
| DC_SANDBOX_NET_TEST: '1' | |
| run: pnpm test | |
| - run: pnpm docs:check | |
| - run: pnpm release:check | |
| - name: Install Chromium | |
| run: pnpm --filter @deepcode/desktop exec playwright install --with-deps chromium | |
| - name: Exercise desktop protocol fixture | |
| run: pnpm --filter @deepcode/desktop test:e2e | |
| - name: Upload validation diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-validation-diagnostics | |
| path: | | |
| apps/vscode/dist/release-gate-report.json | |
| apps/desktop/playwright-report | |
| apps/desktop/test-results | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - id: version | |
| name: Parse version + channel from tag | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| VERSION="${TAG#v}" | |
| CHANNEL="stable" | |
| IS_MANDATORY="false" | |
| if [[ "$VERSION" == *-nightly.* ]]; then CHANNEL="nightly"; fi | |
| if [[ "$VERSION" == *-beta.* ]]; then CHANNEL="beta"; fi | |
| if [[ "$VERSION" == *+security.* ]]; then IS_MANDATORY="true"; fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "channel=$CHANNEL" >> $GITHUB_OUTPUT | |
| echo "is_mandatory=$IS_MANDATORY" >> $GITHUB_OUTPUT | |
| echo "Released as version=$VERSION channel=$CHANNEL mandatory=$IS_MANDATORY" | |
| # ---------------------------------------------------------------------- | |
| # Publish CLI to npm | |
| # ---------------------------------------------------------------------- | |
| publish-cli: | |
| name: Publish deepcode-cli to npm | |
| # Avoid a partial release: do not publish npm until both installable | |
| # desktop/editor artifacts have built successfully. | |
| needs: [validate, build-vscode, build-mac] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| registry-url: https://registry.npmjs.org | |
| - run: pnpm install --frozen-lockfile | |
| # Before `pnpm build`: core's VERSION is compiled into dist, and it is what | |
| # `deepcode --version`, `--help`, `/upgrade` and `/bug` print. | |
| - name: Stamp core VERSION from tag | |
| run: | | |
| sed -i.bak -E "s/^export const VERSION = '.*';/export const VERSION = '${{ needs.validate.outputs.version }}';/" packages/core/src/index.ts | |
| rm -f packages/core/src/index.ts.bak | |
| grep -q "export const VERSION = '${{ needs.validate.outputs.version }}';" packages/core/src/index.ts | |
| # Same workspace-protocol problem as build-vscode. This job had never run | |
| # (it needs both build jobs), so the bug was latent rather than observed. | |
| - name: Set CLI version from tag | |
| run: | | |
| cd apps/cli | |
| node -e ' | |
| const fs = require("fs"); | |
| const p = "package.json"; | |
| const pkg = JSON.parse(fs.readFileSync(p, "utf8")); | |
| pkg.version = process.argv[1]; | |
| fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n"); | |
| ' "${{ needs.validate.outputs.version }}" | |
| node -e 'process.exit(require("./package.json").version === process.argv[1] ? 0 : 1)' "${{ needs.validate.outputs.version }}" | |
| - run: pnpm build | |
| - name: Publish | |
| run: | | |
| cd apps/cli | |
| if [ "${{ needs.validate.outputs.channel }}" = "stable" ]; then | |
| pnpm publish --no-git-checks --access public | |
| else | |
| pnpm publish --no-git-checks --access public --tag ${{ needs.validate.outputs.channel }} | |
| fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # ---------------------------------------------------------------------- | |
| # Build installable VS Code extension | |
| # ---------------------------------------------------------------------- | |
| build-vscode: | |
| name: Build VS Code extension | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| # The extension bundles its own app-server, which bundles core. | |
| - name: Stamp core VERSION from tag | |
| run: | | |
| sed -i.bak -E "s/^export const VERSION = '.*';/export const VERSION = '${{ needs.validate.outputs.version }}';/" packages/core/src/index.ts | |
| rm -f packages/core/src/index.ts.bak | |
| # Not `npm version`: this is a pnpm workspace, and npm rejects the | |
| # `workspace:*` dependency protocol with EUNSUPPORTEDPROTOCOL before it | |
| # ever writes the field. Editing the one key touches nothing else. | |
| - name: Set extension version from tag | |
| run: | | |
| node -e ' | |
| const fs = require("fs"); | |
| const p = "package.json"; | |
| const pkg = JSON.parse(fs.readFileSync(p, "utf8")); | |
| pkg.version = process.argv[1]; | |
| fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n"); | |
| ' "${{ needs.validate.outputs.version }}" | |
| node -e 'process.exit(require("./package.json").version === process.argv[1] ? 0 : 1)' "${{ needs.validate.outputs.version }}" | |
| working-directory: apps/vscode | |
| - name: Package VSIX | |
| run: | | |
| mkdir -p release-artifacts | |
| pnpm --dir apps/vscode package \ | |
| --out "../../release-artifacts/deepcode-${{ needs.validate.outputs.version }}.vsix" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: vscode-release | |
| path: release-artifacts/deepcode-*.vsix | |
| # ---------------------------------------------------------------------- | |
| # Build + sign Mac client (.dmg) via Tauri | |
| # ---------------------------------------------------------------------- | |
| build-mac: | |
| name: Build + sign + notarize Mac client (Tauri) | |
| needs: validate | |
| runs-on: macos-14 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin | |
| - name: Cache Cargo registry | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| apps/desktop/src-tauri/target | |
| key: cargo-mac-${{ hashFiles('**/Cargo.lock') }} | |
| - name: pnpm install | |
| run: pnpm install --frozen-lockfile | |
| - name: Prepare pinned Node sidecar runtime | |
| env: | |
| NODE_SIDECAR_VERSION: 22.23.1 | |
| # Verified against https://nodejs.org/dist/v22.23.1/SHASUMS256.txt and by | |
| # hashing the downloaded artifact. The previous value never matched any | |
| # published tarball, so this check had never once passed. | |
| # Re-derive when bumping: | |
| # curl -sS https://nodejs.org/dist/v<VER>/SHASUMS256.txt \ | |
| # | grep node-v<VER>-darwin-arm64.tar.xz | |
| NODE_SIDECAR_SHA256: fb526811860f81dcac7dd8b2b55eca4accfc5d61c3b7c2508f2639faee8a738d | |
| run: | | |
| archive="node-v${NODE_SIDECAR_VERSION}-darwin-arm64.tar.xz" | |
| curl --fail --location --retry 3 \ | |
| "https://nodejs.org/dist/v${NODE_SIDECAR_VERSION}/${archive}" \ | |
| --output "$RUNNER_TEMP/$archive" | |
| echo "${NODE_SIDECAR_SHA256} $RUNNER_TEMP/$archive" | shasum -a 256 --check | |
| tar -xJf "$RUNNER_TEMP/$archive" -C "$RUNNER_TEMP" | |
| echo "DEEPCODE_NODE_RUNTIME=$RUNNER_TEMP/node-v${NODE_SIDECAR_VERSION}-darwin-arm64/bin/node" >> "$GITHUB_ENV" | |
| - name: Set version | |
| run: | | |
| # core VERSION first — the sidecar it builds into reports it. | |
| sed -i.bak -E "s/^export const VERSION = '.*';/export const VERSION = '${{ needs.validate.outputs.version }}';/" packages/core/src/index.ts | |
| rm -f packages/core/src/index.ts.bak | |
| cd apps/desktop | |
| npm version "${{ needs.validate.outputs.version }}" --no-git-tag-version | |
| # Sync the tauri.conf.json version too | |
| node -e " | |
| const fs=require('fs'); | |
| const p='src-tauri/tauri.conf.json'; | |
| const c=JSON.parse(fs.readFileSync(p,'utf8')); | |
| c.version='${{ needs.validate.outputs.version }}'; | |
| fs.writeFileSync(p, JSON.stringify(c,null,2)+'\n'); | |
| " | |
| # And Cargo.toml + the lockfile entry, which must agree or a --locked | |
| # cargo invocation refuses to build. | |
| sed -i.bak -E 's/^version = ".*"/version = "${{ needs.validate.outputs.version }}"/' src-tauri/Cargo.toml | |
| rm -f src-tauri/Cargo.toml.bak | |
| node -e " | |
| const fs=require('fs'); | |
| const p='src-tauri/Cargo.lock'; | |
| const s=fs.readFileSync(p,'utf8'); | |
| fs.writeFileSync(p, s.replace( | |
| /name = \"deepcode_desktop\"\nversion = \"[^\"]+\"/, | |
| 'name = \"deepcode_desktop\"\nversion = \"${{ needs.validate.outputs.version }}\"' | |
| )); | |
| " | |
| - name: Import Developer ID certificate | |
| env: | |
| CSC_LINK: ${{ secrets.CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
| run: | | |
| # CSC_LINK is base64-encoded .p12 of the Developer ID Application cert | |
| echo "$CSC_LINK" | base64 --decode > /tmp/cert.p12 | |
| security create-keychain -p actions ci.keychain | |
| security default-keychain -s ci.keychain | |
| security unlock-keychain -p actions ci.keychain | |
| security import /tmp/cert.p12 -k ci.keychain -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions ci.keychain | |
| rm -f /tmp/cert.p12 | |
| - name: Store notarization credentials | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| xcrun notarytool store-credentials "DEEPCODE_NOTARY" \ | |
| --apple-id "$APPLE_ID" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" | |
| - name: Build + sign + notarize | |
| env: | |
| DEEPCODE_TARGET: aarch64-apple-darwin | |
| DEEPCODE_NOTARY_PROFILE: DEEPCODE_NOTARY | |
| run: bash scripts/sign-and-notarize.sh | |
| - name: Stage release artifacts | |
| run: | | |
| mkdir -p release-artifacts | |
| cp apps/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/DeepCode_${{ needs.validate.outputs.version }}_aarch64.dmg \ | |
| release-artifacts/DeepCode-${{ needs.validate.outputs.version }}-arm64.dmg | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: mac-release | |
| path: release-artifacts/DeepCode-*.dmg | |
| # ---------------------------------------------------------------------- | |
| # GitHub Release — runs after Mac build so the DMG can be attached | |
| # ---------------------------------------------------------------------- | |
| github-release: | |
| name: Publish GitHub Release | |
| needs: [validate, publish-cli, build-vscode, build-mac] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - name: Download Mac DMG | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: mac-release | |
| path: release-artifacts/ | |
| - name: Download VS Code extension | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: vscode-release | |
| path: release-artifacts/ | |
| - name: Generate release notes | |
| id: notes | |
| run: | | |
| PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") | |
| if [ -n "$PREV_TAG" ]; then | |
| FROM="$PREV_TAG" | |
| else | |
| FROM=$(git rev-list --max-parents=0 HEAD) | |
| fi | |
| npx tsx scripts/gen-release-notes.ts "$FROM" HEAD > release-notes.md | |
| cat release-notes.md | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: v${{ needs.validate.outputs.version }} | |
| body_path: release-notes.md | |
| prerelease: ${{ needs.validate.outputs.channel != 'stable' }} | |
| generate_release_notes: false | |
| files: | | |
| release-artifacts/*.dmg | |
| release-artifacts/*.vsix |