@devrouter/cli 0.5.6 → 0.6.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/README.md +5 -2
- package/assets/preview/github-sweep.yml +23 -0
- package/assets/preview/github.yml +35 -0
- package/assets/preview/gitlab-template.yml +34 -0
- package/assets/preview/gitlab.yml +22 -0
- package/assets/preview/installation.json +54 -0
- package/dist/devrouter-lifecycle-worker.js +6 -4
- package/dist/devrouter-preview-gitlab-ingress.js +182 -0
- package/dist/devrouter.js +7405 -1283
- package/package.json +10 -2
- package/upgrade-prompts/0.6.0.md +57 -0
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ can share their standard ports through TLS/SNI hostname routing.
|
|
|
8
8
|
|
|
9
9
|
## What devrouter owns
|
|
10
10
|
|
|
11
|
-
- One strict
|
|
11
|
+
- One strict local routing/profile configuration: `.devrouter.yml`.
|
|
12
|
+
- Opt-in remote previews in `.devrouter.preview.yml`, with reviewed GitHub/GitLab controller source and Northflank qualification gates.
|
|
12
13
|
- Machine setup, the shared `devnet` network, Traefik, local TLS, and route state.
|
|
13
14
|
- Host, Docker, proxy, and dependency-only application definitions.
|
|
14
15
|
- Exact-checkout devcontainer workspace lifecycle (DevPod or Devsy) for primary and linked Git worktrees.
|
|
@@ -153,7 +154,9 @@ explicit `--write-agents` or `--write-skill` flags.
|
|
|
153
154
|
- [Domain context](./CONTEXT.md) — shared vocabulary consumed by Matt Pocock planning skills.
|
|
154
155
|
- [Contributor guide](./AGENTS.md) — source map, invariants, validation, and release checklist.
|
|
155
156
|
- [Examples](./examples/routing/README.md) — routing without a devcontainer; see also the [managed DevPod example](./examples/devcontainer/README.md) and [parallel workspace example](./examples/workspace/README.md).
|
|
156
|
-
- [Project records](./docs/project/
|
|
157
|
+
- [Project records](./docs/project/) — dated plans and roadmap state.
|
|
157
158
|
- [Release history](./CHANGELOG.md) and [adaptation prompts](./upgrade-prompts/) — versioned change guidance.
|
|
158
159
|
|
|
159
160
|
Run `devrouter --help` for the complete current command and option reference.
|
|
161
|
+
|
|
162
|
+
Remote review environments use opt-in [planning and reviewed installation commands](docs/PREVIEWS.md).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Devrouter installation sweep
|
|
2
|
+
on:
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: '*/5 * * * *'
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
id-token: write
|
|
9
|
+
jobs:
|
|
10
|
+
sweep:
|
|
11
|
+
if: github.event_name == 'schedule' || github.ref_name == github.event.repository.default_branch
|
|
12
|
+
uses: rschlaefli/devrouter/.github/workflows/preview.yml@__CONTROLLER_REF__
|
|
13
|
+
with:
|
|
14
|
+
controller-ref: __CONTROLLER_REF__
|
|
15
|
+
policy-repository: __POLICY_REPOSITORY__
|
|
16
|
+
policy-ref: __POLICY_REF__
|
|
17
|
+
installation: __INSTALLATION__
|
|
18
|
+
mode: sweep
|
|
19
|
+
azure-client-id: ${{ vars.DEVROUTER_PREVIEW_AZURE_CLIENT_ID }}
|
|
20
|
+
azure-tenant-id: ${{ vars.DEVROUTER_PREVIEW_AZURE_TENANT_ID }}
|
|
21
|
+
secrets:
|
|
22
|
+
policy-read-token: ${{ secrets.DEVROUTER_PREVIEW_POLICY_READ_TOKEN }}
|
|
23
|
+
credentials: ${{ secrets.DEVROUTER_PREVIEW_CREDENTIALS }}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Devrouter previews
|
|
2
|
+
on:
|
|
3
|
+
issue_comment:
|
|
4
|
+
types: [created]
|
|
5
|
+
pull_request_target:
|
|
6
|
+
types: [closed, synchronize, reopened]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
change:
|
|
10
|
+
description: PR number
|
|
11
|
+
type: string
|
|
12
|
+
required: true
|
|
13
|
+
# Install a separate central caller with mode=sweep and a */5 schedule once per installation.
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
id-token: write
|
|
17
|
+
jobs:
|
|
18
|
+
preview:
|
|
19
|
+
if: >-
|
|
20
|
+
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview ')) ||
|
|
21
|
+
github.event_name == 'pull_request_target' ||
|
|
22
|
+
(github.event_name == 'workflow_dispatch' && github.ref_name == github.event.repository.default_branch)
|
|
23
|
+
uses: rschlaefli/devrouter/.github/workflows/preview.yml@__CONTROLLER_REF__
|
|
24
|
+
with:
|
|
25
|
+
controller-ref: __CONTROLLER_REF__
|
|
26
|
+
policy-repository: __POLICY_REPOSITORY__
|
|
27
|
+
policy-ref: __POLICY_REF__
|
|
28
|
+
installation: __INSTALLATION__
|
|
29
|
+
project: __PROJECT__
|
|
30
|
+
change: ${{ format('{0}', github.event.issue.number || github.event.pull_request.number || inputs.change) }}
|
|
31
|
+
azure-client-id: ${{ vars.DEVROUTER_PREVIEW_AZURE_CLIENT_ID }}
|
|
32
|
+
azure-tenant-id: ${{ vars.DEVROUTER_PREVIEW_AZURE_TENANT_ID }}
|
|
33
|
+
secrets:
|
|
34
|
+
policy-read-token: ${{ secrets.DEVROUTER_PREVIEW_POLICY_READ_TOKEN }}
|
|
35
|
+
credentials: ${{ secrets.DEVROUTER_PREVIEW_CREDENTIALS }}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Before activation, set controller project pipeline-variable overrides to no_one_allowed.
|
|
2
|
+
# A trusted controller project vendors or immutably includes this template.
|
|
3
|
+
spec:
|
|
4
|
+
inputs:
|
|
5
|
+
controller_image:
|
|
6
|
+
regex: '^[A-Za-z0-9][A-Za-z0-9._:/-]*@sha256:[a-f0-9]{64}$'
|
|
7
|
+
preview_mode:
|
|
8
|
+
default: sweep
|
|
9
|
+
options: [handle-event, sweep]
|
|
10
|
+
preview_project:
|
|
11
|
+
default: ''
|
|
12
|
+
regex: '^[a-z0-9-]*$'
|
|
13
|
+
preview_change:
|
|
14
|
+
default: ''
|
|
15
|
+
regex: '^[0-9]*$'
|
|
16
|
+
---
|
|
17
|
+
devrouter:preview:
|
|
18
|
+
image: $[[ inputs.controller_image ]]
|
|
19
|
+
stage: deploy
|
|
20
|
+
timeout: 18m
|
|
21
|
+
variables:
|
|
22
|
+
GIT_STRATEGY: none
|
|
23
|
+
id_tokens:
|
|
24
|
+
DEVROUTER_PREVIEW_AZURE_ASSERTION:
|
|
25
|
+
aud: api://AzureADTokenExchange
|
|
26
|
+
rules:
|
|
27
|
+
- if: '$CI_COMMIT_REF_PROTECTED == "true" && ($CI_PIPELINE_SOURCE == "trigger" || $CI_PIPELINE_SOURCE == "schedule")'
|
|
28
|
+
- when: never
|
|
29
|
+
script:
|
|
30
|
+
- >-
|
|
31
|
+
node /opt/devrouter/dist/devrouter.js preview '$[[ inputs.preview_mode ]]'
|
|
32
|
+
--installation /opt/devrouter-policy/installation.json
|
|
33
|
+
--project '$[[ inputs.preview_project ]]'
|
|
34
|
+
--change '$[[ inputs.preview_change ]]' --json
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Install in a trusted controller project, outside candidate branch pipelines.
|
|
2
|
+
spec:
|
|
3
|
+
inputs:
|
|
4
|
+
preview_mode:
|
|
5
|
+
default: sweep
|
|
6
|
+
options: [handle-event, sweep]
|
|
7
|
+
preview_project:
|
|
8
|
+
default: ''
|
|
9
|
+
regex: '^[a-z0-9-]*$'
|
|
10
|
+
preview_change:
|
|
11
|
+
default: ''
|
|
12
|
+
regex: '^[0-9]*$'
|
|
13
|
+
---
|
|
14
|
+
stages: [deploy]
|
|
15
|
+
include:
|
|
16
|
+
- remote: https://raw.githubusercontent.com/rschlaefli/devrouter/__CONTROLLER_REF__/assets/preview/gitlab-template.yml
|
|
17
|
+
integrity: __TEMPLATE_INTEGRITY__
|
|
18
|
+
inputs:
|
|
19
|
+
controller_image: __CONTROLLER_IMAGE__
|
|
20
|
+
preview_mode: $[[ inputs.preview_mode ]]
|
|
21
|
+
preview_project: $[[ inputs.preview_project ]]
|
|
22
|
+
preview_change: $[[ inputs.preview_change ]]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"installationId": "example",
|
|
4
|
+
"state": {
|
|
5
|
+
"containerUrl": "https://examplestore.blob.core.windows.net/previews",
|
|
6
|
+
"tokenEnv": "DEVROUTER_PREVIEW_STATE_TOKEN"
|
|
7
|
+
},
|
|
8
|
+
"maxActive": 2,
|
|
9
|
+
"maxReservedCents": 500,
|
|
10
|
+
"projects": [
|
|
11
|
+
{
|
|
12
|
+
"key": "web",
|
|
13
|
+
"forge": {
|
|
14
|
+
"kind": "github",
|
|
15
|
+
"host": "github.com",
|
|
16
|
+
"apiUrl": "https://api.github.com/",
|
|
17
|
+
"repository": "example/app",
|
|
18
|
+
"projectId": "1",
|
|
19
|
+
"botId": "2",
|
|
20
|
+
"tokenEnv": "DEVROUTER_PREVIEW_GITHUB_TOKEN"
|
|
21
|
+
},
|
|
22
|
+
"config": { "file": "app/.devrouter.yml", "sha256": "REPLACE_WITH_REVIEWED_FILE_SHA256" },
|
|
23
|
+
"manifest": {
|
|
24
|
+
"file": "app/.devrouter.preview.yml",
|
|
25
|
+
"sha256": "REPLACE_WITH_REVIEWED_FILE_SHA256"
|
|
26
|
+
},
|
|
27
|
+
"northflank": {
|
|
28
|
+
"projectId": "example-previews",
|
|
29
|
+
"tokenEnv": "DEVROUTER_PREVIEW_NORTHFLANK_TOKEN",
|
|
30
|
+
"graph": {
|
|
31
|
+
"namespace": "example",
|
|
32
|
+
"baseDomain": "preview.example.org",
|
|
33
|
+
"allowedNetworks": ["192.0.2.0/24"],
|
|
34
|
+
"resources": {
|
|
35
|
+
"web": {
|
|
36
|
+
"deploymentPlan": "nf-compute-10",
|
|
37
|
+
"readiness": { "port": "http", "path": "/health" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"qualification": {
|
|
42
|
+
"fixedDeadline": false,
|
|
43
|
+
"sameEnvironment": false,
|
|
44
|
+
"deletionFence": false,
|
|
45
|
+
"scopedRuntimeSecrets": false,
|
|
46
|
+
"restrictedIngress": false
|
|
47
|
+
},
|
|
48
|
+
"runtimeSecrets": {}
|
|
49
|
+
},
|
|
50
|
+
"hourlyCents": { "web": 1 },
|
|
51
|
+
"acceptance": { "command": ["/bin/false"], "environment": [] }
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -5490,7 +5490,7 @@ function retain(session, previous) {
|
|
|
5490
5490
|
|
|
5491
5491
|
// src/core/reliability-operation-store.ts
|
|
5492
5492
|
function installedCliVersion() {
|
|
5493
|
-
return true ? "0.
|
|
5493
|
+
return true ? "0.6.0" : "0.0.0-dev";
|
|
5494
5494
|
}
|
|
5495
5495
|
function compareCliVersions(left, right) {
|
|
5496
5496
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -7578,6 +7578,9 @@ function resolveRepoPath(repoPath) {
|
|
|
7578
7578
|
function getRepoConfigPath(repoPath) {
|
|
7579
7579
|
return import_node_path23.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
7580
7580
|
}
|
|
7581
|
+
function parseRepoConfigSource(source, configPath) {
|
|
7582
|
+
return parseConfig(import_yaml3.default.parse(source) ?? {}, configPath);
|
|
7583
|
+
}
|
|
7581
7584
|
function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.readFileSync(file, "utf-8")) {
|
|
7582
7585
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
7583
7586
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
@@ -7587,11 +7590,10 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.rea
|
|
|
7587
7590
|
);
|
|
7588
7591
|
}
|
|
7589
7592
|
const raw = read2(configPath);
|
|
7590
|
-
const
|
|
7591
|
-
const config = parseConfig(parsed ?? {}, configPath);
|
|
7593
|
+
const config = parseRepoConfigSource(raw, configPath);
|
|
7592
7594
|
const requiredVersion = config.devrouter?.version;
|
|
7593
7595
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
7594
|
-
const cliVersion = true ? "0.
|
|
7596
|
+
const cliVersion = true ? "0.6.0" : "0.0.0-dev";
|
|
7595
7597
|
if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
|
|
7596
7598
|
hasWarnedVersionMismatch = true;
|
|
7597
7599
|
process.stderr.write(
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core/preview/ingress/gitlab.ts
|
|
21
|
+
var gitlab_exports = {};
|
|
22
|
+
__export(gitlab_exports, {
|
|
23
|
+
createGitLabPreviewIngress: () => createGitLabPreviewIngress
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(gitlab_exports);
|
|
26
|
+
var import_node_crypto = require("crypto");
|
|
27
|
+
|
|
28
|
+
// src/core/preview/command.ts
|
|
29
|
+
var MAX_COMMAND_BYTES = 512;
|
|
30
|
+
var MAX_ALIAS_LENGTH = 63;
|
|
31
|
+
var ALIAS_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
32
|
+
function parsePreviewCommand(text) {
|
|
33
|
+
if (Buffer.byteLength(text, "utf8") > MAX_COMMAND_BYTES || /[\r\n]/u.test(text)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const command = text.trim();
|
|
37
|
+
if (command === "/preview start") {
|
|
38
|
+
return { kind: "start", aliases: [] };
|
|
39
|
+
}
|
|
40
|
+
if (command.startsWith("/preview start ")) {
|
|
41
|
+
return parseStart(command.slice("/preview start ".length));
|
|
42
|
+
}
|
|
43
|
+
if (command === "/preview extend" || command === "/preview extend 1h") {
|
|
44
|
+
return { kind: "extend" };
|
|
45
|
+
}
|
|
46
|
+
if (command === "/preview stop") return { kind: "stop" };
|
|
47
|
+
if (command === "/preview status") return { kind: "status" };
|
|
48
|
+
if (command === "/preview profiles") return { kind: "profiles" };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function isPreviewAlias(alias) {
|
|
52
|
+
return typeof alias === "string" && alias.length <= MAX_ALIAS_LENGTH && ALIAS_PATTERN.test(alias);
|
|
53
|
+
}
|
|
54
|
+
function parseStart(rawAliases) {
|
|
55
|
+
const aliases = rawAliases.split(",");
|
|
56
|
+
if (aliases.some((alias) => !isPreviewAlias(alias)) || new Set(aliases).size !== aliases.length) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { kind: "start", aliases };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/preview/http.ts
|
|
63
|
+
var MAX_STATE_BYTES = 1024 * 1024;
|
|
64
|
+
async function readPreviewResponse(response, label, maxBytes = MAX_STATE_BYTES) {
|
|
65
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 16 * MAX_STATE_BYTES)
|
|
66
|
+
throw new Error("Invalid response size bound.");
|
|
67
|
+
const length = response.headers.get("content-length");
|
|
68
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > maxBytes)) {
|
|
69
|
+
await response.body?.cancel();
|
|
70
|
+
throw new Error(`${label} response exceeds its size bound.`);
|
|
71
|
+
}
|
|
72
|
+
const reader = response.body?.getReader();
|
|
73
|
+
if (!reader) throw new Error(`${label} response is empty.`);
|
|
74
|
+
const chunks = [];
|
|
75
|
+
let size = 0;
|
|
76
|
+
try {
|
|
77
|
+
for (; ; ) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done) break;
|
|
80
|
+
size += value.byteLength;
|
|
81
|
+
if (size > maxBytes) throw new Error(`${label} response exceeds its size bound.`);
|
|
82
|
+
chunks.push(value);
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
await reader.cancel();
|
|
86
|
+
}
|
|
87
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/core/preview/forge/status.ts
|
|
91
|
+
function forgeObject(value) {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
93
|
+
throw new Error("Invalid forge object.");
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function forgeId(value) {
|
|
97
|
+
const id = typeof value === "number" && Number.isSafeInteger(value) ? String(value) : value;
|
|
98
|
+
if (typeof id !== "string" || !/^[1-9][0-9]{0,19}$/.test(id))
|
|
99
|
+
throw new Error("Invalid forge numeric identity.");
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/core/preview/ingress/gitlab.ts
|
|
104
|
+
function createGitLabPreviewIngress(options) {
|
|
105
|
+
const base = new URL(options.apiUrl);
|
|
106
|
+
if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash || base.port || base.pathname !== "/api/v4/" || !/^[A-Za-z0-9_./-]{1,128}$/.test(options.controllerRef) || options.controllerRef.includes("..") || !/^[a-z][a-z0-9-]{0,62}$/.test(options.installedProjectKey))
|
|
107
|
+
throw new Error("Invalid registered GitLab ingress policy.");
|
|
108
|
+
forgeId(options.controllerProjectId);
|
|
109
|
+
forgeId(options.candidateProjectId);
|
|
110
|
+
return async (request) => {
|
|
111
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
112
|
+
let secret;
|
|
113
|
+
try {
|
|
114
|
+
secret = await options.hookSecret();
|
|
115
|
+
} catch {
|
|
116
|
+
return new Response(null, { status: 503 });
|
|
117
|
+
}
|
|
118
|
+
const presented = request.headers.get("x-gitlab-token") ?? "";
|
|
119
|
+
if (secret.length < 32 || secret.length > 256 || presented.length > 256 || Buffer.byteLength(secret) !== Buffer.byteLength(presented) || !(0, import_node_crypto.timingSafeEqual)(Buffer.from(secret), Buffer.from(presented)))
|
|
120
|
+
return new Response(null, { status: 401 });
|
|
121
|
+
if (request.headers.get("content-type")?.split(";")[0] !== "application/json")
|
|
122
|
+
return new Response(null, { status: 415 });
|
|
123
|
+
if (request.headers.get("x-gitlab-event") !== "Note Hook" || !/^[a-f0-9-]{36}$/i.test(request.headers.get("x-gitlab-event-uuid") ?? ""))
|
|
124
|
+
return new Response(null, { status: 400 });
|
|
125
|
+
let payload;
|
|
126
|
+
try {
|
|
127
|
+
payload = forgeObject(
|
|
128
|
+
JSON.parse(await readPreviewResponse(new Response(request.body), "GitLab hook", 64 * 1024))
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
return new Response(null, { status: 400 });
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
if (payload.object_kind !== "note") return new Response(null, { status: 204 });
|
|
135
|
+
const note = forgeObject(payload.object_attributes);
|
|
136
|
+
if (note.noteable_type !== "MergeRequest") return new Response(null, { status: 204 });
|
|
137
|
+
const merge = forgeObject(payload.merge_request), project = forgeObject(payload.project);
|
|
138
|
+
if (forgeId(project.id) !== options.candidateProjectId || forgeId(note.project_id) !== options.candidateProjectId || note.action !== "create" || note.system !== false || note.created_at !== note.updated_at || note.line_code || typeof note.note !== "string" || !parsePreviewCommand(note.note) || merge.target_project_id !== Number(options.candidateProjectId) || merge.source_project_id !== Number(options.candidateProjectId))
|
|
139
|
+
return new Response(null, { status: 204 });
|
|
140
|
+
const change = forgeId(merge.iid);
|
|
141
|
+
forgeId(note.id);
|
|
142
|
+
let token;
|
|
143
|
+
try {
|
|
144
|
+
token = await options.triggerToken();
|
|
145
|
+
} catch {
|
|
146
|
+
return new Response(null, { status: 503 });
|
|
147
|
+
}
|
|
148
|
+
if (!token || /\s/.test(token)) return new Response(null, { status: 503 });
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = await (options.fetch ?? fetch)(
|
|
152
|
+
new URL(`projects/${options.controllerProjectId}/trigger/pipeline`, base),
|
|
153
|
+
{
|
|
154
|
+
method: "POST",
|
|
155
|
+
redirect: "error",
|
|
156
|
+
signal: AbortSignal.timeout(1e4),
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
token,
|
|
160
|
+
ref: options.controllerRef,
|
|
161
|
+
inputs: {
|
|
162
|
+
preview_mode: "handle-event",
|
|
163
|
+
preview_project: options.installedProjectKey,
|
|
164
|
+
preview_change: change
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
} catch {
|
|
170
|
+
return new Response(null, { status: 503 });
|
|
171
|
+
}
|
|
172
|
+
await response.body?.cancel();
|
|
173
|
+
return new Response(null, { status: response.ok ? 202 : 503 });
|
|
174
|
+
} catch {
|
|
175
|
+
return new Response(null, { status: 400 });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
180
|
+
0 && (module.exports = {
|
|
181
|
+
createGitLabPreviewIngress
|
|
182
|
+
});
|