name: Do 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.resolve-tag.outputs.tag }} version: ${{ steps.resolve-tag.outputs.version }} defaults: run: shell: bash env: RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout tagged revision uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.ref }} - name: Resolve release tag id: resolve-tag env: INPUT_TAG: ${{ inputs.tag }} GITHUB_REF_VALUE: ${{ github.ref }} run: | set -euo pipefail provided_tag="$(printf '%s' "${INPUT_TAG:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')" if [[ -n "$provided_tag" ]]; then normalized_version="${provided_tag#v}" tag="v${normalized_version}" elif [[ "${GITHUB_REF_VALUE}" == refs/tags/* ]]; then tag="${GITHUB_REF_VALUE#refs/tags/}" normalized_version="${tag#v}" else echo "A tag input is required when the workflow is not running from a tag push" >&2 exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=$normalized_version" >> "$GITHUB_OUTPUT" - name: Checkout requested tag if: ${{ inputs.tag != '' }} uses: actions/checkout@v4 with: fetch-depth: 0 ref: refs/tags/${{ steps.resolve-tag.outputs.tag }} - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.26.1' check-latest: true cache: true cache-dependency-path: go.sum - name: Extract release notes from changelog id: release-notes env: RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }} run: | set -euo pipefail release_notes="$(awk -v version="$RELEASE_VERSION" ' $0 ~ "^## \\[" version "\\] - " {capture=1} capture { if ($0 ~ "^## \\[" && $0 !~ "^## \\[" version "\\] - ") exit print } ' changelog.md)" if [[ -z "${release_notes//[[:space:]]/}" ]]; then echo "Release notes section for ${RELEASE_VERSION} was not found in changelog.md" >&2 exit 1 fi notes_file="$RUNNER_TEMP/release-notes.md" printf '%s\n' "$release_notes" > "$notes_file" echo "notes_file=$notes_file" >> "$GITHUB_OUTPUT" - name: Create or update release id: release env: TAG_NAME: ${{ steps.resolve-tag.outputs.tag }} RELEASE_NOTES_FILE: ${{ steps.release-notes.outputs.notes_file }} run: | set -euo pipefail release_notes="$(cat "$RELEASE_NOTES_FILE")" escaped_release_notes="$(printf '%s' "$release_notes" | sed 's/\\/\\\\/g; s/"/\\"/g; :a;N;$!ba;s/\n/\\n/g')" release_api="${GITHUB_API_URL:-${GITHUB_SERVER_URL%/}/api/v1}/repos/${GITHUB_REPOSITORY}/releases" release_by_tag_api="${release_api}/tags/${TAG_NAME}" status_code="$(curl -sS -o release-existing.json -w '%{http_code}' \ -H "Authorization: token ${RELEASE_TOKEN}" \ -H "Content-Type: application/json" \ "${release_by_tag_api}")" if [[ "$status_code" == "200" ]]; then existing_release_id="$(sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' release-existing.json | head -n 1)" if [[ -z "$existing_release_id" ]]; then echo "Failed to parse existing release id for ${TAG_NAME}" >&2 cat release-existing.json >&2 exit 1 fi curl --fail-with-body \ -X PATCH \ -H "Authorization: token ${RELEASE_TOKEN}" \ -H "Content-Type: application/json" \ "${release_api}/${existing_release_id}" \ --data "{\"tag_name\":\"${TAG_NAME}\",\"target\":\"${GITHUB_SHA}\",\"name\":\"${TAG_NAME}\",\"body\":\"${escaped_release_notes}\",\"draft\":false,\"prerelease\":false}" \ --output release.json elif [[ "$status_code" != "404" ]]; then echo "Unexpected response while checking release ${TAG_NAME}: HTTP ${status_code}" >&2 cat release-existing.json >&2 exit 1 else curl --fail-with-body \ -X POST \ -H "Authorization: token ${RELEASE_TOKEN}" \ -H "Content-Type: application/json" \ "${release_api}" \ --data "{\"tag_name\":\"${TAG_NAME}\",\"target\":\"${GITHUB_SHA}\",\"name\":\"${TAG_NAME}\",\"body\":\"${escaped_release_notes}\",\"draft\":false,\"prerelease\":false}" \ --output release.json fi release_id="$(sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' release.json | head -n 1)" if [[ -z "$release_id" ]]; then echo "Failed to parse release id from API response" >&2 cat release.json >&2 exit 1 fi echo "id=$release_id" >> "$GITHUB_OUTPUT" - name: Build release binaries env: RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }} run: | set -euo pipefail 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 done ( cd dist shasum -a 256 * > checksums.txt ) - name: Upload release binaries env: RELEASE_ID: ${{ steps.release.outputs.id }} run: | set -euo pipefail 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: Summarize published release env: TAG_NAME: ${{ steps.resolve-tag.outputs.tag }} RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }} run: | set -euo pipefail { 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" } >> "$GITHUB_STEP_SUMMARY" 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 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: ${{ github.token }} 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 recommended_tag="$(${RUN_COMMAND} --recommend --root .)" case "$recommended_tag" in v*.*.*) ;; *) echo "Unexpected recommended tag: $recommended_tag" >&2 exit 1 ;; esac { echo "## Released Binary Validation (${{ matrix.asset_arch }})" echo echo "- Release tag: ${TAG_NAME}" echo "- Asset: ${asset_name}" echo "- Recommended tag: ${recommended_tag}" } >> "$GITHUB_STEP_SUMMARY"