Skip to content

Commit 4de5020

Browse files
authored
chore(release): add release task automation (#14)
1 parent f95f6c5 commit 4de5020

4 files changed

Lines changed: 94 additions & 4 deletions

File tree

CONTRIBUTING.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,24 @@ See [step-debug-logs](https://github.com/actions/toolkit/blob/master/docs/action
8383

8484
## Release workflow
8585

86-
Instructions for releasing a new version of the action:
86+
To release a new version, run:
8787

88-
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`).
89-
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/).
90-
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.
88+
```
89+
task release VERSION=X.Y.Z
90+
```
91+
92+
This will:
93+
94+
1. Promote the `Unreleased` section of `CHANGELOG.md` to `vX.Y.Z`.
95+
1. Commit, tag (`vX.Y.Z`), and force-update the major version tag (`vX`).
96+
1. Push the commit and both tags to `origin`.
97+
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.
98+
99+
Before running, make sure to:
100+
101+
- 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`).
102+
- Run `task check` and `task build` to ensure `dist/` is up to date.
103+
- Follow [the SemVer specification](https://semver.org/).
104+
- Have the [`gh` CLI](https://cli.github.com/) installed and authenticated.
105+
106+
After the task completes, review the draft release on GitHub, edit the notes if needed, and publish it.

Taskfile.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,40 @@ tasks:
259259
cmds:
260260
- poetry install --no-root
261261

262+
release:
263+
desc: "Tag and push a new release. Usage: task release VERSION=X.Y.Z"
264+
vars:
265+
MAJOR: '{{splitList "." .VERSION | first}}'
266+
DATE: '{{dateInZone "2006-01-02" now "UTC"}}'
267+
RELEASE_NOTES_PATH:
268+
sh: task utility:mktemp-file TEMPLATE="release-notes-XXXXXXXXXX.md"
269+
preconditions:
270+
- sh: '[[ "{{.VERSION}}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]'
271+
msg: 'VERSION must be X.Y.Z (no "v" prefix). Got: "{{.VERSION}}"'
272+
- sh: '[[ -z "$(git status --porcelain)" ]]'
273+
msg: "Working tree must be clean."
274+
- sh: '[[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]]'
275+
msg: "Must be on main branch."
276+
- sh: 'git fetch origin main && [[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]]'
277+
msg: "main must be up to date with origin/main."
278+
- sh: 'grep -q "^## Unreleased$" CHANGELOG.md'
279+
msg: 'CHANGELOG.md must contain an "## Unreleased" section.'
280+
- sh: '! git rev-parse "v{{.VERSION}}" >/dev/null 2>&1'
281+
msg: "Tag v{{.VERSION}} already exists."
282+
- sh: "command -v gh >/dev/null 2>&1"
283+
msg: "gh CLI is required. Install: https://cli.github.com/"
284+
cmds:
285+
- node scripts/promote-changelog.mjs "{{.VERSION}}" "{{.DATE}}"
286+
- git add CHANGELOG.md
287+
- 'git commit -m "chore: release v{{.VERSION}}"'
288+
- git tag "v{{.VERSION}}"
289+
- git tag -f "v{{.MAJOR}}"
290+
- git push origin main
291+
- git push origin "v{{.VERSION}}"
292+
- git push origin "v{{.MAJOR}}" --force
293+
- node scripts/extract-changelog-section.mjs "{{.VERSION}}" > "{{.RELEASE_NOTES_PATH}}"
294+
- gh release create "v{{.VERSION}}" --title "v{{.VERSION}}" --draft --notes-file "{{.RELEASE_NOTES_PATH}}"
295+
262296
ts:build:
263297
desc: Build the action's TypeScript code.
264298
deps:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { readFileSync } from "node:fs";
2+
3+
const [version] = process.argv.slice(2);
4+
if (!version) {
5+
console.error("Usage: extract-changelog-section.mjs <version>");
6+
process.exit(1);
7+
}
8+
9+
const lines = readFileSync("CHANGELOG.md", "utf8").split("\n");
10+
const header = `## v${version} `;
11+
const start = lines.findIndex((l) => l.startsWith(header));
12+
if (start === -1) {
13+
console.error(`Section for v${version} not found in CHANGELOG.md.`);
14+
process.exit(1);
15+
}
16+
17+
const rest = lines.slice(start + 1);
18+
const nextHeading = rest.findIndex((l) => l.startsWith("## "));
19+
const body = (nextHeading === -1 ? rest : rest.slice(0, nextHeading)).join("\n").trim();
20+
21+
process.stdout.write(body + "\n");

scripts/promote-changelog.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { readFileSync, writeFileSync } from "node:fs";
2+
3+
const [version, date] = process.argv.slice(2);
4+
if (!version || !date) {
5+
console.error("Usage: promote-changelog.mjs <version> <date>");
6+
process.exit(1);
7+
}
8+
9+
const path = "CHANGELOG.md";
10+
const content = readFileSync(path, "utf8");
11+
12+
if (!/^## Unreleased$/m.test(content)) {
13+
console.error(`No "## Unreleased" section found in ${path}.`);
14+
process.exit(1);
15+
}
16+
17+
const updated = content.replace(/^## Unreleased$/m, `## Unreleased\n\n## v${version} - ${date}`);
18+
19+
writeFileSync(path, updated);

0 commit comments

Comments
 (0)