Compare commits
2 Commits
4ae6f34931
...
55a067973e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55a067973e | ||
|
|
647d8cf76f |
@@ -22,8 +22,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: docker.io/catthehacker/ubuntu:act-latest
|
container: docker.io/catthehacker/ubuntu:act-latest
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.resolve-tag.outputs.tag }}
|
tag: ${{ steps.publish.outputs.tag }}
|
||||||
version: ${{ steps.resolve-tag.outputs.version }}
|
version: ${{ steps.publish.outputs.version }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -36,35 +36,12 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.ref }}
|
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
|
- name: Checkout requested tag
|
||||||
if: ${{ inputs.tag != '' }}
|
if: ${{ inputs.tag != '' }}
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
@@ -74,90 +51,16 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: go.sum
|
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
|
- name: Create or update release
|
||||||
id: release
|
id: publish
|
||||||
env:
|
uses: ./publish
|
||||||
TAG_NAME: ${{ steps.resolve-tag.outputs.tag }}
|
with:
|
||||||
RELEASE_NOTES_FILE: ${{ steps.release-notes.outputs.notes_file }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
version: ${{ inputs.tag }}
|
||||||
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"
|
|
||||||
|
|
||||||
- name: Build release binaries
|
- name: Build release binaries
|
||||||
env:
|
env:
|
||||||
RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }}
|
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -178,7 +81,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload release binaries
|
- name: Upload release binaries
|
||||||
env:
|
env:
|
||||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
RELEASE_ID: ${{ steps.publish.outputs.release-id }}
|
||||||
|
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -213,8 +117,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Summarize published release
|
- name: Summarize published release
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ steps.resolve-tag.outputs.tag }}
|
TAG_NAME: ${{ steps.publish.outputs.tag }}
|
||||||
RELEASE_VERSION: ${{ steps.resolve-tag.outputs.version }}
|
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -37,91 +35,20 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: go.sum
|
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
|
- name: Run tests
|
||||||
run: |
|
run: go test ./...
|
||||||
set -euo pipefail
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
- name: Configure git author
|
- name: Prepare and tag release
|
||||||
run: |
|
id: prepare
|
||||||
set -euo pipefail
|
uses: ./prepare
|
||||||
git config user.name "gitea-actions[bot]"
|
with:
|
||||||
git config user.email "gitea-actions[bot]@users.noreply.local"
|
version: ${{ inputs.version }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- 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: Summarize prepared release
|
- name: Summarize prepared release
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
tag="${{ steps.prepare.outputs.version }}"
|
||||||
normalized_version="${RELEASE_VERSION#v}"
|
|
||||||
tag="v${normalized_version}"
|
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "## Release Prepared"
|
echo "## Release Prepared"
|
||||||
echo
|
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
144
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
|
## 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:
|
### `prepare` — update files, commit, and push tag
|
||||||
|
|
||||||
```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:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Prepare Release
|
name: Prepare Release
|
||||||
@@ -118,73 +104,67 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Prepare release files
|
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.0
|
||||||
id: prepare
|
|
||||||
uses: git.hrafn.xyz/aether/vociferate@v1.0.0
|
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version }}
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
curl --fail-with-body -X POST \
|
||||||
tag="${{ steps.prepare.outputs.version }}"
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
git config user.name "gitea-actions[bot]"
|
-H "Content-Type: application/octet-stream" \
|
||||||
git config user.email "gitea-actions[bot]@users.noreply.local"
|
"${{ github.api_url }}/repos/${{ github.repository }}/releases/${{ steps.publish.outputs.release-id }}/assets?name=myapp" \
|
||||||
git add changelog.md release-version
|
--data-binary "@dist/myapp"
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Two dedicated subdirectory composite actions replace the boilerplate inline workflow steps: `prepare/action.yml` runs vociferate, commits updated files, and pushes the release tag; `publish/action.yml` extracts release notes from the changelog and creates or updates the release. Consumer repositories need only a checkout step and a single `uses:` call per workflow stage.
|
||||||
|
- `publish/action.yml` outputs `release-id`, `tag`, and `version` so consumers can upload their own release assets after the release is created.
|
||||||
|
- `prepare/action.yml` accepts a `git-add-files` input so repositories using a custom `version-file` can stage the correct set of files for the release commit.
|
||||||
|
- The `prepare-release.yml` and `do-release.yml` vociferate workflows now use the local `./prepare` and `./publish` actions, validating the actions in the self-release pipeline.
|
||||||
|
|
||||||
- Release version recommendation now reads the current version from the most recent released section in the changelog instead of requiring a separate version file. When no prior releases exist the version defaults to `0.0.0`, yielding `v1.0.0` as the first recommended tag.
|
- Release version recommendation now reads the current version from the most recent released section in the changelog instead of requiring a separate version file. When no prior releases exist the version defaults to `0.0.0`, yielding `v1.0.0` as the first recommended tag.
|
||||||
- `vociferate prepare` creates the `release-version` file if it does not already exist, removing the need to pre-seed it in new repositories.
|
- `vociferate prepare` creates the `release-version` file if it does not already exist, removing the need to pre-seed it in new repositories.
|
||||||
- Release automation is now split into a prepare workflow that updates and tags `main`, and a tag-driven publish workflow that creates the release from the tagged revision.
|
- Release automation is now split into a prepare workflow that updates and tags `main`, and a tag-driven publish workflow that creates the release from the tagged revision.
|
||||||
|
|||||||
210
prepare/action.yml
Normal file
210
prepare/action.yml
Normal 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
152
publish/action.yml
Normal 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
|
||||||
Reference in New Issue
Block a user