diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61db07b..02be707 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,8 +83,24 @@ See [step-debug-logs](https://github.com/actions/toolkit/blob/master/docs/action ## Release workflow -Instructions for releasing a new version of the action: +To release a new version, run: -1. If the release will increment the major version, update the action refs in the examples in README.md (e.g., `uses: go-task/setup-task@v1` -> `uses: go-task/setup-task@v2`). -1. Create a [GitHub release](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository#creating-a-release), following the `vX.Y.Z` tag name convention. Make sure to follow [the SemVer specification](https://semver.org/). -1. Rebase the release branch for that major version (e.g., `v1` branch for the `v1.x.x` tags) on the tag. If no branch exists for the release's major version, create one. +``` +task release VERSION=X.Y.Z +``` + +This will: + +1. Promote the `Unreleased` section of `CHANGELOG.md` to `vX.Y.Z`. +1. Commit, tag (`vX.Y.Z`), and force-update the major version tag (`vX`). +1. Push the commit and both tags to `origin`. +1. Create a **draft** [GitHub release](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository#creating-a-release) for `vX.Y.Z`, pre-filled with the corresponding `CHANGELOG.md` section as release notes. + +Before running, make sure to: + +- Update the action refs in `README.md` examples if incrementing major (e.g., `uses: go-task/setup-task@v1` -> `uses: go-task/setup-task@v2`). +- Run `task check` and `task build` to ensure `dist/` is up to date. +- Follow [the SemVer specification](https://semver.org/). +- Have the [`gh` CLI](https://cli.github.com/) installed and authenticated. + +After the task completes, review the draft release on GitHub, edit the notes if needed, and publish it. diff --git a/Taskfile.yml b/Taskfile.yml index 66e05db..87c9a11 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -259,6 +259,40 @@ tasks: cmds: - poetry install --no-root + release: + desc: "Tag and push a new release. Usage: task release VERSION=X.Y.Z" + vars: + MAJOR: '{{splitList "." .VERSION | first}}' + DATE: '{{dateInZone "2006-01-02" now "UTC"}}' + RELEASE_NOTES_PATH: + sh: task utility:mktemp-file TEMPLATE="release-notes-XXXXXXXXXX.md" + preconditions: + - sh: '[[ "{{.VERSION}}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]' + msg: 'VERSION must be X.Y.Z (no "v" prefix). Got: "{{.VERSION}}"' + - sh: '[[ -z "$(git status --porcelain)" ]]' + msg: "Working tree must be clean." + - sh: '[[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]]' + msg: "Must be on main branch." + - sh: 'git fetch origin main && [[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]]' + msg: "main must be up to date with origin/main." + - sh: 'grep -q "^## Unreleased$" CHANGELOG.md' + msg: 'CHANGELOG.md must contain an "## Unreleased" section.' + - sh: '! git rev-parse "v{{.VERSION}}" >/dev/null 2>&1' + msg: "Tag v{{.VERSION}} already exists." + - sh: "command -v gh >/dev/null 2>&1" + msg: "gh CLI is required. Install: https://cli.github.com/" + cmds: + - node scripts/promote-changelog.mjs "{{.VERSION}}" "{{.DATE}}" + - git add CHANGELOG.md + - 'git commit -m "chore: release v{{.VERSION}}"' + - git tag "v{{.VERSION}}" + - git tag -f "v{{.MAJOR}}" + - git push origin main + - git push origin "v{{.VERSION}}" + - git push origin "v{{.MAJOR}}" --force + - node scripts/extract-changelog-section.mjs "{{.VERSION}}" > "{{.RELEASE_NOTES_PATH}}" + - gh release create "v{{.VERSION}}" --title "v{{.VERSION}}" --draft --notes-file "{{.RELEASE_NOTES_PATH}}" + ts:build: desc: Build the action's TypeScript code. deps: diff --git a/scripts/extract-changelog-section.mjs b/scripts/extract-changelog-section.mjs new file mode 100644 index 0000000..8bebbca --- /dev/null +++ b/scripts/extract-changelog-section.mjs @@ -0,0 +1,21 @@ +import { readFileSync } from "node:fs"; + +const [version] = process.argv.slice(2); +if (!version) { + console.error("Usage: extract-changelog-section.mjs "); + process.exit(1); +} + +const lines = readFileSync("CHANGELOG.md", "utf8").split("\n"); +const header = `## v${version} `; +const start = lines.findIndex((l) => l.startsWith(header)); +if (start === -1) { + console.error(`Section for v${version} not found in CHANGELOG.md.`); + process.exit(1); +} + +const rest = lines.slice(start + 1); +const nextHeading = rest.findIndex((l) => l.startsWith("## ")); +const body = (nextHeading === -1 ? rest : rest.slice(0, nextHeading)).join("\n").trim(); + +process.stdout.write(body + "\n"); diff --git a/scripts/promote-changelog.mjs b/scripts/promote-changelog.mjs new file mode 100644 index 0000000..0901277 --- /dev/null +++ b/scripts/promote-changelog.mjs @@ -0,0 +1,19 @@ +import { readFileSync, writeFileSync } from "node:fs"; + +const [version, date] = process.argv.slice(2); +if (!version || !date) { + console.error("Usage: promote-changelog.mjs "); + process.exit(1); +} + +const path = "CHANGELOG.md"; +const content = readFileSync(path, "utf8"); + +if (!/^## Unreleased$/m.test(content)) { + console.error(`No "## Unreleased" section found in ${path}.`); + process.exit(1); +} + +const updated = content.replace(/^## Unreleased$/m, `## Unreleased\n\n## v${version} - ${date}`); + +writeFileSync(path, updated);