From dfff9c7cdf679402f80bae26b3de9c6259f6e727 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 29 Oct 2026 11:53:28 -0700 Subject: [PATCH] CI: leave :latest to the newest series once 27.1 ships A 2026.x release from this branch moved :latest, GHCR's :latest and the GitHub "latest release" on every tag. That is right while 2026.x is the newest series, and wrong once 27.1 ships: a 2026.x security release after it would take :latest back to the older line. The version job now asks Gitea for the newest final releases and moves :latest only while none is numbered by series (v27.1, v27.1.3), so the switch happens on its own when 27.1 is published, with nothing to change on release day. The release body says which way it went. The check fails rather than guess: a wrong :latest is worse than a re-run. Checked against the live API (no series release yet, :latest moves) and against a list with v27.1 in it (it holds). Both workflow files parse. --- .gitea/workflows/publish.yml | 37 ++++++++++++++++++++++++----- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index b234ded9..a756e41a 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -37,8 +37,10 @@ # fixes through pull requests into it, and is tagged there, so production # can get a fix without everything that has landed on main since. # -# :latest moves with every published tag: tags are cut by the weekly release -# (or by hand for a real release); there are no prerelease tags here. +# :latest moves with a published tag while 2026.x is the newest series. Once +# a final release numbered by series exists (v27.1, v27.1.3), a 2026.x +# security release from release/2026 leaves :latest where it is (the version +# job's `latest` output). # # The push logs in with PACKAGE_TOKEN (jcoffey-dev, write:package): the job's # own token is refused by the container registry. @@ -64,6 +66,7 @@ jobs: image: python:3.13-slim@sha256:8d9d0b8bcf6506481eae4907c18f5e3e7902e629f5f6d684f9e7c32e85e3ddf0 # 3.13-slim outputs: version: ${{ steps.v.outputs.version }} + latest: ${{ steps.latest.outputs.latest }} steps: # Full history: the ancestry check cannot be answered from a shallow # clone. The checkout also fetches every branch as origin/*. @@ -95,6 +98,22 @@ jobs: echo "$TAG is on $on" echo "version=$V" >> "$GITHUB_OUTPUT" echo "version $V" + # inbuxa: :latest belongs to the newest series; see the top of this file + - id: latest + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 - <<'EOF' + import json, os, re, urllib.request + url = (f"{os.environ['CI_SERVER_INTERNAL']}/api/v1/repos/{os.environ['GITHUB_REPOSITORY']}" + "/releases?draft=false&pre-release=false&limit=50") + req = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['TOKEN']}"}) + newer = [r["tag_name"] for r in json.load(urllib.request.urlopen(req)) + if re.match(r"v[0-9]{1,3}\.", r["tag_name"])] + print(f"{newer[0]} is out: :latest stays on its series" if newer else "no series release yet: :latest moves") + with open(os.environ["GITHUB_OUTPUT"], "a") as out: + out.write(f"latest={'false' if newer else 'true'}\n") + EOF publish-amd64: if: ${{ vars.BUILD_ON != 'github' }} @@ -150,6 +169,7 @@ jobs: REGISTRY: ${{ vars.REGISTRY }} IMAGE: ${{ vars.REGISTRY }}/${{ github.repository }} VERSION: ${{ needs.version.outputs.version }} + LATEST: ${{ needs.version.outputs.latest }} PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} steps: - uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec @@ -165,9 +185,11 @@ jobs: --provenance=false --sbom=false \ --tag "$IMAGE:$VERSION-arm64" \ --push . + if [ "$LATEST" = true ]; then latest="--tag $IMAGE:latest"; else latest=""; fi + # $latest unquoted on purpose: empty, or two words docker buildx imagetools create \ --tag "$IMAGE:$VERSION" \ - --tag "$IMAGE:latest" \ + $latest \ "$IMAGE:$VERSION-amd64" "$IMAGE:$VERSION-arm64" docker buildx imagetools inspect "$IMAGE:$VERSION" - if: always() @@ -222,6 +244,7 @@ jobs: env: TAG: ${{ github.ref_name }} VERSION: ${{ needs.version.outputs.version }} + LATEST: ${{ needs.version.outputs.latest }} TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} REGISTRY: ${{ vars.REGISTRY }} @@ -237,9 +260,11 @@ jobs: except urllib.error.HTTPError as e: if e.code != 404: raise image = f"{os.environ['REGISTRY']}/{os.environ['REPO']}:{version}" - body = (f"Container image: `{image}` (linux/amd64, linux/arm64); also `:latest`. " - "amd64 is published first; arm64 is added to the same tag when its build " - "finishes, and `:latest` moves then.\n\n" + latest = os.environ["LATEST"] == "true" + body = (f"Container image: `{image}` (linux/amd64, linux/arm64)" + + ("; also `:latest`. " if latest else ". `:latest` stays on the newer series. ") + + "amd64 is published first; arm64 is added to the same tag when its build finishes" + + (", and `:latest` moves then.\n\n" if latest else ".\n\n") "Binaries for a host install are attached: `inbuxa-linux-amd64.tar.gz` and " "`inbuxa-linux-arm64.tar.gz`, with `SHA256SUMS`. Each is the binary out of this " "release's image for that architecture, so it is the same build. The image " diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33da777d..87a7b67b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,6 +176,7 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.v.outputs.version }} + latest: ${{ steps.latest.outputs.latest }} steps: # Full history, and every branch as origin/*: the ancestry check cannot # be answered from a shallow clone. @@ -205,6 +206,26 @@ jobs: [ -n "$on" ] || { echo "$TAG is not on main or a release/* branch" >&2; exit 1; } echo "$TAG is on $on" echo "version=$V" >> "$GITHUB_OUTPUT" + # inbuxa: :latest belongs to the newest series. Until 27.1 ships it is + # 2026.x's; once a final release numbered by series exists (v27.1, + # v27.1.3), a 2026.x security release from this branch leaves :latest, + # GHCR's :latest and the GitHub "latest release" where they are. The + # check fails rather than guess: a wrong :latest is worse than a re-run. + - id: latest + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -euo pipefail + newer="$(curl -fsS --retry 5 --retry-all-errors --retry-delay 10 -H "Authorization: token $GITEA_TOKEN" \ + "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/releases?draft=false&pre-release=false&limit=50" | + jq -r '[.[].tag_name | select(test("^v[0-9]{1,3}\\."))] | first // ""')" + if [ -n "$newer" ]; then + echo "$newer is out: :latest stays on its series" + echo "latest=false" >> "$GITHUB_OUTPUT" + else + echo "no series release yet: :latest moves" + echo "latest=true" >> "$GITHUB_OUTPUT" + fi # Each architecture on its own native runner, side by side. The Dockerfile # cross-compiles from the build platform, and on the self-hosted runners one @@ -273,6 +294,7 @@ jobs: runs-on: ubuntu-latest env: VERSION: ${{ needs.version.outputs.version }} + LATEST: ${{ needs.version.outputs.latest }} steps: - run: echo "IMAGE=${{ vars.REGISTRY }}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 @@ -282,9 +304,9 @@ jobs: username: jcoffey-dev password: ${{ secrets.GITEA_TOKEN }} - run: | - docker buildx imagetools create \ - --tag "$IMAGE:$VERSION" \ - --tag "$IMAGE:latest" \ + tags=(--tag "$IMAGE:$VERSION") + [ "$LATEST" = true ] && tags+=(--tag "$IMAGE:latest") + docker buildx imagetools create "${tags[@]}" \ "$IMAGE:$VERSION-amd64" "$IMAGE:$VERSION-arm64" docker buildx imagetools inspect "$IMAGE:$VERSION" # Gitea keeps a container package on its owner; linking it shows it on @@ -308,6 +330,7 @@ jobs: - env: TAG: ${{ github.ref_name }} VERSION: ${{ needs.version.outputs.version }} + LATEST: ${{ needs.version.outputs.latest }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} REGISTRY: ${{ vars.REGISTRY }} run: | @@ -317,7 +340,9 @@ jobs: if [ "$code" = 200 ]; then echo "$TAG already has a release"; exit 0; fi [ "$code" = 404 ] || { echo "looking up the release for $TAG answered $code" >&2; exit 1; } image="$REGISTRY/${GITHUB_REPOSITORY,,}:$VERSION" - body="Container image: \`$image\` (linux/amd64, linux/arm64); also \`:latest\`. + also="; also \`:latest\`." + [ "$LATEST" = true ] || also=". \`:latest\` stays on the newer series." + body="Container image: \`$image\` (linux/amd64, linux/arm64)$also Binaries for a host install are attached: \`inbuxa-linux-amd64.tar.gz\` and \`inbuxa-linux-arm64.tar.gz\`, with \`SHA256SUMS\`. Each is the binary out of this release's image for that architecture, so it is the same build. The image grants it \`cap_net_bind_service\`; a host install has to grant that itself (\`setcap\`, or \`AmbientCapabilities\` in the unit) to bind port 25." jq -n --arg tag "$TAG" --arg name "INBUXA $VERSION" --arg body "$body" \ @@ -398,13 +423,16 @@ jobs: - env: GH_TOKEN: ${{ github.token }} TAG: ${{ needs.version.outputs.version }} + LATEST: ${{ needs.version.outputs.latest }} run: | set -euo pipefail src="${{ vars.REGISTRY }}/${GITHUB_REPOSITORY,,}" dst="ghcr.io/${GITHUB_REPOSITORY,,}" tag="$TAG" echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin - docker buildx imagetools create -t "$dst:$tag" -t "$dst:latest" "$src:$tag" + tags=(-t "$dst:$tag") + [ "$LATEST" = true ] && tags+=(-t "$dst:latest") + docker buildx imagetools create "${tags[@]}" "$src:$tag" want="$(docker buildx imagetools inspect "$src:$tag" --format '{{json .Manifest.Digest}}')" got="$(docker buildx imagetools inspect "$dst:$tag" --format '{{json .Manifest.Digest}}')" echo "registry $src:$tag = $want" @@ -420,7 +448,7 @@ jobs: # rewritten to Gitea links: on GitHub a bare #16 is some other PR. github-release: if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'tag' }} - needs: [binaries] + needs: [version, binaries] runs-on: ubuntu-latest permissions: contents: write @@ -428,6 +456,7 @@ jobs: GITEA_URL: ${{ vars.GITEA_URL }} GH_TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} + LATEST: ${{ needs.version.outputs.latest }} steps: - run: | set -euo pipefail @@ -454,7 +483,9 @@ jobs: curl -fsSL -o "files/$name" "$url"; files+=("files/$name") done < <(jq -r '.assets[]? | [.name, .browser_download_url] | @tsv' rel.json) title="$(jq -r '.name // ""' rel.json)"; [ -n "$title" ] || title="$TAG" - if [ "$(jq -r .prerelease rel.json)" = true ]; then kind=--prerelease; else kind=--latest; fi + if [ "$(jq -r .prerelease rel.json)" = true ]; then kind=--prerelease + elif [ "$LATEST" = true ]; then kind=--latest + else kind=--latest=false; fi gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --title "$title" \ --notes-file notes.md "$kind" "${files[@]}" echo "created the GitHub release for $TAG with ${#files[@]} file(s)" -- 2.54.0