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