Release desktop apps #8
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
| # Release pipeline (see docs/RELEASING.md for the maintainer guide). | |
| # | |
| # Two ways to ship, one pipeline: | |
| # 1. Push a tag like v0.2.0 (usually via `pnpm release 0.2.0`). | |
| # 2. GitHub UI: Actions β "Release desktop apps" β Run workflow β enter the | |
| # version (e.g. 0.2.0). The prepare job then bumps both package.json | |
| # files if needed, commits, and pushes the tag itself. GITHUB_TOKEN tag | |
| # pushes do not re-trigger this workflow, so the dispatched run carries | |
| # the release end to end. Leave the version empty for a build rehearsal | |
| # that publishes nothing. | |
| # | |
| # Shape: draft-first, publish-last. A draft GitHub Release (auto-generated | |
| # changelog + install instructions from .github/release-notes-header.md) is | |
| # created before any build starts, the three platform jobs upload assets into | |
| # that draft, and only after every asset and the checksum file are in place is | |
| # the release flipped to published. A failed run therefore never leaves a | |
| # public, half-uploaded release β just a draft that the next attempt reuses | |
| # (uploads use --clobber, so re-running failed jobs is always safe). | |
| name: Release desktop apps | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to release (e.g. 0.2.0). Leave empty to rehearse the build without releasing." | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-desktop-${{ inputs.version || github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| prepare: | |
| name: Prepare release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| is_release: ${{ steps.ctx.outputs.is_release }} | |
| tag: ${{ steps.ctx.outputs.tag }} | |
| build_ref: ${{ steps.ctx.outputs.build_ref }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Resolve release context (validate tag, or bump + tag on dispatch) | |
| id: ctx | |
| env: | |
| INPUT_VERSION: ${{ inputs.version }} | |
| run: | | |
| set -euo pipefail | |
| is_release=false | |
| tag="" | |
| build_ref="$GITHUB_SHA" | |
| if [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| version="$(node -p 'require("./package.json").version')" | |
| if [ "v${version}" != "$GITHUB_REF_NAME" ]; then | |
| echo "::error::Tag $GITHUB_REF_NAME does not match package.json version ${version}. Bump the version (pnpm release) before tagging." >&2 | |
| exit 1 | |
| fi | |
| is_release=true | |
| tag="$GITHUB_REF_NAME" | |
| build_ref="$GITHUB_REF_NAME" | |
| elif [ -n "${INPUT_VERSION}" ]; then | |
| if ! printf '%s' "$INPUT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "::error::Version must look like 0.2.0 (got: ${INPUT_VERSION})." >&2 | |
| exit 1 | |
| fi | |
| tag="v${INPUT_VERSION}" | |
| if git ls-remote --exit-code origin "refs/tags/${tag}" >/dev/null 2>&1; then | |
| echo "::error::Tag ${tag} already exists. Pick a new version, or delete the old tag first (see docs/RELEASING.md)." >&2 | |
| exit 1 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| current="$(node -p 'require("./package.json").version')" | |
| if [ "$current" != "$INPUT_VERSION" ]; then | |
| node scripts/set-version.mjs "$INPUT_VERSION" | |
| git add package.json electron/package.json | |
| git commit -m "release: ${tag}" | |
| git push origin "HEAD:$GITHUB_REF_NAME" | |
| fi | |
| git tag -a "${tag}" -m "Codex Slides ${INPUT_VERSION}" | |
| git push origin "refs/tags/${tag}" | |
| is_release=true | |
| build_ref="${tag}" | |
| fi | |
| { | |
| echo "is_release=${is_release}" | |
| echo "tag=${tag}" | |
| echo "build_ref=${build_ref}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Ensure draft release exists | |
| if: steps.ctx.outputs.is_release == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.ctx.outputs.tag }} | |
| run: | | |
| if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| echo "Release $TAG already exists; reusing it." | |
| exit 0 | |
| fi | |
| gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" -f tag_name="$TAG" --jq .body > changelog.md | |
| cat .github/release-notes-header.md changelog.md > notes.md | |
| gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --draft \ | |
| --title "Codex Slides ${TAG#v}" --notes-file notes.md | |
| build: | |
| name: Build ${{ matrix.platform }} | |
| needs: prepare | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: mac | |
| os: macos-latest | |
| builder_args: --mac --x64 --arm64 | |
| release_files: dist/*.dmg dist/*.zip | |
| - platform: windows | |
| os: windows-latest | |
| builder_args: --win --x64 | |
| release_files: dist/*.exe | |
| - platform: linux | |
| os: ubuntu-latest | |
| builder_args: --linux AppImage deb --x64 | |
| release_files: dist/*.AppImage dist/*.deb | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.prepare.outputs.build_ref }} | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 11.9.0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Verify desktop contract | |
| run: pnpm electron:check | |
| - name: Build standalone web server (includes server smoke test) | |
| run: pnpm build:desktop | |
| - name: Package desktop app | |
| shell: bash | |
| run: pnpm exec electron-builder ${{ matrix.builder_args }} --publish never | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CSC_IDENTITY_AUTO_DISCOVERY: "false" | |
| - name: Smoke test packaged runtime | |
| run: node scripts/smoke-server.mjs --packaged | |
| - name: Upload release assets | |
| if: needs.prepare.outputs.is_release == 'true' | |
| shell: bash | |
| run: gh release upload "$TAG" ${{ matrix.release_files }} --clobber --repo "$GITHUB_REPOSITORY" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| publish: | |
| name: Publish GitHub Release | |
| if: needs.prepare.outputs.is_release == 'true' | |
| needs: [prepare, build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate SHA-256 checksums | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| run: | | |
| gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets \ | |
| --jq '.assets[] | select(.name != "SHA256SUMS.txt" and .digest != null) | "\(.digest | sub("^sha256:"; "")) \(.name)"' \ | |
| | sort > SHA256SUMS.txt | |
| # 2 dmg + 2 zip (mac arm64/x64) + 1 exe + 1 AppImage + 1 deb | |
| test "$(wc -l < SHA256SUMS.txt)" -eq 7 | |
| gh release upload "$TAG" SHA256SUMS.txt --clobber --repo "$GITHUB_REPOSITORY" | |
| - name: Publish release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| run: gh release edit "$TAG" --draft=false --latest --repo "$GITHUB_REPOSITORY" |