-
Notifications
You must be signed in to change notification settings - Fork 21
Stop an AUR outage from masking release verification #600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cec366f
Stop an AUR outage from masking release verification
9f1eb5b
Add a manual skills-sync path for the same outage
dbe7904
Refuse to downgrade the AUR package on manual dispatch
139b1fc
Close the remaining recovery-path gaps
1d59858
Compare AUR package revisions without an arithmetic test
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Manual AUR publish, for when the release-time publish could not run. | ||
| # | ||
| # The AUR goes down for maintenance often enough that a release must not depend | ||
| # on it being reachable — release.yml's aur-publish job is continue-on-error for | ||
| # exactly that reason. This workflow is the recovery path: it republishes any | ||
| # already-released version once the AUR is back, with no need to cut a new tag. | ||
| # | ||
| # scripts/publish-aur.sh derives the PKGBUILD entirely from the published GitHub | ||
| # release assets and no-ops when the AUR copy is already current, so running this | ||
| # against an already-published version is safe and idempotent. | ||
| name: Publish to AUR | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: 'Released version to publish, without the v prefix (e.g. 0.8.0)' | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: {} | ||
|
|
||
| concurrency: | ||
| group: aur-publish | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| publish: | ||
| name: Publish to AUR | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| environment: release | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Validate version input | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "::error::Invalid version '${VERSION}' — expected semver with no v prefix (e.g. 0.8.0)" | ||
| exit 1 | ||
| fi | ||
| # Only publish versions that actually exist as a stable release, so a | ||
| # typo cannot push a PKGBUILD pointing at assets that were never built. | ||
| if ! gh release view "v${VERSION}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | ||
|
jeremy marked this conversation as resolved.
|
||
| echo "::error::No published release found for v${VERSION}" | ||
| exit 1 | ||
| fi | ||
| echo "Publishing v${VERSION} to the AUR" | ||
|
|
||
| - name: Refuse to downgrade the AUR package | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| # publish-aur.sh rewrites pkgver unconditionally, so dispatching an | ||
| # older-but-valid release would push every Arch user backwards. Being | ||
| # a published release is not enough — compare against what is actually | ||
| # in the AUR right now. | ||
| if ! body=$(curl -fsS --max-time 30 --retry 2 \ | ||
| 'https://aur.archlinux.org/rpc/v5/info/basecamp-cli'); then | ||
| # Fail closed: proceeding blind here risks a silent downgrade, and a | ||
| # dispatch is cheap to repeat once the AUR is reachable again. | ||
| echo "::error::Could not reach the AUR to check the published version. Retry when it is reachable." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # AUR reports version as pkgver-pkgrel (e.g. 0.7.2-1). | ||
| full=$(printf '%s' "$body" | jq -r '.results[0].Version // empty') | ||
| if [ -z "$full" ]; then | ||
| echo "basecamp-cli is not in the AUR yet — nothing to downgrade" | ||
| exit 0 | ||
| fi | ||
| if [[ "$full" == *-* ]]; then | ||
| current="${full%-*}" | ||
| currel="${full##*-}" | ||
| else | ||
| current="$full" | ||
| currel=1 | ||
| fi | ||
|
|
||
| if [ "$current" = "$VERSION" ]; then | ||
| # publish-aur.sh hardcodes pkgrel=1, so republishing over a higher | ||
| # revision would silently discard an AUR-side packaging fix. | ||
| # | ||
| # Compare with sort -V, not an arithmetic test: PKGBUILD(5) allows a | ||
| # dotted subrelease (1.1), which `[ -gt ]` rejects as a non-integer | ||
| # with status 2 — and a suppressed stderr would turn that error into | ||
| # a silent "not greater", letting the clobber through. | ||
| highest=$(printf '%s\n%s\n' "$currel" "1" | sort -V | tail -1) | ||
| if [ "$highest" != "1" ]; then | ||
| echo "::error::AUR has ${full}; publish-aur.sh would replace it with ${VERSION}-1 and discard that packaging revision" | ||
| exit 1 | ||
| fi | ||
| echo "AUR already at ${full} — republish is a no-op unless the PKGBUILD drifted" | ||
| exit 0 | ||
| fi | ||
|
|
||
| oldest=$(printf '%s\n%s\n' "$current" "$VERSION" | sort -V | head -1) | ||
| if [ "$oldest" = "$VERSION" ]; then | ||
| echo "::error::Refusing to downgrade the AUR from ${current} to ${VERSION}" | ||
| exit 1 | ||
| fi | ||
| echo "AUR is at ${current}; publishing ${VERSION}" | ||
|
|
||
| - name: Verify AUR key is configured | ||
| env: | ||
| AUR_KEY: ${{ secrets.AUR_KEY }} | ||
| run: | | ||
| if [ -z "$AUR_KEY" ]; then | ||
| echo "::error::AUR_KEY is not configured for the release environment" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Publish to AUR | ||
| env: | ||
| AUR_KEY: ${{ secrets.AUR_KEY }} | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| mkdir -p ~/.ssh | ||
| echo "$AUR_KEY" > ~/.ssh/aur | ||
| chmod 600 ~/.ssh/aur | ||
| echo -e "Host aur.archlinux.org\n IdentityFile ~/.ssh/aur\n User aur\n StrictHostKeyChecking accept-new" >> ~/.ssh/config | ||
| git config --global user.name "37signals" | ||
| git config --global user.email "dev@37signals.com" | ||
| for attempt in 1 2 3; do | ||
| if scripts/publish-aur.sh "$VERSION"; then | ||
| exit 0 | ||
| fi | ||
| if [ "$attempt" -lt 3 ]; then | ||
| echo "AUR publish attempt ${attempt} failed; retrying in $((attempt * 60))s" | ||
| sleep $((attempt * 60)) | ||
| fi | ||
| done | ||
| echo "::error::AUR publish failed after 3 attempts" | ||
| exit 1 | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # Manual skills sync, for when the release-time sync could not run. | ||
| # | ||
| # The sync-skills job in release.yml is `needs: [release]`, so anything that | ||
| # fails the release job after publication — an unreachable AUR, say — skips the | ||
| # sync entirely and leaves basecamp/skills stale against a shipped release. | ||
| # release.yml no longer fails that way, but a skipped or failed sync still needs | ||
| # a way back without cutting a new tag. This is it. | ||
| # | ||
| # scripts/sync-skills.sh mirrors the skills/ tree at the given ref into | ||
| # basecamp/skills and no-ops when the content already matches, so re-running it | ||
| # for an already-synced release is safe. | ||
| name: Sync skills | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: 'Release tag to sync from, with the v prefix (e.g. v0.8.0)' | ||
| required: true | ||
| type: string | ||
| dry_run: | ||
| description: 'Preview the sync without pushing' | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
|
|
||
| permissions: {} | ||
|
|
||
| concurrency: | ||
| group: sync-skills | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| sync: | ||
| name: Sync skills | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| environment: release | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Validate tag input | ||
| env: | ||
| TAG: ${{ inputs.tag }} | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| # No prerelease suffix: the automatic sync in release.yml excludes | ||
| # prereleases deliberately, and this path must not smuggle prerelease | ||
| # content into the distribution repo's main branch. | ||
| if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "::error::Invalid tag '${TAG}' — expected a stable v-prefixed semver tag (e.g. v0.8.0)" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # sync-skills.sh mirrors the tree wholesale, so syncing an older tag | ||
| # would roll basecamp/skills back. Unlike the AUR, there is no | ||
| # independent record of what the distribution repo currently holds, and | ||
| # the only reason to run this by hand is that the newest release failed | ||
| # to sync — so require exactly that release. | ||
| latest=$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName --jq .tagName) | ||
| if [ "$TAG" != "$latest" ]; then | ||
| echo "::error::Refusing to sync ${TAG}: the latest stable release is ${latest}. Syncing an older tag would roll basecamp/skills back." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Check out the tag itself, not main: the sync must mirror the skills tree | ||
| # as it was released, even if main has moved on since. | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ inputs.tag }} | ||
| persist-credentials: false | ||
|
|
||
| # Needed for dry runs too: the honest preview clones the target, so it | ||
| # cannot run tokenless. Dry runs get a read-only token, which is also what | ||
| # stops DRY_RUN=remote from pushing even if the script were wrong. | ||
| - name: Generate token for skills repo | ||
| id: skills-token | ||
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | ||
| with: | ||
| app-id: ${{ vars.RELEASE_CLIENT_ID }} | ||
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | ||
| owner: basecamp | ||
| repositories: skills | ||
| permission-contents: ${{ inputs.dry_run && 'read' || 'write' }} | ||
|
|
||
| # github.sha is the dispatching ref's SHA (main), not the tag's, so resolve | ||
| # the commit actually checked out — otherwise the sync records the wrong | ||
| # provenance for the release it claims to mirror. | ||
| - name: Resolve the tagged commit | ||
| id: source | ||
| run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | ||
|
|
||
| # DRY_RUN=remote, not local: the local path never clones basecamp/skills | ||
| # and diffs against an empty repo, so every skill reads as newly added and | ||
| # the deletions a real sync would make never appear. That is the opposite | ||
| # of what a preview is for. Remote clones the actual target and stops | ||
| # before the push. | ||
| - name: Sync skills to distribution repo | ||
| env: | ||
| SKILLS_TOKEN: ${{ steps.skills-token.outputs.token }} | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| SOURCE_SHA: ${{ steps.source.outputs.sha }} | ||
| DRY_RUN: ${{ inputs.dry_run && 'remote' || '' }} | ||
| run: scripts/sync-skills.sh |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.