Files
vociferate/publish/action.yml
Micheal Wilkinson a23ef16e14
Some checks failed
Push Validation / recommend-release (push) Has been cancelled
Push Validation / coverage-badge (push) Has been cancelled
fix(publish): avoid heredoc parsing in composite json helper
Replace the embedded python heredoc with python3 -c so the composite
action remains valid YAML and shell across teacup parsing.
2026-03-21 20:14:19 +00:00

180 lines
6.5 KiB
YAML

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: >
Personal access token used to authenticate release API calls.
Required to support release updates across workflow boundaries.
required: true
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}"
elif head_tag="$(git describe --exact-match --tags HEAD 2>/dev/null)" && [[ -n "$head_tag" ]]; then
tag="$head_tag"
normalized="${tag#v}"
else
echo "A version input is required when the workflow is not running from a tag push" >&2
echo "Provide version via input or ensure HEAD is at a tagged commit." >&2
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$normalized" >> "$GITHUB_OUTPUT"
- name: Extract release notes
id: extract-notes
uses: ./run-vociferate
with:
root: ${{ github.workspace }}
changelog: ${{ inputs.changelog }}
version: ${{ steps.resolve-version.outputs.version }}
print-release-notes: 'true'
- name: Write release notes file
id: write-notes
shell: bash
env:
RELEASE_NOTES: ${{ steps.extract-notes.outputs.stdout }}
RUNNER_TEMP: ${{ runner.temp }}
run: |
set -euo pipefail
notes_file="${RUNNER_TEMP}/release-notes.md"
printf '%s\n' "$RELEASE_NOTES" > "$notes_file"
printf 'notes_file=%s\n' "$notes_file" >> "$GITHUB_OUTPUT"
- name: Create or update release
id: create-release
shell: bash
env:
TOKEN: ${{ inputs.token }}
TAG_NAME: ${{ steps.resolve-version.outputs.tag }}
RELEASE_NOTES_FILE: ${{ steps.write-notes.outputs.notes_file }}
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
parse_release_id() {
local json_file="$1"
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import json, sys; payload = json.load(open(sys.argv[1], encoding="utf-8")); value = payload.get("id"); print(value if isinstance(value, int) else "")' "$json_file"
return
fi
# Fallback for environments without python3.
sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$json_file" | head -n 1
}
raw_token="$(printf '%s' "${TOKEN:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
if [[ "$raw_token" =~ ^%\!t\(string=(.*)\)$ ]]; then
raw_token="${BASH_REMATCH[1]}"
fi
api_token="$(printf '%s' "$raw_token" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
if [[ -z "$api_token" ]]; then
echo "inputs.token is required (set to secrets.RELEASE_PAT)." >&2
exit 1
fi
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 ${api_token}" \
-H "Content-Type: application/json" \
"${release_by_tag_api}")"
if [[ "$status_code" == "200" ]]; then
existing_release_id="$(parse_release_id release-existing.json)"
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
if ! curl --fail-with-body \
-X PATCH \
-H "Authorization: token ${api_token}" \
-H "Content-Type: application/json" \
"${release_api}/${existing_release_id}" \
--data "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"${escaped_release_notes}\",\"draft\":false,\"prerelease\":false}" \
--output release.json; then
cat release.json >&2 || true
exit 1
fi
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
if ! curl --fail-with-body \
-X POST \
-H "Authorization: token ${api_token}" \
-H "Content-Type: application/json" \
"${release_api}" \
--data "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"${escaped_release_notes}\",\"draft\":false,\"prerelease\":false}" \
--output release.json; then
cat release.json >&2 || true
exit 1
fi
release_id="$(parse_release_id release.json)"
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