reconcile manifests #36
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
| # Reconcile downstream manifests against an existing release — WITHOUT rebuilding | |
| # or cutting a new release. | |
| # | |
| # Why this exists: every publish-* job in release.yml pins a sha256 read from the | |
| # release's SHA256SUMS (the single source of truth) and verifies the bytes match. | |
| # But if a publish job was skipped/failed on the original run, or a binary was | |
| # re-uploaded (`--clobber`) afterwards, a downstream manifest (Homebrew formula, | |
| # Scoop manifest, AUR PKGBUILD) can drift out of sync with the release. | |
| # | |
| # This workflow re-renders + re-pushes those manifests from the CURRENT | |
| # SHA256SUMS of a chosen release. It never builds anything, so it's fast and | |
| # cannot itself introduce new drift. Safe to run any time; idempotent (each job | |
| # no-ops if its manifest already matches). | |
| # | |
| # Actions ▸ reconcile manifests ▸ Run workflow ▸ tag (blank → v<CMake version>) | |
| name: reconcile manifests | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Release tag to reconcile against (blank → v<CMake project version>)' | |
| required: false | |
| default: '' | |
| # Self-heal automatically. Every publish-* job in release.yml is gated behind | |
| # a build leg (needs: build-linux-*/build-windows), so ONE failed/slow build | |
| # leg silently SKIPS its downstream publisher — the public tag exists but the | |
| # package (AUR/Homebrew/scoop/…) stays stale and users file out-of-date flags. | |
| # These triggers re-pin every manifest from the release's SHA256SUMS without | |
| # rebuilding, so a package can never stay behind the latest release for long: | |
| # * after every release run completes (even partially), reconcile the | |
| # latest tag — fills in whatever the release run skipped. | |
| # * weekly, as a belt-and-suspenders backstop against any later drift | |
| # (re-uploaded asset, manual tag move, transient token failure). | |
| workflow_run: | |
| workflows: ["release"] | |
| types: [completed] | |
| schedule: | |
| - cron: '17 6 * * 1' # Mondays 06:17 UTC | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| jobs: | |
| meta: | |
| name: resolve tag + version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.m.outputs.version }} | |
| tag: ${{ steps.m.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - id: m | |
| run: | | |
| # Priority: explicit dispatch input > CMake project version > the | |
| # repo's latest published release. Auto triggers (schedule / | |
| # workflow_run) have no input, so they reconcile whatever release is | |
| # current. We prefer the LATEST published release tag over the CMake | |
| # version so a mid-development bump (CMake ahead of any release) can't | |
| # point reconcile at a tag that doesn't exist yet. | |
| TAG="${{ inputs.tag }}" | |
| if [ -z "$TAG" ]; then | |
| LATEST=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName 2>/dev/null || true) | |
| if [ -n "$LATEST" ]; then | |
| TAG="$LATEST" | |
| else | |
| V=$(grep -m1 'project(agentty' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/') | |
| TAG="v$V" | |
| fi | |
| fi | |
| # Derive the version FROM the tag being reconciled (a manifest for an | |
| # old tag must pin that tag's version, not HEAD's CMake version). | |
| TV="${TAG#v}" | |
| echo "version=$TV" >> "$GITHUB_OUTPUT" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "reconciling $TAG (version $TV)" | |
| # Shared helper is inlined per job (composite actions would need a checkout of | |
| # this repo in each; the duplication is tiny and keeps each job self-contained). | |
| homebrew: | |
| name: reconcile homebrew tap | |
| needs: meta | |
| runs-on: ubuntu-latest | |
| env: | |
| V: ${{ needs.meta.outputs.version }} | |
| TAG: ${{ needs.meta.outputs.tag }} | |
| TAP_TOKEN: ${{ secrets.TAP_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Re-pin formula from SHA256SUMS | |
| if: ${{ env.TAP_TOKEN != '' }} | |
| run: | | |
| set -eu | |
| gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern 'agentty-linux-x86_64' --pattern 'agentty-linux-aarch64' \ | |
| --pattern 'agentty-macos-x86_64' --pattern 'agentty-macos-arm64' \ | |
| --pattern 'SHA256SUMS' --skip-existing | |
| sum_for() { | |
| want=$(awk -v f="$1" '$2==f {print $1}' SHA256SUMS) | |
| [ -n "$want" ] || { echo "::error::$1 missing from SHA256SUMS"; exit 1; } | |
| got=$(sha256sum "$1" | awk '{print $1}') | |
| [ "$want" = "$got" ] || { echo "::error::$1 drift: SHA256SUMS=$want bytes=$got"; exit 1; } | |
| printf '%s' "$want" | |
| } | |
| sed -e "s/^ version \".*\"/ version \"$V\"/" \ | |
| -e "s|@LINUX_X86_64_SHA256@|$(sum_for agentty-linux-x86_64)|g" \ | |
| -e "s|@LINUX_AARCH64_SHA256@|$(sum_for agentty-linux-aarch64)|g" \ | |
| -e "s|@MACOS_X86_64_SHA256@|$(sum_for agentty-macos-x86_64)|g" \ | |
| -e "s|@MACOS_ARM64_SHA256@|$(sum_for agentty-macos-arm64)|g" \ | |
| packaging/homebrew/agentty.rb > agentty.rb | |
| git clone "https://x-access-token:${TAP_TOKEN}@github.com/1ay1/homebrew-tap.git" tap | |
| mkdir -p tap/Formula && cp agentty.rb tap/Formula/agentty.rb | |
| cd tap | |
| git config user.name agentty-ci; git config user.email noreply@github.com | |
| git add Formula/agentty.rb | |
| git commit -m "reconcile agentty $V" || { echo "already in sync"; exit 0; } | |
| git push origin HEAD:master | |
| scoop: | |
| name: reconcile scoop bucket | |
| needs: meta | |
| runs-on: ubuntu-latest | |
| env: | |
| V: ${{ needs.meta.outputs.version }} | |
| TAG: ${{ needs.meta.outputs.tag }} | |
| SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Re-pin manifest from SHA256SUMS | |
| if: ${{ env.SCOOP_TOKEN != '' }} | |
| run: | | |
| set -eu | |
| gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern 'agentty-windows-x86_64.exe' --pattern 'SHA256SUMS' --skip-existing | |
| win=$(awk '$2=="agentty-windows-x86_64.exe" {print $1}' SHA256SUMS) | |
| [ -n "$win" ] || { echo "::error::windows .exe missing from SHA256SUMS"; exit 1; } | |
| got=$(sha256sum agentty-windows-x86_64.exe | awk '{print $1}') | |
| [ "$win" = "$got" ] || { echo "::error::windows .exe drift: SHA256SUMS=$win bytes=$got"; exit 1; } | |
| sed -e "s/\"version\": \".*\"/\"version\": \"$V\"/" \ | |
| -e "s|/download/v[0-9.]*/|/download/v$V/|g" \ | |
| -e "s|@WINDOWS_X86_64_SHA256@|$win|g" \ | |
| packaging/scoop/agentty.json > agentty.json | |
| git clone "https://x-access-token:${SCOOP_TOKEN}@github.com/1ay1/scoop-bucket.git" bucket | |
| mkdir -p bucket/bucket && cp agentty.json bucket/bucket/agentty.json | |
| cd bucket | |
| git config user.name agentty-ci; git config user.email noreply@github.com | |
| git add bucket/agentty.json | |
| git commit -m "reconcile agentty $V" || { echo "already in sync"; exit 0; } | |
| git push origin HEAD:master | |
| aur: | |
| name: reconcile AUR | |
| needs: meta | |
| runs-on: ubuntu-latest | |
| env: | |
| V: ${{ needs.meta.outputs.version }} | |
| TAG: ${{ needs.meta.outputs.tag }} | |
| AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Re-pin PKGBUILD from SHA256SUMS | |
| if: ${{ env.AUR_SSH_KEY != '' }} | |
| run: | | |
| set -eu | |
| gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern 'agentty-linux-x86_64' --pattern 'agentty-linux-aarch64' \ | |
| --pattern 'SHA256SUMS' --skip-existing | |
| sum_for() { | |
| want=$(awk -v f="$1" '$2==f {print $1}' SHA256SUMS) | |
| [ -n "$want" ] || { echo "::error::$1 missing from SHA256SUMS"; exit 1; } | |
| got=$(sha256sum "$1" | awk '{print $1}') | |
| [ "$want" = "$got" ] || { echo "::error::$1 drift: SHA256SUMS=$want bytes=$got"; exit 1; } | |
| printf '%s' "$want" | |
| } | |
| x64=$(sum_for agentty-linux-x86_64) | |
| arm=$(sum_for agentty-linux-aarch64) | |
| sed -e "s/^pkgver=.*/pkgver=$V/" \ | |
| -e "s/sha256sums_x86_64=.*/sha256sums_x86_64=('$x64')/" \ | |
| -e "s/sha256sums_aarch64=.*/sha256sums_aarch64=('$arm')/" \ | |
| packaging/arch/PKGBUILD > PKGBUILD | |
| mkdir -p ~/.ssh | |
| printf '%s\n' "$AUR_SSH_KEY" > ~/.ssh/aur; chmod 600 ~/.ssh/aur | |
| ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null | |
| export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur" | |
| # AUR can be down for maintenance for hours-to-days. This job runs | |
| # after every release.yml completion AND weekly, so an outage | |
| # otherwise means a standing red-X streak for the whole outage | |
| # window with nothing actionable to do about it. Retry with backoff | |
| # to ride out a brief blip; if AUR is still down after that, warn | |
| # and exit 0 instead of failing — the next scheduled/triggered run | |
| # picks it back up automatically once AUR returns, so nothing is | |
| # silently lost. CRITICAL: only a GENUINE maintenance banner soft- | |
| # skips. A bare "Could not read from remote" / "Permission denied" | |
| # with no banner is an SSH AUTH failure (bad/unregistered | |
| # AUR_SSH_KEY) — that must fail LOUD, not masquerade as an outage | |
| # (silently classifying it as maintenance left the package stale | |
| # across multiple releases while every run reported green). | |
| is_outage() { grep -qi 'AUR is down\|due to maintenance' "$1"; } | |
| clone_ok=0 | |
| for attempt in 1 2 3 4 5; do | |
| if git clone ssh://aur@aur.archlinux.org/agentty-bin.git aur 2>clone.err; then | |
| clone_ok=1; break | |
| fi | |
| echo "AUR clone attempt $attempt failed:"; cat clone.err | |
| if is_outage clone.err; then | |
| sleep $((attempt * 20)) | |
| else | |
| break | |
| fi | |
| done | |
| if [ "$clone_ok" -ne 1 ]; then | |
| if is_outage clone.err 2>/dev/null; then | |
| echo "::warning::AUR is down for maintenance after 5 retries — skipping this reconcile. The next scheduled/triggered run will retry automatically." | |
| exit 0 | |
| fi | |
| echo "::error::AUR clone failed and AUR is NOT reporting maintenance — almost certainly an SSH auth failure (AUR_SSH_KEY not registered on the maintainer account, or malformed). Fix the key; do not ignore." | |
| exit 1 | |
| fi | |
| cp PKGBUILD aur/PKGBUILD | |
| cd aur | |
| printf 'pkgbase = agentty-bin\n\tpkgdesc = Blazing-fast Claude in your terminal\n\tpkgver = %s\n\tpkgrel = 1\n\turl = https://github.com/1ay1/agentty\n\tarch = x86_64\n\tarch = aarch64\n\tlicense = MIT\n\tprovides = agentty\n\tconflicts = agentty\n\tsource_x86_64 = agentty-%s-x86_64::https://github.com/1ay1/agentty/releases/download/v%s/agentty-linux-x86_64\n\tsha256sums_x86_64 = %s\n\tsource_aarch64 = agentty-%s-aarch64::https://github.com/1ay1/agentty/releases/download/v%s/agentty-linux-aarch64\n\tsha256sums_aarch64 = %s\n\npkgname = agentty-bin\n' \ | |
| "$V" "$V" "$V" "$x64" "$V" "$V" "$arm" > .SRCINFO | |
| git config user.name agentty-ci; git config user.email noreply@github.com | |
| git add PKGBUILD .SRCINFO | |
| git commit -m "reconcile agentty $V" || { echo "already in sync"; exit 0; } | |
| push_ok=0 | |
| for attempt in 1 2 3; do | |
| if git push origin HEAD:master 2>push.err; then push_ok=1; break; fi | |
| echo "AUR push attempt $attempt failed:"; cat push.err | |
| is_outage push.err && sleep $((attempt * 20)) || break | |
| done | |
| if [ "$push_ok" -ne 1 ]; then | |
| if is_outage push.err 2>/dev/null; then | |
| echo "::warning::AUR push failed due to a maintenance outage — skipping. The next scheduled/triggered run will retry automatically." | |
| exit 0 | |
| fi | |
| echo "::error::AUR push failed and AUR is NOT reporting maintenance — likely an SSH auth problem (AUR_SSH_KEY) or the account lost write access to agentty-bin. Fix it; do not ignore." | |
| exit 1 | |
| fi |