@intentius/chant 0.58.0 → 0.60.0
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/audit/core.d.ts +17 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +15 -4
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/build-params.d.ts +2 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +13 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +0 -16
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +19 -4
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +2 -2
- package/dist/components/verbs/run-agent.d.ts +1 -7
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/lexicon.d.ts +213 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +9 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +4 -4
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +134 -2
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +7 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/reconcile-op.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +55 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- package/dist/op/index.d.ts +6 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts +63 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/types.d.ts +18 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/params.d.ts +1 -1
- package/dist/project-root.d.ts +2 -2
- package/dist/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/core.ts +25 -3
- package/src/audit/discover.ts +122 -14
- package/src/build-params.ts +2 -2
- package/src/cli/commands/audit.test.ts +5 -3
- package/src/cli/commands/audit.ts +5 -1
- package/src/cli/commands/build.test.ts +39 -1
- package/src/cli/commands/build.ts +22 -8
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- package/src/cli/commands/lint.test.ts +297 -1
- package/src/cli/commands/lint.ts +119 -11
- package/src/cli/handlers/graph.test.ts +4 -4
- package/src/cli/handlers/graph.ts +11 -11
- package/src/cli/handlers/lint.test.ts +107 -0
- package/src/cli/handlers/lint.ts +30 -0
- package/src/cli/handlers/operator.ts +81 -0
- package/src/cli/handlers/run.test.ts +132 -3
- package/src/cli/handlers/run.ts +87 -3
- package/src/cli/main.test.ts +9 -11
- package/src/cli/main.ts +5 -27
- package/src/cli/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +14 -0
- package/src/components/README.md +2 -2
- package/src/components/SPRAWL-VALIDATION.md +5 -5
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- package/src/components/cli-support.test.ts +18 -7
- package/src/components/cli-support.ts +8 -3
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +17 -4
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +25 -5
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver.test.ts +20 -1
- package/src/components/driver.ts +12 -5
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +2 -2
- package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
- package/src/components/verbs/run-agent.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +1 -7
- package/src/config.ts +4 -4
- package/src/detectLexicon.ts +18 -1
- package/src/discovery/fold-import.test.ts +2 -2
- package/src/discovery/fold-import.ts +3 -3
- package/src/fold/foldable-helpers.ts +1 -1
- package/src/graph-ops.test.ts +1 -1
- package/src/lexicon.ts +221 -3
- package/src/lifecycle/gate-ledger.ts +12 -1
- package/src/lifecycle/observe.test.ts +2 -2
- package/src/lifecycle/observe.ts +8 -8
- package/src/lifecycle/release-ledger.test.ts +2 -2
- package/src/lint/pipeline-change-gate.test.ts +2 -2
- package/src/lint/rules/comp/comp.test.ts +26 -0
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +4 -2
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +1 -1
- package/src/op/activities/index.ts +9 -2
- package/src/op/activities/reconcile.test.ts +320 -2
- package/src/op/activities/reconcile.ts +423 -2
- package/src/op/builders.ts +3 -3
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +14 -4
- package/src/op/composites/composites.test.ts +17 -4
- package/src/op/composites/reconcile-op.ts +7 -4
- package/src/op/effect-step.test.ts +3 -3
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.test.ts +62 -0
- package/src/op/gate-summary.ts +96 -0
- package/src/op/index.ts +9 -2
- package/src/op/local-executor.test.ts +16 -1
- package/src/op/local-executor.ts +4 -3
- package/src/op/op-ir.test.ts +12 -1
- package/src/op/op-ir.ts +5 -3
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op.test.ts +2 -2
- package/src/op/operator.test.ts +368 -0
- package/src/op/operator.ts +141 -5
- package/src/op/runtimes/local.test.ts +1 -1
- package/src/op/types.ts +23 -3
- package/src/params.ts +1 -1
- package/src/project-root.ts +2 -2
- package/src/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
reconcilePr,
|
|
4
|
+
reconcileSummary,
|
|
5
|
+
reconcileBranchName,
|
|
6
|
+
entriesFromPlan,
|
|
7
|
+
commentMarker,
|
|
8
|
+
pullRequestContextFrom,
|
|
9
|
+
mergeRequestContextFrom,
|
|
10
|
+
gitlabNoteTokenFrom,
|
|
11
|
+
} from "./reconcile";
|
|
3
12
|
|
|
4
13
|
const entries = [
|
|
5
14
|
{ name: "bucket", action: "adopt", type: "AWS::S3::Bucket" },
|
|
@@ -79,3 +88,312 @@ describe("reconcilePr pre-built body (#2087)", () => {
|
|
|
79
88
|
expect(result.entries).toEqual(entries);
|
|
80
89
|
});
|
|
81
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("reconcilePr comment mode: the marker (#2231)", () => {
|
|
93
|
+
test("the marker is deterministic per env, so a re-run finds its own comment", () => {
|
|
94
|
+
expect(commentMarker("app")).toBe("<!-- chant-reconcile:app -->");
|
|
95
|
+
expect(commentMarker("app")).toBe(commentMarker("app"));
|
|
96
|
+
expect(commentMarker("app")).not.toBe(commentMarker("db"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the marker slugifies the env, so nothing it is interpolated next to can be escaped out of", () => {
|
|
100
|
+
// The marker is interpolated into a jq `startswith("…")` string and into a
|
|
101
|
+
// shell word. A quote or a backslash surviving into it would break both.
|
|
102
|
+
const marker = commentMarker('us-east/1" or true; #');
|
|
103
|
+
expect(marker).toBe("<!-- chant-reconcile:us-east-1-or-true- -->");
|
|
104
|
+
expect(marker).not.toMatch(/["'\\]/);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("pullRequestContextFrom (#2231)", () => {
|
|
109
|
+
const repo = "INTENTIUS/chant";
|
|
110
|
+
|
|
111
|
+
test("reads the number off a pull_request event payload", () => {
|
|
112
|
+
expect(pullRequestContextFrom({ GITHUB_REPOSITORY: repo }, { number: 2231 })).toEqual({
|
|
113
|
+
repo,
|
|
114
|
+
number: 2231,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("accepts the nested pull_request.number the same payload also carries", () => {
|
|
119
|
+
expect(
|
|
120
|
+
pullRequestContextFrom({ GITHUB_REPOSITORY: repo }, { pull_request: { number: 7 } }),
|
|
121
|
+
).toEqual({ repo, number: 7 });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("falls back to GITHUB_REF when the payload is unreadable", () => {
|
|
125
|
+
expect(
|
|
126
|
+
pullRequestContextFrom({ GITHUB_REPOSITORY: repo, GITHUB_REF: "refs/pull/42/merge" }),
|
|
127
|
+
).toEqual({ repo, number: 42 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a push run has no pull request", () => {
|
|
131
|
+
expect(
|
|
132
|
+
pullRequestContextFrom(
|
|
133
|
+
{ GITHUB_REPOSITORY: repo, GITHUB_REF: "refs/heads/main" },
|
|
134
|
+
{ ref: "refs/heads/main", after: "abc" },
|
|
135
|
+
),
|
|
136
|
+
).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a cron run off any forge has neither variable", () => {
|
|
140
|
+
expect(pullRequestContextFrom({})).toBeUndefined();
|
|
141
|
+
expect(pullRequestContextFrom({ GITHUB_REF: "refs/pull/1/merge" })).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("reconcilePr comment mode refuses a run with no pull request (#2231)", () => {
|
|
146
|
+
test("the message names the mode and every variable it looked for", async () => {
|
|
147
|
+
vi.stubEnv("GITHUB_REPOSITORY", "");
|
|
148
|
+
vi.stubEnv("GITHUB_REF", "");
|
|
149
|
+
vi.stubEnv("GITHUB_EVENT_PATH", "");
|
|
150
|
+
try {
|
|
151
|
+
// No `gh` is ever reached: the context check runs first, so a failure
|
|
152
|
+
// here is the refusal rather than a missing binary.
|
|
153
|
+
await expect(reconcilePr({ env: "app", mode: "comment", body: "plan" })).rejects.toThrow(
|
|
154
|
+
/mode "comment".*GITHUB_REPOSITORY.*GITHUB_EVENT_PATH.*GITHUB_REF/s,
|
|
155
|
+
);
|
|
156
|
+
await expect(reconcilePr({ env: "app", mode: "comment", body: "plan" })).rejects.toThrow(
|
|
157
|
+
/findingMode "issue" or "report"/,
|
|
158
|
+
);
|
|
159
|
+
} finally {
|
|
160
|
+
vi.unstubAllEnvs();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── The GitLab merge-request note (#2256) ───────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("mergeRequestContextFrom (#2256)", () => {
|
|
168
|
+
const gitlabEnv = {
|
|
169
|
+
CI_API_V4_URL: "https://gitlab.com/api/v4",
|
|
170
|
+
CI_PROJECT_ID: "42",
|
|
171
|
+
CI_PROJECT_PATH: "acme/infra",
|
|
172
|
+
CI_PROJECT_URL: "https://gitlab.com/acme/infra",
|
|
173
|
+
CI_MERGE_REQUEST_IID: "7",
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
test("reads the merge request off a merge_request_event pipeline", () => {
|
|
177
|
+
expect(mergeRequestContextFrom(gitlabEnv)).toEqual({
|
|
178
|
+
api: "https://gitlab.com/api/v4",
|
|
179
|
+
project: "42",
|
|
180
|
+
iid: 7,
|
|
181
|
+
path: "acme/infra",
|
|
182
|
+
webUrl: "https://gitlab.com/acme/infra",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("prefers the merge request's own project over the pipeline's", () => {
|
|
187
|
+
// A merge request from a fork runs its pipeline in the fork's project,
|
|
188
|
+
// and the note belongs on the target project's merge request.
|
|
189
|
+
expect(
|
|
190
|
+
mergeRequestContextFrom({ ...gitlabEnv, CI_MERGE_REQUEST_PROJECT_ID: "9" })?.project,
|
|
191
|
+
).toBe("9");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("derives the API base from CI_SERVER_URL when CI_API_V4_URL is unset", () => {
|
|
195
|
+
const { CI_API_V4_URL: _drop, ...rest } = gitlabEnv;
|
|
196
|
+
expect(mergeRequestContextFrom({ ...rest, CI_SERVER_URL: "https://gl.example.com" })?.api).toBe(
|
|
197
|
+
"https://gl.example.com/api/v4",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a push or scheduled GitLab pipeline has no merge request", () => {
|
|
202
|
+
const { CI_MERGE_REQUEST_IID: _drop, ...rest } = gitlabEnv;
|
|
203
|
+
expect(mergeRequestContextFrom(rest)).toBeUndefined();
|
|
204
|
+
expect(mergeRequestContextFrom({ ...rest, CI_MERGE_REQUEST_IID: "" })).toBeUndefined();
|
|
205
|
+
expect(mergeRequestContextFrom({ ...rest, CI_MERGE_REQUEST_IID: "not-a-number" })).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a GitHub Actions run is not mistaken for a GitLab one", () => {
|
|
209
|
+
expect(mergeRequestContextFrom({ GITHUB_REPOSITORY: "INTENTIUS/chant", GITHUB_REF: "refs/pull/1/merge" })).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("gitlabNoteTokenFrom (#2256)", () => {
|
|
214
|
+
test("a project or personal access token is sent as PRIVATE-TOKEN", () => {
|
|
215
|
+
expect(gitlabNoteTokenFrom({ GITLAB_TOKEN: "glpat-x" })).toEqual({
|
|
216
|
+
header: "PRIVATE-TOKEN",
|
|
217
|
+
value: "glpat-x",
|
|
218
|
+
source: "GITLAB_TOKEN",
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("CHANT_GITLAB_TOKEN wins over GITLAB_TOKEN, which wins over the job token", () => {
|
|
223
|
+
expect(
|
|
224
|
+
gitlabNoteTokenFrom({ CHANT_GITLAB_TOKEN: "a", GITLAB_TOKEN: "b", CI_JOB_TOKEN: "c" })?.source,
|
|
225
|
+
).toBe("CHANT_GITLAB_TOKEN");
|
|
226
|
+
expect(gitlabNoteTokenFrom({ GITLAB_TOKEN: "b", CI_JOB_TOKEN: "c" })?.source).toBe("GITLAB_TOKEN");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("the job token is sent as JOB-TOKEN, which is a different header", () => {
|
|
230
|
+
expect(gitlabNoteTokenFrom({ CI_JOB_TOKEN: "c" })).toEqual({
|
|
231
|
+
header: "JOB-TOKEN",
|
|
232
|
+
value: "c",
|
|
233
|
+
source: "CI_JOB_TOKEN",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("no token at all is undefined rather than an empty header", () => {
|
|
238
|
+
expect(gitlabNoteTokenFrom({})).toBeUndefined();
|
|
239
|
+
expect(gitlabNoteTokenFrom({ GITLAB_TOKEN: "", CI_JOB_TOKEN: "" })).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** One stubbed GitLab REST response. */
|
|
244
|
+
function gitlabResponse(body: unknown, headers: Record<string, string> = {}): Response {
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
status: 200,
|
|
248
|
+
headers: { get: (name: string) => headers[name.toLowerCase()] ?? null },
|
|
249
|
+
json: async () => body,
|
|
250
|
+
text: async () => JSON.stringify(body),
|
|
251
|
+
} as unknown as Response;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
describe("reconcilePr comment mode on GitLab posts one merge-request note (#2256)", () => {
|
|
255
|
+
const gitlabEnv: Record<string, string> = {
|
|
256
|
+
CI_API_V4_URL: "https://gitlab.com/api/v4",
|
|
257
|
+
CI_PROJECT_ID: "42",
|
|
258
|
+
CI_PROJECT_PATH: "acme/infra",
|
|
259
|
+
CI_PROJECT_URL: "https://gitlab.com/acme/infra",
|
|
260
|
+
CI_MERGE_REQUEST_IID: "7",
|
|
261
|
+
GITLAB_TOKEN: "glpat-x",
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
function stubGitlabEnv(): void {
|
|
265
|
+
for (const [k, v] of Object.entries(gitlabEnv)) vi.stubEnv(k, v);
|
|
266
|
+
// A GitLab job carries none of GitHub's variables; make that explicit so
|
|
267
|
+
// the GitHub path can never be the one under test here.
|
|
268
|
+
for (const k of ["GITHUB_REPOSITORY", "GITHUB_REF", "GITHUB_EVENT_PATH"]) vi.stubEnv(k, "");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
test("POSTs a new note carrying the marker as its first line", async () => {
|
|
272
|
+
stubGitlabEnv();
|
|
273
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
274
|
+
vi.stubGlobal("fetch", async (url: string, init?: RequestInit) => {
|
|
275
|
+
calls.push({ url, init });
|
|
276
|
+
if (!init?.method || init.method === "GET") return gitlabResponse([]);
|
|
277
|
+
return gitlabResponse({ id: 555 });
|
|
278
|
+
});
|
|
279
|
+
try {
|
|
280
|
+
const result = await reconcilePr({ env: "app", mode: "comment", body: "the plan" });
|
|
281
|
+
expect(result.commentUrl).toBe("https://gitlab.com/acme/infra/-/merge_requests/7#note_555");
|
|
282
|
+
expect(result.mergeRequest).toBe("acme/infra!7");
|
|
283
|
+
const post = calls[calls.length - 1];
|
|
284
|
+
expect(post.url).toBe("https://gitlab.com/api/v4/projects/42/merge_requests/7/notes");
|
|
285
|
+
expect(post.init?.method).toBe("POST");
|
|
286
|
+
expect((post.init?.headers as Record<string, string>)["PRIVATE-TOKEN"]).toBe("glpat-x");
|
|
287
|
+
expect(JSON.parse(String(post.init?.body)).body).toBe(
|
|
288
|
+
"<!-- chant-reconcile:app -->\n\nthe plan",
|
|
289
|
+
);
|
|
290
|
+
} finally {
|
|
291
|
+
vi.unstubAllEnvs();
|
|
292
|
+
vi.unstubAllGlobals();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("PUTs the note it already owns instead of stacking a second one", async () => {
|
|
297
|
+
stubGitlabEnv();
|
|
298
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
299
|
+
vi.stubGlobal("fetch", async (url: string, init?: RequestInit) => {
|
|
300
|
+
calls.push({ url, init });
|
|
301
|
+
if (!init?.method || init.method === "GET") {
|
|
302
|
+
return gitlabResponse([
|
|
303
|
+
{ id: 1, system: true, body: "changed the description" },
|
|
304
|
+
{ id: 2, system: false, body: "unrelated human note" },
|
|
305
|
+
{ id: 3, system: false, body: "<!-- chant-reconcile:app -->\n\nan older plan" },
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
return gitlabResponse({ id: 3 });
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
const result = await reconcilePr({ env: "app", mode: "comment", body: "a newer plan" });
|
|
312
|
+
const write = calls[calls.length - 1];
|
|
313
|
+
expect(write.init?.method).toBe("PUT");
|
|
314
|
+
expect(write.url).toBe("https://gitlab.com/api/v4/projects/42/merge_requests/7/notes/3");
|
|
315
|
+
expect(result.commentUrl).toBe("https://gitlab.com/acme/infra/-/merge_requests/7#note_3");
|
|
316
|
+
} finally {
|
|
317
|
+
vi.unstubAllEnvs();
|
|
318
|
+
vi.unstubAllGlobals();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("follows GitLab's own pagination rather than reading page one alone", async () => {
|
|
323
|
+
stubGitlabEnv();
|
|
324
|
+
const seen: string[] = [];
|
|
325
|
+
vi.stubGlobal("fetch", async (url: string, init?: RequestInit) => {
|
|
326
|
+
if (!init?.method || init.method === "GET") {
|
|
327
|
+
seen.push(url);
|
|
328
|
+
if (url.endsWith("page=1")) return gitlabResponse([{ id: 1, body: "nope" }], { "x-next-page": "2" });
|
|
329
|
+
return gitlabResponse([{ id: 9, body: "<!-- chant-reconcile:app -->\n\nold" }], { "x-next-page": "" });
|
|
330
|
+
}
|
|
331
|
+
return gitlabResponse({ id: 9 });
|
|
332
|
+
});
|
|
333
|
+
try {
|
|
334
|
+
await reconcilePr({ env: "app", mode: "comment", body: "new" });
|
|
335
|
+
expect(seen).toHaveLength(2);
|
|
336
|
+
expect(seen[1]).toContain("page=2");
|
|
337
|
+
} finally {
|
|
338
|
+
vi.unstubAllEnvs();
|
|
339
|
+
vi.unstubAllGlobals();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("a rejected write fails the step by name, carrying GitLab's own status", async () => {
|
|
344
|
+
stubGitlabEnv();
|
|
345
|
+
vi.stubGlobal("fetch", async (_url: string, init?: RequestInit) => {
|
|
346
|
+
if (!init?.method || init.method === "GET") return gitlabResponse([]);
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
status: 403,
|
|
350
|
+
headers: { get: () => null },
|
|
351
|
+
json: async () => ({}),
|
|
352
|
+
text: async () => '{"message":"403 Forbidden"}',
|
|
353
|
+
} as unknown as Response;
|
|
354
|
+
});
|
|
355
|
+
try {
|
|
356
|
+
await expect(reconcilePr({ env: "app", mode: "comment", body: "plan" })).rejects.toThrow(
|
|
357
|
+
/merge_requests\/7\/notes.*403.*403 Forbidden/s,
|
|
358
|
+
);
|
|
359
|
+
} finally {
|
|
360
|
+
vi.unstubAllEnvs();
|
|
361
|
+
vi.unstubAllGlobals();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("a merge-request pipeline with no token says which variable to set", async () => {
|
|
366
|
+
for (const [k, v] of Object.entries(gitlabEnv)) vi.stubEnv(k, k === "GITLAB_TOKEN" ? "" : v);
|
|
367
|
+
vi.stubEnv("CI_JOB_TOKEN", "");
|
|
368
|
+
vi.stubEnv("CHANT_GITLAB_TOKEN", "");
|
|
369
|
+
try {
|
|
370
|
+
await expect(reconcilePr({ env: "app", mode: "comment", body: "plan" })).rejects.toThrow(
|
|
371
|
+
/GITLAB_TOKEN.*CI_JOB_TOKEN/s,
|
|
372
|
+
);
|
|
373
|
+
} finally {
|
|
374
|
+
vi.unstubAllEnvs();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("the no-context refusal names both forges' variables (#2256)", () => {
|
|
380
|
+
test("a run on neither forge is told what GitLab would have set too", async () => {
|
|
381
|
+
for (const k of [
|
|
382
|
+
"GITHUB_REPOSITORY",
|
|
383
|
+
"GITHUB_REF",
|
|
384
|
+
"GITHUB_EVENT_PATH",
|
|
385
|
+
"CI_MERGE_REQUEST_IID",
|
|
386
|
+
"CI_PROJECT_ID",
|
|
387
|
+
"CI_API_V4_URL",
|
|
388
|
+
]) {
|
|
389
|
+
vi.stubEnv(k, "");
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
await expect(reconcilePr({ env: "app", mode: "comment", body: "plan" })).rejects.toThrow(
|
|
393
|
+
/CI_MERGE_REQUEST_IID/,
|
|
394
|
+
);
|
|
395
|
+
} finally {
|
|
396
|
+
vi.unstubAllEnvs();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|