feat: add prepare and publish composite actions

Add two focused subdirectory composite actions:

- prepare/action.yml: downloads the vociferate binary, runs it to update
  changelog and release-version, then commits, tags, and pushes — replacing
  the boilerplate git steps consumers previously had to write inline.

- publish/action.yml: extracts the matching changelog section and creates or
  updates the Gitea/GitHub release. Outputs release-id, tag, and version so
  consumers can upload their own assets after it runs.

Simplify the vociferate workflows to use ./prepare and ./publish directly,
validating both actions in the self-release pipeline.

Update README to show the clean two-action usage pattern.
This commit is contained in:
Micheal Wilkinson
2026-03-20 20:27:22 +00:00
parent 4ae6f34931
commit 647d8cf76f
5 changed files with 429 additions and 256 deletions

View File

@@ -22,8 +22,8 @@ jobs:
runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest
outputs:
tag: ${{ steps.resolve-tag.outputs.tag }}
version: ${{ steps.resolve-tag.outputs.version }}
tag: ${{ steps.publish.outputs.tag }}
version: ${{ steps.publish.outputs.version }}
defaults:
run:
shell: bash
@@ -36,35 +36,12 @@ jobs:
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 }}
ref: ${{ startsWith(inputs.tag, 'v') && format('refs/tags/{0}', inputs.tag) || format('refs/tags/v{0}', inputs.tag) }}
- name: Setup Go
uses: actions/setup-go@v5
@@ -74,90 +51,16 @@ jobs:
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"
id: publish
uses: ./publish
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.tag }}
- name: Build release binaries
env:
RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }}
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
run: |
set -euo pipefail
@@ -178,7 +81,8 @@ jobs:
- name: Upload release binaries
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
RELEASE_ID: ${{ steps.publish.outputs.release-id }}
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
run: |
set -euo pipefail
@@ -213,8 +117,8 @@ jobs:
- name: Summarize published release
env:
TAG_NAME: ${{ steps.resolve-tag.outputs.tag }}
RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }}
TAG_NAME: ${{ steps.publish.outputs.tag }}
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
run: |
set -euo pipefail

View File

@@ -21,8 +21,6 @@ jobs:
defaults:
run:
shell: bash
env:
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -37,91 +35,20 @@ jobs:
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)" \
--changelog changelog.md
- name: Run tests
run: |
set -euo pipefail
go test ./...
run: 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 release-version
git commit -m "release: prepare ${tag}"
git tag "$tag"
git push origin HEAD
git push origin "$tag"
- name: Prepare and tag release
id: prepare
uses: ./prepare
with:
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Summarize prepared release
run: |
set -euo pipefail
normalized_version="${RELEASE_VERSION#v}"
tag="v${normalized_version}"
tag="${{ steps.prepare.outputs.version }}"
{
echo "## Release Prepared"
echo
echo "- Updated files were committed to main."
echo "- Tag pushed: ${tag}"
echo "- The tag-triggered Do Release workflow will create or update the release and publish binaries."
} >> "$GITHUB_STEP_SUMMARY"

144
README.md
View File

@@ -81,24 +81,10 @@ If a release already exists for the same tag, the workflow updates its release n
## Reuse In Other Repositories
You can reuse vociferate in two ways.
Vociferate ships two composite actions that together cover the full release flow.
Pin both to the same released tag.
Use the composite action directly in your prepare workflow:
```yaml
- name: Prepare release files
uses: git.hrafn.xyz/aether/vociferate@v1.0.0
with:
version-file: internal/myapp/version/version.go
version-pattern: 'const Version = "([^"]+)"'
changelog: changelog.md
```
Set `version` only when you want to override the recommended version.
Pin the composite action to a released tag. It downloads a prebuilt Linux binary from vociferate releases and caches it on the runner, so it does not require installing Go.
For repositories using changelog-based versioning (the default), omit `version-file` and `version-pattern` entirely. Only set them for repositories that embed the version inside source code.
A complete release setup should also split preparation from publication. For example:
### `prepare` — update files, commit, and push tag
```yaml
name: Prepare Release
@@ -118,73 +104,67 @@ jobs:
with:
fetch-depth: 0
- name: Prepare release files
id: prepare
uses: git.hrafn.xyz/aether/vociferate@v1.0.0
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.0
with:
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
```
- name: Commit and push prepared release
Downloads a prebuilt vociferate binary, runs it to update `changelog.md` and
`release-version`, then commits those changes to the default branch and pushes
the release tag. Does not require Go on the runner.
For repositories that embed the version inside source code, pass `version-file`
and `version-pattern`:
```yaml
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.0
with:
version-file: internal/myapp/version/version.go
version-pattern: 'const Version = "([^"]+)"'
git-add-files: changelog.md internal/myapp/version/version.go
token: ${{ secrets.GITHUB_TOKEN }}
```
### `publish` — create release with changelog notes
```yaml
name: Do Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
```
Reads the matching section from `changelog.md` and creates or updates the
Gitea/GitHub release with those notes. The `version` input is optional — when
omitted it is derived from the current tag ref automatically.
The `publish` action outputs `release-id` so you can upload additional release
assets after it runs:
```yaml
- id: publish
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload my binary
run: |
set -euo pipefail
tag="${{ steps.prepare.outputs.version }}"
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@users.noreply.local"
git add changelog.md release-version
git commit -m "release: prepare ${tag}"
git tag "$tag"
git push origin HEAD
git push origin "$tag"
```
Then use a separate tag workflow to publish the release from the tagged revision:
```yaml
name: Do Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
uses: aether/vociferate/.gitea/workflows/do-release.yml@main
secrets: inherit
```
Call the reusable prepare workflow:
```yaml
name: Prepare Release
on:
workflow_dispatch:
inputs:
version:
description: Optional semantic version override.
required: false
jobs:
release:
uses: aether/vociferate/.gitea/workflows/prepare-release.yml@main
with:
version: ${{ inputs.version }}
secrets: inherit
```
And publish from tags with:
```yaml
name: Do Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
uses: aether/vociferate/.gitea/workflows/do-release.yml@main
secrets: inherit
curl --fail-with-body -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
"${{ github.api_url }}/repos/${{ github.repository }}/releases/${{ steps.publish.outputs.release-id }}/assets?name=myapp" \
--data-binary "@dist/myapp"
```

210
prepare/action.yml Normal file
View File

@@ -0,0 +1,210 @@
name: vociferate/prepare
description: >
Download vociferate, prepare release files, then commit, tag, and push.
The repository must be checked out before this action runs.
inputs:
token:
description: >
Token used to download the vociferate binary and to push the release
commit and tag. Defaults to the workflow token.
required: false
default: ''
version:
description: >
Optional semantic version override (with or without leading v). When
omitted, the recommended next version is derived from the changelog.
required: false
default: ''
version-file:
description: >
Path to version file relative to repository root. When omitted, the
current version is derived from the most recent released section in
the changelog.
required: false
default: ''
version-pattern:
description: >
Regular expression with one capture group containing the version value.
Only required when version-file is set.
required: false
default: ''
changelog:
description: Path to changelog file relative to repository root.
required: false
default: changelog.md
git-user-name:
description: Name for the release commit author.
required: false
default: 'gitea-actions[bot]'
git-user-email:
description: Email for the release commit author.
required: false
default: 'gitea-actions[bot]@users.noreply.local'
git-add-files:
description: >
Space-separated list of file paths to stage for the release commit.
Defaults to changelog.md and release-version. Adjust when using a
custom version-file.
required: false
default: 'changelog.md release-version'
outputs:
version:
description: >
The resolved version tag (e.g. v1.2.3) that was committed and pushed.
value: ${{ steps.run-vociferate.outputs.version }}
runs:
using: composite
steps:
- name: Resolve vociferate binary metadata
id: resolve-binary
shell: bash
env:
ACTION_REF: ${{ github.action_ref }}
SERVER_URL: ${{ github.server_url }}
API_URL: ${{ github.api_url }}
TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
RUNNER_ARCH: ${{ runner.arch }}
RUNNER_TEMP: ${{ runner.temp }}
run: |
set -euo pipefail
case "$RUNNER_ARCH" in
X64) arch="amd64" ;;
ARM64) arch="arm64" ;;
*)
echo "Unsupported runner architecture: $RUNNER_ARCH" >&2
exit 1
;;
esac
release_tag="$ACTION_REF"
if [[ -z "$release_tag" || "$release_tag" == refs/* || "$release_tag" != v* ]]; then
release_tag="$(curl -fsSL \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_URL}/repos/aether/vociferate/releases/latest" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
fi
if [[ -z "$release_tag" ]]; then
echo "Unable to resolve a vociferate release tag for binary download" >&2
exit 1
fi
normalized_version="${release_tag#v}"
asset_name="vociferate_${normalized_version}_linux_${arch}"
cache_dir="${RUNNER_TEMP}/vociferate/${release_tag}/linux-${arch}"
binary_path="${cache_dir}/vociferate"
asset_url="${SERVER_URL}/aether/vociferate/releases/download/${release_tag}/${asset_name}"
mkdir -p "$cache_dir"
echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT"
echo "asset_name=$asset_name" >> "$GITHUB_OUTPUT"
echo "asset_url=$asset_url" >> "$GITHUB_OUTPUT"
echo "cache_dir=$cache_dir" >> "$GITHUB_OUTPUT"
echo "binary_path=$binary_path" >> "$GITHUB_OUTPUT"
- name: Restore cached vociferate binary
id: cache-vociferate
uses: actions/cache@v4
with:
path: ${{ steps.resolve-binary.outputs.cache_dir }}
key: vociferate-${{ steps.resolve-binary.outputs.release_tag }}-linux-${{ runner.arch }}
- name: Download vociferate binary
if: steps.cache-vociferate.outputs.cache-hit != 'true'
shell: bash
env:
TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
ASSET_URL: ${{ steps.resolve-binary.outputs.asset_url }}
BINARY_PATH: ${{ steps.resolve-binary.outputs.binary_path }}
run: |
set -euo pipefail
curl --fail --location \
-H "Authorization: token ${TOKEN}" \
-o "$BINARY_PATH" \
"$ASSET_URL"
chmod +x "$BINARY_PATH"
- name: Run vociferate
id: run-vociferate
shell: bash
env:
VOCIFERATE_BIN: ${{ steps.resolve-binary.outputs.binary_path }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
common_args=(--root .)
if [[ -n "${{ inputs.version-file }}" ]]; then
common_args+=(--version-file "${{ inputs.version-file }}")
fi
if [[ -n "${{ inputs.version-pattern }}" ]]; then
common_args+=(--version-pattern "${{ inputs.version-pattern }}")
fi
if [[ -n "${{ inputs.changelog }}" ]]; then
common_args+=(--changelog "${{ inputs.changelog }}")
fi
provided_version="$(printf '%s' "${INPUT_VERSION:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
if [[ -z "$provided_version" ]]; then
provided_version="$("$VOCIFERATE_BIN" "${common_args[@]}" --recommend)"
fi
normalized_version="${provided_version#v}"
tag="v${normalized_version}"
"$VOCIFERATE_BIN" "${common_args[@]}" --version "$provided_version" --date "$(date -u +%F)"
echo "version=$tag" >> "$GITHUB_OUTPUT"
- name: Commit and push release
shell: bash
env:
TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
GIT_USER_NAME: ${{ inputs.git-user-name }}
GIT_USER_EMAIL: ${{ inputs.git-user-email }}
GIT_ADD_FILES: ${{ inputs.git-add-files }}
RELEASE_TAG: ${{ steps.run-vociferate.outputs.version }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
echo "Tag ${RELEASE_TAG} already exists" >&2
exit 1
fi
case "$GITHUB_SERVER_URL" in
https://*)
authed_remote="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
;;
http://*)
authed_remote="http://oauth2:${TOKEN}@${GITHUB_SERVER_URL#http://}/${GITHUB_REPOSITORY}.git"
;;
*)
echo "Unsupported GITHUB_SERVER_URL: ${GITHUB_SERVER_URL}" >&2
exit 1
;;
esac
git config user.name "$GIT_USER_NAME"
git config user.email "$GIT_USER_EMAIL"
git remote set-url origin "$authed_remote"
for f in $GIT_ADD_FILES; do
git add "$f"
done
git commit -m "release: prepare ${RELEASE_TAG}"
git tag "$RELEASE_TAG"
git push origin HEAD
git push origin "$RELEASE_TAG"

152
publish/action.yml Normal file
View File

@@ -0,0 +1,152 @@
name: vociferate/publish
description: >
Extract release notes from the changelog and create or update a
Gitea/GitHub release. The repository must be checked out at the release
tag before this action runs.
inputs:
token:
description: >
Token used to authenticate release API calls. Defaults to the
workflow token.
required: false
default: ''
version:
description: >
Semantic version to publish (with or without leading v). When omitted,
derived from the current git tag ref.
required: false
default: ''
changelog:
description: Path to changelog file relative to repository root.
required: false
default: changelog.md
outputs:
release-id:
description: Numeric ID of the created or updated release.
value: ${{ steps.create-release.outputs.id }}
tag:
description: The tag used for the release (e.g. v1.2.3).
value: ${{ steps.resolve-version.outputs.tag }}
version:
description: The bare version string without leading v (e.g. 1.2.3).
value: ${{ steps.resolve-version.outputs.version }}
runs:
using: composite
steps:
- name: Resolve release version
id: resolve-version
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
GITHUB_REF_VALUE: ${{ github.ref }}
run: |
set -euo pipefail
provided="$(printf '%s' "${INPUT_VERSION:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
if [[ -n "$provided" ]]; then
normalized="${provided#v}"
tag="v${normalized}"
elif [[ "${GITHUB_REF_VALUE}" == refs/tags/* ]]; then
tag="${GITHUB_REF_VALUE#refs/tags/}"
normalized="${tag#v}"
else
echo "A version 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" >> "$GITHUB_OUTPUT"
- name: Extract release notes from changelog
id: extract-notes
shell: bash
env:
CHANGELOG: ${{ inputs.changelog != '' && inputs.changelog || 'changelog.md' }}
RELEASE_VERSION: ${{ steps.resolve-version.outputs.version }}
RUNNER_TEMP: ${{ runner.temp }}
run: |
set -euo pipefail
release_notes="$(awk -v version="$RELEASE_VERSION" '
$0 ~ "^## \\[" version "\\] - " {capture=1}
capture {
if ($0 ~ "^## \\[" && $0 !~ "^## \\[" version "\\] - ") exit
print
}
' "$CHANGELOG")"
if [[ -z "${release_notes//[[:space:]]/}" ]]; then
echo "Release notes section for ${RELEASE_VERSION} was not found in ${CHANGELOG}" >&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: create-release
shell: bash
env:
TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
TAG_NAME: ${{ steps.resolve-version.outputs.tag }}
RELEASE_NOTES_FILE: ${{ steps.extract-notes.outputs.notes_file }}
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REPOSITORY: ${{ github.repository }}
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 ${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 ${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
echo "id=$existing_release_id" >> "$GITHUB_OUTPUT"
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 ${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
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"
fi