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 UPX uses: crazy-max/ghaction-upx@v3 with: install-only: true - 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 upx_cmd="" 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 install step; continuing without binary compression." >&2 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 if [[ -n "${upx_cmd}" ]]; then "${upx_cmd}" --best --lzma "dist/${bin}" fi 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 are compressed with UPX from crazy-max/ghaction-upx@v3 when available, otherwise uploaded uncompressed." } >> "$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