Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: <image>/v<major>.<minor>.<patch>" >&2
echo "Expected format: <image>/v<major>.<minor>.<patch>[-r<revision>]" >&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
Expand Down Expand Up @@ -130,19 +140,19 @@ 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 }}
org.opencontainers.image.licenses=${{ steps.catalog_meta.outputs.license }}
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 }}
Expand Down Expand Up @@ -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 }}
Expand All @@ -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}` +
Expand All @@ -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}\``,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: <image>/v<major>.<minor>.<patch>" >&2
echo "Expected format: <image>/v<major>.<minor>.<patch>[-r<revision>]" >&2
exit 1
fi

Expand Down
28 changes: 20 additions & 8 deletions docs/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,23 @@ separate where practical.
Images are released independently from tags in this form:

```text
<tool>/v<version>
<tool>/v<upstream-version>[-r<packaging-revision>]
```

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 `<tool>/v<upstream-version>-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 <tool> to <version>`,
optionally followed by GitHub's squash-merge suffix ` (#<pull-request>)`,
Expand All @@ -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.

<!-- tiny-cli-images:version:xh:start -->

Expand All @@ -174,14 +184,16 @@ ghcr.io/<owner>/xh:0.26.2

<!-- tiny-cli-images:version:xh:end -->

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 `<tool>/v<upstream-version>-r1` instead publishes
`ghcr.io/<owner>/<tool>:<upstream-version>-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.
Expand Down
9 changes: 6 additions & 3 deletions images/age/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<!-- tiny-cli-images:version:age:end -->

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 `<version>-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:<digest>
Expand Down
9 changes: 6 additions & 3 deletions images/xh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<!-- tiny-cli-images:version:xh:end -->

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 `<version>-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:<digest>
Expand Down