From be3c59d5ec2c5c917d1a22b080daa77b104f69ba Mon Sep 17 00:00:00 2001 From: Aleksandr Cupacenko Date: Sun, 9 Aug 2026 22:11:06 +0300 Subject: [PATCH] Support exceptional packaging revision tags --- .github/workflows/release.yml | 44 +++++++++++++++++++++++++---------- .github/workflows/tag.yml | 6 ++--- docs/PROJECT.md | 28 +++++++++++++++------- images/age/README.md | 9 ++++--- images/xh/README.md | 9 ++++--- 5 files changed, 67 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64d5b00..8d8a078 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: tag: - description: Existing release tag to rebuild, for example age/v1.3.1 + description: Existing release tag, optionally ending in -rN required: true type: string @@ -43,15 +43,25 @@ jobs: tag="$RELEASE_TAG" - if [[ ! "$tag" =~ ^([a-z0-9][a-z0-9-]*)/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + if [[ ! "$tag" =~ ^([a-z0-9][a-z0-9-]*)/v([0-9]+\.[0-9]+\.[0-9]+)(-r([1-9][0-9]*))?$ ]]; then echo "Invalid release tag: $tag" >&2 - echo "Expected format: /v.." >&2 + echo "Expected format: /v..[-r]" >&2 exit 1 fi + tool="${BASH_REMATCH[1]}" + version="${BASH_REMATCH[2]}" + packaging_revision="${BASH_REMATCH[4]}" + image_version="$version" + if [[ -n "$packaging_revision" ]]; then + image_version="${image_version}-r${packaging_revision}" + fi + echo "name=$tag" >> "$GITHUB_OUTPUT" - echo "tool=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" - echo "version=${BASH_REMATCH[2]}" >> "$GITHUB_OUTPUT" + echo "tool=$tool" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "image_version=$image_version" >> "$GITHUB_OUTPUT" + echo "packaging_revision=$packaging_revision" >> "$GITHUB_OUTPUT" - name: Checkout release tag uses: actions/checkout@v7 @@ -130,11 +140,11 @@ jobs: with: images: ${{ steps.image.outputs.name }} tags: | - type=raw,value=${{ steps.meta.outputs.version }} + type=raw,value=${{ steps.tag.outputs.image_version }} labels: | org.opencontainers.image.title=${{ steps.meta.outputs.name }} org.opencontainers.image.description=${{ steps.catalog_meta.outputs.description }} - org.opencontainers.image.version=${{ steps.meta.outputs.version }} + org.opencontainers.image.version=${{ steps.tag.outputs.image_version }} org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.revision=${{ steps.release_revision.outputs.sha }} org.opencontainers.image.documentation=https://github.com/${{ github.repository }}/tree/${{ steps.catalog_revision.outputs.sha }}/images/${{ steps.tag.outputs.tool }} @@ -142,7 +152,7 @@ jobs: annotations: | org.opencontainers.image.title=${{ steps.meta.outputs.name }} org.opencontainers.image.description=${{ steps.catalog_meta.outputs.description }} - org.opencontainers.image.version=${{ steps.meta.outputs.version }} + org.opencontainers.image.version=${{ steps.tag.outputs.image_version }} org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.revision=${{ steps.release_revision.outputs.sha }} org.opencontainers.image.documentation=https://github.com/${{ github.repository }}/tree/${{ steps.catalog_revision.outputs.sha }}/images/${{ steps.tag.outputs.tool }} @@ -213,6 +223,8 @@ jobs: TOOL: ${{ steps.meta.outputs.name }} DESCRIPTION: ${{ steps.catalog_meta.outputs.description }} VERSION: ${{ steps.meta.outputs.version }} + IMAGE_VERSION: ${{ steps.tag.outputs.image_version }} + PACKAGING_REVISION: ${{ steps.tag.outputs.packaging_revision }} DOCUMENTATION_REVISION: ${{ steps.catalog_revision.outputs.sha }} UPSTREAM: ${{ steps.meta.outputs.upstream }} IMAGE: ${{ steps.image.outputs.name }} @@ -225,12 +237,14 @@ jobs: const tool = process.env.TOOL; const description = process.env.DESCRIPTION; const version = process.env.VERSION; + const imageVersion = process.env.IMAGE_VERSION; + const packagingRevision = process.env.PACKAGING_REVISION; const documentationRevision = process.env.DOCUMENTATION_REVISION; const upstream = process.env.UPSTREAM; const image = process.env.IMAGE; const digest = process.env.DIGEST; const attestationUrl = process.env.ATTESTATION_URL; - const imageTag = `${image}:${version}`; + const imageTag = `${image}:${imageVersion}`; const imageDigest = `${image}@${digest}`; const workflowUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}` + @@ -251,11 +265,17 @@ jobs: links.push(`[Attestation](${attestationUrl})`); } + const releaseSummary = packagingRevision + ? `This release packages ${tool} v${version} as packaging revision ` + + `\`r${packagingRevision}\` for \`linux/amd64\` and \`linux/arm64\`.` + : `This release packages ${tool} v${version} for ` + + "`linux/amd64` and `linux/arm64`."; + const details = [ startMarker, `${description}.`, "", - `This release packages ${tool} v${version} for \`linux/amd64\` and \`linux/arm64\`.`, + releaseSummary, "", `**Image:** \`${imageTag}\``, `**Digest:** \`${digest}\``, @@ -287,7 +307,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, tag_name: tag, - name: `${tool} v${version} container image`, + name: `${tool} v${imageVersion} container image`, body: details, draft: false, prerelease: false, @@ -314,7 +334,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, release_id: release.id, - name: `${tool} v${version} container image`, + name: `${tool} v${imageVersion} container image`, body, draft: false, prerelease: release.prerelease, diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 07ac63f..8f601dd 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: tag: - description: Release tag to create, for example age/v1.3.1 + description: Release tag to create, optionally ending in -rN required: true type: string @@ -60,9 +60,9 @@ jobs: exit 0 fi - if [[ ! "$tag" =~ ^([a-z0-9][a-z0-9-]*)/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + if [[ ! "$tag" =~ ^([a-z0-9][a-z0-9-]*)/v([0-9]+\.[0-9]+\.[0-9]+)(-r([1-9][0-9]*))?$ ]]; then echo "Invalid release tag: $tag" >&2 - echo "Expected format: /v.." >&2 + echo "Expected format: /v..[-r]" >&2 exit 1 fi diff --git a/docs/PROJECT.md b/docs/PROJECT.md index 7ce53f1..ebe9083 100644 --- a/docs/PROJECT.md +++ b/docs/PROJECT.md @@ -140,14 +140,23 @@ separate where practical. Images are released independently from tags in this form: ```text -/v +/v[-r] ``` +Normal upstream releases use the exact upstream version in both the release tag +and image tag. A packaging revision is an exceptional recovery mechanism used +only when GitHub has permanently reserved the original tag after deletion of an +immutable release. In that case, an explicitly numbered suffix is appended, +for example `/v-r1`. It does not change the upstream +binary version or verified checksums. + The Release workflow can also be dispatched manually with an existing release tag. It builds the tagged source for traceability while reading package-page metadata and documentation from the current catalog, then recreates the image, attestations, signature, and GitHub Release without moving the tag. This is the -recovery path for a deliberately deleted GHCR package or GitHub Release. +recovery path for a deliberately deleted GHCR package or mutable GitHub +Release. A deleted immutable GitHub Release cannot reuse its tag; publish the +next packaging revision instead. A push to `main` whose commit subject matches `Update to `, optionally followed by GitHub's squash-merge suffix ` (#)`, @@ -162,7 +171,8 @@ creates the corresponding release tag. For example: Other commit subjects do not create a tag. The Tag workflow can also be dispatched manually from the default branch with an explicit tag override; both automatic and manual tags must match an existing image and its committed -manifest version. Existing tags are never moved. +manifest version. Packaging revision tags are created only through this manual +override for immutable-release recovery. Existing tags are never moved. @@ -174,14 +184,16 @@ ghcr.io//xh:0.26.2 -Only the full upstream version tag is published. Floating tags such as -`latest` and shortened version tags such as `0.26` are not published. Users can -also pin the image by its immutable OCI digest. +A packaging revision such as `/v-r1` instead publishes +`ghcr.io//:-r1`. Floating tags such as `latest` +and shortened version tags such as `0.26` are not published. Users can also pin +the image by its immutable OCI digest. The release workflow must: -1. Parse the tool and version from the tag. -2. Verify that the tag version equals the committed manifest version. +1. Parse the tool, upstream version, and optional packaging revision from the + tag. +2. Verify that the upstream version equals the committed manifest version. 3. Build and publish `linux/amd64` and `linux/arm64` as one OCI image. 4. Publish SBOM and build provenance attestations. 5. Sign the resulting digest using keyless OIDC signing. diff --git a/images/age/README.md b/images/age/README.md index 316f35e..44443ea 100644 --- a/images/age/README.md +++ b/images/age/README.md @@ -48,12 +48,15 @@ docker run --rm \ ## Pinning -Each release publishes only its full upstream version tag, such as `1.3.1`. +An upstream release publishes its full version tag, such as `1.3.1`. -The image does not publish `latest` or shortened version tags. For immutable -deployments, use the digest shown by the GitHub Release and release workflow: +Normal release and image tags match the upstream version exactly. An exceptional +recovery from a deleted immutable GitHub Release may use a packaging revision +such as `-r1`, without changing the upstream age version. The image +does not publish `latest` or shortened version tags. For immutable deployments, +use the digest shown by the GitHub Release and release workflow: ```text ghcr.io/unitmatrix/age@sha256: diff --git a/images/xh/README.md b/images/xh/README.md index e2cdddd..222c384 100644 --- a/images/xh/README.md +++ b/images/xh/README.md @@ -32,12 +32,15 @@ manager, and its entrypoint is `/xh`. ## Pinning -Each release publishes only its full upstream version tag, such as `0.26.2`. +An upstream release publishes its full version tag, such as `0.26.2`. -The image does not publish `latest` or shortened version tags. For immutable -deployments, use the digest shown by the GitHub Release and release workflow: +Normal release and image tags match the upstream version exactly. An exceptional +recovery from a deleted immutable GitHub Release may use a packaging revision +such as `-r1`, without changing the upstream xh version. The image does +not publish `latest` or shortened version tags. For immutable deployments, use +the digest shown by the GitHub Release and release workflow: ```text ghcr.io/unitmatrix/xh@sha256: