diff --git a/.gitea/workflows/do-release.yml b/.gitea/workflows/do-release.yml index 1fd573a..dcb9090 100644 --- a/.gitea/workflows/do-release.yml +++ b/.gitea/workflows/do-release.yml @@ -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 diff --git a/.gitea/workflows/prepare-release.yml b/.gitea/workflows/prepare-release.yml index f9b239e..cbead5a 100644 --- a/.gitea/workflows/prepare-release.yml +++ b/.gitea/workflows/prepare-release.yml @@ -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" diff --git a/README.md b/README.md index 44d4ae9..b077614 100644 --- a/README.md +++ b/README.md @@ -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,26 +104,29 @@ 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 }} - - - name: Commit and push prepared release - 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" + token: ${{ secrets.GITHUB_TOKEN }} ``` -Then use a separate tag workflow to publish the release from the tagged revision: +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 @@ -149,42 +138,33 @@ on: jobs: release: - uses: aether/vociferate/.gitea/workflows/do-release.yml@main - secrets: inherit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} ``` -Call the reusable prepare workflow: +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 -name: Prepare Release +- id: publish + uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} -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 +- name: Upload my binary + run: | + 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" ``` diff --git a/prepare/action.yml b/prepare/action.yml new file mode 100644 index 0000000..33d6f1e --- /dev/null +++ b/prepare/action.yml @@ -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" diff --git a/publish/action.yml b/publish/action.yml new file mode 100644 index 0000000..69d00e4 --- /dev/null +++ b/publish/action.yml @@ -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