RedFlag/.gitea/workflows/promote-release.yml
Fimeg 67e26be2d9 projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally.

Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
2026-09-08 21:59:33 -04:00

238 lines
9.8 KiB
YAML

name: promote-release
on:
workflow_dispatch:
inputs:
tag:
description: Signed internal Gitea alpha tag to promote, such as v0.2.9.4
required: true
type: string
receipt_sha256:
description: SHA-256 of the tested internal files.sha256 receipt
required: true
type: string
permissions:
contents: read
jobs:
promote:
runs-on: release-trusted
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Verify selected internal release tag
id: release
env:
PROMOTE_TAG: ${{ github.event.inputs.tag }}
run: |
set -euo pipefail
TAG=$PROMOTE_TAG
[[ "$TAG" =~ ^v[0-9]+(\.[0-9]+){2,3}([.-][0-9A-Za-z]+)*$ ]]
[[ "$TAG" != */* ]]
git check-ref-format "refs/tags/$TAG"
TOKEN_FILE=/srv/release-trusted/.secrets/gitea-redflag-release-token
test -r "$TOKEN_FILE"
GIT_ASKPASS="$PWD/scripts/git-askpass-token.sh" \
GIT_TOKEN_FILE="$TOKEN_FILE" \
GIT_TOKEN_USERNAME=release-redflag \
GIT_TERMINAL_PROMPT=0 \
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
git config gpg.format ssh
git config gpg.ssh.allowedSignersFile .gitea/allowed_signers
test "$(git cat-file -t "refs/tags/$TAG")" = tag
git tag -v "$TAG"
SOURCE_SHA=$(git rev-parse "${TAG}^{commit}")
VERSION=${TAG#v}
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
- name: Fetch accepted internal Gitea release bytes
env:
TAG: ${{ steps.release.outputs.tag }}
VERSION: ${{ steps.release.outputs.version }}
SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
ACCEPTED_RECEIPT_SHA256: ${{ github.event.inputs.receipt_sha256 }}
run: |
set -euo pipefail
[[ "$ACCEPTED_RECEIPT_SHA256" =~ ^[0-9a-f]{64}$ ]]
command -v jq >/dev/null
API="${GITHUB_SERVER_URL}/api/v1"
TOKEN_FILE=/srv/release-trusted/.secrets/gitea-redflag-release-token
test -r "$TOKEN_FILE"
GITEA_RELEASE_TOKEN=$(<"$TOKEN_FILE")
test -n "$GITEA_RELEASE_TOKEN"
gitea_curl() {
curl --config <(printf 'silent\nshow-error\nheader = "Authorization: token %s"\n' "$GITEA_RELEASE_TOKEN") "$@"
}
gitea_curl -f "$API/repos/${GITHUB_REPOSITORY}/releases/tags/$TAG" \
>/tmp/gitea-release.json
test "$(jq -r .tag_name /tmp/gitea-release.json)" = "$TAG"
test "$(jq -r .draft /tmp/gitea-release.json)" = false
RELEASE_ID=$(jq -er .id /tmp/gitea-release.json)
gitea_curl -f \
"$API/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets?limit=100" \
>/tmp/gitea-assets.json
asset_url() {
jq -er --arg name "$1" \
'[.[] | select(.name == $name)] | if length == 1 then .[0].browser_download_url else error("asset count is not one") end' \
/tmp/gitea-assets.json
}
rm -rf artifacts/internal
mkdir -p artifacts/internal
gitea_curl -fL -o artifacts/internal/files.sha256 \
"$(asset_url files.sha256)"
RECEIPT_SHA256=$(sha256sum artifacts/internal/files.sha256 | awk '{print $1}')
test "$RECEIPT_SHA256" = "$ACCEPTED_RECEIPT_SHA256"
: >/tmp/expected-release-assets
echo files.sha256 >>/tmp/expected-release-assets
while read -r expected name extra; do
test -z "${extra:-}"
[[ "$expected" =~ ^[0-9a-f]{64}$ ]]
case "$name" in
''|/*|*'..'*|*/*)
echo "::error::unsafe internal release filename: $name"
exit 1
;;
esac
echo "$name" >>/tmp/expected-release-assets
gitea_curl -fL -o "artifacts/internal/$name" "$(asset_url "$name")"
printf '%s %s\n' "$expected" "$name" |
(cd artifacts/internal && sha256sum -c -)
done <artifacts/internal/files.sha256
LC_ALL=C sort /tmp/expected-release-assets >/tmp/expected-release-assets.sorted
test "$(uniq -d /tmp/expected-release-assets.sorted | wc -l)" -eq 0
jq -r '.[].name' /tmp/gitea-assets.json | LC_ALL=C sort \
>/tmp/actual-release-assets.sorted
diff -u /tmp/expected-release-assets.sorted /tmp/actual-release-assets.sorted
python3 scripts/release-contract.py verify artifacts/internal "$VERSION" "$SOURCE_SHA" "$TAG"
echo "Accepted internal alpha $TAG receipt $RECEIPT_SHA256"
- name: Promote the same bytes to Forgejo
env:
TAG: ${{ steps.release.outputs.tag }}
VERSION: ${{ steps.release.outputs.version }}
SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
ACCEPTED_RECEIPT_SHA256: ${{ github.event.inputs.receipt_sha256 }}
run: |
set -euo pipefail
FORGE_URL='https://forge.caseytunturi.com/Fimeg/RedFlag.git'
FORGE_API='https://forge.caseytunturi.com/api/v1/repos/Fimeg/RedFlag'
TOKEN_FILE=/srv/release-trusted/.secrets/forge-redflag-release-token
test -r "$TOKEN_FILE"
PUBLIC_FORGE_TOKEN=$(<"$TOKEN_FILE")
test -n "$PUBLIC_FORGE_TOKEN"
forge_curl() {
curl --config <(printf 'silent\nshow-error\nheader = "Authorization: token %s"\n' "$PUBLIC_FORGE_TOKEN") "$@"
}
git fetch --force --no-tags "$FORGE_URL" \
refs/heads/public:refs/remotes/public-forge/public
if ! git merge-base --is-ancestor "$SOURCE_SHA" refs/remotes/public-forge/public; then
echo "::error::Forgejo public branch does not contain release commit $SOURCE_SHA"
exit 1
fi
LOCAL_TAG_OBJECT=$(git rev-parse "refs/tags/$TAG")
remote_tag=$(git ls-remote --refs "$FORGE_URL" "refs/tags/$TAG" | awk '{print $1}')
if [ -n "$remote_tag" ]; then
git fetch --force --no-tags "$FORGE_URL" \
"refs/tags/$TAG:refs/public-forge/release-tag"
test "$(git cat-file -t refs/public-forge/release-tag)" = tag
test "$(git rev-parse refs/public-forge/release-tag)" = "$LOCAL_TAG_OBJECT"
test "$(git rev-parse 'refs/public-forge/release-tag^{commit}')" = "$SOURCE_SHA"
else
GIT_ASKPASS="$PWD/scripts/git-askpass-token.sh" \
GIT_TOKEN_FILE="$TOKEN_FILE" \
GIT_TOKEN_USERNAME=publisher-redflag \
GIT_TERMINAL_PROMPT=0 \
git push "$FORGE_URL" "refs/tags/$TAG:refs/tags/$TAG"
fi
STABLE_FLOOR='0.3.0'
if [ "$(printf '%s\n%s\n' "$VERSION" "$STABLE_FLOOR" | sort -V | head -1)" = "$VERSION" ] \
&& [ "$VERSION" != "$STABLE_FLOOR" ]; then
PRERELEASE=true
else
PRERELEASE=false
fi
release_status=$(forge_curl -o /tmp/forgejo-release.json -w '%{http_code}' \
"$FORGE_API/releases/tags/$TAG")
case "$release_status" in
200)
;;
404)
payload=$(jq -nc \
--arg tag "$TAG" \
--arg name "$TAG" \
--arg body "Promoted from tested internal receipt SHA-256 $ACCEPTED_RECEIPT_SHA256" \
--argjson prerelease "$PRERELEASE" \
'{tag_name:$tag,name:$name,body:$body,draft:false,prerelease:$prerelease}')
forge_curl -f -X POST "$FORGE_API/releases" \
-H 'Content-Type: application/json' \
--data "$payload" >/tmp/forgejo-release.json
;;
*)
echo "::error::Forgejo release lookup failed with HTTP $release_status"
exit 1
;;
esac
RELEASE_ID=$(jq -er .id /tmp/forgejo-release.json)
forge_curl -f \
"$FORGE_API/releases/$RELEASE_ID/assets?limit=100" >/tmp/forgejo-assets.json
mapfile -d '' release_files < <(
find artifacts/internal -maxdepth 1 -type f -print0 | LC_ALL=C sort -z
)
test "${#release_files[@]}" -gt 1
for file in "${release_files[@]}"; do
name=$(basename "$file")
asset_count=$(jq -r --arg name "$name" \
'[.[] | select(.name == $name)] | length' /tmp/forgejo-assets.json)
case "$asset_count" in
0)
forge_curl -f -X POST \
"$FORGE_API/releases/$RELEASE_ID/assets?name=$name" \
-F "attachment=@$file" >/tmp/forgejo-upload.json
download_url=$(jq -er .browser_download_url /tmp/forgejo-upload.json)
;;
1)
download_url=$(jq -r --arg name "$name" \
'.[] | select(.name == $name) | .browser_download_url' \
/tmp/forgejo-assets.json)
;;
*)
echo "::error::duplicate Forgejo release assets named $name"
exit 1
;;
esac
expected_hash=$(sha256sum "$file" | awk '{print $1}')
public_hash=$(curl -fsSL "$download_url" | sha256sum | awk '{print $1}')
test "$public_hash" = "$expected_hash"
echo "Verified public release asset: $name $expected_hash"
done
anonymous_tag=$(curl -fsS "$FORGE_API/releases/tags/$TAG" | jq -r .tag_name)
test "$anonymous_tag" = "$TAG"
public_tag_object=$(git ls-remote --refs "$FORGE_URL" "refs/tags/$TAG" | awk '{print $1}')
test "$public_tag_object" = "$LOCAL_TAG_OBJECT"
echo "Forgejo release $TAG is anonymous and byte-exact"