@catladder/cli 5.0.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/cli/src/release/changesetsReleaseJob.js +3 -0
- package/dist/apps/cli/src/release/changesetsReleaseJob.js.map +1 -1
- package/dist/apps/cli/src/release/releaseEntry.d.ts +12 -0
- package/dist/apps/cli/src/release/releaseEntry.js +71 -0
- package/dist/apps/cli/src/release/releaseEntry.js.map +1 -0
- package/dist/bundles/catci/index.js +4 -4
- package/dist/bundles/runner-images/semantic-release/Dockerfile +1 -0
- package/dist/bundles/runner-images/semantic-release/scripts/semanticRelease +14 -4
- package/dist/bundles/skills/catladder-releases/SKILL.md +16 -2
- package/dist/runner-images/semantic-release/Dockerfile +1 -0
- package/dist/runner-images/semantic-release/scripts/semanticRelease +14 -4
- package/dist/skills/catladder-releases/SKILL.md +16 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/release/__tests__/releaseEntry.test.ts +88 -0
- package/src/release/changesetsReleaseJob.ts +3 -0
- package/src/release/releaseEntry.ts +86 -0
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createReleaseEntry } from "../releaseEntry";
|
|
3
|
+
|
|
4
|
+
const gitlabEnv = {
|
|
5
|
+
GITHUB_ACTIONS: undefined,
|
|
6
|
+
GL_TOKEN: "glpat-token",
|
|
7
|
+
CI_API_V4_URL: "https://git.example.com/api/v4",
|
|
8
|
+
CI_PROJECT_ID: "42",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const githubEnv = {
|
|
12
|
+
GITHUB_ACTIONS: "true",
|
|
13
|
+
GITHUB_TOKEN: "gh-token",
|
|
14
|
+
GITHUB_REPOSITORY: "panter/catladder",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const stubEnv = (env: Record<string, string | undefined>) => {
|
|
18
|
+
Object.entries(env).forEach(([key, value]) => vi.stubEnv(key, value as any));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const stubFetch = (response: Partial<Response> = {}) => {
|
|
22
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
23
|
+
ok: true,
|
|
24
|
+
status: 201,
|
|
25
|
+
text: async () => "",
|
|
26
|
+
...response,
|
|
27
|
+
});
|
|
28
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
29
|
+
return fetchMock;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.unstubAllEnvs();
|
|
34
|
+
vi.unstubAllGlobals();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("createReleaseEntry", () => {
|
|
38
|
+
it("creates the gitlab release for the pushed tag", async () => {
|
|
39
|
+
stubEnv(gitlabEnv);
|
|
40
|
+
const fetchMock = stubFetch();
|
|
41
|
+
|
|
42
|
+
await createReleaseEntry("v1.2.0", "- Add the export endpoint");
|
|
43
|
+
|
|
44
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
45
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
46
|
+
expect(url).toBe("https://git.example.com/api/v4/projects/42/releases");
|
|
47
|
+
expect(init.method).toBe("POST");
|
|
48
|
+
expect(init.headers["PRIVATE-TOKEN"]).toBe("glpat-token");
|
|
49
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
50
|
+
tag_name: "v1.2.0",
|
|
51
|
+
description: "- Add the export endpoint",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("creates the github release for the pushed tag", async () => {
|
|
56
|
+
stubEnv(githubEnv);
|
|
57
|
+
const fetchMock = stubFetch();
|
|
58
|
+
|
|
59
|
+
await createReleaseEntry("v1.2.0", "- Add the export endpoint");
|
|
60
|
+
|
|
61
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
62
|
+
expect(url).toBe("https://api.github.com/repos/panter/catladder/releases");
|
|
63
|
+
expect(init.headers.authorization).toBe("Bearer gh-token");
|
|
64
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
65
|
+
tag_name: "v1.2.0",
|
|
66
|
+
name: "v1.2.0",
|
|
67
|
+
body: "- Add the export endpoint",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not throw when the api call fails — the tag is already pushed", async () => {
|
|
72
|
+
stubEnv(gitlabEnv);
|
|
73
|
+
stubFetch({ ok: false, status: 403, text: async () => "forbidden" });
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
createReleaseEntry("v1.2.0", "- notes"),
|
|
77
|
+
).resolves.toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("skips without api context instead of failing the release", async () => {
|
|
81
|
+
stubEnv({ ...gitlabEnv, GL_TOKEN: undefined });
|
|
82
|
+
const fetchMock = stubFetch();
|
|
83
|
+
|
|
84
|
+
await createReleaseEntry("v1.2.0", "- notes");
|
|
85
|
+
|
|
86
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
git,
|
|
24
24
|
gitWithEnv,
|
|
25
25
|
} from "./releaseGit";
|
|
26
|
+
import { createReleaseEntry } from "./releaseEntry";
|
|
26
27
|
import { appendStepSummary } from "./stepSummary";
|
|
27
28
|
|
|
28
29
|
const CHANGESET_DIR = ".changeset";
|
|
@@ -276,6 +277,8 @@ export const changesetsReleaseJob = async () => {
|
|
|
276
277
|
await pushCommitAndTag(tag);
|
|
277
278
|
console.log(`released ${tag}`);
|
|
278
279
|
appendStepSummary(`🚀 **Released ${tag}**\n\n${entries}`);
|
|
280
|
+
// the tag alone creates no entry on the releases page of the git host
|
|
281
|
+
await createReleaseEntry(tag, entries);
|
|
279
282
|
|
|
280
283
|
if (process.env.GITHUB_ACTIONS === "true") {
|
|
281
284
|
await dispatchTaggedReleaseWorkflow(tag);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* the release entry on the git host — gitlab's `/-/releases`, github's
|
|
3
|
+
* `/releases`. Pushing a tag does NOT create one: on the
|
|
4
|
+
* semantic-release path `@semantic-release/gitlab` used to do it, the
|
|
5
|
+
* changesets path creates it here (on both backends).
|
|
6
|
+
*
|
|
7
|
+
* Never fatal: when this runs, the release commit and tag are already
|
|
8
|
+
* pushed and the taggedRelease pipeline is on its way — a failing api
|
|
9
|
+
* call must not turn a done release into a red job (and a rerun would
|
|
10
|
+
* fail on the existing tag).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const post = async (
|
|
14
|
+
url: string,
|
|
15
|
+
headers: Record<string, string>,
|
|
16
|
+
body: unknown,
|
|
17
|
+
) => {
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "content-type": "application/json", ...headers },
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`POST ${url} failed: ${response.status} ${await response.text()}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* GL_TOKEN is the project access token the release push already used,
|
|
32
|
+
* so its api scope is available here too
|
|
33
|
+
*/
|
|
34
|
+
const createGitlabReleaseEntry = async (tag: string, notes: string) => {
|
|
35
|
+
const token = process.env.GL_TOKEN;
|
|
36
|
+
const apiUrl = process.env.CI_API_V4_URL;
|
|
37
|
+
const projectId = process.env.CI_PROJECT_ID;
|
|
38
|
+
if (!token || !apiUrl || !projectId) {
|
|
39
|
+
console.warn(
|
|
40
|
+
"no GL_TOKEN / gitlab api context — skipping the release entry (the tag is pushed)",
|
|
41
|
+
);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
await post(
|
|
45
|
+
`${apiUrl}/projects/${projectId}/releases`,
|
|
46
|
+
{ "PRIVATE-TOKEN": token },
|
|
47
|
+
// name defaults to the tag on gitlab
|
|
48
|
+
{ tag_name: tag, description: notes },
|
|
49
|
+
);
|
|
50
|
+
console.log(`created the gitlab release entry for ${tag}`);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** the workflow token, granted `contents: write` by the release job */
|
|
54
|
+
const createGithubReleaseEntry = async (tag: string, notes: string) => {
|
|
55
|
+
const token = process.env.GITHUB_TOKEN;
|
|
56
|
+
const apiUrl = process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
57
|
+
const repository = process.env.GITHUB_REPOSITORY;
|
|
58
|
+
if (!token || !repository) {
|
|
59
|
+
console.warn(
|
|
60
|
+
"no GITHUB_TOKEN / repository context — skipping the release entry (the tag is pushed)",
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await post(
|
|
65
|
+
`${apiUrl}/repos/${repository}/releases`,
|
|
66
|
+
{ authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
67
|
+
{ tag_name: tag, name: tag, body: notes },
|
|
68
|
+
);
|
|
69
|
+
console.log(`created the github release entry for ${tag}`);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const createReleaseEntry = async (tag: string, notes: string) => {
|
|
73
|
+
try {
|
|
74
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
75
|
+
await createGithubReleaseEntry(tag, notes);
|
|
76
|
+
} else {
|
|
77
|
+
await createGitlabReleaseEntry(tag, notes);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`⚠️ could not create the release entry for ${tag}: ${error}\n` +
|
|
82
|
+
"the release itself is done (commit, tag and changelog are pushed) — " +
|
|
83
|
+
"create the entry by hand if you need it",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
};
|