Skip to content

release

release #4

Workflow file for this run

name: release
# Cuts a calendar-versioned release every Sunday from a no-cache rebuild, so
# the published image tracks upstream tool and base-image updates.
#
# CalVer rather than SemVer on purpose: almost every version in versions.json
# is "install latest", so a tag records what the world looked like on a given
# day. It cannot promise a compatibility contract, and SemVer would imply one.
#
# NOTE: GitHub disables scheduled workflows after 60 days without repository
# activity. If releases stop appearing, check whether the schedule was
# disabled before looking for a bug.
on:
schedule:
- cron: '0 3 * * 0'
workflow_dispatch:
inputs:
no-cache:
description: Rebuild from scratch so unpinned tools pick up upstream updates
type: boolean
default: true
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
jobs:
version:
name: resolve version
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
version: ${{ steps.calc.outputs.version }}
month: ${{ steps.calc.outputs.month }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Compute CalVer
id: calc
run: |
set -euo pipefail
base="$(date -u +%Y.%m.%d)"
version="${base}"
n=1
# A second release on the same day becomes .2, .3, and so on
while git rev-parse -q --verify "refs/tags/v${version}" > /dev/null 2>&1; do
n=$((n + 1))
version="${base}.${n}"
done
{
echo "version=${version}"
echo "month=$(date -u +%Y.%m)"
} >> "$GITHUB_OUTPUT"
echo "Releasing v${version}"
build:
name: build
needs: version
uses: ./.github/workflows/build.yml
permissions:
contents: read
packages: write
security-events: write
id-token: write
attestations: write
artifact-metadata: write
with:
publish: true
no-cache: ${{ github.event_name == 'schedule' || inputs.no-cache }}
tags: |
type=raw,value=${{ needs.version.outputs.version }}
type=raw,value=${{ needs.version.outputs.month }}
type=raw,value=latest
release:
name: publish release
needs: [version, build]
runs-on: ubuntu-24.04
permissions:
contents: write
packages: read
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# The SBOM is already attached to the image as a build attestation; this
# pulls it out into a file so the release has a downloadable copy.
# Two architectures' worth of package inventory is well over 100 MB of
# JSON, which is a hostile download to hang off a release page. It is
# highly repetitive, so it compresses hard - measured at 138.5 MB down to
# 10.0 MB on v2026.08.04, a saving of 93%.
- name: Extract SBOM
run: |
set -euo pipefail
docker buildx imagetools inspect \
"ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}" \
--format '{{ json .SBOM }}' > sbom.json
raw="$(wc -c < sbom.json)"
gzip -9 sbom.json
gz="$(wc -c < sbom.json.gz)"
echo "SBOM: ${raw} bytes raw, ${gz} bytes gzipped"
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.version.outputs.version }}
DIGEST: ${{ needs.build.outputs.digest }}
run: |
set -euo pipefail
cat > notes.md <<EOF
Multi-architecture devcontainer image for \`linux/amd64\` and \`linux/arm64\`.
\`\`\`
docker pull ghcr.io/${{ github.repository }}:${VERSION}
\`\`\`
Digest: \`${DIGEST}\`
This tag is a point-in-time snapshot. Most tools in \`versions.json\` are
installed at "latest", so the same tag rebuilt on another day would not
produce the same image - pin the digest above if you need one exact image.
Verify the build provenance:
\`\`\`
gh attestation verify oci://ghcr.io/${{ github.repository }}:${VERSION} -R ${{ github.repository }}
\`\`\`
\`sbom.json.gz\` below is the CycloneDX SBOM for both architectures,
gunzip to read. The same data is attached to the image itself as a
build attestation:
\`\`\`
docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${VERSION} --format '{{ json .SBOM }}'
\`\`\`
EOF
gh release create "v${VERSION}" \
--target "${GITHUB_SHA}" \
--title "v${VERSION}" \
--notes-file notes.md \
sbom.json.gz