Skip to content

Commit c0ee188

Browse files
committed
add nsolid release automation
1 parent 4120f3e commit c0ee188

6 files changed

Lines changed: 520 additions & 0 deletions

File tree

.github/nsolid-release-lines.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"nodeReleaseLines": [22, 24]
3+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Create N|Solid Release Proposal
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
node-line:
7+
required: true
8+
type: choice
9+
options:
10+
- '22'
11+
- '24'
12+
description: Node.js release line
13+
14+
concurrency: ${{ github.workflow }}-${{ inputs.node-line }}
15+
16+
permissions:
17+
contents: write
18+
issues: write
19+
pull-requests: write
20+
21+
jobs:
22+
create:
23+
if: github.repository == 'nodesource/nsolid'
24+
runs-on: ubuntu-slim
25+
steps:
26+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27+
with:
28+
fetch-depth: 0
29+
persist-credentials: true
30+
token: ${{ secrets.GH_USER_TOKEN || github.token }}
31+
32+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
33+
with:
34+
node-version: lts/*
35+
36+
- name: Create release proposal
37+
run: tools/actions/create-nsolid-release-proposal.sh
38+
env:
39+
GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN || github.token }}
40+
GH_TOKEN: ${{ secrets.GH_USER_TOKEN || github.token }}
41+
NODE_LINE: ${{ inputs.node-line }}
42+
43+
- name: Upload conflict report
44+
if: failure() && hashFiles('nsolid-release-conflicts.md') != ''
45+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
46+
with:
47+
name: nsolid-release-conflicts
48+
path: nsolid-release-conflicts.md
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Watch Node.js Releases
2+
3+
on:
4+
schedule:
5+
- cron: '17 12 * * *'
6+
workflow_dispatch:
7+
8+
concurrency: ${{ github.workflow }}
9+
10+
permissions:
11+
contents: write
12+
issues: write
13+
pull-requests: write
14+
15+
jobs:
16+
watch:
17+
if: github.repository == 'nodesource/nsolid'
18+
runs-on: ubuntu-slim
19+
steps:
20+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21+
22+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
23+
with:
24+
node-version: lts/*
25+
26+
- name: Watch Node.js release lines
27+
run: node tools/actions/watch-node-releases.mjs
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN || github.token }}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# N|Solid release automation
2+
3+
This document describes the GitHub Actions automation that tracks Node.js
4+
releases and prepares N|Solid release proposal pull requests.
5+
6+
The automation is intentionally small. A releaser should be able to run it from
7+
the GitHub Actions UI by selecting only the Node.js release line.
8+
9+
## What it does
10+
11+
The automation has two workflows:
12+
13+
* `Watch Node.js Releases`
14+
* runs on a schedule and can also be started manually;
15+
* reads `.github/nsolid-release-lines.json`;
16+
* checks the latest stable Node.js release for each configured line;
17+
* skips the release if an N|Solid release branch or tag already exists;
18+
* creates or reuses a tracking issue;
19+
* attempts to create the release proposal.
20+
* `Create N|Solid Release Proposal`
21+
* is manually started with one input: `node-line`;
22+
* finds the latest stable Node.js tag for that line;
23+
* checks out `node-v<line>.x-nsolid-v6.x`;
24+
* reads the current N|Solid version from `src/node_version.h`;
25+
* creates `node-vX.Y.Z-nsolid-vA.B.C-release`;
26+
* merges the upstream Node.js tag;
27+
* sets `NSOLID_VERSION_IS_RELEASE` to `1`;
28+
* prepends the N|Solid changelog entry;
29+
* pushes the release branch;
30+
* opens a draft release PR.
31+
32+
If the Node.js tag merge conflicts, the workflow stops, comments on the
33+
tracking issue when one exists, and uploads `nsolid-release-conflicts.md` as an
34+
artifact.
35+
36+
The automation does not create a release tag. Release tags should still be
37+
created by the releaser after the proposal is reviewed.
38+
39+
## Supported release lines
40+
41+
The supported lines are configured in `.github/nsolid-release-lines.json`:
42+
43+
```json
44+
{
45+
"nodeReleaseLines": [22, 24]
46+
}
47+
```
48+
49+
Only lines listed there are watched by the scheduled workflow and shown in the
50+
manual workflow selector.
51+
52+
## Add a release line
53+
54+
When N|Solid starts supporting a new Node.js release line:
55+
56+
1. Add the major version to `.github/nsolid-release-lines.json`.
57+
2. Add the same value to the `node-line` options in
58+
`.github/workflows/create-nsolid-release-proposal.yml`.
59+
3. Ensure the base branch exists:
60+
`node-v<line>.x-nsolid-v6.x`.
61+
4. Ensure the N|Solid changelog exists:
62+
`doc/changelogs/NSOLID_CHANGELOG_V6_NODE_V<line>.md`.
63+
5. Run the workflow manually once for that line.
64+
65+
For example, to add Node.js v26 support, add `26` to the JSON file and add
66+
`'26'` to the workflow options.
67+
68+
## Remove a release line
69+
70+
When N|Solid stops supporting a Node.js release line:
71+
72+
1. Remove the major version from `.github/nsolid-release-lines.json`.
73+
2. Remove the same value from the `node-line` options in
74+
`.github/workflows/create-nsolid-release-proposal.yml`.
75+
76+
Do not delete existing release branches, tags, or changelog files as part of
77+
removing the line from automation.
78+
79+
## Manual use
80+
81+
To prepare a release proposal manually:
82+
83+
1. Open the `Create N|Solid Release Proposal` workflow.
84+
2. Select the Node.js release line.
85+
3. Run the workflow.
86+
87+
The workflow always uses the latest stable Node.js release for the selected
88+
line. If a different upstream tag is required, prepare the release manually.
89+
90+
## Failure handling
91+
92+
If the workflow fails because the release branch already exists, no action is
93+
required unless the existing branch is wrong.
94+
95+
If the workflow fails with merge conflicts:
96+
97+
1. Open the workflow artifact named `nsolid-release-conflicts`.
98+
2. Resolve the listed conflicts locally from the generated release branch steps.
99+
3. Continue the release manually.
100+
101+
If the workflow fails while creating the PR, the release branch may already have
102+
been pushed. Open the PR manually using the branch named in the workflow logs.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
CONFIG_PATH="${CONFIG_PATH:-.github/nsolid-release-lines.json}"
6+
NODE_RELEASE_INDEX_URL="${NODE_RELEASE_INDEX_URL:-https://nodejs.org/download/release/index.json}"
7+
NODE_LINE="${NODE_LINE:-}"
8+
ISSUE_NUMBER="${ISSUE_NUMBER:-}"
9+
GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-}"
10+
11+
if [[ -z "$NODE_LINE" ]]; then
12+
echo "Usage: NODE_LINE=22 $0" >&2
13+
exit 1
14+
fi
15+
16+
node -e '
17+
const fs = require("node:fs");
18+
const [configPath, nodeLine] = process.argv.slice(1);
19+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
20+
if (!config.nodeReleaseLines.map(String).includes(nodeLine)) {
21+
throw new Error(`Node.js v${nodeLine} is not enabled in ${configPath}`);
22+
}
23+
' "$CONFIG_PATH" "$NODE_LINE"
24+
25+
latest_node_tag() {
26+
node -e '
27+
const line = Number(process.argv[1]);
28+
const url = process.env.NODE_RELEASE_INDEX_URL;
29+
const response = await fetch(url);
30+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status}`);
31+
const releases = await response.json();
32+
const stable = releases
33+
.map((release) => release.version)
34+
.filter((version) => new RegExp(`^v${line}\\.\\d+\\.\\d+$`).test(version))
35+
.sort((a, b) => {
36+
const left = a.slice(1).split(".").map(Number);
37+
const right = b.slice(1).split(".").map(Number);
38+
for (let i = 0; i < 3; i++) {
39+
if (left[i] !== right[i]) return right[i] - left[i];
40+
}
41+
return 0;
42+
});
43+
if (stable.length === 0) throw new Error(`No stable Node.js v${line} release found`);
44+
console.log(stable[0]);
45+
' "$NODE_LINE"
46+
}
47+
48+
BASE_BRANCH="node-v${NODE_LINE}.x-nsolid-v6.x"
49+
CHANGELOG_PATH="doc/changelogs/NSOLID_CHANGELOG_V6_NODE_V${NODE_LINE}.md"
50+
NODE_TAG="${NODE_TAG:-$(latest_node_tag)}"
51+
NODE_VERSION="${NODE_TAG#v}"
52+
RELEASE_DATE="${RELEASE_DATE:-$(date -u +%F)}"
53+
CONFLICT_REPORT="nsolid-release-conflicts.md"
54+
55+
if [[ ! "$NODE_TAG" =~ ^v${NODE_LINE}\.[0-9]+\.[0-9]+$ ]]; then
56+
echo "Latest Node.js tag for v${NODE_LINE} is invalid: $NODE_TAG" >&2
57+
exit 1
58+
fi
59+
60+
comment_issue() {
61+
local body_file="$1"
62+
if [[ -n "$ISSUE_NUMBER" && -n "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]]; then
63+
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$body_file" || true
64+
fi
65+
}
66+
67+
write_conflict_report() {
68+
{
69+
echo "## Release proposal merge conflict"
70+
echo
71+
echo "- Base branch: \`$BASE_BRANCH\`"
72+
echo "- Upstream Node.js tag: \`$NODE_TAG\`"
73+
echo "- Release branch: \`$RELEASE_BRANCH\`"
74+
echo
75+
echo "The merge could not be completed automatically. No release PR was opened."
76+
echo
77+
echo "### Conflicted files"
78+
echo
79+
git diff --name-only --diff-filter=U | sed 's/^/- `/' | sed 's/$/`/'
80+
echo
81+
echo "### Git status"
82+
echo
83+
echo '```text'
84+
git status --short
85+
echo '```'
86+
} > "$CONFLICT_REPORT"
87+
}
88+
89+
git config --local user.email "${GIT_AUTHOR_EMAIL:-nsolid-bot@nodesource.com}"
90+
git config --local user.name "${GIT_AUTHOR_NAME:-N|Solid GitHub Bot}"
91+
92+
git fetch --no-tags origin "$BASE_BRANCH"
93+
git remote add nodejs https://github.com/nodejs/node.git 2>/dev/null || true
94+
git fetch --no-tags nodejs "refs/tags/${NODE_TAG}:refs/tags/${NODE_TAG}"
95+
96+
git switch --detach "origin/$BASE_BRANCH"
97+
NSOLID_VERSION="$(python3 tools/getnsolidversion.py)"
98+
RELEASE_BRANCH="node-${NODE_TAG}-nsolid-v${NSOLID_VERSION}-release"
99+
NSOLID_TAG="node-${NODE_TAG}-nsolid-v${NSOLID_VERSION}"
100+
101+
git fetch --no-tags origin "+refs/heads/${RELEASE_BRANCH}:refs/remotes/origin/${RELEASE_BRANCH}" 2>/dev/null || true
102+
if git rev-parse --verify "origin/${RELEASE_BRANCH}" >/dev/null 2>&1; then
103+
echo "Release branch origin/${RELEASE_BRANCH} already exists; nothing to do."
104+
exit 0
105+
fi
106+
107+
git switch -c "$RELEASE_BRANCH"
108+
109+
if ! git merge "$NODE_TAG" --no-edit; then
110+
write_conflict_report
111+
comment_issue "$CONFLICT_REPORT"
112+
echo "::error::Merge conflict while merging $NODE_TAG into $BASE_BRANCH"
113+
exit 2
114+
fi
115+
116+
node -e '
117+
const fs = require("node:fs");
118+
const path = "src/node_version.h";
119+
const text = fs.readFileSync(path, "utf8");
120+
const next = text.replace(
121+
/#define NSOLID_VERSION_IS_RELEASE [01]/,
122+
"#define NSOLID_VERSION_IS_RELEASE 1",
123+
);
124+
if (text === next) {
125+
throw new Error("Could not update NSOLID_VERSION_IS_RELEASE");
126+
}
127+
fs.writeFileSync(path, next);
128+
'
129+
130+
CODENAME="$(awk '/#define NODE_VERSION_IS_LTS / { gsub(/"/, "", $3); print $3; exit }' src/node_version.h)"
131+
MERGE_SHA="$(git rev-parse --short=10 HEAD)"
132+
MERGE_SUBJECT="$(git log -1 --format=%s)"
133+
MERGE_AUTHOR="$(git log -1 --format=%an)"
134+
135+
node -e '
136+
const fs = require("node:fs");
137+
const [
138+
changelogPath,
139+
releaseDate,
140+
nodeVersion,
141+
nsolidVersion,
142+
codename,
143+
mergeSha,
144+
mergeSubject,
145+
mergeAuthor,
146+
] = process.argv.slice(1);
147+
const text = fs.readFileSync(changelogPath, "utf8");
148+
const displayCodename = codename ? " \x27" + codename + "\x27" : "";
149+
const entry = `## ${releaseDate}, Version ${nodeVersion}-nsolid-v${nsolidVersion}${displayCodename}
150+
151+
### Commits
152+
153+
* \\[[\`${mergeSha}\`](https://github.com/nodesource/nsolid/commit/${mergeSha})] - ${mergeSubject} (${mergeAuthor})
154+
155+
`;
156+
fs.writeFileSync(changelogPath, entry + text);
157+
' "$CHANGELOG_PATH" "$RELEASE_DATE" "$NODE_VERSION" "$NSOLID_VERSION" "$CODENAME" "$MERGE_SHA" "$MERGE_SUBJECT" "$MERGE_AUTHOR"
158+
159+
git add src/node_version.h "$CHANGELOG_PATH"
160+
git commit -m "${RELEASE_DATE}, Version ${NODE_VERSION}-nsolid-v${NSOLID_VERSION} '${CODENAME}'"
161+
162+
git push origin "$RELEASE_BRANCH"
163+
164+
PR_BODY="nsolid-release-pr-body.md"
165+
{
166+
echo "Release PR for Node.js ${NODE_TAG} with N|Solid v${NSOLID_VERSION}."
167+
echo
168+
echo "Generated by the N|Solid release workflow."
169+
} > "$PR_BODY"
170+
171+
PR_URL="$(gh pr create \
172+
--repo "$GITHUB_REPOSITORY" \
173+
--base "$BASE_BRANCH" \
174+
--head "$RELEASE_BRANCH" \
175+
--title "${RELEASE_DATE}, Version ${NODE_VERSION}-nsolid-v${NSOLID_VERSION} '${CODENAME}'" \
176+
--body-file "$PR_BODY" \
177+
--draft)"
178+
179+
COMMENT_BODY="nsolid-release-success.md"
180+
{
181+
echo "## Release proposal created"
182+
echo
183+
echo "- PR: $PR_URL"
184+
echo "- Release branch: \`$RELEASE_BRANCH\`"
185+
echo "- Release tag: not created by automation"
186+
} > "$COMMENT_BODY"
187+
comment_issue "$COMMENT_BODY"
188+
189+
echo "$PR_URL"

0 commit comments

Comments
 (0)