Repository navigation
142 lines (138 loc) · 7.19 KB
/
Copy pathrelease.yml
File metadata and controls
142 lines (138 loc) · 7.19 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
# A release is a tag on a green commit (I15). Pushing a tag vX.Y.Z checks the Linux gate jobs for that
# exact commit on main, checks package.json's version, and publishes to npm with provenance through npm's
# trusted publishing: no token is stored anywhere. npmjs.com must list this workflow (release.yml in
# pullboard-dev/pullboard) as the package's trusted publisher, set once by the person.
name: release
on:
push:
tags: ['v*']
# The person, 27 Oct: GitHub CI is a second opinion. When it finds something, that becomes an item; it does not
# hold a release the board already proved. A tag whose Linux jobs failed publishes by hand through publish-proven.
workflow_dispatch:
inputs:
tag:
description: 'An existing tag vX.Y.Z on main, already proven by the board'
required: true
permissions:
contents: read
actions: read
id-token: write
jobs:
publish:
permissions:
contents: write
actions: read
id-token: write
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: The tag names package.json's version
run: test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
- name: Main CI passed this commit
id: main-ci
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
run_id=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow gate.yml --branch main --event push --commit "$GITHUB_SHA" --limit 1 --json databaseId --jq '.[0].databaseId // empty')
if [ -z "$run_id" ]; then
printf '::error::No gate.yml run for commit %s on main; refusing npm publish.\n' "$GITHUB_SHA"
exit 1
fi
if ! gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json databaseId,headSha,headBranch,event,url,jobs > "$RUNNER_TEMP/release-gate.json"; then
printf '::error::Could not read gate.yml run %s; refusing npm publish.\n' "$run_id"
exit 1
fi
node --input-type=module <<'NODE'
import { appendFileSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const run = JSON.parse(readFileSync(join(process.env.RUNNER_TEMP, 'release-gate.json'), 'utf8'));
const label = `gate.yml run ${run.databaseId} (${run.url})`;
const linux = (run.jobs ?? []).filter(job => /^linux(?:\s|$)/u.test(job.name));
const failed = linux.filter(job => job.status !== 'completed' || job.conclusion !== 'success');
if (run.headSha !== process.env.GITHUB_SHA || run.headBranch !== 'main' || run.event !== 'push') {
throw new Error(`${label} is not main CI for tagged commit ${process.env.GITHUB_SHA}; refusing npm publish.`);
}
if (!linux.length) throw new Error(`${label} has no Linux jobs; refusing npm publish.`);
if (failed.length) {
const names = failed.map(job => `${job.name}: ${job.status}/${job.conclusion || 'pending'}`).join(', ');
throw new Error(`${label} has Linux jobs that did not succeed: ${names}; refusing npm publish.`);
}
console.log(`${label}: all ${linux.length} Linux jobs succeeded for ${run.headSha}.`);
appendFileSync(process.env.GITHUB_OUTPUT, 'passed=true\n');
NODE
- name: Release notes from the tagged changelog section
run: |
node --input-type=module <<'NOTES'
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const tag = process.env.GITHUB_REF_NAME;
if (!/^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(tag ?? '')) {
throw new Error('The release tag must name a version; refusing publication.');
}
const source = readFileSync('CHANGELOG.md', 'utf8');
const headings = [...source.matchAll(/^##[ \t]+([^\r\n]+)(?:\r?\n|$)/gmu)];
const title = '[' + tag.slice(1) + ']';
// Match the whole version label, never a prefix or the Unreleased section.
const selected = headings.filter(heading => heading[1] === title || heading[1].startsWith(title + ' ') || heading[1].startsWith(title + '\t'));
if (selected.length !== 1) throw new Error('CHANGELOG.md must contain exactly one section for ' + tag + '; refusing publication.');
const heading = selected[0];
const next = headings.find(value => value.index > heading.index);
const notes = source.slice(heading.index + heading[0].length, next?.index);
if (!notes.trim()) throw new Error('The changelog section for ' + tag + ' is empty; refusing publication.');
writeFileSync(join(process.env.RUNNER_TEMP, 'release-notes.md'), notes);
NOTES
- name: npm new enough for trusted publishing
run: npm install -g npm@latest
- if: ${{ steps.main-ci.outputs.passed == 'true' }}
run: npm publish --provenance --access public
- name: Publish the GitHub release with its changelog notes
if: ${{ steps.main-ci.outputs.passed == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
# Run by hand for a proven tag: the same version and notes checks as above, without the GitHub CI gate.
publish-proven:
permissions:
contents: write
id-token: write
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
TAG: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: The tag names package.json's version and its commit is on main
run: |
set -euo pipefail
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::$TAG is not a vX.Y.Z tag; refusing npm publish."; exit 1; }
test "v$(node -p "require('./package.json').version")" = "$TAG"
git fetch -q origin main
git merge-base --is-ancestor "$(git rev-parse "refs/tags/$TAG^{commit}")" origin/main \
|| { echo "::error::$TAG is not on main; refusing npm publish."; exit 1; }
- name: Release notes from the tagged changelog section, with the tag's own extraction step
run: |
set -euo pipefail
awk "/<<'NOTES'\$/{f=1;next} f&&/^ *NOTES\$/{exit} f" .github/workflows/release.yml | sed 's/^ \{10\}//' > "$RUNNER_TEMP/notes.mjs"
GITHUB_REF_NAME="$TAG" node "$RUNNER_TEMP/notes.mjs"
- name: npm new enough for trusted publishing
run: npm install -g npm@latest
- run: npm publish --provenance --access public
- name: Publish the GitHub release with its changelog notes
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$TAG" --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md"