Rename the reusable workflows to release.yml and update-release.yml, add UPX compression for release binaries, and sync the standalone update-release workflow with the active release pipeline fixes. Update README, AGENTS, compliance notes, and changelog references to match the new workflow names and usage patterns.
388 lines
13 KiB
YAML
388 lines
13 KiB
YAML
name: Update Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*.*.*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Semantic version tag to publish, with or without leading v. Defaults to the current tag ref when dispatched from a tag.
|
|
required: false
|
|
workflow_call:
|
|
inputs:
|
|
tag:
|
|
description: Semantic version tag to publish, with or without leading v. When omitted, the current tag ref is used.
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
container: docker.io/catthehacker/ubuntu:act-latest
|
|
outputs:
|
|
tag: ${{ steps.publish.outputs.tag }}
|
|
version: ${{ steps.publish.outputs.version }}
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
RELEASE_TOKEN: ${{ secrets.RELEASE_PAT }}
|
|
SUMMARY_FILE: ${{ runner.temp }}/update-release-summary.md
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Fetch and detect release tag
|
|
id: detect-tag
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Fetch all tags from origin first
|
|
git fetch origin --tags --force --quiet 2>/dev/null || true
|
|
|
|
# Check if HEAD is at a tag (prepare-release may have just tagged it)
|
|
if head_tag="$(git describe --exact-match --tags HEAD 2>/dev/null)" && [[ -n "$head_tag" ]]; then
|
|
echo "detected_tag=$head_tag" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# Fall back to finding the most recent tag
|
|
if latest_tag="$(git describe --tags --abbrev=0 2>/dev/null)" && [[ -n "$latest_tag" ]]; then
|
|
echo "detected_tag=$latest_tag" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Resolve release version
|
|
id: resolve-version
|
|
env:
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
DETECTED_TAG: ${{ steps.detect-tag.outputs.detected_tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
normalize_candidate() {
|
|
local raw="$1"
|
|
|
|
raw="$(printf '%s' "$raw" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
|
|
|
# Teacup can surface expression strings as %!t(string=value); unwrap it.
|
|
if [[ "$raw" =~ ^%\!t\(string=(.*)\)$ ]]; then
|
|
raw="${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
raw="$(printf '%s' "$raw" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
|
printf '%s' "$raw"
|
|
}
|
|
|
|
input_tag="$(normalize_candidate "${INPUT_TAG}")"
|
|
detected_tag="$(normalize_candidate "${DETECTED_TAG}")"
|
|
|
|
# Try explicit input first.
|
|
requested_tag="$input_tag"
|
|
|
|
# Fall back to detected tag if neither input nor caller tag is available.
|
|
if [[ -z "$requested_tag" && -n "$detected_tag" ]]; then
|
|
requested_tag="$detected_tag"
|
|
fi
|
|
|
|
# Try GITHUB_REF if still empty
|
|
if [[ -z "$requested_tag" && "$GITHUB_REF" == refs/tags/* ]]; then
|
|
requested_tag="${GITHUB_REF#refs/tags/}"
|
|
fi
|
|
|
|
if [[ -n "$requested_tag" ]]; then
|
|
# Normalize to v-prefixed format
|
|
normalized="${requested_tag#v}"
|
|
tag="v${normalized}"
|
|
else
|
|
echo "Error: Could not resolve release version" >&2
|
|
echo " - inputs.tag(raw): '$INPUT_TAG'" >&2
|
|
echo " - detected_tag(raw): '${DETECTED_TAG}'" >&2
|
|
echo " - GITHUB_REF: '$GITHUB_REF'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
|
echo "version=${normalized}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout release tag
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
ref: refs/tags/${{ steps.resolve-version.outputs.tag }}
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
go-version-file: go.mod
|
|
check-latest: false
|
|
cache: true
|
|
cache-dependency-path: go.sum
|
|
|
|
- name: Install release build tools
|
|
run: |
|
|
set -euo pipefail
|
|
apt-get update
|
|
apt-get install -y upx-ucl || apt-get install -y upx
|
|
|
|
- name: Preflight release API access
|
|
env:
|
|
TAG_NAME: ${{ steps.resolve-version.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [[ -z "${RELEASE_TOKEN:-}" ]]; then
|
|
echo "No release token available. Set secrets.RELEASE_PAT." >&2
|
|
exit 1
|
|
fi
|
|
|
|
api_base="${GITHUB_API_URL:-${GITHUB_SERVER_URL%/}/api/v1}"
|
|
repo_api="${api_base}/repos/${GITHUB_REPOSITORY}"
|
|
|
|
curl --fail-with-body -sS \
|
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"${repo_api}" >/dev/null
|
|
|
|
curl --fail-with-body -sS \
|
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"${repo_api}/releases?limit=1" >/dev/null
|
|
|
|
if ! git rev-parse --verify --quiet "refs/tags/${TAG_NAME}" >/dev/null; then
|
|
echo "Tag ${TAG_NAME} was not found in the checked out repository." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Create or update release
|
|
id: publish
|
|
uses: ./publish
|
|
with:
|
|
token: ${{ secrets.RELEASE_PAT }}
|
|
version: ${{ steps.resolve-version.outputs.version }}
|
|
|
|
- name: Build release binaries
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if command -v upx >/dev/null 2>&1; then
|
|
upx_cmd=upx
|
|
elif command -v upx-ucl >/dev/null 2>&1; then
|
|
upx_cmd=upx-ucl
|
|
else
|
|
echo "UPX is not available on PATH after installation." >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p dist
|
|
|
|
for target in linux/amd64 linux/arm64; do
|
|
os="${target%/*}"
|
|
arch="${target#*/}"
|
|
|
|
bin="vociferate_${RELEASE_VERSION}_${os}_${arch}"
|
|
GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags="-s -w" -o "dist/${bin}" ./cmd/vociferate
|
|
"${upx_cmd}" --best --lzma "dist/${bin}"
|
|
done
|
|
|
|
(
|
|
cd dist
|
|
shasum -a 256 * > checksums.txt
|
|
)
|
|
|
|
- name: Upload release binaries
|
|
env:
|
|
RELEASE_ID: ${{ steps.publish.outputs.release-id }}
|
|
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
raw_release_id="$(printf '%s' "${RELEASE_ID:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
|
if [[ "$raw_release_id" =~ ^%\!t\(string=(.*)\)$ ]]; then
|
|
raw_release_id="${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
release_id="$(printf '%s' "$raw_release_id" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
|
if ! [[ "$release_id" =~ ^[0-9]+$ ]]; then
|
|
echo "Invalid release id from publish step: '${RELEASE_ID}'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
release_detail_api="${GITHUB_API_URL:-${GITHUB_SERVER_URL%/}/api/v1}/repos/${GITHUB_REPOSITORY}/releases/${release_id}"
|
|
if ! curl --fail-with-body -sS \
|
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"$release_detail_api" >/dev/null; then
|
|
echo "Resolved release endpoint is not accessible: ${release_detail_api}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
release_api="${GITHUB_API_URL:-${GITHUB_SERVER_URL%/}/api/v1}/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets"
|
|
|
|
for asset in dist/*; do
|
|
name="$(basename "$asset")"
|
|
|
|
assets_json="$(curl -sS --fail-with-body \
|
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"${release_api}")"
|
|
|
|
escaped_name="$(printf '%s' "$name" | sed 's/[][(){}.^$*+?|\\/]/\\&/g')"
|
|
existing_asset_id="$(printf '%s' "$assets_json" | tr -d '\n' | sed -n "s/.*{\"id\":\([0-9][0-9]*\)[^}]*\"name\":\"${escaped_name}\".*/\1/p")"
|
|
|
|
if [[ -n "$existing_asset_id" ]]; then
|
|
curl --fail-with-body \
|
|
-X DELETE \
|
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"${release_api}/${existing_asset_id}"
|
|
fi
|
|
|
|
curl --fail-with-body \
|
|
-X POST \
|
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
"${release_api}?name=${name}" \
|
|
--data-binary "@${asset}"
|
|
done
|
|
|
|
- name: Summary
|
|
if: ${{ always() }}
|
|
env:
|
|
TAG_NAME: ${{ steps.publish.outputs.tag }}
|
|
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
|
|
PUBLISH_OUTCOME: ${{ steps.publish.outcome }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [[ "${PUBLISH_OUTCOME}" == "success" ]]; then
|
|
{
|
|
echo "## Release Published"
|
|
echo
|
|
echo "- Tag: ${TAG_NAME}"
|
|
echo "- Release notes sourced from changelog entry ${RELEASE_VERSION}."
|
|
echo "- Published assets: vociferate_${RELEASE_VERSION}_linux_amd64, vociferate_${RELEASE_VERSION}_linux_arm64, checksums.txt"
|
|
echo "- Release binaries were compressed with UPX before upload."
|
|
} >> "$SUMMARY_FILE"
|
|
else
|
|
{
|
|
echo "## Release Failed"
|
|
echo
|
|
echo "- Tag: ${TAG_NAME:-unknown}"
|
|
echo "- Create or update release step did not complete successfully."
|
|
} >> "$SUMMARY_FILE"
|
|
fi
|
|
|
|
echo 'Summary'
|
|
echo
|
|
|
|
if [[ -s "$SUMMARY_FILE" ]]; then
|
|
cat "$SUMMARY_FILE"
|
|
else
|
|
echo 'No summary generated.'
|
|
fi
|
|
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
container: docker.io/catthehacker/ubuntu:act-latest
|
|
needs: release
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- asset_arch: amd64
|
|
run_command: ./vociferate
|
|
- asset_arch: arm64
|
|
run_command: qemu-aarch64-static ./vociferate
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
SUMMARY_FILE: ${{ runner.temp }}/update-release-validate-summary.md
|
|
steps:
|
|
- name: Checkout tagged revision
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: refs/tags/${{ needs.release.outputs.tag }}
|
|
|
|
- name: Install arm64 emulator
|
|
if: ${{ matrix.asset_arch == 'arm64' }}
|
|
run: |
|
|
set -euo pipefail
|
|
apt-get update
|
|
apt-get install -y qemu-user-static
|
|
|
|
- name: Download released binary
|
|
env:
|
|
TOKEN: ${{ secrets.RELEASE_PAT }}
|
|
TAG_NAME: ${{ needs.release.outputs.tag }}
|
|
RELEASE_VERSION: ${{ needs.release.outputs.version }}
|
|
ASSET_ARCH: ${{ matrix.asset_arch }}
|
|
SERVER_URL: ${{ github.server_url }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
asset_name="vociferate_${RELEASE_VERSION}_linux_${ASSET_ARCH}"
|
|
asset_url="${SERVER_URL}/aether/vociferate/releases/download/${TAG_NAME}/${asset_name}"
|
|
|
|
curl --fail --location \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
-o vociferate \
|
|
"$asset_url"
|
|
chmod +x vociferate
|
|
|
|
echo "asset_name=${asset_name}" >> "$GITHUB_ENV"
|
|
|
|
- name: Smoke test released binary
|
|
env:
|
|
RUN_COMMAND: ${{ matrix.run_command }}
|
|
TAG_NAME: ${{ needs.release.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
${RUN_COMMAND} --help >/dev/null
|
|
|
|
recommend_stderr="$(mktemp)"
|
|
if ${RUN_COMMAND} --recommend --root . >/dev/null 2>"${recommend_stderr}"; then
|
|
echo "Expected --recommend to fail on the tagged release checkout" >&2
|
|
exit 1
|
|
fi
|
|
|
|
recommend_error="$(cat "${recommend_stderr}")"
|
|
case "${recommend_error}" in
|
|
*"unreleased section is empty"*)
|
|
;;
|
|
*)
|
|
echo "Unexpected recommend failure output: ${recommend_error}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
{
|
|
echo "## Released Binary Validation (${{ matrix.asset_arch }})"
|
|
echo
|
|
echo "- Release tag: ${TAG_NAME}"
|
|
echo "- Asset: ${asset_name}"
|
|
echo "- Binary executed successfully via --help."
|
|
echo "- --recommend failed as expected on the tagged checkout because Unreleased is empty."
|
|
} >> "$SUMMARY_FILE"
|
|
|
|
- name: Summary
|
|
if: ${{ always() }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo 'Summary'
|
|
echo
|
|
|
|
if [[ -s "$SUMMARY_FILE" ]]; then
|
|
cat "$SUMMARY_FILE"
|
|
else
|
|
echo 'No summary generated.'
|
|
fi
|