Mirror tagged releases to the GitHub Releases.

The Buildkite release pipeline uploads artifacts to gs://gvisor/releases,
but nothing publishes them as a GitHub Release, so the tags pushed by
tools/tag_release.sh carry no notes and no binaries.

Add a github action that triggers on release-* tags, waits for every release
object to appear in GCS, verifies each raw artifact against its published
sha512, creates or updates the release with a changelog.

It only mirrors what Buildkite already built; apt and the GCS bucket remain
canonical.
This commit is contained in:
Jing Chen
2026-08-24 22:51:45 +00:00
parent 02534b0728
commit c6fa3dfd4e
2 changed files with 267 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
# This workflow mirrors an existing gVisor release to the GitHub Releases page.
#
# It does NOT build anything. The release itself is produced by the internal
# Buildkite pipeline (see .buildkite/release.yaml), which builds both
# architectures and uploads the artifacts to gs://gvisor/releases/. This
# workflow waits for those artifacts to appear, verifies them against the
# published sha512 digests, and attaches them to a GitHub Release created from
# the tag pushed by tools/tag_release.sh.
#
# The GCS bucket and the apt repository remain the canonical distribution
# channels; see g3doc/user_guide/install.md.
name: "Release Mirror"
"on":
push:
tags:
- 'release-[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish (e.g. release-20260817.0)'
required: true
type: string
# Never cancel a partially-published release; queue instead.
concurrency:
group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
publish-release:
name: Mirror release to GitHub
runs-on: ubuntu-latest
# The Buildkite release step allows 180 minutes plus retries, and this job
# polls for its output, so allow generous headroom.
timeout-minutes: 300
permissions:
contents: write
steps:
- name: Determine tag and version
id: meta
env:
INPUT_TAG: ${{ inputs.tag }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
TAG="${INPUT_TAG:-${REF_NAME}}"
# Mirrors the validation in tools/tag_release.sh.
if ! [[ "${TAG}" =~ ^release-20[0-9]{6}\.[0-9]+$ ]]; then
echo "::error::Not a valid release tag: '${TAG}'"
exit 1
fi
echo "tag=${TAG}" >>"${GITHUB_OUTPUT}"
echo "version=${TAG#release-}" >>"${GITHUB_OUTPUT}"
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
# Full history and tags are required to compute the changelog range.
fetch-depth: 0
persist-credentials: false
- name: Verify tag exists
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail
git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null || {
echo "::error::Tag '${TAG}' does not exist in this repository."
exit 1
}
- name: Wait for Buildkite artifacts in GCS
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
source .github/workflows/release_mirror_lib.sh
declare -a urls=()
for arch in "${ARCHS[@]}"; do
for f in "${RAW_FILES[@]}"; do
urls+=("$(raw_url "${VERSION}" "${arch}" "${f}")")
urls+=("$(raw_url "${VERSION}" "${arch}" "${f}.sha512")")
done
done
# gcloud uploads the repo tree in an unspecified order, so wait for
# every required object rather than using one of them as a proxy.
readonly TIMEOUT_SECONDS=$((240 * 60))
start="$(date +%s)"
for url in "${urls[@]}"; do
while true; do
# HEAD only: the tarballs are large and this loop is hot.
# `|| true` keeps a transient network failure from tripping -e;
# curl still emits 000 in that case.
status="$(curl -sS -o /dev/null -w '%{http_code}' -I "${url}" 2>/dev/null || true)"
if [[ "${status:-000}" == "200" ]]; then
echo "ready: ${url}"
break
fi
elapsed=$(( $(date +%s) - start ))
if (( elapsed >= TIMEOUT_SECONDS )); then
echo "::error::Timed out after $((elapsed / 60))m waiting for ${url} (last status ${status})."
exit 1
fi
echo "waiting for ${url} (status ${status}, elapsed $((elapsed / 60))m)"
sleep 30
done
done
- name: Download and verify artifacts
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
source .github/workflows/release_mirror_lib.sh
mkdir -p dist
for arch in "${ARCHS[@]}"; do
stage="$(mktemp -d)"
for f in "${RAW_FILES[@]}"; do
curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors \
"$(raw_url "${VERSION}" "${arch}" "${f}")" -o "${stage}/${f}"
curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors \
"$(raw_url "${VERSION}" "${arch}" "${f}.sha512")" -o "${stage}/${f}.sha512"
# The published digest records the bare basename, so verify
# before renaming for the flat release-asset namespace.
(cd "${stage}" && sha512sum -c "${f}.sha512")
mv "${stage}/${f}" "dist/$(asset_name "${f}" "${arch}")"
done
rm -rf "${stage}"
done
# Compute both manifests over the same file list so that neither
# includes the other.
(
cd dist
mapfile -t assets < <(find . -maxdepth 1 -type f -printf '%P\n' | sort)
sha256sum -- "${assets[@]}" >SHA256SUMS
sha512sum -- "${assets[@]}" >SHA512SUMS
)
ls -l dist
- name: Generate release notes
id: notes
env:
TAG: ${{ steps.meta.outputs.tag }}
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
prev_tag="$(git describe --tags --abbrev=0 --match 'release-*' "${TAG}^" 2>/dev/null || true)"
if [[ -z "${prev_tag}" ]]; then
echo "::error::Could not determine the release tag preceding ${TAG}; refusing to publish empty notes."
exit 1
fi
notes="${RUNNER_TEMP}/release_notes.md"
{
echo "## gVisor release ${VERSION}"
echo
echo "Binaries are also available from the apt repository and the release bucket;"
echo "see [Installation](https://gvisor.dev/docs/user_guide/install/)."
echo
echo "### Changes since \`${prev_tag}\`"
echo
# Fenced: commit subjects are not written as Markdown and regularly
# contain characters that would otherwise be interpreted.
echo '```text'
git log "${prev_tag}..${TAG}" --no-merges --pretty=format:'%s (%h)'
echo
echo '```'
echo
echo "**Full changelog**: https://github.com/${GITHUB_REPOSITORY}/compare/${prev_tag}...${TAG}"
} >"${notes}"
echo "path=${notes}" >>"${GITHUB_OUTPUT}"
cat "${notes}"
- name: Create or update the GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ steps.meta.outputs.tag }}
VERSION: ${{ steps.meta.outputs.version }}
NOTES: ${{ steps.notes.outputs.path }}
run: |
set -euo pipefail
# Only mark the newest release tag as "latest", so that backfilling an
# older tag by hand does not move the pointer backwards.
newest="$(git tag --list 'release-*' --sort=-v:refname | head -n1)"
if [[ "${TAG}" == "${newest}" ]]; then is_latest=true; else is_latest=false; fi
# The Buildkite release step retries automatically, so this has to be
# safe to run more than once for the same tag.
if gh release view "${TAG}" >/dev/null 2>&1; then
echo "Release ${TAG} already exists; updating."
gh release upload "${TAG}" dist/* --clobber
gh release edit "${TAG}" \
--title "gVisor ${VERSION}" \
--notes-file "${NOTES}" \
--latest="${is_latest}"
else
# --verify-tag: without it, a mistyped workflow_dispatch input would
# create a bogus tag at the default branch head.
gh release create "${TAG}" \
--verify-tag \
--title "gVisor ${VERSION}" \
--notes-file "${NOTES}" \
--latest="${is_latest}" \
dist/*
fi
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Copyright 2026 The gVisor Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Shared release-artifact layout for .github/workflows/release_mirror.yml.
# Sourced, not executed; keeps the "wait" and "download" steps in agreement
# about exactly which objects make up a release.
declare -r RELEASES_URL="https://storage.googleapis.com/gvisor/releases"
# Architecture directory names for raw artifacts. These are derived by
# tools/make_release.sh from `file` output, and are also what
# g3doc/user_guide/install.md tells users to plug in from `uname -m`.
declare -ra ARCHS=(x86_64 aarch64)
# Raw artifacts to mirror, each with a sibling .sha512.
#
# Only the release tarball is mirrored. It already contains runsc,
# runsc-metric-server and containerd-shim-runsc-v1, and those standalone
# binaries are being retired. Debian packages are deliberately not mirrored
# either: they are served by the apt repository, which also provides the
# signed release metadata and the upgrade path that a release asset cannot.
declare -ra RAW_FILES=(
gvisor.tar.bz2
)
# raw_url <version> <arch> <filename>
raw_url() {
echo "${RELEASES_URL}/release/$1/$2/$3"
}
# asset_name <filename> <arch>
#
# GitHub release assets share one flat namespace, so per-architecture files
# have to carry the architecture in their name.
asset_name() {
case "$1" in
*.tar.bz2) echo "${1%.tar.bz2}-$2.tar.bz2" ;;
*) echo "$1-$2" ;;
esac
}