From cec366f187fba0bc456cb3de9f71329b5ff40d65 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 3 Aug 2026 03:04:39 -0700 Subject: [PATCH 1/5] Stop an AUR outage from masking release verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AUR publish ran as a step inside the release job, sitting after both GoReleaser and attestation. When aur.archlinux.org went down for maintenance during v0.8.0, that step failed with the release already published — turning the job red and taking macos-verify, windows-verify, nix-verify and sync-skills down with it, all four skipped because they are `needs: [release]`. So an unreachable third party could silently cost us every signing verification on a release that had already shipped. Split AUR into its own continue-on-error job. The release job now ends at attestation, so its conclusion reflects only what we control, and the verification jobs run regardless of the AUR's availability. Three attempts with backoff absorb brief blips, and a failure opens an issue rather than passing quietly. Add an aur-publish workflow_dispatch workflow for the longer outages retries cannot cover. publish-aur.sh builds the PKGBUILD from the published release assets and no-ops when the AUR copy is current, so republishing an existing version is idempotent and needs no new tag. It refuses versions that are not actually published, so a typo cannot point the PKGBUILD at assets that were never built. --- .github/workflows/aur-publish.yml | 87 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 62 ++++++++++++++++++++-- 2 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/aur-publish.yml diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml new file mode 100644 index 00000000..4df758ff --- /dev/null +++ b/.github/workflows/aur-publish.yml @@ -0,0 +1,87 @@ +# 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 + echo "::error::No published release found for v${VERSION}" + exit 1 + fi + echo "Publishing v${VERSION} to the AUR" + + - 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1864320a..67abcfa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -281,9 +281,30 @@ jobs: with: subject-checksums: ./dist/checksums.txt + # AUR publishing runs as its own job, not a step in `release`. As a step it + # sat *after* GoReleaser and attestation, so an AUR-side outage turned the + # whole job red with the release already published — and that took the four + # `needs: [release]` verification jobs down with it as skipped. Isolated and + # continue-on-error, an AUR outage can no longer mask signing verification. + # Recover a missed publish with the aur-publish workflow (workflow_dispatch). + aur-publish: + name: Publish to AUR + needs: [release] + if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') + continue-on-error: true + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: release + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Check AUR publishing configuration - id: aur-publish - if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') + id: aur-config env: AUR_KEY: ${{ secrets.AUR_KEY }} run: | @@ -294,7 +315,7 @@ jobs: fi - name: Publish to AUR - if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') && steps.aur-publish.outputs.enabled == 'true' + if: steps.aur-config.outputs.enabled == 'true' env: AUR_KEY: ${{ secrets.AUR_KEY }} run: | @@ -305,7 +326,40 @@ jobs: 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" - scripts/publish-aur.sh "$VERSION" + # publish-aur.sh derives everything from the published release assets + # and no-ops when the PKGBUILD is already current, so retrying is safe. + 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 "AUR publish failed after 3 attempts" + exit 1 + + - name: Notify on AUR publish failure + if: failure() + env: + GH_TOKEN: ${{ github.token }} + REF_NAME: ${{ github.ref_name }} + RUN_ID: ${{ github.run_id }} + run: | + TITLE="AUR publish failure" + BODY="Publishing [${REF_NAME}](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID}) to the AUR failed. The GitHub release is unaffected — only the Arch package is stale. Re-run the \`Publish to AUR\` workflow with version \`${REF_NAME#v}\` once the AUR is reachable." + + # Check for existing open issue before creating a new one + existing=$(gh issue list --repo ${{ github.repository }} --state open --search "in:title $TITLE" --json number,title --jq '[.[] | select(.title == "'"$TITLE"'")][0].number // empty' 2>/dev/null || true) + if [ -n "$existing" ]; then + gh issue comment --repo ${{ github.repository }} "$existing" --body "$BODY" || true + else + gh issue create --repo ${{ github.repository }} --title "$TITLE" --body "$BODY" || true + fi + + # Always emit annotation so the failure is visible in the workflow summary + echo "::error::AUR publish failed for ${REF_NAME}. See https://github.com/${{ github.repository }}/actions/runs/${RUN_ID}" macos-verify: name: Verify macOS signing From 9f1eb5bd0405429f626da7470dd6b79d47a50173 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 3 Aug 2026 03:09:08 -0700 Subject: [PATCH 2/5] Add a manual skills-sync path for the same outage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AUR outage also skipped sync-skills, and basecamp/skills is now stale against v0.8.0 — the skills tree changed by +375/-54 lines since v0.7.2, so this is a real gap, not a no-op. release.yml no longer fails that way, but a sync that is skipped or fails on its own still needs a way back that does not involve cutting a new tag. Checks out the tag rather than main, so the sync mirrors the skills tree as it was released even after main moves on, and resolves the tagged commit for provenance instead of trusting github.sha, which points at the dispatching ref. Carries the script's DRY_RUN=local mode through as a preview toggle. --- .github/workflows/sync-skills.yml | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/sync-skills.yml diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml new file mode 100644 index 00000000..527a632e --- /dev/null +++ b/.github/workflows/sync-skills.yml @@ -0,0 +1,82 @@ +# 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 }} + run: | + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Invalid tag '${TAG}' — expected a v-prefixed semver tag (e.g. v0.8.0)" + 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 + + - name: Generate token for skills repo + id: skills-token + if: ${{ !inputs.dry_run }} + 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: 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" + + - 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 && 'local' || '' }} + run: scripts/sync-skills.sh From dbe790477fef989550fb205fcb29eed09c924f69 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 3 Aug 2026 03:15:56 -0700 Subject: [PATCH 3/5] Refuse to downgrade the AUR package on manual dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checking that the requested tag has a published release was not enough. publish-aur.sh rewrites pkgver unconditionally, so dispatching an older but perfectly valid release — after a newer one had already reached the AUR — would push every Arch user backwards. Compare against what is actually in the AUR via its RPC rather than against GitHub's release list, since the AUR is the thing being protected and may lag or lead for unrelated reasons. sort -V does the comparison so 0.10.0 ranks above 0.9.0 instead of below it. Fails closed when the RPC is unreachable: publishing blind risks a silent downgrade for every Arch user, while re-dispatching once the AUR responds costs nothing. Republishing the current version stays a permitted no-op, which is the ordinary recovery case. --- .github/workflows/aur-publish.yml | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 4df758ff..5f63d10f 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -54,6 +54,40 @@ jobs: 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); we compare pkgver. + current=$(printf '%s' "$body" | jq -r '.results[0].Version // empty' | sed 's/-[0-9]*$//') + if [ -z "$current" ]; then + echo "basecamp-cli is not in the AUR yet — nothing to downgrade" + exit 0 + fi + if [ "$current" = "$VERSION" ]; then + echo "AUR already at ${current} — 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 }} From 139b1fcb0fd275b3c5cbfe718f84f351d37a824d Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 3 Aug 2026 03:27:54 -0700 Subject: [PATCH 4/5] Close the remaining recovery-path gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four more holes in the manual recovery workflows, all reachable: Preserve newer AUR packaging revisions. Comparing only pkgver meant an AUR-side packaging fix at 0.8.0-2 counted as "same version", and publish-aur.sh, which hardcodes pkgrel=1, would then quietly replace it with 0.8.0-1. Refuse when the published revision is above -1; republishing over -1 stays the permitted no-op. Serialize the two publishers. The recovery workflow's concurrency group covered only itself, so a recovery that had already passed its version check could clone after a newer automatic publish landed and push the older PKGBUILD as a fast-forward. Both now share the aur-publish group. Require the latest stable tag when syncing skills. The tag pattern accepted prereleases, which release.yml deliberately excludes, and older stable tags, which would roll the distribution repo back — sync-skills.sh mirrors the tree wholesale. Unlike the AUR there is no independent record of what basecamp/skills holds, and the only reason to run this by hand is that the newest release failed to sync, so require exactly that release. Make the dry run honest. DRY_RUN=local never clones the target and diffs against an empty repo, so every skill reads as newly added and the deletions a real sync would make never appear — the opposite of what a preview is for. Use the script's remote mode, which clones the real target and stops before pushing, and scope the token to read for dry runs so the preview cannot write regardless. --- .github/workflows/aur-publish.yml | 22 +++++++++++++++++---- .github/workflows/release.yml | 6 ++++++ .github/workflows/sync-skills.yml | 32 ++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 5f63d10f..48ff038a 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -70,14 +70,28 @@ jobs: exit 1 fi - # AUR reports version as pkgver-pkgrel (e.g. 0.7.2-1); we compare pkgver. - current=$(printf '%s' "$body" | jq -r '.results[0].Version // empty' | sed 's/-[0-9]*$//') - if [ -z "$current" ]; then + # 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 - echo "AUR already at ${current} — republish is a no-op unless the PKGBUILD drifted" + # publish-aur.sh hardcodes pkgrel=1, so republishing over a higher + # revision would silently discard an AUR-side packaging fix. + if [ "$currel" -gt 1 ] 2>/dev/null; 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67abcfa5..26bbe0e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -294,6 +294,12 @@ jobs: continue-on-error: true runs-on: ubuntu-latest timeout-minutes: 20 + # Shared with the aur-publish workflow's recovery runs. Without this, a + # recovery that already passed its version check could clone after a newer + # automatic publish landed and push the older PKGBUILD as a fast-forward. + concurrency: + group: aur-publish + cancel-in-progress: false environment: release permissions: contents: read diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index 527a632e..46ba345b 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -42,9 +42,24 @@ jobs: - name: Validate tag input env: TAG: ${{ inputs.tag }} + GH_TOKEN: ${{ github.token }} run: | - if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then - echo "::error::Invalid tag '${TAG}' — expected a v-prefixed semver tag (e.g. v0.8.0)" + # 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 @@ -55,16 +70,18 @@ jobs: 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 - if: ${{ !inputs.dry_run }} 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: write + 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 @@ -73,10 +90,15 @@ jobs: 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 && 'local' || '' }} + DRY_RUN: ${{ inputs.dry_run && 'remote' || '' }} run: scripts/sync-skills.sh From 1d59858e0d44d493542002244b90e8a397e1c7db Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 3 Aug 2026 03:39:00 -0700 Subject: [PATCH 5/5] Compare AUR package revisions without an arithmetic test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PKGBUILD(5) permits a dotted subrelease, so pkgrel can be 1.1. `[ -gt ]` rejects that as a non-integer and exits 2 — and the 2>/dev/null guarding the test turned that error into a silent "not greater", so the clobber the previous commit set out to prevent went through anyway for exactly the revisions most likely to carry a hand-made packaging fix. Compare with sort -V instead, which orders dotted revisions correctly and cannot fail open: 1.10 ranks above 1.1, and anything other than a bare 1 is refused. --- .github/workflows/aur-publish.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 48ff038a..4cfd7be2 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -87,7 +87,13 @@ jobs: if [ "$current" = "$VERSION" ]; then # publish-aur.sh hardcodes pkgrel=1, so republishing over a higher # revision would silently discard an AUR-side packaging fix. - if [ "$currel" -gt 1 ] 2>/dev/null; then + # + # 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