Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 19 additions & 138 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,182 +13,63 @@ permissions:
contents: read

jobs:
prepare:
name: Prepare release PR
release:
name: Release
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && !contains(github.event.workflow_run.head_commit.message, 'chore(release):')
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
issues: write
pull-requests: write
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
steps:
- name: Check release token
id: release-token
run: |
set -euo pipefail
if [[ -z "${RELEASE_TOKEN}" ]]; then
echo "::notice::Skipping release preparation because RELEASE_TOKEN is not configured."
echo "configured=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "configured=true" >> "$GITHUB_OUTPUT"

- name: Checkout
if: steps.release-token.outputs.configured == 'true'
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: main

- name: Set up Bun
if: steps.release-token.outputs.configured == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14

- name: Install dependencies
if: steps.release-token.outputs.configured == 'true'
run: bun install --frozen-lockfile

- name: Configure release git author
if: steps.release-token.outputs.configured == 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${RELEASE_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

- name: Prepare release
if: steps.release-token.outputs.configured == 'true'
- name: Release
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bun run release --no-push --no-github

- name: Ensure release PR runs CI
if: steps.release-token.outputs.configured == 'true' && steps.release.outputs.published == 'true'
run: |
set -euo pipefail
subject="$(git log -1 --pretty=%s)"
if [[ "$subject" == *" [skip ci]" ]]; then
body="$(git log -1 --pretty=%b)"
subject="${subject% [skip ci]}"
git commit --amend -m "$subject" -m "$body"
fi

- name: Open release PR
if: steps.release-token.outputs.configured == 'true' && steps.release.outputs.published == 'true'
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
branch="hooversion/release-main"
title="chore(release): @openhoo/hooversion ${RELEASE_VERSION}"
body_file="$(mktemp)"

git switch -c "$branch"
git push --force origin "HEAD:${branch}"
run: bun run release

cat > "$body_file" <<BODY
Automated Hooversion release PR.

- Package: @openhoo/hooversion
- Version: ${RELEASE_VERSION}
- Tag: ${RELEASE_TAG}

Merging this PR runs the required CI checks before the release workflow publishes the tag, GitHub release, and package.
BODY

pr="$(gh pr list --base main --head "$branch" --state open --json number --jq '.[0].number')"
if [[ -n "$pr" ]]; then
gh pr edit "$pr" --title "$title" --body-file "$body_file"
else
gh pr create --base main --head "$branch" --title "$title" --body-file "$body_file"
fi

publish:
name: Publish release
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && contains(github.event.workflow_run.head_commit.message, 'chore(release):')
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Node for npm trusted publishing
if: steps.release.outputs.published == 'true'
uses: actions/setup-node@v6
with:
fetch-depth: 0
ref: main

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Read package metadata
id: package
run: |
set -euo pipefail
name="$(bun -e 'const p = await Bun.file("package.json").json(); console.log(p.name)')"
version="$(bun -e 'const p = await Bun.file("package.json").json(); console.log(p.version)')"
private="$(bun -e 'const p = await Bun.file("package.json").json(); console.log(p.private === true ? "true" : "false")')"
{
echo "name=${name}"
echo "version=${version}"
echo "tag=v${version}"
echo "private=${private}"
} >> "$GITHUB_OUTPUT"

- name: Create tag and GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PACKAGE_NAME: ${{ steps.package.outputs.name }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
RELEASE_TAG: ${{ steps.package.outputs.tag }}
run: |
set -euo pipefail
if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
echo "Tag ${RELEASE_TAG} already exists."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$RELEASE_TAG" -m "${PACKAGE_NAME} ${PACKAGE_VERSION}"
git push origin "$RELEASE_TAG"

awk -v version="$PACKAGE_VERSION" '
$0 ~ "^## " version " \\(" { found=1; print; next }
found && /^## / { exit }
found { print }
' CHANGELOG.md > release-notes.md
if [[ ! -s release-notes.md ]]; then
printf '%s %s\n' "$PACKAGE_NAME" "$PACKAGE_VERSION" > release-notes.md
fi
gh release create "$RELEASE_TAG" --title "${PACKAGE_NAME} ${PACKAGE_VERSION}" --notes-file release-notes.md --verify-tag
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false

- name: Check published package version
if: steps.package.outputs.private != 'true'
id: registry
if: steps.release.outputs.published == 'true'
id: package
env:
PACKAGE_NAME: ${{ steps.package.outputs.name }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
PACKAGE_VERSION: ${{ steps.release.outputs.version }}
run: |
set -euo pipefail
if bun info "${PACKAGE_NAME}@${PACKAGE_VERSION}" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
if bun info "@openhoo/hooversion@${PACKAGE_VERSION}" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish package
if: steps.package.outputs.private != 'true' && steps.registry.outputs.exists != 'true'
env:
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bun publish --access public --registry=https://registry.npmjs.org
- name: Publish package to npm
if: steps.release.outputs.published == 'true' && steps.package.outputs.exists != 'true'
run: npm publish --access public --provenance
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ The release command updates manifests and changelogs, runs configured hooks,
creates a release commit and tags, pushes them by default, creates GitHub
Releases with `GITHUB_TOKEN`, and writes CI outputs to `.hooversion/outputs.json`.
For a single release it also writes `.release-version` for compatibility with
existing workflows. In protected repositories, run release preparation with
`push: "false"` and `github: "false"`, open a release PR, then publish tags and
GitHub Releases after that PR merges.
existing workflows. In protected repositories that allow GitHub Actions to
bypass release-only branch protections, this keeps releases automatic while
human changes to `main` remain pull-request gated.

## GitHub Actions

Expand Down Expand Up @@ -80,13 +80,10 @@ jobs:
uses: openhoo/hooversion/actions/release@v0.1.1
with:
version: 0.1.1
push: "false"
github: "false"
github-token: ${{ secrets.RELEASE_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
```

`actions/lint` automatically uses the PR base/head range on pull requests and
`--last` on pushes. `actions/release` exposes `published`, `version`, `tag`, and
`releases-json` outputs for release PR and downstream publishing jobs. Release
PR preparation should be skipped until `RELEASE_TOKEN` is configured, so regular
main CI stays green before release automation is enabled.
`releases-json` outputs for downstream package, Docker, or archive publishing
jobs.
13 changes: 5 additions & 8 deletions actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,11 @@ repositories.
uses: openhoo/hooversion/actions/release@v0.1.1
with:
version: 0.1.1
push: "false"
github: "false"
github-token: ${{ secrets.RELEASE_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
```

Use `steps.release.outputs.published`, `version`, `tag`, and `releases-json`
to open release PRs or gate downstream package, Docker, or archive publishing
jobs. For repositories where `main` requires pull requests, use a
`RELEASE_TOKEN` that can push the release branch and open the release PR. Skip
release preparation when that token is absent so normal main CI does not fail
before release automation is enabled.
to gate downstream package, Docker, or archive publishing jobs. In repositories
where `main` requires pull requests, allow GitHub Actions to bypass release-only
branch protections so Hooversion can keep releases automatic while human changes
remain pull-request gated.
Loading
Loading