From 595b6a9d43a67a2912d36a566b910d9bdae5c423 Mon Sep 17 00:00:00 2001 From: Zac Clifton <43915749+Cliftonz@users.noreply.github.com> Date: Wed, 27 May 2026 11:56:59 -0400 Subject: [PATCH 1/2] ci(publish): decouple channels from crates.io + fix winget regex + gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase A of automating all eight distribution channels: kill the single-point-of-failure dependency on crates.io, fix winget's wrong installer regex, and add secret-presence gates so missing credentials surface as warnings instead of release-breaking job errors. Before: every channel had `needs: [publish-crates-io]`. A crates.io verify failure (as happened at v0.1.0 because jarvy-templates was marked `publish = false`) cascaded to skip AUR, winget, Chocolatey, Homebrew. None of those channels actually consume crates.io — they all pull from the GitHub release artifacts, so the dependency was spurious. After: each channel runs independently the moment the GitHub release publishes. A crates.io failure no longer blocks `.msi` reaching winget or Chocolatey. Per-channel fixes: * update-homebrew — drop the crates.io needs gate. (Tarball gap to produce `.tar.gz` artifacts is a separate Phase B blocker; this job still skips when HOMEBREW_TAP_DEPLOY_KEY is unset.) * update-aur — drop the crates.io needs gate, add secret-presence gate for AUR_SSH_PRIVATE_KEY + AUR_USERNAME + AUR_EMAIL, compute real sha256sums from SHA256SUMS.txt instead of leaving `SHA256_PLACEHOLDER_X86/_ARM` tokens in the pushed PKGBUILD (the prior code would have written a broken PKGBUILD that failed user `makepkg` integrity check). AUR-bin push still depends on the upstream tarball gap closing; the sha256 lookup step fails loud with a clear `release.yml does not yet produce *.tar.gz` error in that case rather than pushing garbage. * update-winget — drop the crates.io needs gate, add WINGET_TOKEN presence gate. Fix `installers-regex`: the prior value `jarvy-.*-x86_64-pc-windows-.*\.zip$` was a leftover from a never- wired cargo-dist plan; the release matrix has only ever produced `jarvy__x64_en-US.msi`, so the regex matched zero assets and the action silently no-op'd. New regex `^jarvy_\d+\.\d+\.\d+_x64_en-US\.msi$` matches real assets. * update-chocolatey — drop the crates.io needs gate, add CHOCOLATEY_API_KEY presence gate. Substitution + sha256 lookup steps unchanged (those were already correct for .msi shape). * `continue-on-error: true` removed from update-aur, update-winget, update-chocolatey. The secret-presence gate replaces the masking: missing setup surfaces as a workflow warning + job-skipped status; a real publish failure (after secret is set) now fails the job visibly so it can be triaged. Phase B follow-up: add `.tar.gz` artifact production to release.yml (macOS arm64 + Linux x86_64-gnu / aarch64-gnu / x86_64-musl). When that lands, AUR-bin + Homebrew + the universal install scripts (install.sh / install.ps1) all start working without further publish-packages changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/publish-packages.yml | 143 ++++++++++++++++++++----- 1 file changed, 119 insertions(+), 24 deletions(-) diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index ea190c2..bdf64de 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -80,10 +80,15 @@ jobs: update-homebrew: name: Update Homebrew Formula + # Independent of `publish-crates-io`. The Homebrew formula consumes + # tarballs from the GitHub release, not crates.io — a crates.io + # publish failure (like the v0.1.0 manifest-verification regression) + # must not block channels that don't depend on crates.io. + # # Gate twofold: # 1. Skip prereleases (rc / beta / alpha) so a soak release - # does not push the stable Cargo.toml version to crates.io - # as `latest`. + # does not push the stable version to the Homebrew tap as + # `latest`. # 2. Skip tags that don't match `vX.Y.Z` shape so a chart # release (e.g. `helm-vX.Y.Z`) does not trigger CLI # publishing. workflow_dispatch (empty event.release) @@ -95,7 +100,6 @@ jobs: startsWith(github.event.release.tag_name, 'v')) }} runs-on: ubuntu-latest - needs: [publish-crates-io] steps: - uses: actions/checkout@v4 @@ -166,10 +170,19 @@ jobs: update-aur: name: Update AUR Package + # Independent of `publish-crates-io`. AUR-bin consumes the + # GitHub-release binary tarballs, not crates.io. Status of the + # underlying tarball production: BROKEN until release.yml is + # extended to produce `jarvy-v--.tar.gz` + # artifacts (see docs/release-quirks-jarvy.md). When that lands, + # this job will start succeeding for users who run `yay -S + # jarvy-bin` — the AUR push itself only writes the PKGBUILD; the + # user-side `makepkg` is what hits the tarball URL. + # # Gate twofold: # 1. Skip prereleases (rc / beta / alpha) so a soak release - # does not push the stable Cargo.toml version to crates.io - # as `latest`. + # does not push the stable version to AUR as the user-facing + # latest. # 2. Skip tags that don't match `vX.Y.Z` shape so a chart # release (e.g. `helm-vX.Y.Z`) does not trigger CLI # publishing. workflow_dispatch (empty event.release) @@ -181,7 +194,6 @@ jobs: startsWith(github.event.release.tag_name, 'v')) }} runs-on: ubuntu-latest - needs: [publish-crates-io] steps: - uses: actions/checkout@v4 @@ -192,12 +204,58 @@ jobs: VERSION="${VERSION#v}" echo "version=$VERSION" >> $GITHUB_OUTPUT + # Skip when AUR credentials aren't configured. Mirror the + # Homebrew secret-presence gate so missing setup surfaces as a + # workflow warning instead of a release-breaking failure. The + # three secrets ship together (username + email + ssh key) and + # all three are required by the AUR action. + - name: Check AUR credentials + id: aur_secret + run: | + if [ -z "${{ secrets.AUR_SSH_PRIVATE_KEY }}" ] \ + || [ -z "${{ secrets.AUR_USERNAME }}" ] \ + || [ -z "${{ secrets.AUR_EMAIL }}" ]; then + echo "::warning::AUR credentials not configured; skipping AUR push. Set AUR_SSH_PRIVATE_KEY + AUR_USERNAME + AUR_EMAIL secrets to enable AUR distribution." + echo "have_secret=false" >> $GITHUB_OUTPUT + else + echo "have_secret=true" >> $GITHUB_OUTPUT + fi + - name: Update PKGBUILD + if: steps.aur_secret.outputs.have_secret == 'true' run: | VERSION="${{ steps.version.outputs.version }}" sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" dist/aur/PKGBUILD-bin + # Compute real sha256sums from SHA256SUMS.txt. The PKGBUILD + # ships with `SHA256_PLACEHOLDER_X86` / `SHA256_PLACEHOLDER_ARM` + # tokens that previously were never substituted — the AUR push + # would land a broken PKGBUILD that fails `makepkg` integrity + # check on user `yay -S jarvy-bin`. Pull the real sha256s from + # the published checksum file; if the tarball assets aren't + # there yet (pre-tarball-production gap), this step fails loud + # so the AUR push doesn't quietly write garbage. + - name: Substitute sha256sums from release checksums + if: steps.aur_secret.outputs.have_secret == 'true' + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + curl -fsSL \ + "https://github.com/bearbinary/jarvy/releases/download/v${VERSION}/SHA256SUMS.txt" \ + -o /tmp/sha256sums.txt + X86_SHA=$(awk -v p="jarvy-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \ + '$2 ~ p { print $1; exit }' /tmp/sha256sums.txt) + ARM_SHA=$(awk -v p="jarvy-v${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \ + '$2 ~ p { print $1; exit }' /tmp/sha256sums.txt) + if [ -z "$X86_SHA" ] || [ -z "$ARM_SHA" ]; then + echo "::error::AUR linux-gnu tarballs not present in release. release.yml does not yet produce `jarvy-v--unknown-linux-gnu.tar.gz` (see docs/release-quirks-jarvy.md tarball pipeline gap). Skip AUR push until tarball production lands." + exit 1 + fi + sed -i "s/SHA256_PLACEHOLDER_X86/$X86_SHA/g" dist/aur/PKGBUILD-bin + sed -i "s/SHA256_PLACEHOLDER_ARM/$ARM_SHA/g" dist/aur/PKGBUILD-bin + - name: Publish to AUR + if: steps.aur_secret.outputs.have_secret == 'true' uses: KSXGitHub/github-actions-deploy-aur@v2.7.2 with: pkgname: jarvy-bin @@ -206,18 +264,22 @@ jobs: commit_email: ${{ secrets.AUR_EMAIL }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Update to ${{ steps.version.outputs.version }}" - continue-on-error: true # AUR publish may require manual setup update-winget: name: Submit to winget + # Independent of `publish-crates-io`. winget consumes the .msi + # from the GitHub release; crates.io publish state is irrelevant. + # + # winget submissions open a PR to microsoft/winget-pkgs which + # Microsoft maintainers review/merge. `wingetcreate --submit` (the + # API the action uses) is the canonical user-facing flow — see + # docs/release-quirks-jarvy.md for the `public-pr-guard` skill + # carve-out covering this case. + # # Gate twofold: - # 1. Skip prereleases (rc / beta / alpha) so a soak release - # does not push the stable Cargo.toml version to crates.io - # as `latest`. + # 1. Skip prereleases (rc / beta / alpha). # 2. Skip tags that don't match `vX.Y.Z` shape so a chart - # release (e.g. `helm-vX.Y.Z`) does not trigger CLI - # publishing. workflow_dispatch (empty event.release) - # remains unaffected — manual reruns still publish. + # release (e.g. `helm-vX.Y.Z`) does not trigger. if: >- ${{ !github.event.release.prerelease && @@ -225,7 +287,6 @@ jobs: startsWith(github.event.release.tag_name, 'v')) }} runs-on: windows-latest - needs: [publish-crates-io] steps: - uses: actions/checkout@v4 @@ -237,30 +298,50 @@ jobs: VERSION="${VERSION#v}" echo "version=$VERSION" >> $GITHUB_OUTPUT + # Skip when WINGET_TOKEN isn't configured. The token is a fine- + # grained GitHub PAT with `public_repo` scope; the action uses + # it to fork microsoft/winget-pkgs and open the submission PR. + - name: Check winget token + id: winget_secret + shell: bash + run: | + if [ -z "${{ secrets.WINGET_TOKEN }}" ]; then + echo "::warning::WINGET_TOKEN not configured; skipping winget submission. Generate a fine-grained PAT with public_repo scope on your account, add as WINGET_TOKEN secret to enable winget distribution." + echo "have_secret=false" >> $GITHUB_OUTPUT + else + echo "have_secret=true" >> $GITHUB_OUTPUT + fi + - name: Update winget manifest + if: steps.winget_secret.outputs.have_secret == 'true' shell: bash run: | VERSION="${{ steps.version.outputs.version }}" sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" dist/windows/winget.yaml + # installers-regex matches the actual asset name pattern produced + # by cargo-packager on Windows: `jarvy__x64_en-US.msi`. The + # previous regex (`jarvy-.*-x86_64-pc-windows-.*\.zip$`) was a + # leftover from a cargo-dist plan never wired up — the release + # matrix has only ever produced `.msi`, so the regex matched + # zero assets and the action silently no-op'd. - name: Submit to winget-pkgs + if: steps.winget_secret.outputs.have_secret == 'true' uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: Jarvy.Jarvy - installers-regex: 'jarvy-.*-x86_64-pc-windows-.*\.zip$' + installers-regex: '^jarvy_\d+\.\d+\.\d+_x64_en-US\.msi$' token: ${{ secrets.WINGET_TOKEN }} - continue-on-error: true # May require manual approval update-chocolatey: name: Publish to Chocolatey + # Independent of `publish-crates-io`. Chocolatey consumes the .msi + # from the GitHub release; crates.io publish state is irrelevant. + # # Gate twofold: - # 1. Skip prereleases (rc / beta / alpha) so a soak release - # does not push the stable Cargo.toml version to crates.io - # as `latest`. + # 1. Skip prereleases (rc / beta / alpha). # 2. Skip tags that don't match `vX.Y.Z` shape so a chart - # release (e.g. `helm-vX.Y.Z`) does not trigger CLI - # publishing. workflow_dispatch (empty event.release) - # remains unaffected — manual reruns still publish. + # release (e.g. `helm-vX.Y.Z`) does not trigger. if: >- ${{ !github.event.release.prerelease && @@ -268,7 +349,6 @@ jobs: startsWith(github.event.release.tag_name, 'v')) }} runs-on: windows-latest - needs: [publish-crates-io] steps: - uses: actions/checkout@v4 @@ -280,7 +360,22 @@ jobs: VERSION="${VERSION#v}" echo "version=$VERSION" >> $GITHUB_OUTPUT + # Skip when CHOCOLATEY_API_KEY isn't configured. Get one at + # https://community.chocolatey.org/account/profile/api after + # signing up and getting your maintainer account approved. + - name: Check Chocolatey API key + id: choco_secret + shell: bash + run: | + if [ -z "${{ secrets.CHOCOLATEY_API_KEY }}" ]; then + echo "::warning::CHOCOLATEY_API_KEY not configured; skipping Chocolatey push. Get your API key at https://community.chocolatey.org/account/profile/api and add as CHOCOLATEY_API_KEY secret to enable Chocolatey distribution." + echo "have_secret=false" >> $GITHUB_OUTPUT + else + echo "have_secret=true" >> $GITHUB_OUTPUT + fi + - name: Update nuspec + install script (substitute placeholders) + if: steps.choco_secret.outputs.have_secret == 'true' # The Chocolatey package ships TWO templated files: # - jarvy.nuspec: VERSION_PLACEHOLDER (the package version) # - tools/chocolateyinstall.ps1: VERSION_PLACEHOLDER (URL path) @@ -320,9 +415,9 @@ jobs: echo "Substituted version=$VERSION sha256=$MSI_SHA_UPPER into both files." - name: Pack and push to Chocolatey + if: steps.choco_secret.outputs.have_secret == 'true' shell: powershell run: | cd dist/windows/chocolatey choco pack choco push jarvy.${{ steps.version.outputs.version }}.nupkg --source https://push.chocolatey.org/ --api-key ${{ secrets.CHOCOLATEY_API_KEY }} - continue-on-error: true # May require manual setup From 2465c916f9959e8e3814cd6859f2f105fba8ed8a Mon Sep 17 00:00:00 2001 From: Zac Clifton <43915749+Cliftonz@users.noreply.github.com> Date: Wed, 27 May 2026 11:57:21 -0400 Subject: [PATCH 2/2] ci(release): cap build_and_package at 60 min to kill queue-starved legs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.1.0-rc.1 and v0.1.0-rc.2 each burned a full GitHub-imposed 24 h job timeout on the (now-removed) macos-13 matrix leg. The smoking-gun line from both jobs (runs 25340850348 + 25342271485) was the standard runner wait message — the job never got picked up by a runner; it sat in queue the entire 24 h: Waiting for a runner to pick up this job... (This was queue starvation, not a build hang. GitHub does not bill for queue time, but the leg still blocked downstream sign/SBOM/upload jobs until the 24 h kill fired, delaying every release by a day.) The macos-13 leg itself was already dropped in 9bbed66. This is the belt-and-braces defense so the next runner-pool exhaustion (whether from re-adding macos-13 or any other label going scarce) dies in 60 min instead of 24 h. Healthy matrix legs all finish well under the cap: - ubuntu-latest ~ 5-8 min - windows-latest ~ 8-10 min - macos-latest arm ~ 6-15 min Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37fda28..6db8502 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,6 +60,15 @@ jobs: needs: [verify_tag] # verify_tag is skipped on workflow_dispatch; allow that to count as success. if: always() && (needs.verify_tag.result == 'success' || needs.verify_tag.result == 'skipped') + # Cap matrix legs at 60 min. Healthy runs are <10 min (Linux/Windows) + # and <15 min (macos-latest arm64). Without a cap, a queue-starved + # leg silently waits up to GitHub's hard 24 h job timeout — happened + # twice on the dropped macos-13 Intel slot during v0.1.0-rc.1 / rc.2 + # (runs 25340850348 + 25342271485), where each job sat at "Waiting + # for a runner to pick up this job..." for the full 24 h before + # GitHub killed it. Re-adding macos-13 or hitting any other + # runner-pool exhaustion now dies in 60 min instead. + timeout-minutes: 60 permissions: contents: read strategy: