Skip to content

Stage Release Draft #10

Stage Release Draft

Stage Release Draft #10

Workflow file for this run

name: Stage Release Draft
# workflow_run uses this workflow's definition from the default branch. The
# candidate tag can produce data, but cannot rewrite the job that receives the
# contents:write token.
on:
workflow_run: # zizmor: ignore[dangerous-triggers] -- guarded below before any write token is used
workflows: [Release]
types: [completed]
permissions:
contents: read
concurrency:
# Draft creation is repository-global. Per-tag groups would allow two
# contents:write jobs to race on release state.
group: stage-release-writer
cancel-in-progress: false
jobs:
configuration-gate:
name: Require protected release configuration
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.path == '.github/workflows/release.yml' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Require release-environment configuration
env:
RELEASE_ENVIRONMENT_CONFIGURED: ${{ vars.LTA_RELEASE_ENVIRONMENT_CONFIGURED }}
shell: bash
run: |
set -Eeuo pipefail
[[ "$RELEASE_ENVIRONMENT_CONFIGURED" == true ]] || {
echo "LTA_RELEASE_ENVIRONMENT_CONFIGURED is not true; configure the protected release-staging environment first" >&2
exit 1
}
stage-draft:
name: Stage unsigned release draft
needs: configuration-gate
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
name: release-staging
permissions:
actions: read # download only the artifact from the validated Release run
contents: write # create the new unsigned draft after all tag gates pass
steps:
- name: Validate triggering tag and commit
env:
GH_TOKEN: ${{ github.token }}
GH_HOST: github.com
GH_PROMPT_DISABLED: '1'
GH_PAGER: 'cat'
GH_REPO: ${{ github.repository }}
TAG: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
shell: bash
run: |
set -Eeuo pipefail
[[ "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9A-Za-z]+([.-][0-9A-Za-z]+)*))?$ ]]
major="${BASH_REMATCH[1]}"
(( ${#major} > 1 || 10#$major >= 2 )) \
|| { echo "release tags below v2 are not supported" >&2; exit 1; }
[[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]
ref_json="$(timeout -k 5 60 gh api "repos/${GH_REPO}/git/ref/tags/${TAG}")"
tag_object="$(jq -r '.object.sha' <<<"$ref_json")"
[[ "$(jq -r '.object.type' <<<"$ref_json")" == tag ]]
tag_json="$(timeout -k 5 60 gh api "repos/${GH_REPO}/git/tags/${tag_object}")"
[[ "$(jq -r '.tag' <<<"$tag_json")" == "$TAG" ]]
[[ "$(jq -r '.object.type + " " + .object.sha' <<<"$tag_json")" == "commit $HEAD_SHA" ]]
[[ "$(jq -r '.verification.verified|tostring' <<<"$tag_json")" == true \
&& "$(jq -r '.verification.reason' <<<"$tag_json")" == valid \
&& "$(jq -r '.verification.signature' <<<"$tag_json")" == '-----BEGIN PGP SIGNATURE-----'* ]] \
|| { echo "tag must have a valid GitHub-recognized OpenPGP signature" >&2; exit 1; }
ancestry="$(timeout -k 5 60 gh api "repos/${GH_REPO}/compare/${HEAD_SHA}...main" --jq '.status')"
[[ "$ancestry" == identical || "$ancestry" == ahead ]] \
|| { echo "tag commit is not contained in main" >&2; exit 1; }
- name: Download gated unsigned artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unsigned-release-${{ github.event.workflow_run.head_branch }}
path: dist
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Validate transferred artifact
shell: bash
run: |
set -Eeuo pipefail
expected=$'SHA256SUMS\tf\nlinux-temp-admin-linux-amd64\tf\nlinux-temp-admin-linux-arm64\tf'
got="$(find dist -mindepth 1 -printf '%P\t%y\n' | LC_ALL=C sort)"
[[ "$got" == "$expected" ]]
[[ -f dist/SHA256SUMS && ! -L dist/SHA256SUMS \
&& "$(wc -c < dist/SHA256SUMS)" -gt 0 \
&& "$(wc -c < dist/SHA256SUMS)" -le 1048576 ]] \
|| { echo "SHA256SUMS is empty, special, or exceeds the 1 MiB metadata limit" >&2; exit 1; }
for asset in dist/linux-temp-admin-linux-amd64 dist/linux-temp-admin-linux-arm64; do
[[ -f "$asset" && ! -L "$asset" && -s "$asset" && "$(wc -c < "$asset")" -le 67108864 ]] \
|| { echo "release binary is empty, special, or exceeds the 64 MiB client limit: $asset" >&2; exit 1; }
done
[[ "$(awk 'NF {print $2}' dist/SHA256SUMS)" == $'linux-temp-admin-linux-amd64\nlinux-temp-admin-linux-arm64' ]]
( cd dist && sha256sum -c --strict SHA256SUMS )
- name: Create a new unsigned DRAFT release
env:
GH_TOKEN: ${{ github.token }}
GH_HOST: github.com
GH_PROMPT_DISABLED: '1'
GH_PAGER: 'cat'
GH_REPO: ${{ github.repository }}
TAG: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
shell: bash
run: |
set -Eeuo pipefail
# The tag-specific REST endpoint exposes only published releases. The
# authenticated list also exposes drafts to this write-capable token,
# so enumerate every page and refuse any existing use of the tag.
release_records="$(timeout -k 5 60 gh api --paginate \
"repos/${GH_REPO}/releases?per_page=100" \
--jq '.[] | [.tag_name, (.id|tostring)] | @tsv')" || {
echo "could not prove release $TAG is absent" >&2
exit 1
}
match_count=0
if [[ -n "$release_records" ]]; then
while IFS=$'\t' read -r actual_tag release_id extra; do
[[ -z "$extra" && -n "$actual_tag" && "$release_id" =~ ^[1-9][0-9]*$ ]] \
|| { echo "release enumeration returned malformed identity data" >&2; exit 1; }
[[ "$actual_tag" != "$TAG" ]] || match_count=$((match_count + 1))
done <<<"$release_records"
fi
(( match_count == 0 )) || {
echo "release or draft $TAG already exists; refusing to refresh any remote asset" >&2
exit 1
}
# Re-resolve the protected tag immediately before the first write;
# neither queueing nor an environment delay can make it stale.
ref_json="$(timeout -k 5 60 gh api "repos/${GH_REPO}/git/ref/tags/${TAG}")"
tag_object="$(jq -r '.object.sha' <<<"$ref_json")"
[[ "$(jq -r '.object.type' <<<"$ref_json")" == tag ]]
tag_json="$(timeout -k 5 60 gh api "repos/${GH_REPO}/git/tags/${tag_object}")"
[[ "$(jq -r '.tag' <<<"$tag_json")" == "$TAG" ]]
[[ "$(jq -r '.object.type + " " + .object.sha' <<<"$tag_json")" == "commit $HEAD_SHA" ]]
[[ "$(jq -r '.verification.verified|tostring' <<<"$tag_json")" == true \
&& "$(jq -r '.verification.reason' <<<"$tag_json")" == valid \
&& "$(jq -r '.verification.signature' <<<"$tag_json")" == '-----BEGIN PGP SIGNATURE-----'* ]] \
|| { echo "tag signature changed before draft creation" >&2; exit 1; }
ancestry="$(timeout -k 5 60 gh api "repos/${GH_REPO}/compare/${HEAD_SHA}...main" --jq '.status')"
[[ "$ancestry" == identical || "$ancestry" == ahead ]] \
|| { echo "tag commit left main before draft creation" >&2; exit 1; }
flags=(--draft --verify-tag --title "$TAG" --generate-notes)
[[ "$TAG" != *-* ]] || flags+=(--prerelease)
timeout -k 5 600 gh release create "$TAG" dist/linux-temp-admin-linux-amd64 \
dist/linux-temp-admin-linux-arm64 dist/SHA256SUMS "${flags[@]}"
echo "Unsigned draft only. Continue with the trusted three-phase process in docs/releasing.md."