Skip to content

Commit ee39be7

Browse files
committed
move inline to shared scripts (also usable from cli-release.yaml later). Add cliff-toml for controller
On-behalf-of: Gerald Morrison (SAP) <gerald.morrison@sap.com> Signed-off-by: Gerald Morrison (SAP) <gerald.morrison@sap.com>
1 parent 87c529d commit ee39be7

File tree

6 files changed

+379
-187
lines changed

6 files changed

+379
-187
lines changed

.github/scripts/create-tag.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execFileSync } from "child_process";
33
import fs from "fs";
44

55
// --------------------------
6-
// Shared helpers
6+
// Helpers
77
// --------------------------
88

99
/**

.github/scripts/publish-final-release.js

Lines changed: 132 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,16 @@
22
import fs from "fs";
33
import path from "path";
44

5-
// --------------------------
6-
// GitHub Actions entrypoint
7-
// --------------------------
8-
/** @param {import('@actions/github-script').AsyncFunctionArguments} args */
9-
export default async function publishFinalRelease({ github, context, core }) {
10-
const {
11-
FINAL_TAG: finalTag,
12-
FINAL_VERSION: finalVersion,
13-
RC_TAG: rcTag,
14-
IMAGE_REPO: imageRepo,
15-
CHART_REPO: chartRepo,
16-
IMAGE_DIGEST: imageDigest,
17-
CHART_DIR: chartDir,
18-
NOTES_FILE: notesFile,
19-
SET_LATEST: setLatest,
20-
HIGHEST_FINAL_VERSION: highestFinalVersion,
21-
} = process.env;
22-
23-
if (!finalTag || !finalVersion || !rcTag || !chartDir || !notesFile) {
24-
core.setFailed("Missing required environment variables");
25-
return;
26-
}
27-
28-
const isLatest = setLatest === "true";
29-
const notes = prepareReleaseNotes(notesFile, rcTag, finalTag);
30-
const release = await getOrCreateRelease(github, context, {
31-
finalTag,
32-
finalVersion,
33-
notes,
34-
isLatest,
35-
});
36-
await uploadChartAssets(github, context, core, release.id, chartDir);
37-
await writeSummary(core, {
38-
finalTag,
39-
rcTag,
40-
finalVersion,
41-
imageRepo,
42-
chartRepo,
43-
imageDigest,
44-
isLatest,
45-
highestFinalVersion,
46-
releaseUrl: release.html_url,
47-
});
48-
}
49-
505
// --------------------------
516
// Helpers
527
// --------------------------
538

549
/**
55-
* Read the RC changelog and rewrite the header for the final release.
56-
* Falls back to a simple "Promoted from …" message if the file is missing.
10+
* Promote changelog from RC: Read RC changelog and rewrite header for the final release.
11+
* Falls back to a simple "Promoted from …" message if file is missing.
12+
*
13+
* The header pattern is derived dynamically from the RC tag, so it works for
14+
* any component prefix (cli/v…, kubernetes/controller/v…, etc.).
5715
*
5816
* @param {string} notesFile - Path to the changelog markdown file.
5917
* @param {string} rcTag - The RC tag being promoted (e.g. "kubernetes/controller/v0.1.0-rc.1").
@@ -73,9 +31,22 @@ export function prepareReleaseNotes(notesFile, rcTag, finalTag) {
7331
}
7432

7533
const today = new Date().toISOString().split("T")[0];
34+
35+
// Build a regex that matches the RC header line produced by git-cliff.
36+
// Example header: "## [kubernetes/controller/v0.1.0-rc.1] - 2026-03-08"
37+
// We escape the RC tag for safe regex usage, then match the full line.
38+
const escapedRcTag = rcTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39+
const rcHeaderPattern = new RegExp(`^## \\[${escapedRcTag}\\].*$`, "m");
40+
41+
if (!rcHeaderPattern.test(notes)) {
42+
// If no RC header found, prepend a final header instead of failing.
43+
// This handles edge cases like manually edited release notes.
44+
return `## [${finalTag}] - promoted from [${rcTag}] on ${today}\n\n${notes}`;
45+
}
46+
7647
return notes.replace(
77-
/^\[([^\]]+)\]\s*-\s*[\d-]+/,
78-
`[${finalTag}] - promoted from [${rcTag}] on ${today}`,
48+
rcHeaderPattern,
49+
`## [${finalTag}] - promoted from [${rcTag}] on ${today}`,
7950
);
8051
}
8152

@@ -87,14 +58,16 @@ export function prepareReleaseNotes(notesFile, rcTag, finalTag) {
8758
* @param {object} opts
8859
* @param {string} opts.finalTag
8960
* @param {string} opts.finalVersion
61+
* @param {string} opts.componentName
9062
* @param {string} opts.notes
9163
* @param {boolean} opts.isLatest
9264
* @returns {Promise<{id: number, html_url: string}>}
9365
*/
9466
export async function getOrCreateRelease(github, context, opts) {
95-
const { finalTag, finalVersion, notes, isLatest } = opts;
67+
const { finalTag, finalVersion, componentName, notes, isLatest } = opts;
9668
const repo = { owner: context.repo.owner, repo: context.repo.repo };
9769
const makeLatest = isLatest ? "true" : "false";
70+
const releaseName = `${componentName} ${finalVersion}`;
9871

9972
try {
10073
const existing = await github.rest.repos.getReleaseByTag({
@@ -105,7 +78,7 @@ export async function getOrCreateRelease(github, context, opts) {
10578
...repo,
10679
release_id: existing.data.id,
10780
tag_name: finalTag,
108-
name: `Controller ${finalVersion}`,
81+
name: releaseName,
10982
body: notes,
11083
prerelease: false,
11184
make_latest: makeLatest,
@@ -116,7 +89,7 @@ export async function getOrCreateRelease(github, context, opts) {
11689
const created = await github.rest.repos.createRelease({
11790
...repo,
11891
tag_name: finalTag,
119-
name: `Controller ${finalVersion}`,
92+
name: releaseName,
12093
body: notes,
12194
prerelease: false,
12295
make_latest: makeLatest,
@@ -126,15 +99,16 @@ export async function getOrCreateRelease(github, context, opts) {
12699
}
127100

128101
/**
129-
* Upload .tgz chart files as release assets, replacing duplicates.
102+
* Upload all files from assets directory as release assets, replacing duplicates.
130103
*
131104
* @param {object} github - Octokit instance.
132105
* @param {object} context - GitHub Actions context.
133106
* @param {object} core - GitHub Actions core module.
134107
* @param {number} releaseId - The release to attach assets to.
135-
* @param {string} chartDir - Directory containing chart .tgz files.
108+
* @param {string} assetsDir - Directory containing files to upload.
109+
* @returns {Promise<number>} Number of uploaded files.
136110
*/
137-
export async function uploadChartAssets(github, context, core, releaseId, chartDir) {
111+
export async function uploadAssets(github, context, core, releaseId, assetsDir) {
138112
const repo = { owner: context.repo.owner, repo: context.repo.repo };
139113
const existing = (
140114
await github.rest.repos.listReleaseAssets({
@@ -144,7 +118,11 @@ export async function uploadChartAssets(github, context, core, releaseId, chartD
144118
})
145119
).data;
146120

147-
const files = fs.readdirSync(chartDir).filter((f) => f.endsWith(".tgz"));
121+
const files = fs.readdirSync(assetsDir).filter((f) => {
122+
const stat = fs.statSync(path.join(assetsDir, f));
123+
return stat.isFile();
124+
});
125+
148126
for (const file of files) {
149127
const dup = existing.find((a) => a.name === file);
150128
if (dup) {
@@ -154,18 +132,21 @@ export async function uploadChartAssets(github, context, core, releaseId, chartD
154132
asset_id: dup.id,
155133
});
156134
}
157-
const data = fs.readFileSync(path.join(chartDir, file));
135+
const data = fs.readFileSync(path.join(assetsDir, file));
158136
await github.rest.repos.uploadReleaseAsset({
159137
...repo,
160138
release_id: releaseId,
161139
name: file,
162140
data,
163141
headers: {
164-
"content-type": "application/gzip",
142+
"content-type": "application/octet-stream",
165143
"content-length": data.length,
166144
},
167145
});
146+
core.info(`Uploaded: ${file}`);
168147
}
148+
149+
return files.length;
169150
}
170151

171152
/**
@@ -179,38 +160,112 @@ export async function writeSummary(core, data) {
179160
finalTag,
180161
rcTag,
181162
finalVersion,
163+
componentName,
182164
imageRepo,
183165
chartRepo,
184166
imageDigest,
185167
isLatest,
186168
highestFinalVersion,
169+
uploadedCount,
187170
releaseUrl,
188171
} = data;
189172

190-
const imageTags = isLatest
191-
? `${imageRepo}:${finalVersion}, ${imageRepo}:latest`
192-
: `${imageRepo}:${finalVersion}`;
173+
const rows = [
174+
[
175+
{ data: "Field", header: true },
176+
{ data: "Value", header: true },
177+
],
178+
["Component", componentName],
179+
["Final Tag", finalTag],
180+
["Promoted from RC", rcTag],
181+
["Highest Final Version", highestFinalVersion || "(none)"],
182+
["GitHub Latest", isLatest ? "Yes" : "No (older version)"],
183+
["Uploaded Assets", String(uploadedCount)],
184+
];
185+
186+
// Add optional OCI/Helm fields when present
187+
if (imageRepo) {
188+
const imageTags = isLatest
189+
? `${imageRepo}:${finalVersion}, ${imageRepo}:latest`
190+
: `${imageRepo}:${finalVersion}`;
191+
rows.push(["Image Tags", imageTags]);
192+
}
193+
if (imageDigest) {
194+
rows.push(["Image Digest", imageDigest.substring(0, 19) + "..."]);
195+
}
196+
if (chartRepo) {
197+
rows.push(["Helm Chart", `${chartRepo}:${finalVersion}`]);
198+
}
193199

194200
await core.summary
195201
.addHeading("Final Release Published")
196-
.addTable([
197-
[
198-
{ data: "Field", header: true },
199-
{ data: "Value", header: true },
200-
],
201-
["Final Tag", finalTag],
202-
["Promoted from RC", rcTag],
203-
["Highest Final Version", highestFinalVersion || "(none)"],
204-
["Image Tags", imageTags],
205-
["Helm Chart", `${chartRepo}:${finalVersion}`],
206-
[
207-
"Image Digest",
208-
imageDigest ? imageDigest.substring(0, 19) + "..." : "N/A",
209-
],
210-
["GitHub Latest", isLatest ? "Yes" : "No (older version)"],
211-
])
202+
.addTable(rows)
212203
.addEOL()
213204
.addLink("View Release", releaseUrl)
214205
.addEOL()
215206
.write();
216207
}
208+
209+
// --------------------------
210+
// GitHub Actions entrypoint
211+
// --------------------------
212+
213+
/**
214+
* Publish a final GitHub release by promoting an RC.
215+
*
216+
* Required env vars:
217+
* FINAL_TAG, FINAL_VERSION, RC_TAG, COMPONENT_NAME, ASSETS_DIR, NOTES_FILE,
218+
* SET_LATEST, HIGHEST_FINAL_VERSION
219+
*
220+
* Optional env vars (for summary):
221+
* IMAGE_REPO, IMAGE_DIGEST, CHART_REPO
222+
*
223+
* @param {import('@actions/github-script').AsyncFunctionArguments} args
224+
*/
225+
export default async function publishFinalRelease({ github, context, core }) {
226+
const {
227+
FINAL_TAG: finalTag,
228+
FINAL_VERSION: finalVersion,
229+
RC_TAG: rcTag,
230+
COMPONENT_NAME: componentName,
231+
ASSETS_DIR: assetsDir,
232+
NOTES_FILE: notesFile,
233+
SET_LATEST: setLatest,
234+
HIGHEST_FINAL_VERSION: highestFinalVersion,
235+
// Optional — only used in summary
236+
IMAGE_REPO: imageRepo,
237+
IMAGE_DIGEST: imageDigest,
238+
CHART_REPO: chartRepo,
239+
} = process.env;
240+
241+
if (!finalTag || !finalVersion || !rcTag || !componentName || !assetsDir || !notesFile) {
242+
core.setFailed(
243+
"Missing required env vars: FINAL_TAG, FINAL_VERSION, RC_TAG, COMPONENT_NAME, ASSETS_DIR, NOTES_FILE",
244+
);
245+
return;
246+
}
247+
248+
const isLatest = setLatest === "true";
249+
const notes = prepareReleaseNotes(notesFile, rcTag, finalTag);
250+
const release = await getOrCreateRelease(github, context, {
251+
finalTag,
252+
finalVersion,
253+
componentName,
254+
notes,
255+
isLatest,
256+
});
257+
const uploadedCount = await uploadAssets(github, context, core, release.id, assetsDir);
258+
await writeSummary(core, {
259+
finalTag,
260+
rcTag,
261+
finalVersion,
262+
componentName,
263+
imageRepo,
264+
chartRepo,
265+
imageDigest,
266+
isLatest,
267+
highestFinalVersion,
268+
uploadedCount,
269+
releaseUrl: release.html_url,
270+
});
271+
}

0 commit comments

Comments
 (0)