@catladder/cli 5.1.1 → 5.1.3

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.
Files changed (43) hide show
  1. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.d.ts +30 -0
  2. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.js +89 -0
  3. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.js.map +1 -0
  4. package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js +2 -0
  5. package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -1
  6. package/dist/apps/cli/src/apps/cli/commands/project/githubMergeGating.d.ts +4 -1
  7. package/dist/apps/cli/src/apps/cli/commands/project/githubMergeGating.js +4 -1
  8. package/dist/apps/cli/src/apps/cli/commands/project/githubMergeGating.js.map +1 -1
  9. package/dist/apps/cli/src/catci.js +13 -0
  10. package/dist/apps/cli/src/catci.js.map +1 -1
  11. package/dist/apps/cli/src/release/changesetsReleaseJob.js +20 -42
  12. package/dist/apps/cli/src/release/changesetsReleaseJob.js.map +1 -1
  13. package/dist/apps/cli/src/release/githubDeployKey.d.ts +36 -0
  14. package/dist/apps/cli/src/release/githubDeployKey.js +126 -0
  15. package/dist/apps/cli/src/release/githubDeployKey.js.map +1 -0
  16. package/dist/bundles/catci/index.js +5 -5
  17. package/dist/bundles/catenv/index.js +31 -7
  18. package/dist/bundles/cli/index.js +133 -8
  19. package/dist/bundles/runner-images/semantic-release/Dockerfile +3 -1
  20. package/dist/bundles/runner-images/semantic-release/scripts/semanticRelease +37 -4
  21. package/dist/bundles/skills/catladder-migrate-ci-backend/SKILL.md +15 -6
  22. package/dist/bundles/skills/catladder-pipelines/SKILL.md +3 -2
  23. package/dist/bundles/skills/catladder-releases/SKILL.md +14 -0
  24. package/dist/packages/pipeline/src/backends/github/GithubBackend.js +12 -3
  25. package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
  26. package/dist/packages/pipeline/src/catci/shippedCatci.d.ts +8 -0
  27. package/dist/packages/pipeline/src/catci/shippedCatci.js +19 -4
  28. package/dist/packages/pipeline/src/catci/shippedCatci.js.map +1 -1
  29. package/dist/runner-images/semantic-release/Dockerfile +3 -1
  30. package/dist/runner-images/semantic-release/scripts/semanticRelease +37 -4
  31. package/dist/skills/catladder-migrate-ci-backend/SKILL.md +15 -6
  32. package/dist/skills/catladder-pipelines/SKILL.md +3 -2
  33. package/dist/skills/catladder-releases/SKILL.md +14 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +1 -1
  36. package/src/apps/cli/commands/project/__tests__/checkGithubReleaseWorkflow.test.ts +89 -0
  37. package/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.ts +125 -0
  38. package/src/apps/cli/commands/project/doctor/index.ts +2 -0
  39. package/src/apps/cli/commands/project/githubMergeGating.ts +4 -1
  40. package/src/catci.ts +15 -0
  41. package/src/release/__tests__/githubDeployKey.test.ts +148 -0
  42. package/src/release/changesetsReleaseJob.ts +28 -57
  43. package/src/release/githubDeployKey.ts +141 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * the github release deploy key — how a release push gets past the
3
+ * merge-gating ruleset.
4
+ *
5
+ * The ruleset `project setup` creates requires the `catladder ✅` check
6
+ * on the default branch, which a fresh release commit can never carry.
7
+ * Only a deploy key can bypass the ruleset (github deliberately never
8
+ * lets the workflow token bypass one), so setup provisions a write
9
+ * deploy key and stores its private half as the CATLADDER_RELEASE_KEY
10
+ * actions secret, which the generated release jobs pass into the job
11
+ * env. Both release methods push with it:
12
+ *
13
+ * - changesets: catci makes the push itself (pushWithDeployKey)
14
+ * - semantic-release: `@semantic-release/git` and semantic-release
15
+ * itself push, to the repository url they are given. catci prepares
16
+ * the ssh setup (configureDeployKeyRemote) and the runner script
17
+ * passes the ssh url as `--repository-url`. That flag is needed —
18
+ * semantic-release derives the url from package.json's `repository`
19
+ * field before it looks at `origin`, and actions/checkout leaves an
20
+ * https origin authenticated with the workflow token behind, which
21
+ * the ruleset rejects (GH013).
22
+ *
23
+ * Without the secret both methods push with the workflow token, which
24
+ * works on repositories without merge gating.
25
+ */
26
+ import { mkdtemp, rm, writeFile } from "fs/promises";
27
+ import { tmpdir } from "os";
28
+ import { join } from "path";
29
+ import { git, gitWithEnv } from "./releaseGit";
30
+
31
+ export const RELEASE_DEPLOY_KEY_ENV = "CATLADDER_RELEASE_KEY";
32
+
33
+ /** title of the deploy key `project setup` registers (see githubMergeGating) */
34
+ const RELEASE_DEPLOY_KEY_TITLE = "catladder release";
35
+
36
+ const requireEnv = (name: string): string => {
37
+ const value = process.env[name];
38
+ if (!value) {
39
+ throw new Error(`${name} is not set — cannot push the release`);
40
+ }
41
+ return value;
42
+ };
43
+
44
+ /** the private key from the job env, or null when not provisioned */
45
+ export const getReleaseDeployKey = (): string | null =>
46
+ process.env[RELEASE_DEPLOY_KEY_ENV] || null;
47
+
48
+ /** the ssh url of the repository, from the github-provided env */
49
+ export const deployKeyRemoteUrl = (): string => {
50
+ const serverUrl = process.env.GITHUB_SERVER_URL ?? "https://github.com";
51
+ const host = new URL(serverUrl).host;
52
+ const repository = requireEnv("GITHUB_REPOSITORY");
53
+ return `git@${host}:${repository}.git`;
54
+ };
55
+
56
+ /** the ssh command using exactly the deploy key (GIT_SSH_COMMAND / core.sshCommand) */
57
+ export const deployKeySshCommand = (keyFile: string): string =>
58
+ `ssh -i ${keyFile} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`;
59
+
60
+ /**
61
+ * writes the private key into a fresh temp dir and returns the file
62
+ * path. A key file without a trailing newline is rejected by openssh,
63
+ * and the secret may come without one.
64
+ */
65
+ export const writeDeployKeyFile = async (privateKey: string) => {
66
+ const dir = await mkdtemp(join(tmpdir(), "release-key-"));
67
+ const keyFile = join(dir, "id_ed25519");
68
+ await writeFile(
69
+ keyFile,
70
+ privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`,
71
+ { mode: 0o600 },
72
+ );
73
+ return { dir, keyFile };
74
+ };
75
+
76
+ /**
77
+ * pushes the refs over ssh with the release deploy key — the changesets
78
+ * release path (catci pushes itself)
79
+ */
80
+ export const pushWithDeployKey = async (privateKey: string, refs: string[]) => {
81
+ const { dir, keyFile } = await writeDeployKeyFile(privateKey);
82
+ try {
83
+ await gitWithEnv(
84
+ { GIT_SSH_COMMAND: deployKeySshCommand(keyFile) },
85
+ "push",
86
+ "--atomic",
87
+ deployKeyRemoteUrl(),
88
+ ...refs,
89
+ );
90
+ } finally {
91
+ await rm(dir, { recursive: true, force: true });
92
+ }
93
+ };
94
+
95
+ /**
96
+ * the semantic-release release path: prepares the checkout so that git
97
+ * pushes to the ssh url of the repository authenticate with the release
98
+ * deploy key, verifies the key actually reaches the repository, and
99
+ * returns the ssh url (the runner script passes it to semantic-release
100
+ * as `--repository-url`). Nothing is cleaned up on purpose — the key
101
+ * file has to outlive this process, semantic-release runs afterwards
102
+ * in the same (ephemeral) job container.
103
+ */
104
+ export const configureDeployKeyRemote = async (
105
+ privateKey: string,
106
+ ): Promise<string> => {
107
+ const remote = deployKeyRemoteUrl();
108
+ const { keyFile } = await writeDeployKeyFile(privateKey);
109
+ // the repo config, not the env: every git process semantic-release
110
+ // and its plugins spawn picks it up
111
+ await git("config", "core.sshCommand", deployKeySshCommand(keyFile));
112
+ try {
113
+ await git("ls-remote", "--exit-code", remote, "HEAD");
114
+ } catch (e) {
115
+ throw new Error(
116
+ `the release deploy key cannot reach ${remote}: ${e?.message ?? e}\n` +
117
+ `The ${RELEASE_DEPLOY_KEY_ENV} secret does not match the '${RELEASE_DEPLOY_KEY_TITLE}' deploy key of the repository ` +
118
+ `(or the key was removed). Run \`catladder project setup\` to provision both again, then rerun this job.`,
119
+ );
120
+ }
121
+ return remote;
122
+ };
123
+
124
+ /**
125
+ * `catci release github-deploy-key-remote`: only the ssh url goes to
126
+ * stdout (the caller captures it), everything else to stderr
127
+ */
128
+ export const githubDeployKeyRemoteJob = async () => {
129
+ const privateKey = getReleaseDeployKey();
130
+ if (!privateKey) {
131
+ throw new Error(
132
+ `${RELEASE_DEPLOY_KEY_ENV} is not set — the release push cannot use the deploy key ` +
133
+ `(without merge gating the workflow token push works: run semantic-release without --repository-url)`,
134
+ );
135
+ }
136
+ const remote = await configureDeployKeyRemote(privateKey);
137
+ console.error(
138
+ `release push: ${remote} over ssh with the release deploy key (bypasses the merge-gating ruleset)`,
139
+ );
140
+ process.stdout.write(`${remote}\n`);
141
+ };