@catladder/cli 5.6.0 → 5.7.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/catci.js +22 -0
- package/dist/apps/cli/src/catci.js.map +1 -1
- package/dist/apps/cli/src/commands/index.d.ts +1 -0
- package/dist/apps/cli/src/commands/index.js +4 -1
- package/dist/apps/cli/src/commands/index.js.map +1 -1
- package/dist/apps/cli/src/commands/project/commandMrPin.d.ts +9 -0
- package/dist/apps/cli/src/commands/project/commandMrPin.js +11 -13
- package/dist/apps/cli/src/commands/project/commandMrPin.js.map +1 -1
- package/dist/apps/cli/src/commands/project/commandMrReviewApp.d.ts +21 -0
- package/dist/apps/cli/src/commands/project/commandMrReviewApp.js +102 -0
- package/dist/apps/cli/src/commands/project/commandMrReviewApp.js.map +1 -0
- package/dist/apps/cli/src/reviewApps/github.d.ts +10 -0
- package/dist/apps/cli/src/reviewApps/github.js +240 -0
- package/dist/apps/cli/src/reviewApps/github.js.map +1 -0
- package/dist/bundles/catci/index.js +5 -5
- package/dist/bundles/catenv/index.js +897 -25
- package/dist/bundles/cli/index.js +1021 -39
- package/dist/bundles/skills/catladder-cli/references/commands.md +13 -0
- package/dist/bundles/skills/catladder-config/SKILL.md +4 -0
- package/dist/bundles/skills/catladder-deploys/SKILL.md +6 -0
- package/dist/bundles/skills/catladder-pipelines/SKILL.md +85 -1
- package/dist/packages/pipeline/src/backends/github/GithubBackend.js +60 -3
- package/dist/packages/pipeline/src/backends/github/GithubBackend.js.map +1 -1
- package/dist/packages/pipeline/src/backends/github/aggregateCheckJob.d.ts +1 -1
- package/dist/packages/pipeline/src/backends/github/aggregateCheckJob.js +22 -8
- package/dist/packages/pipeline/src/backends/github/aggregateCheckJob.js.map +1 -1
- package/dist/packages/pipeline/src/backends/github/reviewDeployWorkflow.d.ts +128 -0
- package/dist/packages/pipeline/src/backends/github/reviewDeployWorkflow.js +510 -0
- package/dist/packages/pipeline/src/backends/github/reviewDeployWorkflow.js.map +1 -0
- package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js +29 -4
- package/dist/packages/pipeline/src/backends/gitlab/GitlabBackend.js.map +1 -1
- package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.d.ts +11 -1
- package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js +17 -5
- package/dist/packages/pipeline/src/backends/gitlab/createGitlabPipeline.js.map +1 -1
- package/dist/packages/pipeline/src/backends/gitlab/reviewDeployGate.d.ts +26 -0
- package/dist/packages/pipeline/src/backends/gitlab/reviewDeployGate.js +94 -0
- package/dist/packages/pipeline/src/backends/gitlab/reviewDeployGate.js.map +1 -0
- package/dist/packages/pipeline/src/deploy/base/deploy.js +10 -3
- package/dist/packages/pipeline/src/deploy/base/deploy.js.map +1 -1
- package/dist/packages/pipeline/src/index.d.ts +1 -0
- package/dist/packages/pipeline/src/index.js +1 -0
- package/dist/packages/pipeline/src/index.js.map +1 -1
- package/dist/packages/pipeline/src/pipeline/resolveRequirements.d.ts +2 -0
- package/dist/packages/pipeline/src/pipeline/resolveRequirements.js +3 -2
- package/dist/packages/pipeline/src/pipeline/resolveRequirements.js.map +1 -1
- package/dist/packages/pipeline/src/reviewApps.d.ts +38 -0
- package/dist/packages/pipeline/src/reviewApps.js +86 -0
- package/dist/packages/pipeline/src/reviewApps.js.map +1 -0
- package/dist/packages/pipeline/src/reviewAppsPolicy.d.ts +46 -0
- package/dist/packages/pipeline/src/reviewAppsPolicy.js +37 -0
- package/dist/packages/pipeline/src/reviewAppsPolicy.js.map +1 -0
- package/dist/packages/pipeline/src/types/config.d.ts +55 -0
- package/dist/packages/pipeline/src/types/config.js.map +1 -1
- package/dist/skills/catladder-cli/references/commands.md +13 -0
- package/dist/skills/catladder-config/SKILL.md +4 -0
- package/dist/skills/catladder-deploys/SKILL.md +6 -0
- package/dist/skills/catladder-pipelines/SKILL.md +85 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/catci.ts +29 -0
- package/src/commands/index.ts +4 -0
- package/src/commands/project/commandMrPin.ts +15 -14
- package/src/commands/project/commandMrReviewApp.ts +143 -0
- package/src/reviewApps/__tests__/github.test.ts +118 -0
- package/src/reviewApps/github.ts +307 -0
package/package.json
CHANGED
package/src/catci.ts
CHANGED
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
githubQueuedGuardJob,
|
|
31
31
|
} from "./release/githubQueuedRelease";
|
|
32
32
|
import { npmPublishJob, parseNpmPublishArgs } from "./publish/npmPublishJob";
|
|
33
|
+
import {
|
|
34
|
+
githubReviewDeployGuardJob,
|
|
35
|
+
githubReviewDeployTriggerJob,
|
|
36
|
+
} from "./reviewApps/github";
|
|
33
37
|
import { getGitlabHostUrl } from "./utils/gitlabHost";
|
|
34
38
|
|
|
35
39
|
const USAGE = `catci — catladder CI companion
|
|
@@ -78,6 +82,17 @@ usage:
|
|
|
78
82
|
queue marker when it matches the completed main run and decides
|
|
79
83
|
whether the release step runs (release=true step output)
|
|
80
84
|
|
|
85
|
+
catci review-app github-trigger
|
|
86
|
+
github only, last job of the review workflow: once the PR's CI is
|
|
87
|
+
green, dispatches the review deploy workflow when the reviewApps
|
|
88
|
+
policy (labels, draft) says the PR deploys
|
|
89
|
+
|
|
90
|
+
catci review-app github-guard
|
|
91
|
+
github only, first job of the review deploy workflow: decides
|
|
92
|
+
whether the run deploys (deploy=true step output) — explicit
|
|
93
|
+
dispatches always do, label events and ci dispatches follow the
|
|
94
|
+
policy; all need the PR open and its CI green
|
|
95
|
+
|
|
81
96
|
catci publish npm --dir <dir> --env-type <envType> [--access <access>] [--registry <url>] [--dist-tag <tag>]
|
|
82
97
|
npmPackage deploy job: derives version + dist-tag from the
|
|
83
98
|
pipeline trigger (tagged release -> tag version @latest; branch/MR
|
|
@@ -204,6 +219,20 @@ const main = async () => {
|
|
|
204
219
|
) {
|
|
205
220
|
return githubQueuedGuardJob(args[0], args[1]);
|
|
206
221
|
}
|
|
222
|
+
if (
|
|
223
|
+
group === "review-app" &&
|
|
224
|
+
command === "github-trigger" &&
|
|
225
|
+
args.length === 0
|
|
226
|
+
) {
|
|
227
|
+
return githubReviewDeployTriggerJob();
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
group === "review-app" &&
|
|
231
|
+
command === "github-guard" &&
|
|
232
|
+
args.length === 0
|
|
233
|
+
) {
|
|
234
|
+
return githubReviewDeployGuardJob();
|
|
235
|
+
}
|
|
207
236
|
if (group === "publish" && command === "npm") {
|
|
208
237
|
const options = parseNpmPublishArgs(args);
|
|
209
238
|
if (options) {
|
package/src/commands/index.ts
CHANGED
|
@@ -30,6 +30,10 @@ export { commandCiJobOpen, commandCiJobLog } from "./project/commandGitlabCi";
|
|
|
30
30
|
export { commandProjectRestoreDb } from "./project/commandProjectRestoreDb";
|
|
31
31
|
export { commandSecurityEvaluate } from "./project/commandSecurityEvaluate";
|
|
32
32
|
export { commandMrPin, commandMrUnpin } from "./project/commandMrPin";
|
|
33
|
+
export {
|
|
34
|
+
commandMrReviewAppOn,
|
|
35
|
+
commandMrReviewAppOff,
|
|
36
|
+
} from "./project/commandMrReviewApp";
|
|
33
37
|
|
|
34
38
|
// Project k8s commands
|
|
35
39
|
export { commandNamespace } from "./project/k8s/commandNamespace";
|
|
@@ -10,7 +10,7 @@ import { doGitlabRequest, getProjectInfo } from "../../utils/gitlab";
|
|
|
10
10
|
|
|
11
11
|
const PIN_LABEL_COLOR = "#428fdc";
|
|
12
12
|
|
|
13
|
-
type GitlabMr = {
|
|
13
|
+
export type GitlabMr = {
|
|
14
14
|
iid: number;
|
|
15
15
|
title: string;
|
|
16
16
|
web_url: string;
|
|
@@ -40,7 +40,7 @@ const getPinLabel = async (): Promise<string> => {
|
|
|
40
40
|
return pinLabel;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
const findMr = async (
|
|
43
|
+
export const findMr = async (
|
|
44
44
|
io: IO,
|
|
45
45
|
projectId: string,
|
|
46
46
|
mrIid: number | undefined,
|
|
@@ -61,30 +61,26 @@ const findMr = async (
|
|
|
61
61
|
return mrs[0];
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
const
|
|
64
|
+
export const ensureLabelExists = async (
|
|
65
65
|
io: IO,
|
|
66
66
|
projectId: string,
|
|
67
|
-
|
|
67
|
+
name: string,
|
|
68
|
+
description: string,
|
|
68
69
|
) => {
|
|
69
70
|
const existing = await doGitlabRequest<Array<{ name: string }>>(
|
|
70
71
|
io,
|
|
71
|
-
`projects/${projectId}/labels?search=${encodeURIComponent(
|
|
72
|
+
`projects/${projectId}/labels?search=${encodeURIComponent(name)}`,
|
|
72
73
|
);
|
|
73
|
-
if (existing?.some((label) => label.name ===
|
|
74
|
+
if (existing?.some((label) => label.name === name)) {
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
77
|
await doGitlabRequest(
|
|
77
78
|
io,
|
|
78
79
|
`projects/${projectId}/labels`,
|
|
79
|
-
{
|
|
80
|
-
name: pinLabel,
|
|
81
|
-
color: PIN_LABEL_COLOR,
|
|
82
|
-
description:
|
|
83
|
-
"review apps of this MR are pinned — deploys use auto_stop_in: never (managed by catladder)",
|
|
84
|
-
},
|
|
80
|
+
{ name, color: PIN_LABEL_COLOR, description },
|
|
85
81
|
"POST",
|
|
86
82
|
);
|
|
87
|
-
io.log(`created the '${
|
|
83
|
+
io.log(`created the '${name}' label in the project`);
|
|
88
84
|
};
|
|
89
85
|
|
|
90
86
|
export const commandMrPin = defineCommand({
|
|
@@ -112,7 +108,12 @@ export const commandMrPin = defineCommand({
|
|
|
112
108
|
const { id: projectId } = await getProjectInfo(ctx);
|
|
113
109
|
const mr = await findMr(ctx, projectId, await ctx.get("mr"));
|
|
114
110
|
|
|
115
|
-
await
|
|
111
|
+
await ensureLabelExists(
|
|
112
|
+
ctx,
|
|
113
|
+
projectId,
|
|
114
|
+
pinLabel,
|
|
115
|
+
"review apps of this MR are pinned — deploys use auto_stop_in: never (managed by catladder)",
|
|
116
|
+
);
|
|
116
117
|
|
|
117
118
|
if (mr.labels?.includes(pinLabel)) {
|
|
118
119
|
ctx.log(`already pinned: mr!${mr.iid} carries the '${pinLabel}' label`);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getEnabledPipelineTypes,
|
|
3
|
+
getReviewAppsConfig,
|
|
4
|
+
getReviewAppsSwitchLabel,
|
|
5
|
+
} from "@catladder/pipeline";
|
|
6
|
+
import { getProjectConfig } from "../../config/getProjectConfig";
|
|
7
|
+
import { defineCommand } from "../../core/defineCommand";
|
|
8
|
+
import { doGitlabRequest, getProjectInfo } from "../../utils/gitlab";
|
|
9
|
+
import { ensureLabelExists, findMr } from "./commandMrPin";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* the label switch of the project's review apps — errors when the
|
|
13
|
+
* project has none (github, or a policy without labels)
|
|
14
|
+
*/
|
|
15
|
+
const getSwitch = async () => {
|
|
16
|
+
const config = await getProjectConfig();
|
|
17
|
+
if (!config) {
|
|
18
|
+
throw new Error("no catladder config found");
|
|
19
|
+
}
|
|
20
|
+
const reviewApps = getReviewAppsConfig(config);
|
|
21
|
+
const switchLabel = getReviewAppsSwitchLabel(reviewApps);
|
|
22
|
+
if (!switchLabel) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
reviewApps.deploy === "manual"
|
|
25
|
+
? "review apps deploy on request in this project (reviewApps.deploy: manual) — play the '🚀 deploy review' job of the MR pipeline"
|
|
26
|
+
: "review apps of this project are not switched by label (set reviewApps.deploy to optIn or optOut)",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!getEnabledPipelineTypes(config).includes("gitlab")) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`this command switches gitlab merge requests — on github, set the label on the pull request instead (gh pr edit --${switchLabel.deployOn === "labeled" ? "add" : "remove"}-label ${switchLabel.label}); the label change deploys by itself`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return switchLabel;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mrInput = {
|
|
38
|
+
mr: {
|
|
39
|
+
type: "number",
|
|
40
|
+
message:
|
|
41
|
+
"merge request IID (defaults to the open MR of the current branch)",
|
|
42
|
+
positional: true,
|
|
43
|
+
required: false,
|
|
44
|
+
},
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export const commandMrReviewAppOn = defineCommand({
|
|
48
|
+
name: "mr review-app-on",
|
|
49
|
+
description:
|
|
50
|
+
"deploy the review apps of a merge request: switches the reviewApps label on (opt-in: adds it, opt-out: removes the skip label) and triggers a pipeline, since gitlab doesn't start one for label changes",
|
|
51
|
+
group: "mr",
|
|
52
|
+
inputs: {
|
|
53
|
+
...mrInput,
|
|
54
|
+
pipeline: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
message: "trigger a merge-request pipeline so the review apps deploy now",
|
|
57
|
+
required: false,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
execute: async (ctx) => {
|
|
61
|
+
const switchLabel = await getSwitch();
|
|
62
|
+
const { id: projectId } = await getProjectInfo(ctx);
|
|
63
|
+
const mr = await findMr(ctx, projectId, await ctx.get("mr"));
|
|
64
|
+
|
|
65
|
+
if (switchLabel.deployOn === "labeled") {
|
|
66
|
+
await ensureLabelExists(
|
|
67
|
+
ctx,
|
|
68
|
+
projectId,
|
|
69
|
+
switchLabel.label,
|
|
70
|
+
"review apps of this MR deploy (managed by catladder)",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const hasLabel = mr.labels?.includes(switchLabel.label);
|
|
74
|
+
if (hasLabel === (switchLabel.deployOn === "labeled")) {
|
|
75
|
+
ctx.log(`review apps of mr!${mr.iid} are already switched on`);
|
|
76
|
+
} else {
|
|
77
|
+
await doGitlabRequest(
|
|
78
|
+
ctx,
|
|
79
|
+
`projects/${projectId}/merge_requests/${mr.iid}`,
|
|
80
|
+
switchLabel.deployOn === "labeled"
|
|
81
|
+
? { add_labels: switchLabel.label }
|
|
82
|
+
: { remove_labels: switchLabel.label },
|
|
83
|
+
"PUT",
|
|
84
|
+
);
|
|
85
|
+
ctx.log(
|
|
86
|
+
`review apps of mr!${mr.iid} "${mr.title}" switched on — label '${switchLabel.label}' ${switchLabel.deployOn === "labeled" ? "added" : "removed"}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ((await ctx.get("pipeline")) ?? true) {
|
|
91
|
+
const pipeline = await doGitlabRequest<{ web_url: string }>(
|
|
92
|
+
ctx,
|
|
93
|
+
`projects/${projectId}/merge_requests/${mr.iid}/pipelines`,
|
|
94
|
+
{},
|
|
95
|
+
"POST",
|
|
96
|
+
);
|
|
97
|
+
ctx.log(`🚀 triggered a pipeline that deploys them: ${pipeline.web_url}`);
|
|
98
|
+
} else {
|
|
99
|
+
ctx.log("the review apps deploy with the next pipeline of the MR");
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const commandMrReviewAppOff = defineCommand({
|
|
105
|
+
name: "mr review-app-off",
|
|
106
|
+
description:
|
|
107
|
+
"stop deploying the review apps of a merge request: switches the reviewApps label off (opt-in: removes it, opt-out: adds the skip label). Running review apps stay until the MR closes or they auto-stop",
|
|
108
|
+
group: "mr",
|
|
109
|
+
inputs: mrInput,
|
|
110
|
+
execute: async (ctx) => {
|
|
111
|
+
const switchLabel = await getSwitch();
|
|
112
|
+
const { id: projectId } = await getProjectInfo(ctx);
|
|
113
|
+
const mr = await findMr(ctx, projectId, await ctx.get("mr"));
|
|
114
|
+
|
|
115
|
+
if (switchLabel.stopOn === "labeled") {
|
|
116
|
+
await ensureLabelExists(
|
|
117
|
+
ctx,
|
|
118
|
+
projectId,
|
|
119
|
+
switchLabel.label,
|
|
120
|
+
"review apps of this MR don't deploy (managed by catladder)",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const hasLabel = mr.labels?.includes(switchLabel.label);
|
|
124
|
+
if (hasLabel === (switchLabel.stopOn === "labeled")) {
|
|
125
|
+
ctx.log(`review apps of mr!${mr.iid} are already switched off`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await doGitlabRequest(
|
|
129
|
+
ctx,
|
|
130
|
+
`projects/${projectId}/merge_requests/${mr.iid}`,
|
|
131
|
+
switchLabel.stopOn === "labeled"
|
|
132
|
+
? { add_labels: switchLabel.label }
|
|
133
|
+
: { remove_labels: switchLabel.label },
|
|
134
|
+
"PUT",
|
|
135
|
+
);
|
|
136
|
+
ctx.log(
|
|
137
|
+
`review apps of mr!${mr.iid} switched off — label '${switchLabel.label}' ${switchLabel.stopOn === "labeled" ? "added" : "removed"}`,
|
|
138
|
+
);
|
|
139
|
+
ctx.log(
|
|
140
|
+
"already running review apps stay until the MR is closed or they auto-stop — stop them earlier with the stop job of the MR pipeline",
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { githubReviewDeployGuardJob } from "../github";
|
|
3
|
+
|
|
4
|
+
const HEAD = "4a60abe0000000000000000000000000000000aa";
|
|
5
|
+
|
|
6
|
+
type Call = { url: string; method: string; body?: any };
|
|
7
|
+
|
|
8
|
+
/** a github api double: one PR, a `catladder ✅` check in a given state */
|
|
9
|
+
const mockGithub = (
|
|
10
|
+
check: { status: string; conclusion: string | null } | null,
|
|
11
|
+
) => {
|
|
12
|
+
const calls: Call[] = [];
|
|
13
|
+
vi.stubGlobal(
|
|
14
|
+
"fetch",
|
|
15
|
+
vi.fn(async (url: string, init: any = {}) => {
|
|
16
|
+
const path = url.replace("https://api.github.com/repos/o/r/", "");
|
|
17
|
+
calls.push({
|
|
18
|
+
url: path,
|
|
19
|
+
method: init.method ?? "GET",
|
|
20
|
+
body: init.body ? JSON.parse(init.body) : undefined,
|
|
21
|
+
});
|
|
22
|
+
const json = (data: unknown) => ({
|
|
23
|
+
ok: true,
|
|
24
|
+
status: 200,
|
|
25
|
+
json: async () => data,
|
|
26
|
+
text: async () => JSON.stringify(data),
|
|
27
|
+
});
|
|
28
|
+
if (path.startsWith("pulls/")) {
|
|
29
|
+
return json({
|
|
30
|
+
number: 159,
|
|
31
|
+
state: "open",
|
|
32
|
+
draft: false,
|
|
33
|
+
labels: [{ name: "catladder:review-app" }],
|
|
34
|
+
head: { sha: HEAD, ref: "feat/x" },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (path.startsWith("commits/")) {
|
|
38
|
+
return json({ check_runs: check ? [{ id: 1, ...check }] : [] });
|
|
39
|
+
}
|
|
40
|
+
return json({});
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return calls;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("review deploy guard: label added while the CI runs", () => {
|
|
47
|
+
let output: string;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
output = "";
|
|
50
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
51
|
+
process.env.GITHUB_REPOSITORY = "o/r";
|
|
52
|
+
process.env.GITHUB_TOKEN = "t";
|
|
53
|
+
process.env.GITHUB_RUN_ID = "1";
|
|
54
|
+
process.env.CL_PR_NUMBER = "159";
|
|
55
|
+
process.env.CL_HEAD_SHA = HEAD;
|
|
56
|
+
process.env.CL_DEPLOY_SOURCE = "label";
|
|
57
|
+
process.env.CL_REVIEW_APPS = JSON.stringify({
|
|
58
|
+
deploy: "optIn",
|
|
59
|
+
label: "catladder:review-app",
|
|
60
|
+
skipLabel: "catladder:no-review-app",
|
|
61
|
+
drafts: "likeReady",
|
|
62
|
+
});
|
|
63
|
+
// capture the step output
|
|
64
|
+
const file = `${process.env.TMPDIR ?? "/tmp"}/gh-output-${process.pid}-${Math.random()}`;
|
|
65
|
+
process.env.GITHUB_OUTPUT = file;
|
|
66
|
+
output = file;
|
|
67
|
+
});
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.unstubAllGlobals();
|
|
70
|
+
vi.restoreAllMocks();
|
|
71
|
+
});
|
|
72
|
+
const deployOutput = async () =>
|
|
73
|
+
(await import("fs")).readFileSync(output, "utf8");
|
|
74
|
+
|
|
75
|
+
it("defers and reports a pending status while the CI is not finished", async () => {
|
|
76
|
+
const calls = mockGithub({ status: "in_progress", conclusion: null });
|
|
77
|
+
await githubReviewDeployGuardJob();
|
|
78
|
+
expect(await deployOutput()).toContain("deploy=false");
|
|
79
|
+
const status = calls.find((c) => c.url === `statuses/${HEAD}`);
|
|
80
|
+
expect(status?.method).toBe("POST");
|
|
81
|
+
expect(status?.body).toMatchObject({
|
|
82
|
+
state: "pending",
|
|
83
|
+
context: "catladder review app",
|
|
84
|
+
});
|
|
85
|
+
expect(status?.body.description).toContain("waiting for the CI");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("defers and reports pending while the CI has not even reported", async () => {
|
|
89
|
+
const calls = mockGithub(null);
|
|
90
|
+
await githubReviewDeployGuardJob();
|
|
91
|
+
expect(await deployOutput()).toContain("deploy=false");
|
|
92
|
+
expect(calls.some((c) => c.url === `statuses/${HEAD}`)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("deploys without a pending status when the CI already finished green", async () => {
|
|
96
|
+
const calls = mockGithub({ status: "completed", conclusion: "success" });
|
|
97
|
+
await githubReviewDeployGuardJob();
|
|
98
|
+
expect(await deployOutput()).toContain("deploy=true");
|
|
99
|
+
expect(calls.some((c) => c.url.startsWith("statuses/"))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("trusts a ci dispatch: deploys although the checks api still reports the CI in progress", async () => {
|
|
103
|
+
process.env.CL_DEPLOY_SOURCE = "ci";
|
|
104
|
+
const calls = mockGithub({ status: "in_progress", conclusion: null });
|
|
105
|
+
await githubReviewDeployGuardJob();
|
|
106
|
+
expect(await deployOutput()).toContain("deploy=true");
|
|
107
|
+
expect(calls.some((c) => c.url.startsWith("commits/"))).toBe(false);
|
|
108
|
+
expect(calls.some((c) => c.url.startsWith("statuses/"))).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("a ci dispatch for a head the PR moved away from does not deploy", async () => {
|
|
112
|
+
process.env.CL_DEPLOY_SOURCE = "ci";
|
|
113
|
+
process.env.CL_HEAD_SHA = "0000000000000000000000000000000000000000";
|
|
114
|
+
mockGithub({ status: "completed", conclusion: "success" });
|
|
115
|
+
await githubReviewDeployGuardJob();
|
|
116
|
+
expect(await deployOutput()).toContain("deploy=false");
|
|
117
|
+
});
|
|
118
|
+
});
|