-
Notifications
You must be signed in to change notification settings - Fork 388
158 lines (146 loc) · 6.9 KB
/
Copy pathrelease.yml
File metadata and controls
158 lines (146 loc) · 6.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
name: Create release PR
# Cutting a release is one deliberate act by a person, and everything after it is automatic. This
# workflow only proposes: it writes the version and the notes and opens a pull request. Merging that
# pull request is what publishes, so the release itself goes through review like anything else.
on:
workflow_dispatch:
inputs:
bump:
description: Version bump
required: true
type: choice
options: [patch, minor, major]
dry_run:
description: Show the version and notes without opening a PR
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: openbot-release-pr
cancel-in-progress: false
jobs:
create:
name: create
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
pull-requests: write
steps:
# Two open release PRs would each carry a version computed before the other existed, and
# whichever merged second would publish notes that skip a release.
- name: Refuse a second open release PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const { data: pulls } = await github.rest.pulls.list({ owner, repo, state: "open" });
const open = pulls.filter((pull) => pull.head.ref.startsWith("release/publish/"));
if (open.length > 0) {
core.setFailed(`A release PR is already open: ${open.map((p) => p.html_url).join(", ")}`);
}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The whole history, because the previous tag is what says where these notes start.
fetch-depth: 0
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- id: prepare
name: Write the version and promote the notes
env:
BUMP: ${{ inputs.bump }}
run: |
set -euo pipefail
current=$(bun -e 'console.log(require("./package.json").version)')
# The version in package.json must be one that was actually released, or the bump is
# computed from a number nobody published. The exception is the first release, where there
# is no tag to find yet.
if [ -n "$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --merged HEAD)" ]; then
git rev-parse --verify "refs/tags/v$current" >/dev/null 2>&1 || {
echo "::error::package.json says $current but v$current was never tagged."
exit 1
}
fi
# CHANGELOG.md is written by hand, for the person deciding whether to upgrade. This
# promotes what is already there; it never generates notes from commit subjects, because a
# commit subject is written for the person reading the diff.
grep -q '^## Unreleased$' CHANGELOG.md || {
echo "::error::CHANGELOG.md has no '## Unreleased' section."
exit 1
}
unreleased=$(awk '/^## Unreleased$/{found=1; next} /^## /{found=0} found' CHANGELOG.md | grep -c '[^[:space:]]' || true)
if [ "$unreleased" -eq 0 ]; then
echo "::error::Nothing under '## Unreleased'. A release nobody can describe is not a release."
exit 1
fi
bun -e '
const fs = require("fs");
// `bun -e` passes argv as [bun, ...args], so the argument is the last element. Reading
// it positionally from the front silently yields undefined and every bump becomes a patch.
const bump = process.argv.at(-1);
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
const [major, minor, patch] = pkg.version.split(".").map(Number);
const next =
bump === "major" ? [major + 1, 0, 0] :
bump === "minor" ? [major, minor + 1, 0] :
[major, minor, patch + 1];
pkg.version = next.join(".");
fs.writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`);
console.log(pkg.version);
' -- "$BUMP" > /tmp/version
version=$(cat /tmp/version)
# Unreleased becomes the version, and a fresh empty Unreleased takes its place so the next
# change has somewhere to go without anyone hand-editing a heading.
bun -e '
const fs = require("fs");
const version = process.argv[process.argv.length - 1];
const text = fs.readFileSync("CHANGELOG.md", "utf8");
fs.writeFileSync(
"CHANGELOG.md",
text.replace(/^## Unreleased$/m, `## Unreleased\n\n## ${version}`),
);
' -- "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
{
echo "## Release v$version"
echo
echo "Merging this publishes the image, tags the commit and creates the GitHub Release."
echo
echo "Before merging:"
echo "- [ ] The notes below describe what a deployment does differently"
echo "- [ ] The smoke journey passed against a licensed deployment, and the result is"
echo " pasted in a comment: \`bash scripts/start.sh && bun run test:smoke\`"
echo
echo "Merging runs the full suite against this commit before it builds, so there is"
echo "nothing to check about CI here. The journey is the part CI cannot do: it needs a"
echo "licence, and a licence belongs to the machine it was issued for."
echo
echo '```'
awk -v v="## $version" '$0==v{f=1;next} /^## /{if(f)exit} f' CHANGELOG.md
echo '```'
} > /tmp/pr-body.md
- name: Preview
if: inputs.dry_run
run: |
git --no-pager diff -- package.json CHANGELOG.md
cat /tmp/pr-body.md
# A pull request opened by a workflow does not trigger the pull_request workflows, so the
# release PR arrives without its own checks. That is deliberate rather than tolerated: the
# publish path runs the full suite against the release commit before it builds anything, which
# gates the release itself instead of the proposal for one.
- name: Open the release PR
if: '!inputs.dry_run'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: release/publish/v${{ steps.prepare.outputs.version }}
delete-branch: true
labels: release
commit-message: "Release v${{ steps.prepare.outputs.version }}"
title: "Release v${{ steps.prepare.outputs.version }}"
body-path: /tmp/pr-body.md