ci(release): skip if git tag already exists (#26)

This commit is contained in:
Sebastian Poxhofer 2025-03-01 01:08:28 +01:00 committed by GitHub
parent 3bcdef40e2
commit 66adc32ff7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 45 deletions

View file

@ -31,21 +31,26 @@ describe("push-charts", () => {
describe("main", () => { describe("main", () => {
test("should push single chart", async () => { test("should push single chart", async () => {
fs.readdir.mockResolvedValue([myChartDirentMock]); fs.readdir.mockResolvedValue([myChartDirentMock]);
await main(["node", "push-chart.ts", "my-chart"]); // mock `git tag` to return empty string
expect(child_process.exec).toHaveBeenCalledWith( child_process.execSync.mockReturnValue(Buffer.from(""));
await main(["node", "push-chart.ts", "charts/my-chart"]);
expect(child_process.execSync).toHaveBeenNthCalledWith(
2,
`helm push ".cr-release-packages/my-chart-1.0.0.tgz" "oci://ghcr.io/lore/ipsum"`, `helm push ".cr-release-packages/my-chart-1.0.0.tgz" "oci://ghcr.io/lore/ipsum"`,
); );
}); });
test("should push multiple charts", async () => { test("should push multiple charts", async () => {
fs.readdir.mockResolvedValue([myChartDirentMock, fooChartDirentMock]); fs.readdir.mockResolvedValue([myChartDirentMock, fooChartDirentMock]);
await main(["node", "push-chart.ts", "my-chart,foo"]); child_process.execSync.mockReturnValue(Buffer.from(""));
expect(child_process.exec).toHaveBeenNthCalledWith(
1, await main(["node", "push-chart.ts", "charts/my-chart,charts/foo"]);
expect(child_process.execSync).toHaveBeenNthCalledWith(
2,
`helm push ".cr-release-packages/my-chart-1.0.0.tgz" "oci://ghcr.io/lore/ipsum"`, `helm push ".cr-release-packages/my-chart-1.0.0.tgz" "oci://ghcr.io/lore/ipsum"`,
); );
expect(child_process.exec).toHaveBeenNthCalledWith( expect(child_process.execSync).toHaveBeenNthCalledWith(
2, 3,
`helm push ".cr-release-packages/foo-2.0.0.tgz" "oci://ghcr.io/lore/ipsum"`, `helm push ".cr-release-packages/foo-2.0.0.tgz" "oci://ghcr.io/lore/ipsum"`,
); );
}); });
@ -54,24 +59,56 @@ describe("push-charts", () => {
describe("getChangedCharts", () => { describe("getChangedCharts", () => {
test("should return changed chart if only one exists", async () => { test("should return changed chart if only one exists", async () => {
fs.readdir.mockResolvedValue([myChartDirentMock]); fs.readdir.mockResolvedValue([myChartDirentMock]);
child_process.execSync.mockReturnValue(Buffer.from(""));
await expect(getChangedChartsArchives(["my-chart"])).resolves.toEqual([ await expect(getChangedChartsArchives(["my-chart"])).resolves.toEqual([
".cr-release-packages/my-chart-1.0.0.tgz", {
path: ".cr-release-packages/my-chart-1.0.0.tgz",
fileName: "my-chart-1.0.0",
},
]); ]);
}); });
test("should return changed chart if multiple exists", async () => { test("should return multiple", async () => {
fs.readdir.mockResolvedValue([fooChartDirentMock, myChartDirentMock]); fs.readdir.mockResolvedValue([fooChartDirentMock, myChartDirentMock]);
child_process.execSync.mockReturnValue(Buffer.from(""));
await expect(getChangedChartsArchives(["foo"])).resolves.toEqual([ await expect(getChangedChartsArchives(["foo"])).resolves.toEqual([
".cr-release-packages/foo-2.0.0.tgz", {
path: ".cr-release-packages/foo-2.0.0.tgz",
fileName: "foo-2.0.0",
},
{
path: ".cr-release-packages/my-chart-1.0.0.tgz",
fileName: "my-chart-1.0.0",
},
]); ]);
}); });
test("should return multiple changed charts if multiple exists and changed", async () => { test("should return multiple changed charts if multiple exists and changed", async () => {
fs.readdir.mockResolvedValue([fooChartDirentMock, myChartDirentMock]); fs.readdir.mockResolvedValue([fooChartDirentMock, myChartDirentMock]);
child_process.execSync.mockReturnValue(Buffer.from(""));
await expect(getChangedChartsArchives(["my-chart,foo"])).resolves.toEqual( await expect(getChangedChartsArchives(["my-chart,foo"])).resolves.toEqual(
[ [
".cr-release-packages/my-chart-1.0.0.tgz", {
".cr-release-packages/foo-2.0.0.tgz", path: ".cr-release-packages/foo-2.0.0.tgz",
fileName: "foo-2.0.0",
},
{
path: ".cr-release-packages/my-chart-1.0.0.tgz",
fileName: "my-chart-1.0.0",
},
],
);
});
test("should skip already existing image", async () => {
fs.readdir.mockResolvedValue([fooChartDirentMock, myChartDirentMock]);
child_process.execSync.mockReturnValue(Buffer.from("my-chart-1.0.0\n"));
await expect(getChangedChartsArchives(["my-chart,foo"])).resolves.toEqual(
[
{
path: ".cr-release-packages/foo-2.0.0.tgz",
fileName: "foo-2.0.0",
},
], ],
); );
}); });

View file

@ -1,73 +1,73 @@
import * as fs from "node:fs/promises"; import * as fs from "node:fs/promises";
import * as process from "node:process"; import * as process from "node:process";
import * as path from "node:path"; import * as path from "node:path";
import { exec } from "node:child_process"; import { execSync } from "node:child_process";
const CR_RELEASE_PACKAGE_PATH = ".cr-release-packages"; const CR_RELEASE_PACKAGE_PATH = ".cr-release-packages";
interface Archive {
path: string;
fileName: string;
}
export async function main(rawArgs: string[]) { export async function main(rawArgs: string[]) {
// remove file path node and script name // remove file path node and script name
const args = rawArgs.slice(2); const args = rawArgs.slice(2);
const archives = await getChangedChartsArchives(args); const archives = await getChangedChartsArchives(args);
console.log("Pushing charts:"); console.log("Pushing charts");
for (const archive of archives) { for (const archive of archives) {
console.log(`- ${archive}`); const cmd = `helm push "${archive.path}" "oci://ghcr.io/${process.env.GITHUB_REPOSITORY}"`;
exec( console.log(cmd);
`helm push "${archive}" "oci://ghcr.io/${process.env.GITHUB_REPOSITORY}"`, console.log(execSync(cmd).toString());
);
} }
} }
export async function getChangedChartsArchives( export async function getChangedChartsArchives(
args: string[], args: string[],
): Promise<string[]> { ): Promise<Archive[]> {
if (args.length === 0) {
console.log("Usage: push-charts <chart-list>");
process.exit(1);
}
const changedCharts = parseChartList(args[0]);
const chartArchives = await getChartArchives(); const chartArchives = await getChartArchives();
const tags = getGitTags();
// translate chart names to chart archives // translate chart names to chart archives
const changedChartArchives: string[] = []; const changedChartArchives: Archive[] = [];
for (const chart of changedCharts) { for (const [, value] of Object.entries(chartArchives)) {
if (chartArchives[chart]) { if (tags.includes(value.fileName)) {
const archive = chartArchives[chart]; // the chart has already pushed
changedChartArchives.push(archive); console.log(
`Skipping ${value.path}. Tag ${value.fileName} already exists`,
);
continue;
} }
changedChartArchives.push(value);
} }
return changedChartArchives; return changedChartArchives;
} }
function parseChartList(chartListArg: string): string[] { async function getChartArchives(): Promise<Record<string, Archive>> {
const parsed: string[] = [];
for (const chartPath of chartListArg.split(",")) {
const name = path.parse(chartPath).name;
parsed.push(name);
}
return parsed;
}
async function getChartArchives(): Promise<Record<string, string>> {
const archiveList = await fs.readdir(CR_RELEASE_PACKAGE_PATH, { const archiveList = await fs.readdir(CR_RELEASE_PACKAGE_PATH, {
withFileTypes: true, withFileTypes: true,
}); });
const chartNames: Record<string, string> = {}; const chartNames: Record<string, Archive> = {};
for (const dirent of archiveList) { for (const dirent of archiveList) {
if (dirent.isFile() && dirent.name.endsWith(".tgz")) { if (dirent.isFile() && dirent.name.endsWith(".tgz")) {
const parsed = /^(?<name>[\w-]+?)-\d/.exec(dirent.name); const parsed = /^(?<name>[\w-]+?)-\d/.exec(dirent.name);
// immich --> .cr-release-packages/immich-1.0.0.tgz // immich --> .cr-release-packages/immich-1.0.0.tgz
chartNames[parsed.groups.name] = path.join( chartNames[parsed.groups.name] = {
CR_RELEASE_PACKAGE_PATH, path: path.join(CR_RELEASE_PACKAGE_PATH, dirent.name),
dirent.name, fileName: path.parse(dirent.name).name,
); };
} }
} }
return chartNames; return chartNames;
} }
export function getGitTags(): string[] {
const result = execSync("git tag");
const tagString = result.toString();
return tagString.split("\n");
}
// do not run main if this script is being tested // do not run main if this script is being tested
if (!process.env.VITEST_WORKER_ID) { if (!process.env.VITEST_WORKER_ID) {
main(process.argv); main(process.argv);