ci: release packages (#187) #14
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: Desktop Release | |
| on: | |
| push: | |
| tags: ['desktop-v*'] | |
| workflow_call: | |
| inputs: | |
| publish_nightly: | |
| description: Explicit permission for the reusable workflow to publish Nightly | |
| required: true | |
| type: boolean | |
| workflow_dispatch: | |
| inputs: | |
| channel: | |
| description: Update feed to rehearse without publishing | |
| required: true | |
| default: stable | |
| type: choice | |
| options: [stable, beta, nightly] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: desktop-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Stable and Beta publish from reviewed desktop-v* package versions. Nightly is | |
| # called only by the default-branch Nightly workflow and derives one version per | |
| # main commit. A manual run remains a non-publishing release rehearsal. | |
| jobs: | |
| # Create one draft before either platform uploads. It stays private until both | |
| # jobs and the final downloaded-asset validation succeed. | |
| prepare: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.resolve.outputs.version }} | |
| tag: ${{ steps.resolve.outputs.tag }} | |
| channel: ${{ steps.resolve.outputs.channel }} | |
| mac_manifest: ${{ steps.resolve.outputs.mac_manifest }} | |
| win_manifest: ${{ steps.resolve.outputs.win_manifest }} | |
| publish: ${{ steps.resolve.outputs.publish }} | |
| prerelease: ${{ steps.resolve.outputs.prerelease }} | |
| build: ${{ steps.release.outputs.build }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned from v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Resolve the desktop release | |
| id: resolve | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ inputs.publish_nightly && 'workflow_call' || github.event_name }} | |
| PUBLISH_NIGHTLY: ${{ inputs.publish_nightly }} | |
| REQUESTED_CHANNEL: ${{ inputs.channel }} | |
| TAG_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| package_version="$(node -p 'require("./apps/desktop/package.json").version')" | |
| commit_count="$(git rev-list --count HEAD)" | |
| node apps/desktop/scripts/desktop-release.mjs resolve \ | |
| "$EVENT_NAME" "$package_version" "${REQUESTED_CHANNEL:-}" "${TAG_NAME:-}" "$commit_count" \ | |
| "${PUBLISH_NIGHTLY:-false}" | |
| # A release built from a commit that never landed on main ships code no | |
| # review gate ever saw. `compare` reports `identical` or `behind` when the | |
| # commit is an ancestor of main, and `diverged`/`ahead` when it is not. | |
| - name: Require the release commit to be on main | |
| if: steps.resolve.outputs.publish == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| status="$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${GITHUB_SHA}" --jq '.status')" | |
| case "$status" in | |
| identical|behind) echo "${GITHUB_SHA} is on main (${status})." ;; | |
| *) | |
| echo "::error::${GITHUB_SHA} is not on main (compare status: ${status})." >&2 | |
| exit 1 ;; | |
| esac | |
| - name: Mint releases-repo token | |
| if: steps.resolve.outputs.publish == 'true' | |
| id: releases_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0 | |
| with: | |
| app-id: ${{ secrets.DESKTOP_RELEASES_APP_ID }} | |
| private-key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} | |
| owner: PyModel | |
| repositories: pythinker-desktop-releases | |
| permission-contents: write | |
| - name: Prepare the draft release or rehearsal | |
| id: release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.releases_token.outputs.token }} | |
| PUBLISH: ${{ steps.resolve.outputs.publish }} | |
| PRERELEASE: ${{ steps.resolve.outputs.prerelease }} | |
| RELEASE_CHANNEL: ${{ steps.resolve.outputs.channel }} | |
| RELEASE_TAG: ${{ steps.resolve.outputs.tag }} | |
| RELEASE_REPO: PyModel/pythinker-desktop-releases | |
| SOURCE_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PUBLISH" != 'true' ]; then | |
| echo 'build=true' >> "$GITHUB_OUTPUT" | |
| echo "Rehearsing ${RELEASE_CHANNEL} ${RELEASE_TAG}; nothing will be published." | |
| exit 0 | |
| fi | |
| # Drafts are safe to resume. A published release is immutable here: | |
| # platform uploads use --clobber and must never replace live assets. | |
| if release_json="$(gh release view "$RELEASE_TAG" --repo "$RELEASE_REPO" --json body,isDraft,isPrerelease 2>/dev/null)"; then | |
| is_draft="$(jq -r '.isDraft' <<< "$release_json")" | |
| is_prerelease="$(jq -r '.isPrerelease' <<< "$release_json")" | |
| if ! jq -e --arg source_url "$SOURCE_URL" '(.body // "") | contains($source_url)' >/dev/null <<< "$release_json"; then | |
| echo "::error::Release ${RELEASE_TAG} belongs to a different source commit." >&2 | |
| exit 1 | |
| fi | |
| if [ "$is_draft" != 'true' ]; then | |
| if [ "$RELEASE_CHANNEL" = 'nightly' ] && [ "$is_prerelease" = 'true' ]; then | |
| echo 'build=false' >> "$GITHUB_OUTPUT" | |
| echo "Nightly release ${RELEASE_TAG} already exists; no new main commit to publish." | |
| exit 0 | |
| fi | |
| echo "::error::Release ${RELEASE_TAG} already exists and is published; refusing to replace live assets." >&2 | |
| exit 1 | |
| fi | |
| if [ "$is_prerelease" != "$PRERELEASE" ]; then | |
| echo "::error::Draft ${RELEASE_TAG} has the wrong prerelease state." >&2 | |
| exit 1 | |
| fi | |
| echo 'build=true' >> "$GITHUB_OUTPUT" | |
| echo "Draft release ${RELEASE_TAG} already exists; resuming it." | |
| exit 0 | |
| fi | |
| args=( | |
| "$RELEASE_TAG" | |
| --repo "$RELEASE_REPO" | |
| --draft | |
| --title "$RELEASE_TAG" | |
| --notes "Pythinker Desktop ${RELEASE_TAG#v} (${RELEASE_CHANNEL} channel), built from ${SOURCE_URL}." | |
| ) | |
| if [ "$PRERELEASE" = 'true' ]; then args+=(--prerelease); fi | |
| gh release create "${args[@]}" | |
| echo 'build=true' >> "$GITHUB_OUTPUT" | |
| echo "Created draft release ${RELEASE_TAG}." | |
| mac: | |
| needs: prepare | |
| if: needs.prepare.outputs.build == 'true' | |
| runs-on: macos-15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned from v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # pinned from v6 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # pinned from v7.0.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Configure desktop release | |
| env: | |
| DESKTOP_CHANNEL: ${{ needs.prepare.outputs.channel }} | |
| DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node apps/desktop/scripts/desktop-release.mjs configure apps/desktop/package.json "$DESKTOP_VERSION" "$DESKTOP_CHANNEL" | |
| - name: Build workspace | |
| run: pnpm --workspace-root run build | |
| - name: Stage desktop runtime | |
| working-directory: apps/desktop | |
| run: node --import tsx scripts/stage-runtime.ts | |
| # An unset GitHub secret interpolates to an empty string, and | |
| # electron-builder resolves an empty CSC_LINK as a certificate path | |
| # (path.resolve(appDir, '') === appDir), failing with "not a file". | |
| # Export only the variables that carry a value. | |
| - name: Resolve macOS signing credentials | |
| shell: bash | |
| env: | |
| IN_CSC_LINK: ${{ secrets.MAC_CSC_LINK }} | |
| IN_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} | |
| IN_CSC_NAME: ${{ secrets.MAC_CSC_NAME }} | |
| IN_APPLE_ID: ${{ secrets.APPLE_ID }} | |
| IN_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| IN_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| IN_APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| IN_APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| IN_APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} | |
| run: | | |
| # A fixed heredoc marker lets a credential that happens to contain that | |
| # line close its own value early and turn the rest into environment | |
| # entries. Draw the delimiter at random so no secret can carry it. | |
| delimiter="EOF_$(openssl rand -hex 16)" | |
| for name in CSC_LINK CSC_KEY_PASSWORD CSC_NAME APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID APPLE_API_KEY_ID APPLE_API_ISSUER; do | |
| input="IN_${name}" | |
| value="${!input:-}" | |
| if [ -n "$value" ]; then printf '%s<<%s\n%s\n%s\n' "$name" "$delimiter" "$value" "$delimiter" >> "$GITHUB_ENV"; fi | |
| done | |
| # The App Store Connect key is held as base64 because it is a file, not a | |
| # string. notarytool and electron-builder both want a path, so materialize | |
| # it outside the workspace to keep it out of the packaged app. | |
| if [ -n "${IN_APPLE_API_KEY_P8:-}" ]; then | |
| key_path="${RUNNER_TEMP}/AuthKey.p8" | |
| printf '%s' "$IN_APPLE_API_KEY_P8" | base64 -d > "$key_path" | |
| chmod 600 "$key_path" | |
| echo "APPLE_API_KEY=${key_path}" >> "$GITHUB_ENV" | |
| fi | |
| if [ -z "${IN_CSC_LINK:-}" ]; then | |
| echo 'CSC_IDENTITY_AUTO_DISCOVERY=false' >> "$GITHUB_ENV" | |
| echo 'No macOS signing certificate configured; building unsigned.' | |
| fi | |
| # A build that quietly produces an unsigned app is worse than a failed | |
| # one: macOS rejects unsigned updates, so it ships a release users cannot | |
| # install or update from. Manual runs are held to the same bar, which is | |
| # what makes them a rehearsal rather than a smoke test. | |
| - name: Require signing | |
| working-directory: apps/desktop | |
| run: node --import tsx scripts/assert-release-signing.ts | |
| - name: Package desktop release | |
| working-directory: apps/desktop | |
| run: pnpm exec electron-builder --mac dmg zip --publish never | |
| - name: Verify macOS update artifacts | |
| shell: bash | |
| env: | |
| DESKTOP_CHANNEL: ${{ needs.prepare.outputs.channel }} | |
| DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} | |
| UPDATE_MANIFEST: ${{ needs.prepare.outputs.mac_manifest }} | |
| run: | | |
| set -euo pipefail | |
| app_bundle="$(find apps/desktop/dist -maxdepth 2 -type d -name '*.app' -print -quit)" | |
| if [ -z "$app_bundle" ]; then | |
| echo 'macOS application bundle not found' >&2 | |
| exit 1 | |
| fi | |
| test -f "$app_bundle/Contents/Resources/app-update.yml" | |
| node --import tsx apps/desktop/scripts/verify-update-manifest.ts mac apps/desktop/dist "$DESKTOP_VERSION" "$DESKTOP_CHANNEL" | |
| test -f "apps/desktop/dist/${UPDATE_MANIFEST}" | |
| - name: Verify macOS signatures and notarization | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| verify_app() { | |
| codesign --verify --deep --strict --verbose=2 "$1" | |
| spctl --assess --type execute --verbose=2 "$1" | |
| } | |
| app_bundle="$(find apps/desktop/dist -maxdepth 2 -type d -name '*.app' -print -quit)" | |
| zip_path="$(find apps/desktop/dist -maxdepth 1 -type f -name '*-mac.zip' -print -quit)" | |
| dmg_path="$(find apps/desktop/dist -maxdepth 1 -type f -name '*.dmg' -print -quit)" | |
| if [ -z "$app_bundle" ] || [ -z "$zip_path" ] || [ -z "$dmg_path" ]; then | |
| echo 'Complete macOS release artifacts were not found' >&2 | |
| exit 1 | |
| fi | |
| verify_app "$app_bundle" | |
| zip_dir="${RUNNER_TEMP}/macos-zip" | |
| mkdir "$zip_dir" | |
| ditto -x -k "$zip_path" "$zip_dir" | |
| zip_app="$(find "$zip_dir" -maxdepth 2 -type d -name '*.app' -print -quit)" | |
| if [ -z "$zip_app" ]; then | |
| echo 'macOS ZIP does not contain an application bundle' >&2 | |
| exit 1 | |
| fi | |
| verify_app "$zip_app" | |
| hdiutil verify "$dmg_path" | |
| mount_point="${RUNNER_TEMP}/macos-dmg" | |
| mkdir "$mount_point" | |
| hdiutil attach "$dmg_path" -readonly -nobrowse -mountpoint "$mount_point" | |
| cleanup_mount() { hdiutil detach "$mount_point" || true; } | |
| trap cleanup_mount EXIT | |
| dmg_app="$(find "$mount_point" -maxdepth 2 -type d -name '*.app' -print -quit)" | |
| if [ -z "$dmg_app" ]; then | |
| echo 'macOS DMG does not contain an application bundle' >&2 | |
| exit 1 | |
| fi | |
| verify_app "$dmg_app" | |
| # electron-builder notarizes and staples the .app, then packs the | |
| # already-stapled bundle into the disk image; the image itself never | |
| # receives a ticket. Validate the staple where it actually lives. | |
| xcrun stapler validate "$dmg_app" | |
| cleanup_mount | |
| trap - EXIT | |
| - name: Mint releases-repo token | |
| if: needs.prepare.outputs.publish == 'true' | |
| id: releases_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0 | |
| with: | |
| app-id: ${{ secrets.DESKTOP_RELEASES_APP_ID }} | |
| private-key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} | |
| owner: PyModel | |
| repositories: pythinker-desktop-releases | |
| permission-contents: write | |
| - name: Upload verified macOS release assets | |
| if: needs.prepare.outputs.publish == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.releases_token.outputs.token }} | |
| UPDATE_MANIFEST: ${{ needs.prepare.outputs.mac_manifest }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.tag }} | |
| RELEASE_REPO: PyModel/pythinker-desktop-releases | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| assets=(apps/desktop/dist/*.dmg apps/desktop/dist/*-mac.zip apps/desktop/dist/*.blockmap "apps/desktop/dist/${UPDATE_MANIFEST}") | |
| gh release upload "$RELEASE_TAG" "${assets[@]}" --repo "$RELEASE_REPO" --clobber | |
| - name: Upload macOS artifacts for manual runs | |
| if: needs.prepare.outputs.publish != 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pinned from v7 | |
| with: | |
| name: desktop-macos | |
| path: | | |
| apps/desktop/dist/*.dmg | |
| apps/desktop/dist/*.zip | |
| apps/desktop/dist/*.yml | |
| if-no-files-found: error | |
| windows: | |
| needs: prepare | |
| if: needs.prepare.outputs.build == 'true' | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned from v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # pinned from v6 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # pinned from v7.0.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Configure desktop release | |
| env: | |
| DESKTOP_CHANNEL: ${{ needs.prepare.outputs.channel }} | |
| DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node apps/desktop/scripts/desktop-release.mjs configure apps/desktop/package.json "$DESKTOP_VERSION" "$DESKTOP_CHANNEL" | |
| - name: Build workspace | |
| run: pnpm --workspace-root run build | |
| - name: Stage desktop runtime | |
| working-directory: apps/desktop | |
| run: node --import tsx scripts/stage-runtime.ts | |
| # Only non-empty WIN_CSC_* signing secrets are exported. Without them the | |
| # artifacts are unsigned and installers trigger a SmartScreen warning, so | |
| # the credential check below fails the run rather than shipping one. | |
| - name: Resolve Windows signing credentials | |
| shell: bash | |
| env: | |
| IN_WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }} | |
| IN_WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} | |
| IN_WINDOWS_SIGNING_PUBLISHER_NAME: ${{ secrets.WINDOWS_SIGNING_PUBLISHER_NAME }} | |
| run: | | |
| delimiter="EOF_$(openssl rand -hex 16)" | |
| for name in WIN_CSC_LINK WIN_CSC_KEY_PASSWORD WINDOWS_SIGNING_PUBLISHER_NAME; do | |
| input="IN_${name}" | |
| value="${!input:-}" | |
| if [ -n "$value" ]; then printf '%s<<%s\n%s\n%s\n' "$name" "$delimiter" "$value" "$delimiter" >> "$GITHUB_ENV"; fi | |
| done | |
| - name: Resolve Azure signing configuration | |
| shell: bash | |
| env: | |
| IN_AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| IN_AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| IN_AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} | |
| IN_AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} | |
| IN_AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} | |
| IN_AZURE_SIGNING_CERT_PROFILE: ${{ secrets.AZURE_SIGNING_CERT_PROFILE }} | |
| IN_AZURE_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_SIGNING_PUBLISHER_NAME }} | |
| run: | | |
| delimiter="EOF_$(openssl rand -hex 16)" | |
| for name in AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_SIGNING_ENDPOINT AZURE_SIGNING_ACCOUNT AZURE_SIGNING_CERT_PROFILE AZURE_SIGNING_PUBLISHER_NAME; do | |
| input="IN_${name}" | |
| value="${!input:-}" | |
| if [ -n "$value" ]; then printf '%s<<%s\n%s\n%s\n' "$name" "$delimiter" "$value" "$delimiter" >> "$GITHUB_ENV"; fi | |
| done | |
| - name: Require signing | |
| working-directory: apps/desktop | |
| run: node --import tsx scripts/assert-windows-release-signing.ts | |
| - name: Package desktop release | |
| working-directory: apps/desktop | |
| run: node --import tsx scripts/package-win.ts --publish never | |
| - name: Verify Windows update artifacts | |
| shell: bash | |
| env: | |
| DESKTOP_CHANNEL: ${{ needs.prepare.outputs.channel }} | |
| DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} | |
| UPDATE_MANIFEST: ${{ needs.prepare.outputs.win_manifest }} | |
| run: | | |
| set -euo pipefail | |
| test -f apps/desktop/dist/win-unpacked/resources/app-update.yml | |
| node --import tsx apps/desktop/scripts/verify-update-manifest.ts win apps/desktop/dist "$DESKTOP_VERSION" "$DESKTOP_CHANNEL" | |
| test -f "apps/desktop/dist/${UPDATE_MANIFEST}" | |
| - name: Verify Windows release signatures | |
| working-directory: apps/desktop | |
| run: node --import tsx scripts/verify-windows-signatures.ts | |
| - name: Mint releases-repo token | |
| if: needs.prepare.outputs.publish == 'true' | |
| id: releases_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0 | |
| with: | |
| app-id: ${{ secrets.DESKTOP_RELEASES_APP_ID }} | |
| private-key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} | |
| owner: PyModel | |
| repositories: pythinker-desktop-releases | |
| permission-contents: write | |
| - name: Upload verified Windows release assets | |
| if: needs.prepare.outputs.publish == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.releases_token.outputs.token }} | |
| UPDATE_MANIFEST: ${{ needs.prepare.outputs.win_manifest }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.tag }} | |
| RELEASE_REPO: PyModel/pythinker-desktop-releases | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| assets=(apps/desktop/dist/*.exe apps/desktop/dist/*.blockmap "apps/desktop/dist/${UPDATE_MANIFEST}") | |
| gh release upload "$RELEASE_TAG" "${assets[@]}" --repo "$RELEASE_REPO" --clobber | |
| - name: Upload Windows artifacts for manual runs | |
| if: needs.prepare.outputs.publish != 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pinned from v7 | |
| with: | |
| name: desktop-windows | |
| path: | | |
| apps/desktop/dist/*.exe | |
| apps/desktop/dist/*.yml | |
| if-no-files-found: error | |
| # Publishing last, and only once both platforms uploaded, is what makes the | |
| # release atomic. A platform that fails leaves a draft nobody can download, | |
| # which is recoverable by re-running that job; publishing per-platform instead | |
| # would leave a live release missing an operating system. | |
| publish: | |
| if: needs.prepare.outputs.publish == 'true' && needs.prepare.outputs.build == 'true' | |
| needs: [prepare, mac, windows] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned from v4 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # pinned from v6 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # pinned from v7.0.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Mint releases-repo token | |
| id: releases_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0 | |
| with: | |
| app-id: ${{ secrets.DESKTOP_RELEASES_APP_ID }} | |
| private-key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} | |
| owner: PyModel | |
| repositories: pythinker-desktop-releases | |
| permission-contents: write | |
| - name: Download and validate every release asset | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.releases_token.outputs.token }} | |
| DESKTOP_CHANNEL: ${{ needs.prepare.outputs.channel }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.tag }} | |
| RELEASE_REPO: PyModel/pythinker-desktop-releases | |
| DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| mkdir release-assets | |
| gh release download "$RELEASE_TAG" --repo "$RELEASE_REPO" --dir release-assets | |
| node --import tsx apps/desktop/scripts/verify-update-manifest.ts mac release-assets "$DESKTOP_VERSION" "$DESKTOP_CHANNEL" | |
| node --import tsx apps/desktop/scripts/verify-update-manifest.ts win release-assets "$DESKTOP_VERSION" "$DESKTOP_CHANNEL" | |
| - name: Publish the release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.releases_token.outputs.token }} | |
| EXPECTED_PRERELEASE: ${{ needs.prepare.outputs.prerelease }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.tag }} | |
| RELEASE_REPO: PyModel/pythinker-desktop-releases | |
| run: | | |
| set -euo pipefail | |
| gh release edit "$RELEASE_TAG" --repo "$RELEASE_REPO" --draft=false | |
| state="$(gh release view "$RELEASE_TAG" --repo "$RELEASE_REPO" --json isDraft,isPrerelease --jq '[.isDraft, .isPrerelease] | @tsv')" | |
| IFS=$'\t' read -r is_draft is_prerelease <<< "$state" | |
| if [ "$is_draft" != 'false' ] || [ "$is_prerelease" != "$EXPECTED_PRERELEASE" ]; then | |
| echo "::error::Published ${RELEASE_TAG} has the wrong release state." >&2 | |
| exit 1 | |
| fi | |
| echo "Published ${RELEASE_TAG}." |