name: Prepare 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 defaults: run: shell: bash env: RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - 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: Resolve release version id: resolve-version env: INPUT_VERSION: ${{ inputs.version }} run: | set -euo pipefail provided_version="$(printf '%s' "${INPUT_VERSION:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')" if [[ -n "$provided_version" ]]; then release_version="$provided_version" else if ! release_version="$(go run ./cmd/vociferate --recommend --root . 2>release-recommendation.err)"; then recommendation_error="$(tr '\n' ' ' < release-recommendation.err | sed 's/[[:space:]]\+/ /g' | sed 's/^ //; s/ $//')" echo "Resolve release version: ${recommendation_error}" >&2 exit 1 fi fi echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV" echo "version=$release_version" >> "$GITHUB_OUTPUT" - name: Prepare release files run: | set -euo pipefail go run ./cmd/vociferate \ --root . \ --version "$RELEASE_VERSION" \ --date "$(date -u +%F)" \ --version-file internal/vociferate/version/version.go \ --version-pattern 'const String = "([^"]+)"' \ --changelog changelog.md - name: Run tests run: | set -euo pipefail go test ./... - name: Configure git author run: | set -euo pipefail git config user.name "gitea-actions[bot]" git config user.email "gitea-actions[bot]@users.noreply.local" - name: Commit release changes and push tag run: | set -euo pipefail normalized_version="${RELEASE_VERSION#v}" tag="v${normalized_version}" if git rev-parse "$tag" >/dev/null 2>&1; then echo "Tag ${tag} already exists" >&2 exit 1 fi case "$GITHUB_SERVER_URL" in https://*) authed_remote="https://oauth2:${RELEASE_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" ;; http://*) authed_remote="http://oauth2:${RELEASE_TOKEN}@${GITHUB_SERVER_URL#http://}/${GITHUB_REPOSITORY}.git" ;; *) echo "Unsupported GITHUB_SERVER_URL: ${GITHUB_SERVER_URL}" >&2 exit 1 ;; esac git remote set-url origin "$authed_remote" git add changelog.md internal/vociferate/version/version.go git commit -m "release: prepare ${tag}" git tag "$tag" git push origin HEAD git push origin "$tag" - name: Create release with changelog notes run: | set -euo pipefail normalized_version="${RELEASE_VERSION#v}" tag="v${normalized_version}" release_notes="$(awk -v version="$normalized_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 ${normalized_version} was not found in changelog.md" >&2 exit 1 fi 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}" 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}" >&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}\",\"target\":\"${GITHUB_SHA}\",\"name\":\"${tag}\",\"body\":\"${escaped_release_notes}\",\"draft\":false,\"prerelease\":false}" \ --output release.json elif [[ "$status_code" != "404" ]]; then echo "Unexpected response while checking release ${tag}: 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}\",\"target\":\"${GITHUB_SHA}\",\"name\":\"${tag}\",\"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 "RELEASE_ID=$release_id" >> "$GITHUB_ENV" - name: Build release binaries run: | set -euo pipefail normalized_version="${RELEASE_VERSION#v}" mkdir -p dist for target in linux/amd64 linux/arm64; do os="${target%/*}" arch="${target#*/}" bin="vociferate_${normalized_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 run: | set -euo pipefail if [[ -z "${RELEASE_ID:-}" ]]; then echo "RELEASE_ID is not available for asset upload" >&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