@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.
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.d.ts +30 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.js +89 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.js.map +1 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js +2 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/githubMergeGating.d.ts +4 -1
- package/dist/apps/cli/src/apps/cli/commands/project/githubMergeGating.js +4 -1
- package/dist/apps/cli/src/apps/cli/commands/project/githubMergeGating.js.map +1 -1
- package/dist/apps/cli/src/catci.js +13 -0
- package/dist/apps/cli/src/catci.js.map +1 -1
- package/dist/apps/cli/src/release/changesetsReleaseJob.js +20 -42
- package/dist/apps/cli/src/release/changesetsReleaseJob.js.map +1 -1
- package/dist/apps/cli/src/release/githubDeployKey.d.ts +36 -0
- package/dist/apps/cli/src/release/githubDeployKey.js +126 -0
- package/dist/apps/cli/src/release/githubDeployKey.js.map +1 -0
- package/dist/bundles/catci/index.js +5 -5
- package/dist/bundles/catenv/index.js +31 -7
- package/dist/bundles/cli/index.js +133 -8
- package/dist/bundles/runner-images/semantic-release/Dockerfile +3 -1
- package/dist/bundles/runner-images/semantic-release/scripts/semanticRelease +37 -4
- package/dist/bundles/skills/catladder-migrate-ci-backend/SKILL.md +15 -6
- package/dist/bundles/skills/catladder-pipelines/SKILL.md +3 -2
- package/dist/bundles/skills/catladder-releases/SKILL.md +14 -0
- package/dist/packages/pipeline/src/backends/github/GithubBackend.js +12 -3
- package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
- package/dist/packages/pipeline/src/catci/shippedCatci.d.ts +8 -0
- package/dist/packages/pipeline/src/catci/shippedCatci.js +19 -4
- package/dist/packages/pipeline/src/catci/shippedCatci.js.map +1 -1
- package/dist/runner-images/semantic-release/Dockerfile +3 -1
- package/dist/runner-images/semantic-release/scripts/semanticRelease +37 -4
- package/dist/skills/catladder-migrate-ci-backend/SKILL.md +15 -6
- package/dist/skills/catladder-pipelines/SKILL.md +3 -2
- package/dist/skills/catladder-releases/SKILL.md +14 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/apps/cli/commands/project/__tests__/checkGithubReleaseWorkflow.test.ts +89 -0
- package/src/apps/cli/commands/project/doctor/checkGithubReleaseWorkflow.ts +125 -0
- package/src/apps/cli/commands/project/doctor/index.ts +2 -0
- package/src/apps/cli/commands/project/githubMergeGating.ts +4 -1
- package/src/catci.ts +15 -0
- package/src/release/__tests__/githubDeployKey.test.ts +148 -0
- package/src/release/changesetsReleaseJob.ts +28 -57
- 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
|
+
};
|