Skip to content

release

release #10

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
artifact-metadata: read
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
# One SPDX document per architecture, scanned in the build job. Each is
# around 52 MB of JSON, which is a hostile download to hang off a release
# page, and it is highly repetitive - measured at 93% compression on
# v2026.08.04 - so both ship gzipped.
- name: Download SBOMs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: /tmp/sbom
pattern: sbom-*
merge-multiple: true
- name: Compress SBOMs
run: |
set -euo pipefail
shopt -s nullglob
files=(/tmp/sbom/*.spdx.json)
if [ ${#files[@]} -eq 0 ]; then
echo "::error::no SBOMs were downloaded - nothing to attach"
exit 1
fi
for f in "${files[@]}"; do
raw="$(wc -c < "$f")"
gzip -9 "$f"
gz="$(wc -c < "${f}.gz")"
echo "$(basename "$f"): ${raw} bytes raw, ${gz} bytes gzipped"
done
- 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-amd64.spdx.json.gz\` and \`sbom-arm64.spdx.json.gz\` below are the
SPDX SBOMs for each architecture, generated with Syft. Gunzip to read;
each is an ordinary SPDX 2.3 document any SPDX tool will accept.
EOF
gh release create "v${VERSION}" \
--target "${GITHUB_SHA}" \
--title "v${VERSION}" \
--notes-file notes.md \
/tmp/sbom/*.spdx.json.gz