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.
466 lines
14 KiB
YAML
466 lines
14 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: Optional semantic version override, with or without leading v. When omitted, the recommended version is used.
|
|
required: false
|
|
workflow_call:
|
|
inputs:
|
|
version:
|
|
description: Optional semantic version override, with or without leading v. When omitted, the recommended version is used.
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
container: docker.io/catthehacker/ubuntu:act-latest
|
|
outputs:
|
|
tag: ${{ steps.prepare.outputs.version }}
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
SUMMARY_FILE: ${{ runner.temp }}/release-prepare-summary.md
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- 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: Validate formatting
|
|
run: test -z "$(gofmt -l .)"
|
|
|
|
- name: Module hygiene
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
go mod tidy
|
|
|
|
if ! go mod verify; then
|
|
echo "go mod verify failed; refreshing module cache and retrying" >&2
|
|
go clean -modcache
|
|
go mod download
|
|
go mod verify
|
|
fi
|
|
|
|
- name: Restore cached gosec binary
|
|
id: cache-gosec
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ${{ runner.temp }}/gosec-bin
|
|
key: gosec-v2.22.4-${{ runner.os }}-${{ runner.arch }}
|
|
|
|
- name: Install gosec binary
|
|
if: steps.cache-gosec.outputs.cache-hit != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p "${RUNNER_TEMP}/gosec-bin"
|
|
GOBIN="${RUNNER_TEMP}/gosec-bin" go install github.com/securego/gosec/v2/cmd/gosec@v2.22.4
|
|
|
|
- name: Run gosec security analysis
|
|
run: |
|
|
set -euo pipefail
|
|
"${RUNNER_TEMP}/gosec-bin/gosec" ./...
|
|
|
|
- name: Run govulncheck
|
|
uses: golang/govulncheck-action@v1.0.4
|
|
with:
|
|
go-version-file: go.mod
|
|
check-latest: false
|
|
go-package: ./...
|
|
cache: true
|
|
cache-dependency-path: go.sum
|
|
|
|
- name: Run tests
|
|
run: |
|
|
set -euo pipefail
|
|
go test ./...
|
|
|
|
- name: Resolve cache token
|
|
id: cache-token
|
|
run: echo "value=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve release tag
|
|
id: resolve-version
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
provided_version="$(printf '%s' "${{ inputs.version }}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
|
if [[ -z "$provided_version" ]]; then
|
|
release_tag="$(go run ./cmd/vociferate --recommend --root .)"
|
|
elif [[ "$provided_version" == v* ]]; then
|
|
release_tag="$provided_version"
|
|
else
|
|
release_tag="v${provided_version}"
|
|
fi
|
|
|
|
echo "tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Update agent docs action tags
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
release_tag="${{ steps.resolve-version.outputs.tag }}"
|
|
for file in README.md AGENTS.md; do
|
|
sed -E -i "s/@v[0-9]+\.[0-9]+\.[0-9]+/@${release_tag}/g" "$file"
|
|
done
|
|
|
|
- name: Prepare and tag release
|
|
id: prepare
|
|
uses: ./prepare
|
|
env:
|
|
VOCIFERATE_CACHE_TOKEN: ${{ steps.cache-token.outputs.value }}
|
|
with:
|
|
version: ${{ steps.resolve-version.outputs.tag }}
|
|
token: ${{ secrets.RELEASE_PAT }}
|
|
git-add-files: CHANGELOG.md release-version README.md AGENTS.md
|
|
|
|
- name: Summary
|
|
if: ${{ always() }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
tag="${{ steps.prepare.outputs.version }}"
|
|
{
|
|
echo "## Release Prepared"
|
|
echo
|
|
echo "- Tag pushed: ${tag}"
|
|
} >> "$SUMMARY_FILE"
|
|
|
|
echo 'Summary'
|
|
echo
|
|
|
|
if [[ -s "$SUMMARY_FILE" ]]; then
|
|
cat "$SUMMARY_FILE"
|
|
else
|
|
echo 'No summary generated.'
|
|
fi
|
|
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
container: docker.io/catthehacker/ubuntu:act-latest
|
|
needs: prepare
|
|
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 }}/release-summary.md
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve release version
|
|
id: resolve-version
|
|
env:
|
|
PREPARE_TAG: ${{ needs.prepare.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
tag="$(printf '%s' "${PREPARE_TAG}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
|
|
|
# Unwrap Teacup expression strings if present.
|
|
if [[ "${tag}" =~ ^%\!t\(string=(.*)\)$ ]]; then
|
|
tag="${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
normalized="${tag#v}"
|
|
tag="v${normalized}"
|
|
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: 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 }}/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
|