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
8 changes: 8 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Version Update

Select exactly one version update type. This choice is used by CI after the PR is merged into
`main` to update `package.json`, create the release tag, and publish the package to npm.

- [ ] major
- [ ] minor
- [ ] patch
150 changes: 150 additions & 0 deletions .github/workflows/publish-npm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Auto Version, Publish, and Tag NPM

on:
push:
branches:
- main
workflow_dispatch:
inputs:
update_type:
description: Version update type
required: true
default: patch
type: choice
options:
- major
- minor
- patch

concurrency:
group: npm-release
cancel-in-progress: false

jobs:
publish:
if: "${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'chore: release ') }}"
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
pull-requests: read

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.DEPLOY_PAT || github.token }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14.0'
registry-url: 'https://registry.npmjs.org'

- name: Install npm with OIDC support
run: npm install --global npm@^11.5.1

- name: Get version update type
id: release-type
uses: actions/github-script@v7
with:
script: |
const updateTypes = ['major', 'minor', 'patch'];

function selectedUpdateTypes(body) {
return updateTypes.filter((type) => {
const pattern = new RegExp(`^- \\[[xX]\\]\\s*${type}\\s*$`, 'im');
return pattern.test(body ?? '');
});
}

if (context.eventName === 'workflow_dispatch') {
const updateType = context.payload.inputs?.update_type;

if (!updateTypes.includes(updateType)) {
core.setFailed(`Invalid version update type: ${updateType}`);
return;
}

core.setOutput('update-type', updateType);
core.info(`Selected version update: ${updateType}`);
return;
}

const { owner, repo } = context.repo;
const response = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: context.sha,
});

const pullRequest =
response.data.find((pr) => pr.merged_at && pr.base.ref === 'main') ??
response.data.find((pr) => pr.merged_at);

if (!pullRequest) {
core.setFailed(`No merged pull request found for commit ${context.sha}.`);
return;
}

const selected = selectedUpdateTypes(pullRequest.body);

if (selected.length !== 1) {
core.setFailed(
`PR #${pullRequest.number} must have exactly one version update checkbox selected. Found ${selected.length}.`,
);
return;
}

core.setOutput('update-type', selected[0]);
core.setOutput('pr-number', String(pullRequest.number));
core.info(`Selected version update from PR #${pullRequest.number}: ${selected[0]}`);

- name: Bump package version
id: package-version
run: |
npm version "$UPDATE_TYPE" --no-git-tag-version
PACKAGE_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
echo "package-name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Bumped $PACKAGE_NAME to $VERSION"
env:
UPDATE_TYPE: ${{ steps.release-type.outputs.update-type }}

- name: Check release availability
run: |
if git tag --list "v${PACKAGE_VERSION}" | grep -q .; then
echo "::error::Tag v${PACKAGE_VERSION} already exists"
exit 1
fi

if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version --registry=https://registry.npmjs.org > /dev/null 2>&1; then
echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm"
exit 1
fi
env:
PACKAGE_NAME: ${{ steps.package-version.outputs.package-name }}
PACKAGE_VERSION: ${{ steps.package-version.outputs.version }}

- name: Install dependencies
run: npm ci

- name: Build package
run: npm run build

- name: Publish to NPM
run: npm publish

- name: Commit version bump and tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json
git commit -m "chore: release v${PACKAGE_VERSION} [skip ci]"
git tag "v${PACKAGE_VERSION}"
git push origin HEAD:main
git push origin "v${PACKAGE_VERSION}"
env:
PACKAGE_VERSION: ${{ steps.package-version.outputs.version }}
41 changes: 41 additions & 0 deletions .github/workflows/validate-version-selection.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Validate Version Selection

on:
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
- edited
- ready_for_review

permissions:
contents: read

jobs:
validate-version-selection:
name: Validate version selection
runs-on: ubuntu-latest

steps:
- name: Validate release checkbox
uses: actions/github-script@v7
with:
script: |
const body = context.payload.pull_request?.body ?? '';
const updateTypes = ['major', 'minor', 'patch'];
const selected = updateTypes.filter((type) => {
const pattern = new RegExp(`^- \\[[xX]\\]\\s*${type}\\s*$`, 'im');
return pattern.test(body);
});

if (selected.length !== 1) {
core.setFailed(
`Select exactly one version update checkbox in the PR body. Found ${selected.length}.`,
);
return;
}

core.info(`Selected version update: ${selected[0]}`);
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/xylophonez/hyperbalance.git"
"url": "https://github.com/xylophonez/hyperbalance"
},
"bugs": {
"url": "https://github.com/xylophonez/hyperbalance/issues"
Expand Down
Loading