name: Release on: push: tags: - 'v*.*.*' # stable tags (e.g. v0.3.0) - 'v*.*.*-*' # pre-release tags (e.g. v0.3.0-rc.1, v0.3.0-beta.2) permissions: contents: write concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: electron: name: Electron — ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm - name: Install dependencies run: npm ci - name: Build extension + electron run: npm run compile && npm run compile:electron - name: Build + publish Electron installers run: npx electron-builder --publish always env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # macOS code signing — populated only on macOS runners that have the secrets set. # When the secrets are absent (e.g. on a fork or before the user sets them up), # electron-builder falls back to producing an unsigned, non-notarized build that # installs fine on first launch but cannot apply electron-updater updates. # See docs/releasing.md#setting-up-macos-code-signing. CSC_LINK: ${{ secrets.MAC_CERTS }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} vscode: name: VSCode extension (.vsix) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm - run: npm ci - name: Build extension run: npm run compile - name: Package .vsix run: npx vsce package --no-dependencies - name: Attach .vsix to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" VSIX=$(ls *.vsix | head -n 1) # The electron matrix jobs run in parallel — one of them creates # the GitHub Release on first `electron-builder --publish always`. # vsix often gets there first, so make this idempotent: if the # release doesn't exist yet, create it as a draft placeholder. # The release-notes job rewrites the body afterwards, and # electron-builder / release-notes flip off the draft state. if ! gh release view "$TAG" >/dev/null 2>&1; then gh release create "$TAG" --draft --title "$TAG" --notes "Build in progress..." fi gh release upload "$TAG" "$VSIX" --clobber - name: Publish to VSCode Marketplace if: ${{ env.VSCE_PAT != '' }} env: VSCE_PAT: ${{ secrets.VSCE_PAT }} run: npx vsce publish --packagePath *.vsix chrome: name: Chrome extension (.zip) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm cache-dependency-path: apps/chrome-extension/package-lock.json - name: Install run: npm ci working-directory: apps/chrome-extension - name: Build + zip run: npm run build && npm run zip working-directory: apps/chrome-extension - name: Attach zip to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" ZIP=$(ls apps/chrome-extension/dist-zip/*.zip | head -n 1) # Same idempotency dance as the vsix job — whichever job gets here # first creates the draft release; release-notes promotes it later. if ! gh release view "$TAG" >/dev/null 2>&1; then gh release create "$TAG" --draft --title "$TAG" --notes "Build in progress..." fi gh release upload "$TAG" "$ZIP" --clobber release-notes: name: Generate release notes from CHANGELOG runs-on: ubuntu-latest # Only gate on `electron` — that's the job whose first runner creates # the GitHub Release. The `vscode` job is decoupled so a vsix-side # failure (or a re-run) doesn't block notes from landing, and so # release-notes can be re-run independently to fix a wrong body. needs: [electron] steps: - name: Wait for the GitHub Release to exist env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" # `gh release view` returns nonzero for draft releases in some auth # contexts, so query the raw API which lists drafts unconditionally # for a token with the metadata scope. Poll for up to ~60s in case # the release record is still propagating after electron-builder's # final upload. for i in $(seq 1 30); do if gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=30" \ --jq ".[] | select(.tag_name == \"${TAG}\") | .id" \ | grep -q .; then echo "Release $TAG is visible." exit 0 fi echo "Waiting for release $TAG... ($i/30)" sleep 2 done echo "Release $TAG never showed up." >&2 exit 1 - uses: actions/checkout@v4 - name: Extract section for this version id: extract env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" VERSION="${TAG#v}" NOTES=$(awk -v v="$VERSION" ' BEGIN { capture = 0 } /^## \[/ { if (capture) exit if ($0 ~ "\\[" v "\\]") { capture = 1; next } } capture { print } ' CHANGELOG.md) if [ -z "$NOTES" ]; then NOTES="See CHANGELOG.md for details." fi { echo "notes<> "$GITHUB_OUTPUT" - name: Update release body env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" gh release edit "$TAG" --notes "${{ steps.extract.outputs.notes }}" - name: Mark as pre-release if tag has a hyphen, then promote out of draft env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" # Pre-release tags look like v0.3.0-rc.1 / v0.3.0-beta.2 — flip # the GitHub release's prerelease flag so the stable channel # poller (which uses /releases/latest) skips them automatically. if [[ "$TAG" == *-* ]]; then gh release edit "$TAG" --prerelease echo "Marked $TAG as pre-release" else gh release edit "$TAG" --prerelease=false echo "Marked $TAG as stable" fi # The vsix idempotency block (#269) creates the release as a draft, # and electron-builder uploads to it without flipping the draft off. # Promote here so the release is publicly visible without manual # `gh release edit --draft=false` after every tag (#306). gh release edit "$TAG" --draft=false echo "Promoted $TAG out of draft"