Skip to content

Release

Release #2

Workflow file for this run

name: Release
on:
workflow_dispatch:
permissions:
contents: read
actions: read
concurrency:
group: keyload-release
cancel-in-progress: false
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: managedcode/KeyLoad
jobs:
version:
name: Reserve release version
if: github.repository == 'managedcode/KeyLoad' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.reserve.outputs.version }}
tag: ${{ steps.reserve.outputs.tag }}
assembly_version: ${{ steps.reserve.outputs.assembly_version }}
file_version: ${{ steps.reserve.outputs.file_version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Recover this run's immutable version reservation
shell: bash
run: |
mkdir -p artifacts/reservation
gh api --paginate "repos/$GH_REPO/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" --jq '.artifacts[]' | jq -s > artifacts/reservation-inventory.json
name="release-version-$GITHUB_RUN_ID"
count=$(jq --arg name "$name" '[.[] | select(.name == $name)] | length' artifacts/reservation-inventory.json)
[[ "$count" == 0 || "$count" == 1 ]]
if [[ "$count" == 1 ]]; then
jq -e --arg name "$name" '.[] | select(.name == $name) | .expired == false' artifacts/reservation-inventory.json
gh run download "$GITHUB_RUN_ID" --repo "$GH_REPO" --name "$name" --dir artifacts/reservation
elif [[ "$GITHUB_RUN_ATTEMPT" != 1 ]]; then
printf '%s\n' 'Missing original version reservation; start a new release run.' >&2
exit 1
fi
printf 'RELEASE_RESERVATION_EXISTS=%s\n' "$count" >> "$GITHUB_ENV"
- name: Authenticate release identity and reserve the UTC daily version
id: reserve
shell: bash
run: |
args=(version --output=artifacts/version-input.json)
if [[ "$RELEASE_RESERVATION_EXISTS" == 1 ]]; then args+=(--reservation=artifacts/reservation/release-version.json); fi
python3 scripts/Features/ReleaseDelivery/release-github-context.py "${args[@]}"
node scripts/Features/ReleaseDelivery/release-version-cli.mjs --input=artifacts/version-input.json --output=artifacts/reservation/release-version.json
- name: Retain the immutable version reservation
if: env.RELEASE_RESERVATION_EXISTS == '0'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-version-${{ github.run_id }}
path: artifacts/reservation/release-version.json
if-no-files-found: error
retention-days: 90
build:
name: Build release assets
needs: version
runs-on: ubuntu-latest
timeout-minutes: 60
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
RELEASE_ASSEMBLY_VERSION: ${{ needs.version.outputs.assembly_version }}
RELEASE_FILE_VERSION: ${{ needs.version.outputs.file_version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: release-version-${{ github.run_id }}
path: artifacts/reservation
- name: Recover completed assets without rebuilding an existing release
shell: bash
run: |
mkdir -p artifacts/assets
gh api --paginate "repos/$GH_REPO/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" --jq '.artifacts[]' | jq -s > artifacts/asset-inventory.json
name="release-assets-$GITHUB_RUN_ID"
count=$(jq --arg name "$name" '[.[] | select(.name == $name)] | length' artifacts/asset-inventory.json)
[[ "$count" == 0 || "$count" == 1 ]]
if [[ "$count" == 1 ]]; then
jq -e --arg name "$name" '.[] | select(.name == $name) | .expired == false' artifacts/asset-inventory.json
gh run download "$GITHUB_RUN_ID" --repo "$GH_REPO" --name "$name" --dir artifacts/assets
fi
printf 'RELEASE_ASSETS_EXISTS=%s\n' "$count" >> "$GITHUB_ENV"
- name: Restore, build, format and verify the complete source
if: env.RELEASE_ASSETS_EXISTS == '0'
shell: bash
run: |
node scripts/Features/RepositoryGovernance/verify.mjs
dotnet restore KeyLoad.slnx
dotnet build KeyLoad.slnx --no-restore --configuration Release -p:Version="$RELEASE_VERSION" -p:AssemblyVersion="$RELEASE_ASSEMBLY_VERSION" -p:FileVersion="$RELEASE_FILE_VERSION"
dotnet format KeyLoad.slnx --verify-no-changes --no-restore
- name: Build all NuGet packages and the Linux x64 RF3 database distribution
if: env.RELEASE_ASSETS_EXISTS == '0'
shell: bash
run: |
args=(-p:Version="$RELEASE_VERSION" -p:AssemblyVersion="$RELEASE_ASSEMBLY_VERSION" -p:FileVersion="$RELEASE_FILE_VERSION")
dotnet pack KeyLoad.slnx --no-build --no-restore --configuration Release "${args[@]}" --output artifacts/assets
dotnet publish src/KeyLoad.Server/KeyLoad.Server.csproj --configuration Release --runtime linux-x64 --self-contained true "${args[@]}" --output artifacts/distribution/server
dotnet publish src/KeyLoad.Cli/KeyLoad.Cli.csproj --configuration Release --runtime linux-x64 --self-contained true "${args[@]}" --output artifacts/distribution/cli
cp scripts/Features/ReleaseDelivery/distribution/{compose.yml,initialize.sh,README.md,.env.example} artifacts/distribution/
cp artifacts/reservation/release-version.json artifacts/distribution/
chmod 755 artifacts/distribution/initialize.sh
tar --owner=0 --group=0 -C artifacts/distribution -czf "artifacts/assets/keyload-database-$RELEASE_VERSION-linux-x64.tar.gz" server cli compose.yml initialize.sh README.md .env.example release-version.json
- name: Build and export versioned database and benchmark Docker images
if: env.RELEASE_ASSETS_EXISTS == '0'
shell: bash
run: |
docker version
for role in server benchmarks; do
dockerfile=Dockerfile
if [[ "$role" == benchmarks ]]; then dockerfile=benchmarks/KeyLoad.ComparisonHost/Features/BenchmarkComparisons/Dockerfile; fi
reference="ghcr.io/managedcode/keyload-$role:$RELEASE_VERSION"
archive="keyload-$role-$RELEASE_VERSION-linux-amd64.docker.tar.gz"
docker build --platform linux/amd64 --file "$dockerfile" --tag "$reference" --build-arg "KEYLOAD_RELEASE_VERSION=$RELEASE_VERSION" --build-arg "KEYLOAD_ASSEMBLY_VERSION=$RELEASE_ASSEMBLY_VERSION" --build-arg "KEYLOAD_FILE_VERSION=$RELEASE_FILE_VERSION" --label "org.opencontainers.image.version=$RELEASE_VERSION" --label "org.opencontainers.image.revision=$GITHUB_SHA" --label "org.opencontainers.image.source=https://github.com/$GH_REPO" .
docker save "$reference" | gzip -n > "artifacts/assets/$archive"
docker image inspect "$reference" | jq --arg role "$role" --arg ref "$reference" --arg archive "$archive" '.[0] | {role:$role,reference:$ref,id:.Id,labels:.Config.Labels,archive:$archive}' >> artifacts/images.jsonl
done
jq -s . artifacts/images.jsonl > artifacts/images.json
- name: Verify embedded package versions, real image IDs and every asset hash
shell: bash
run: |
if [[ "$RELEASE_ASSETS_EXISTS" == 1 ]]; then
jq '[.images[] | {role,reference,id,labels,archive:.file}]' artifacts/assets/release-manifest.json > artifacts/images.json
(cd artifacts/assets && sha256sum --check SHA256SUMS)
fi
python3 scripts/Features/ReleaseDelivery/release-assets.py --reservation=artifacts/reservation/release-version.json --assets=artifacts/assets --images=artifacts/images.json
- name: Retain immutable release packages, distribution and image exports
if: env.RELEASE_ASSETS_EXISTS == '0'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-assets-${{ github.run_id }}
path: artifacts/assets/*
if-no-files-found: error
retention-days: 90
- name: Retain compiler diagnostic reports
if: always() && env.RELEASE_ASSETS_EXISTS == '0'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: code-quality-release-${{ github.run_attempt }}
path: artifacts/code-quality/**
if-no-files-found: error
publish:
name: Publish tagged database release
needs: [version, build]
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
packages: write
actions: read
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
RELEASE_TAG: ${{ needs.version.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: release-version-${{ github.run_id }}
path: artifacts/reservation
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: release-assets-${{ github.run_id }}
path: artifacts/assets
- name: Require successful CI for this exact source before publication
shell: bash
run: |
mkdir -p artifacts/publication
python3 scripts/Features/ReleaseDelivery/release-github-context.py ci --output=artifacts/publication/ci-proof.json
(cd artifacts/assets && sha256sum --check SHA256SUMS)
jq '[.images[] | {role,reference,id,labels,archive:.file}]' artifacts/assets/release-manifest.json > artifacts/images.json
python3 scripts/Features/ReleaseDelivery/release-assets.py --reservation=artifacts/reservation/release-version.json --assets=artifacts/assets --images=artifacts/images.json
- name: Create or verify the immutable annotated source tag
shell: bash
run: |
manifest_hash=$(sha256sum artifacts/assets/release-manifest.json | cut -d ' ' -f 1)
printf 'KeyLoad release %s\n\nSource: %s\nRelease-Run: %s\nManifest-SHA256: %s\n' "$RELEASE_VERSION" "$GITHUB_SHA" "$GITHUB_RUN_ID" "$manifest_hash" > artifacts/publication/tag-message.txt
if git show-ref --verify --quiet "refs/tags/$RELEASE_TAG"; then
[[ "$(git cat-file -t "refs/tags/$RELEASE_TAG")" == tag ]]
[[ "$(git rev-parse "$RELEASE_TAG^{commit}")" == "$GITHUB_SHA" ]]
[[ "$(git for-each-ref --format='%(contents)' "refs/tags/$RELEASE_TAG")" == "$(cat artifacts/publication/tag-message.txt)" ]]
else
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git tag --annotate "$RELEASE_TAG" "$GITHUB_SHA" --file artifacts/publication/tag-message.txt
git -c credential.helper='!gh auth git-credential' push origin "refs/tags/$RELEASE_TAG"
fi
[[ "$(git ls-remote origin "refs/tags/$RELEASE_TAG^{}" | cut -f 1)" == "$GITHUB_SHA" ]]
- name: Publish immutable versioned GHCR images and verify their native digests
shell: bash
run: |
printf '%s' "$GH_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin
jq -c '.images[]' artifacts/assets/release-manifest.json > artifacts/publication/image-inputs.jsonl
while IFS= read -r record; do
reference=$(jq -r .reference <<< "$record")
expected=$(jq -r .id <<< "$record")
archive=$(jq -r .file <<< "$record")
docker load --input "artifacts/assets/$archive"
[[ "$(docker image inspect "$reference" --format '{{.Id}}')" == "$expected" ]]
if docker pull "$reference" > artifacts/publication/pull.log 2>&1; then
[[ "$(docker image inspect "$reference" --format '{{.Id}}')" == "$expected" ]]
elif rg -q 'manifest unknown|manifest not found' artifacts/publication/pull.log; then
docker push "$reference"
docker pull "$reference"
[[ "$(docker image inspect "$reference" --format '{{.Id}}')" == "$expected" ]]
else
cat artifacts/publication/pull.log >&2
exit 1
fi
docker image inspect "$reference" | jq --arg ref "$reference" '.[0] | {reference:$ref,id:.Id,digests:.RepoDigests,labels:.Config.Labels} | select(.digests | length > 0)' >> artifacts/publication/image-results.jsonl
done < artifacts/publication/image-inputs.jsonl
jq -se 'length == 2 and all(.[]; .digests | length > 0)' artifacts/publication/image-results.jsonl
jq -s . artifacts/publication/image-results.jsonl > artifacts/publication/images.json
- name: Create the owned GitHub Release and verify all immutable assets
shell: bash
run: |
marker="<!-- KeyLoad release run=$GITHUB_RUN_ID source=$GITHUB_SHA manifest=$(sha256sum artifacts/assets/release-manifest.json | cut -d ' ' -f 1) -->"
if ! gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" > artifacts/publication/release.json 2> artifacts/publication/release-error.txt; then
rg -q 'HTTP 404' artifacts/publication/release-error.txt
printf '%s\n\nVersion `%s`; source `%s`.\n\nIncludes NuGet packages, the Linux x64 RF3 distribution and exported server/benchmark images. Packaging does not establish production readiness.\n\nCI: %s\n' "$marker" "$RELEASE_VERSION" "$GITHUB_SHA" "$(jq -er .runUrl artifacts/publication/ci-proof.json)" > artifacts/publication/release-notes.md
gh release create "$RELEASE_TAG" --repo "$GH_REPO" --verify-tag --target "$GITHUB_SHA" --title "KeyLoad $RELEASE_VERSION" --notes-file artifacts/publication/release-notes.md --draft --prerelease
gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" > artifacts/publication/release.json
fi
jq -e --arg marker "$marker" --arg tag "$RELEASE_TAG" '.tag_name == $tag and (.body | contains($marker))' artifacts/publication/release.json
for file in artifacts/assets/*; do
name=$(basename "$file")
count=$(jq --arg name "$name" '[.assets[] | select(.name == $name)] | length' artifacts/publication/release.json)
[[ "$count" == 0 || "$count" == 1 ]]
if [[ "$count" == 0 ]]; then
jq -e '.draft == true' artifacts/publication/release.json
gh release upload "$RELEASE_TAG" "$file" --repo "$GH_REPO"
gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" > artifacts/publication/release.json
fi
id=$(jq -er --arg name "$name" '.assets[] | select(.name == $name) | .id' artifacts/publication/release.json)
gh api "repos/$GH_REPO/releases/assets/$id" -H 'Accept: application/octet-stream' > artifacts/publication/asset-verification.bin
[[ "$(sha256sum "$file" | cut -d ' ' -f 1)" == "$(sha256sum artifacts/publication/asset-verification.bin | cut -d ' ' -f 1)" ]]
done
[[ "$(jq '.assets | length' artifacts/publication/release.json)" == "$(find artifacts/assets -maxdepth 1 -type f | wc -l)" ]]
rm -f artifacts/publication/asset-verification.bin
gh release edit "$RELEASE_TAG" --repo "$GH_REPO" --draft=false
gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" > artifacts/publication/release.json
jq -e --arg tag "$RELEASE_TAG" '.tag_name == $tag and .draft == false and (.html_url | startswith("https://github.com/managedcode/KeyLoad/releases/tag/"))' artifacts/publication/release.json
- name: Retain actual CI, tag, image and GitHub Release receipts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-publication-${{ github.run_id }}-${{ github.run_attempt }}
path: artifacts/publication/**
if-no-files-found: error