diff --git a/.github/actions/resolve-release/action.yml b/.github/actions/resolve-release/action.yml new file mode 100644 index 0000000000..01e92a0df6 --- /dev/null +++ b/.github/actions/resolve-release/action.yml @@ -0,0 +1,73 @@ +name: Resolve Release +description: >- + Resolve a bump workflow's target tag and that release's real published state. + Companion to assert-stable-release: this one FETCHES the facts, that one + ENFORCES them. Split so the enforcement stays a pure function of its inputs. + + Handles both entry points of the bump workflows: + - workflow_run -> tag comes from the triggering run's head_branch + - workflow_dispatch (manual recovery) -> tag comes from the input + In both cases draft/prerelease are read back from the GitHub API rather than + inferred, so a draft or prerelease can never slip through to a third-party + package repo just because the trigger payload lacked the flags. + +inputs: + tag: + description: "Release tag to resolve (e.g. vX.Y.Z)" + required: true + github_token: + description: "Token used to read the release via the GH API" + required: true + +outputs: + tag: + description: "The resolved tag, verbatim (e.g. v1.13.1)" + value: ${{ steps.resolve.outputs.tag }} + ver: + description: "The tag with the leading 'v' stripped (e.g. 1.13.1)" + value: ${{ steps.resolve.outputs.ver }} + is_prerelease: + description: "'true' if the GH Release is flagged prerelease" + value: ${{ steps.resolve.outputs.is_prerelease }} + is_draft: + description: "'true' if the GH Release is still a draft" + value: ${{ steps.resolve.outputs.is_draft }} + +runs: + using: composite + steps: + - name: Resolve tag and release state + id: resolve + shell: bash + env: + TAG: ${{ inputs.tag }} + GH_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + if [[ -z "$TAG" ]]; then + echo "::error::No tag to resolve (neither workflow_run.head_branch nor the dispatch input was set)" + exit 1 + fi + + # A missing release here is a real fault, not something to paper over: + # every caller is about to publish this version to an external package + # manager. Fail loudly and let the caller's Report-failure step file it. + if ! META=$(gh release view "$TAG" --repo "$REPO" --json isDraft,isPrerelease 2>&1); then + echo "::error::No GH Release found for tag $TAG in $REPO -- refusing to bump" + echo "$META" + exit 1 + fi + + IS_DRAFT=$(echo "$META" | jq -r '.isDraft') + IS_PRERELEASE=$(echo "$META" | jq -r '.isPrerelease') + + { + echo "tag=$TAG" + echo "ver=${TAG#v}" + echo "is_draft=$IS_DRAFT" + echo "is_prerelease=$IS_PRERELEASE" + } >> "$GITHUB_OUTPUT" + + echo "resolved tag=$TAG ver=${TAG#v} draft=$IS_DRAFT prerelease=$IS_PRERELEASE" diff --git a/.github/workflows/bump-homebrew-formula.yml b/.github/workflows/bump-homebrew-formula.yml index d229d22eea..53c7ae42af 100644 --- a/.github/workflows/bump-homebrew-formula.yml +++ b/.github/workflows/bump-homebrew-formula.yml @@ -19,9 +19,14 @@ name: Bump Homebrew Formula (amy CLI) # auto-bump here (symmetric to the cask action in bump-homebrew.yml) — see the # "TODO(bootstrap)" note at the bottom of this file. +# Trigger: after "Create Release Assets" succeeds for a tag push. NOT +# `release: types: [released]` — that event never fires, because the release is +# created by create-release.yml under GITHUB_TOKEN and GitHub suppresses +# workflow-triggering events for it. See the full note in bump-homebrew.yml. on: - release: - types: [released] + workflow_run: + workflows: ["Create Release Assets"] + types: [completed] workflow_dispatch: inputs: tag: @@ -38,44 +43,49 @@ permissions: concurrency: # Serialize per tag; do not cancel in-progress runs. - group: bump-homebrew-formula-${{ github.event.release.tag_name || inputs.tag }} + group: bump-homebrew-formula-${{ github.event.workflow_run.head_branch || inputs.tag }} cancel-in-progress: false jobs: sync-formula: - if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false + # See bump-homebrew.yml for why these three conditions: successful, tag-push + # (not a dry-run dispatch), v-prefixed. Exact format enforced downstream. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v7 + - name: Resolve release + id: rel + uses: ./.github/actions/resolve-release + with: + tag: ${{ github.event.workflow_run.head_branch || inputs.tag }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Re-assert stable release uses: ./.github/actions/assert-stable-release with: - tag: ${{ github.event.release.tag_name || inputs.tag }} - is_prerelease: ${{ github.event.release.prerelease || 'false' }} - is_draft: ${{ github.event.release.draft || 'false' }} - - - name: Resolve version - id: ver - run: | - set -euo pipefail - TAG="${{ github.event.release.tag_name || inputs.tag }}" - VER="${TAG#v}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "ver=$VER" >> "$GITHUB_OUTPUT" + tag: ${{ steps.rel.outputs.tag }} + is_prerelease: ${{ steps.rel.outputs.is_prerelease }} + is_draft: ${{ steps.rel.outputs.is_draft }} - name: Download jvm bundle and compute sha256 id: asset run: | set -euo pipefail - TAG="${{ steps.ver.outputs.tag }}" - VER="${{ steps.ver.outputs.ver }}" + TAG="${{ steps.rel.outputs.tag }}" + VER="${{ steps.rel.outputs.ver }}" URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/amy-${VER}-jvm.tar.gz" echo "Fetching $URL" - # The `released` event can fire a hair before every matrix leg finishes - # uploading; retry with backoff (mirrors the repo's push/pull retry ethos). + # workflow_run fires only after every upload leg has finished, so the + # asset should already be there. Retry anyway for release-CDN + # propagation (mirrors the repo's push/pull retry ethos). ok=0 for i in 1 2 3 4 5; do if curl -fsSL -o amy-jvm.tar.gz "$URL"; then ok=1; break; fi @@ -110,13 +120,13 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} base: main - branch: chore/bump-amy-formula-${{ steps.ver.outputs.tag }} + branch: chore/bump-amy-formula-${{ steps.rel.outputs.tag }} add-paths: cli/packaging/homebrew/amy.rb - commit-message: 'chore: sync amy Homebrew formula to ${{ steps.ver.outputs.tag }}' - title: 'chore: sync amy Homebrew formula to ${{ steps.ver.outputs.tag }}' + commit-message: 'chore: sync amy Homebrew formula to ${{ steps.rel.outputs.tag }}' + title: 'chore: sync amy Homebrew formula to ${{ steps.rel.outputs.tag }}' body: | Auto-synced `cli/packaging/homebrew/amy.rb` to the - `${{ steps.ver.outputs.tag }}` release: + `${{ steps.rel.outputs.tag }}` release: - `url` -> `${{ steps.asset.outputs.url }}` - `sha256` -> `${{ steps.asset.outputs.sha256 }}` @@ -136,7 +146,7 @@ jobs: uses: actions/github-script@v9 with: script: | - const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown'; + const tag = context.payload.workflow_run?.head_branch || context.payload.inputs?.tag || 'unknown'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; await github.rest.issues.create({ owner: context.repo.owner, diff --git a/.github/workflows/bump-homebrew-geode-formula.yml b/.github/workflows/bump-homebrew-geode-formula.yml index b55d926245..3c38519c64 100644 --- a/.github/workflows/bump-homebrew-geode-formula.yml +++ b/.github/workflows/bump-homebrew-geode-formula.yml @@ -17,9 +17,14 @@ name: Bump Homebrew Formula (geode relay) # human-reviewed new-formula PR (the one-time bootstrap). Once it lands, wire the # auto-bump here — see the "TODO(bootstrap)" note at the bottom of this file. +# Trigger: after "Create Release Assets" succeeds for a tag push. NOT +# `release: types: [released]` — that event never fires, because the release is +# created by create-release.yml under GITHUB_TOKEN and GitHub suppresses +# workflow-triggering events for it. See the full note in bump-homebrew.yml. on: - release: - types: [released] + workflow_run: + workflows: ["Create Release Assets"] + types: [completed] workflow_dispatch: inputs: tag: @@ -36,44 +41,49 @@ permissions: concurrency: # Serialize per tag; do not cancel in-progress runs. - group: bump-homebrew-geode-formula-${{ github.event.release.tag_name || inputs.tag }} + group: bump-homebrew-geode-formula-${{ github.event.workflow_run.head_branch || inputs.tag }} cancel-in-progress: false jobs: sync-formula: - if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false + # See bump-homebrew.yml for why these three conditions: successful, tag-push + # (not a dry-run dispatch), v-prefixed. Exact format enforced downstream. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v7 + - name: Resolve release + id: rel + uses: ./.github/actions/resolve-release + with: + tag: ${{ github.event.workflow_run.head_branch || inputs.tag }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Re-assert stable release uses: ./.github/actions/assert-stable-release with: - tag: ${{ github.event.release.tag_name || inputs.tag }} - is_prerelease: ${{ github.event.release.prerelease || 'false' }} - is_draft: ${{ github.event.release.draft || 'false' }} - - - name: Resolve version - id: ver - run: | - set -euo pipefail - TAG="${{ github.event.release.tag_name || inputs.tag }}" - VER="${TAG#v}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "ver=$VER" >> "$GITHUB_OUTPUT" + tag: ${{ steps.rel.outputs.tag }} + is_prerelease: ${{ steps.rel.outputs.is_prerelease }} + is_draft: ${{ steps.rel.outputs.is_draft }} - name: Download jvm bundle and compute sha256 id: asset run: | set -euo pipefail - TAG="${{ steps.ver.outputs.tag }}" - VER="${{ steps.ver.outputs.ver }}" + TAG="${{ steps.rel.outputs.tag }}" + VER="${{ steps.rel.outputs.ver }}" URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/geode-${VER}-jvm.tar.gz" echo "Fetching $URL" - # The `released` event can fire a hair before every matrix leg finishes - # uploading; retry with backoff (mirrors the repo's push/pull retry ethos). + # workflow_run fires only after every upload leg has finished, so the + # asset should already be there. Retry anyway for release-CDN + # propagation (mirrors the repo's push/pull retry ethos). ok=0 for i in 1 2 3 4 5; do if curl -fsSL -o geode-jvm.tar.gz "$URL"; then ok=1; break; fi @@ -108,13 +118,13 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} base: main - branch: chore/bump-geode-formula-${{ steps.ver.outputs.tag }} + branch: chore/bump-geode-formula-${{ steps.rel.outputs.tag }} add-paths: geode/packaging/homebrew/geode.rb - commit-message: 'chore: sync geode Homebrew formula to ${{ steps.ver.outputs.tag }}' - title: 'chore: sync geode Homebrew formula to ${{ steps.ver.outputs.tag }}' + commit-message: 'chore: sync geode Homebrew formula to ${{ steps.rel.outputs.tag }}' + title: 'chore: sync geode Homebrew formula to ${{ steps.rel.outputs.tag }}' body: | Auto-synced `geode/packaging/homebrew/geode.rb` to the - `${{ steps.ver.outputs.tag }}` release: + `${{ steps.rel.outputs.tag }}` release: - `url` -> `${{ steps.asset.outputs.url }}` - `sha256` -> `${{ steps.asset.outputs.sha256 }}` @@ -134,7 +144,7 @@ jobs: uses: actions/github-script@v9 with: script: | - const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown'; + const tag = context.payload.workflow_run?.head_branch || context.payload.inputs?.tag || 'unknown'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; await github.rest.issues.create({ owner: context.repo.owner, diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml index 257b83d5a2..f2979658bb 100644 --- a/.github/workflows/bump-homebrew.yml +++ b/.github/workflows/bump-homebrew.yml @@ -1,11 +1,25 @@ name: Bump Homebrew Cask -# Fires when a GH Release is published (not draft, not prerelease). -# `release.types: [released]` event fires only for stable releases — still -# double-checked by .github/actions/assert-stable-release for defense-in-depth. +# Fires after "Create Release Assets" finishes successfully for a tag push. +# +# NOT `release: types: [released]`. That event never fires here: the release is +# created by create-release.yml using GITHUB_TOKEN, and GitHub deliberately does +# not raise workflow-triggering events for actions taken by GITHUB_TOKEN (the +# recursion guard). This workflow sat silently dead through every release up to +# v1.13.1 for exactly that reason. +# +# `workflow_run` has no such restriction, and it is also strictly better timed: +# it fires once ALL upload legs have finished, whereas `released` fired while +# assets were still uploading (hence the download retry loops in the sibling +# formula workflows). +# +# Draft/prerelease state is not in the workflow_run payload, so it is read back +# from the API by .github/actions/resolve-release and enforced by +# .github/actions/assert-stable-release. on: - release: - types: [released] + workflow_run: + workflows: ["Create Release Assets"] + types: [completed] workflow_dispatch: inputs: tag: @@ -22,39 +36,72 @@ permissions: concurrency: # Serialize bumps per tag; do not cancel in-progress bumps. - group: bump-homebrew-${{ github.event.release.tag_name || inputs.tag }} + group: bump-homebrew-${{ github.event.workflow_run.head_branch || inputs.tag }} cancel-in-progress: false jobs: bump: - if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false + # On workflow_run, filter down to a SUCCESSFUL TAG build. `event == 'push'` + # excludes create-release.yml's workflow_dispatch dry-runs (which carry a + # synthetic test_tag and publish nothing), and the head_branch prefix keeps + # non-tag runs out. Exact tag format is still enforced downstream. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest # brew runs on Linux — saves macOS runner quota timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v7 + - name: Resolve release + id: rel + uses: ./.github/actions/resolve-release + with: + tag: ${{ github.event.workflow_run.head_branch || inputs.tag }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Re-assert stable release uses: ./.github/actions/assert-stable-release with: - tag: ${{ github.event.release.tag_name || inputs.tag }} - is_prerelease: ${{ github.event.release.prerelease || 'false' }} - is_draft: ${{ github.event.release.draft || 'false' }} + tag: ${{ steps.rel.outputs.tag }} + is_prerelease: ${{ steps.rel.outputs.is_prerelease }} + is_draft: ${{ steps.rel.outputs.is_draft }} + + # `brew bump-cask-pr` can only bump a cask that ALREADY EXISTS in the tap. + # `amethyst-nostr` has never been submitted to Homebrew/homebrew-cask, so + # until the one-time bootstrap PR lands (BUILDING.md § Bootstrap) this + # workflow must no-op rather than fail — otherwise every single release + # files a spurious [release-ops] issue. + - name: Check the cask exists in homebrew-cask + id: cask + run: | + set -euo pipefail + if curl -fsSL -o /dev/null https://formulae.brew.sh/api/cask/amethyst-nostr.json; then + echo "bootstrapped=true" >> "$GITHUB_OUTPUT" + echo "cask amethyst-nostr found in homebrew-cask; proceeding with bump" + else + echo "bootstrapped=false" >> "$GITHUB_OUTPUT" + echo "::warning::Cask 'amethyst-nostr' is not in Homebrew/homebrew-cask yet, so there is nothing to bump for ${{ steps.rel.outputs.tag }}. Amethyst is NOT shipping via Homebrew. Submit the one-time new-cask PR (BUILDING.md § Bootstrap) to activate this channel." + fi - name: Bump cask (push-or-update PR) + if: steps.cask.outputs.bootstrapped == 'true' uses: macauley/action-homebrew-bump-cask@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 with: token: ${{ secrets.HOMEBREW_TOKEN }} tap: homebrew/cask cask: amethyst-nostr - tag: ${{ github.event.release.tag_name || inputs.tag }} + tag: ${{ steps.rel.outputs.tag }} - name: Report failure if: failure() uses: actions/github-script@v9 with: script: | - const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown'; + const tag = context.payload.workflow_run?.head_branch || context.payload.inputs?.tag || 'unknown'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; await github.rest.issues.create({ owner: context.repo.owner, diff --git a/.github/workflows/bump-winget.yml b/.github/workflows/bump-winget.yml index 0c00610d56..c50965e4e5 100644 --- a/.github/workflows/bump-winget.yml +++ b/.github/workflows/bump-winget.yml @@ -1,8 +1,16 @@ name: Bump Winget Manifest +# Fires after "Create Release Assets" finishes successfully for a tag push. +# +# NOT `release: types: [released]`. That event never fires here: the release is +# created by create-release.yml using GITHUB_TOKEN, and GitHub does not raise +# workflow-triggering events for GITHUB_TOKEN actions. This workflow sat +# silently dead through every release up to v1.13.1 for exactly that reason. +# See the longer note in bump-homebrew.yml. on: - release: - types: [released] + workflow_run: + workflows: ["Create Release Assets"] + types: [completed] workflow_dispatch: inputs: tag: @@ -18,30 +26,62 @@ permissions: issues: write concurrency: - group: bump-winget-${{ github.event.release.tag_name || inputs.tag }} + group: bump-winget-${{ github.event.workflow_run.head_branch || inputs.tag }} cancel-in-progress: false jobs: bump: - if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false + # See bump-homebrew.yml for why these three conditions: successful, tag-push + # (not a dry-run dispatch), v-prefixed. Exact format enforced downstream. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: windows-latest timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v7 + - name: Resolve release + id: rel + uses: ./.github/actions/resolve-release + with: + tag: ${{ github.event.workflow_run.head_branch || inputs.tag }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Re-assert stable release uses: ./.github/actions/assert-stable-release with: - tag: ${{ github.event.release.tag_name || inputs.tag }} - is_prerelease: ${{ github.event.release.prerelease || 'false' }} - is_draft: ${{ github.event.release.draft || 'false' }} + tag: ${{ steps.rel.outputs.tag }} + is_prerelease: ${{ steps.rel.outputs.is_prerelease }} + is_draft: ${{ steps.rel.outputs.is_draft }} + + # winget-releaser UPDATES an existing package; `VitorPamplona.Amethyst` + # has never been submitted to microsoft/winget-pkgs. Until the one-time + # new-package PR lands (BUILDING.md § Bootstrap), no-op instead of failing + # every release with a spurious [release-ops] issue. + - name: Check the package exists in winget-pkgs + id: pkg + shell: bash + run: | + set -euo pipefail + URL=https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/v/VitorPamplona/Amethyst + if curl -fsSL -o /dev/null -H "Accept: application/vnd.github+json" "$URL"; then + echo "bootstrapped=true" >> "$GITHUB_OUTPUT" + echo "VitorPamplona.Amethyst found in winget-pkgs; proceeding with submission" + else + echo "bootstrapped=false" >> "$GITHUB_OUTPUT" + echo "::warning::Package 'VitorPamplona.Amethyst' is not in microsoft/winget-pkgs yet, so there is nothing to update for ${{ steps.rel.outputs.tag }}. Amethyst is NOT shipping via Winget. Submit the one-time new-package PR (BUILDING.md § Bootstrap) to activate this channel." + fi - name: Submit manifest to winget-pkgs + if: steps.pkg.outputs.bootstrapped == 'true' uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 with: identifier: VitorPamplona.Amethyst - version: ${{ github.event.release.tag_name || inputs.tag }} + version: ${{ steps.rel.outputs.tag }} # Asset naming contract: scripts/asset-name.sh installers-regex: '^amethyst-desktop-.*-windows-x64\.msi$' token: ${{ secrets.WINGET_TOKEN }} @@ -51,7 +91,7 @@ jobs: uses: actions/github-script@v9 with: script: | - const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown'; + const tag = context.payload.workflow_run?.head_branch || context.payload.inputs?.tag || 'unknown'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; await github.rest.issues.create({ owner: context.repo.owner, diff --git a/BUILDING.md b/BUILDING.md index e719e3aad2..1fd7b61bf5 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -325,15 +325,29 @@ Quartz library in one pipeline. 3. **Wait** for the `Create Release Assets` workflow to finish (~25–30 min). -4. **Verify**: - - GH Release contains 8 desktop assets + 12 Android assets +4. **Verify** — the GH Release should hold **31 assets**: + - **8 desktop** — `dmg` (macOS arm64), `msi` + `zip` (Windows), `deb`, `rpm`, + `AppImage`, `flatpak`, `tar.gz` (Linux). There is **no Intel/x64 macOS + DMG** — `jpackage` cannot cross-compile and no Intel runner leg is + configured, so macOS ships arm64-only. + - **13 Android** — 5 Google Play APKs + 5 F-Droid APKs + 2 AABs + the + F-Droid `.apks` set built for Accrescent. + - **5 amy** + **5 geode** bundles. - Asset sizes look sane (see §Enforce asset size budget — CI auto-fails at 1 GB/asset) - - Intel + ARM DMGs both present - Android flow unchanged + Quick diff against the previous release, which catches a silently-dropped + matrix leg better than any count: + + ```bash + diff <(gh release view v1.13.0 --json assets --jq '.assets[].name' | sed 's/1\.13\.0/VER/g' | sort) \ + <(gh release view v1.13.1 --json assets --jq '.assets[].name' | sed 's/1\.13\.1/VER/g' | sort) + ``` + 5. **Stable vs prerelease** — a tag containing `-rc`, `-beta`, `-alpha`, `-dev`, - or `-snapshot` is auto-classified as prerelease. Stable tags trigger the - Homebrew + Winget bump workflows. + or `-snapshot` is auto-classified as prerelease. Only stable tags run the + Homebrew + Winget bump workflows (and those are no-ops until the one-time + bootstrap PRs land — see § Bootstrap). ### Dry-run (no tag push) @@ -473,11 +487,11 @@ distributes; the official Amethyst rollout for each is in | Channel | How it ships | Push or pull | |---|---|---| | **GitHub Releases** | The release workflow builds + signs all assets and attaches them to the tag's Release | Automatic (CI) | -| **Maven Central** | Same workflow runs `publishAllPublicationsToMavenCentral` for `quartz` | Automatic (CI) | +| **Maven Central** | Same workflow runs `publishAllPublicationsToMavenCentral` for `quartz` — a *step* at the end of the `deploy-android` job, not a job of its own, so it does not appear in a job list | Automatic (CI) | | **Google Play** | Download the signed `amethyst-googleplay-.aab` from the GH Release and upload it in Play Console | **Manual push** | | **F-Droid** | F-Droid's build server detects the new tag and **builds the `fdroid` flavor from source** per its recipe in the external [`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo, then signs + publishes itself | **Pull (build-from-source)** | | **Zapstore** | The [`zsp`](https://zapstore.dev/) CLI reads [`zapstore.yaml`](zapstore.yaml) and publishes a Nostr software-release event signed with the app's nsec | **Manual push (Nostr)** | -| **Homebrew + Winget** | `bump-homebrew.yml` / `bump-winget.yml` open version-bump PRs on stable tags | Automatic (CI) | +| **Homebrew + Winget** | `bump-homebrew.yml` / `bump-winget.yml` open version-bump PRs on stable tags — **currently no-ops**: neither package has been bootstrapped upstream yet (§ Bootstrap) | Automatic (CI), inactive | Two channels need the build to stay split into product flavors (see `amethyst/build.gradle.kts` → `productFlavors`): @@ -498,6 +512,15 @@ reads an optional per-release changelog from ## Bootstrap runbook (one-time) +> **Status as of v1.13.1: neither Homebrew nor Winget has been bootstrapped.** +> `https://formulae.brew.sh/api/cask/amethyst-nostr.json` and +> `microsoft/winget-pkgs/manifests/v/VitorPamplona/Amethyst` both 404, so +> **Amethyst does not currently ship through either channel.** The bump +> workflows detect this and skip with a `::warning::` instead of failing, so a +> green release run does *not* mean Homebrew/Winget shipped. The two subsections +> below are the work that activates them; until then treat the desktop app as +> GitHub-Releases-only on macOS and Windows. + ### Secrets to provision in GitHub repo settings The full secret inventory is in [§ Secrets the CI needs](#secrets-the-ci-needs). @@ -510,8 +533,11 @@ their token type and scope: | `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) | Rotate both on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md` -or equivalent issue tracker. On rotation, paste new token and run -`gh workflow run bump-homebrew.yml` on the most recent stable tag to verify. +or equivalent issue tracker. On rotation, paste the new token and run +`gh workflow run bump-homebrew.yml -f tag=` to verify +(the `tag` input is required — that dispatch path exists for exactly this). +Note the verification is only meaningful once the cask exists upstream; +before that the run skips the token-using step entirely. ### Homebrew cask (one-time initial PR) diff --git a/RELEASE_OPS.md b/RELEASE_OPS.md index 1f0975423e..ab641f769c 100644 --- a/RELEASE_OPS.md +++ b/RELEASE_OPS.md @@ -14,7 +14,7 @@ specific to shipping the official Amethyst artifacts. ## At a glance -A release is one tag push that fans out to five distribution channels: +A release is one tag push that fans out to four live distribution channels: | Channel | Mechanism | Who pushes | |---|---|---| @@ -22,10 +22,11 @@ A release is one tag push that fans out to five distribution channels: | **Google Play** | **Manual** — download the signed AAB from the GH Release, upload in Play Console | Maintainer | | **F-Droid** | **Pull** — F-Droid's build server builds the `fdroid` flavor from source when it sees the new tag | F-Droid (we just maintain the recipe + metadata) | | **Zapstore** | `zsp publish` reads `zapstore.yaml`, signs a Nostr release event with Amethyst's nsec | Maintainer | -| **Homebrew + Winget** | Automatic — `bump-homebrew.yml` / `bump-winget.yml` fire on stable tags | CI | +| **Homebrew + Winget** | ⚠️ **Not shipping.** The bump workflows run, but skip: neither package exists upstream yet | Nobody (see § 3) | Maven Central (the `quartz` library) also publishes automatically from the same -workflow. +workflow — as a *step* at the end of the `deploy-android` job, not a job of its +own, so don't expect to find it in the run's job list. --- @@ -61,8 +62,10 @@ workflow. `scripts/translators.sh --seed` with `CROWDIN_PROJECT_ID` / `CROWDIN_PERSONAL_TOKEN` set, which refreshes the file.) -3. **Publish the release-notes note on Nostr** with Amethyst's account and paste - its event id into `amethyst/build.gradle.kts`: +3. **Publish the release-notes note on Nostr** — *minor releases only.* In + practice this id has only ever been bumped on `x.y.0` (1.11.0, 1.12.0, + 1.13.0); patch releases leave it pointing at their minor's note. Publish with + Amethyst's account and paste the event id into `amethyst/build.gradle.kts`: ```kotlin buildConfigField("String", "RELEASE_NOTES_ID", "\"\"") ``` @@ -85,17 +88,33 @@ workflow. Commit, tag, push — see [`BUILDING.md` § Release runbook](BUILDING.md#release-runbook) for the exact commands. The tag must equal `app` from the catalog (the workflow asserts this and fails fast otherwise). A clean `vMAJOR.MINOR.PATCH` tag is -classified **stable** and triggers the Homebrew/Winget bumps; anything with a -`-rc`/`-beta`/`-alpha`/`-dev` suffix is a prerelease and skips them. +classified **stable** and runs the Homebrew/Winget bump workflows; anything with +a `-rc`/`-beta`/`-alpha`/`-dev` suffix is a prerelease and skips them. + +Heads-up on the `git push`: this repo has `git-credential-manager` configured as +a credential helper, and it blocks on an interactive prompt (a plain +`GIT_TERMINAL_PROMPT=0` does **not** stop it — the push just hangs). If that +happens, push using `gh`'s helper for the one command: + +```bash +git -c credential.helper= -c credential.helper='!gh auth git-credential' push upstream main +``` When the `Create Release Assets` workflow finishes (~25–30 min) the GH Release -holds, per the asset-name contract: +holds **31 assets**, per the asset-name contract: -- **Android:** 5 Google Play APKs + 5 F-Droid APKs + 2 AABs - (`amethyst-googleplay-*-v…apk` / `.aab`, `amethyst-fdroid-*-v…apk` / `.aab`) -- **Desktop:** 8 assets (DMG/MSI/DEB/RPM/AppImage/zip/tar.gz) -- **CLI:** the `amy` artifacts -- **Maven Central:** `com.vitorpamplona.quartz:quartz:` published +- **Android (13):** 5 Google Play APKs + 5 F-Droid APKs + 2 AABs + the F-Droid + `.apks` set for Accrescent + (`amethyst-googleplay-*-v…apk` / `.aab`, `amethyst-fdroid-*-v…apk` / `.aab` / `.apks`) +- **Desktop (8):** DMG (macOS **arm64 only** — there is no Intel DMG), + MSI + zip, DEB, RPM, AppImage, flatpak, tar.gz +- **CLI (5):** the `amy` artifacts +- **Relay (5):** the `geode` artifacts, plus the geode Docker image +- **Maven Central:** `com.vitorpamplona.quartz:quartz:` published. + `repo1.maven.org` lags the publish by tens of minutes — a 404 right after the + run is normal. Confirm the step's log says "Deployment is being published to + Maven Central", and compare against the *previous* version's POM before + concluding anything is broken. --- @@ -165,11 +184,36 @@ RELAY_URLS="wss://relay.zapstore.dev,wss://relay.damus.io,wss://nos.lol,wss://vi Keep `wss://relay.zapstore.dev` in the list — that is the relay the Zapstore app itself reads from. -### Homebrew + Winget — automatic -`bump-homebrew.yml` and `bump-winget.yml` fire on stable tags and open PRs -against `Homebrew/homebrew-cask` (cask `amethyst-nostr`) and -`microsoft/winget-pkgs` (`VitorPamplona.Amethyst`). No action unless one fails — -then see BUILDING.md § Bootstrap and § Incident response. +### Homebrew + Winget — ⚠️ not shipping yet + +`bump-homebrew.yml` and `bump-winget.yml` are wired to open PRs against +`Homebrew/homebrew-cask` (cask `amethyst-nostr`) and `microsoft/winget-pkgs` +(`VitorPamplona.Amethyst`) — but **neither package has ever been submitted +upstream**, so both workflows detect that and skip with a `::warning::`. As of +**v1.13.1** these two channels deliver nothing; macOS and Windows users get the +desktop app from GitHub Releases only. + +Two separate faults kept this invisible until v1.13.1, both now fixed: + +1. **The workflows never ran at all** — for *any* release. They triggered on + `release: types: [released]`, and GitHub does not raise workflow-triggering + events for a release created by `GITHUB_TOKEN`, which is exactly how + `create-release.yml` creates it. They now trigger on `workflow_run` after + `Create Release Assets` succeeds, which also fixes a latent race — the old + event fired while assets were still uploading. +2. **Nothing exists upstream to bump.** `brew bump-cask-pr` and + `winget-releaser` can only *update* an existing package. The first + submission is a manual, human-reviewed PR: BUILDING.md § Homebrew cask + (one-time initial PR) and § Winget (one-time initial submission). + +Until someone does that bootstrap, a green release run means the bump workflows +*skipped cleanly* — not that Homebrew/Winget shipped. Check the run's warnings +if you want to confirm which case you're in. + +The two `amy` / `geode` Homebrew **formula** workflows are different: they only +sync the in-repo reference `.rb` files and open a PR against *this* repo, so +they do real work on every release. Merge those PRs to keep the formulae ready +for their eventual homebrew-core submission. --- @@ -222,13 +266,19 @@ Owner assignments and rotation reminders live with the team (issue tracker). ## 6. Post-release verification -- [ ] GH Release: expected asset count, Intel + ARM DMGs, sizes sane. -- [ ] Maven Central: `quartz:` resolves (allow propagation time). +- [ ] GH Release: 31 assets, sizes sane, and the asset-name set matches the + previous release (see the `diff` one-liner in BUILDING.md § Release + runbook). macOS is arm64-only — do **not** look for an Intel DMG. +- [ ] Maven Central: `quartz:` resolves (allow tens of minutes of + propagation; the publish step's log is the authoritative signal). - [ ] Play Console: rollout started, no policy rejection. - [ ] Zapstore: release event visible. - [ ] F-Droid: new version detected (may lag days). -- [ ] Homebrew + Winget bump PRs opened (stable only). -- [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID`. +- [ ] `amy` / `geode` Homebrew formula-sync PRs opened against this repo — merge them. +- [ ] Homebrew + Winget: **expected to skip** until bootstrapped (§ 3). If they + ever start opening real PRs, that means someone landed the bootstrap. +- [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID` + (only bumped on minor releases — patches keep pointing at the x.y.0 note). - [ ] Push still works on a `play` build (only if the push contract changed — see § 4); UnifiedPush still works on an `fdroid` build.