govgate 0.3.2 → 0.4.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 +18 -0
- package/dist/index.js +155 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,15 @@ standard reporter.
|
|
|
40
40
|
Completes a run left open by `--no-complete` and evaluates the gate over **all** results
|
|
41
41
|
in the run (including those posted by other jobs).
|
|
42
42
|
|
|
43
|
+
### `govgate sync-env`
|
|
44
|
+
|
|
45
|
+
Upserts the environments declared in `govgate/config.json` to the tool. Idempotent by
|
|
46
|
+
slug: an existing `dev` is updated in place, never duplicated. Takes `--url`,
|
|
47
|
+
`--api-key`, `--config` (same resolution as `report`). `report` also runs this sync
|
|
48
|
+
automatically before creating a run when the block is present, so a brand-new stage
|
|
49
|
+
can never fail on an unknown environment — the explicit command exists for
|
|
50
|
+
onboarding-time provisioning and for verifying the catalog without reporting.
|
|
51
|
+
|
|
43
52
|
## Exit codes
|
|
44
53
|
|
|
45
54
|
| Code | Meaning |
|
|
@@ -67,6 +76,15 @@ Resolution per test (results are unioned):
|
|
|
67
76
|
{
|
|
68
77
|
"suite": "checkout-regression",
|
|
69
78
|
"defaultEnvironment": "dev",
|
|
79
|
+
// Code-first environment catalog (v0.4.0+): synced to the tool by slug
|
|
80
|
+
// (existing slugs update in place — never duplicated). Slugs are scoped per
|
|
81
|
+
// application, so the plain dev/uat/prod trio is safe for every service; add
|
|
82
|
+
// more (test, pre-prod, demo, …) as needed, up to 20.
|
|
83
|
+
"environments": [
|
|
84
|
+
{ "slug": "dev", "name": "DEV", "baseUrl": "https://dev.example.com" },
|
|
85
|
+
{ "slug": "uat", "name": "UAT" },
|
|
86
|
+
{ "slug": "prod", "name": "PROD", "baseUrl": "https://www.example.com", "notes": "Live." }
|
|
87
|
+
],
|
|
70
88
|
"mappings": {
|
|
71
89
|
"4": ["checkout.spec.ts::pays with saved card"],
|
|
72
90
|
"7": ["auth/*.spec.ts::*expired session*", "Login flow > redirects*"],
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,11 @@ var ApiClient = class {
|
|
|
46
46
|
}
|
|
47
47
|
return JSON.parse(text2);
|
|
48
48
|
}
|
|
49
|
+
// Idempotent by (application, slug): existing environments are updated in
|
|
50
|
+
// place, never duplicated. The API key scopes everything to one application.
|
|
51
|
+
putEnvironments(environments) {
|
|
52
|
+
return this.request("PUT", "/api/v1/environments", { environments });
|
|
53
|
+
}
|
|
49
54
|
createRun(input) {
|
|
50
55
|
return this.request("POST", "/api/v1/runs", { ...input, source: "ci" });
|
|
51
56
|
}
|
|
@@ -114,16 +119,27 @@ function emitRunIdVariable(runId, ctx, env = process.env) {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
// src/config.ts
|
|
117
|
-
import { readFileSync, existsSync } from "fs";
|
|
122
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
118
123
|
import { dirname, join, resolve } from "path";
|
|
119
124
|
var ConfigError = class extends Error {
|
|
120
125
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
127
|
+
"node_modules",
|
|
128
|
+
".git",
|
|
129
|
+
".next",
|
|
130
|
+
".vs",
|
|
131
|
+
".vscode",
|
|
132
|
+
".idea",
|
|
133
|
+
"dist",
|
|
134
|
+
"build",
|
|
135
|
+
"bin",
|
|
136
|
+
"obj",
|
|
137
|
+
"coverage",
|
|
138
|
+
".turbo",
|
|
139
|
+
".cache"
|
|
140
|
+
]);
|
|
141
|
+
var DOWNWARD_MAX_DEPTH = 6;
|
|
142
|
+
function findConfigUpward(startDir) {
|
|
127
143
|
let dir = resolve(startDir);
|
|
128
144
|
for (; ; ) {
|
|
129
145
|
const candidate = join(dir, "govgate", "config.json");
|
|
@@ -133,6 +149,58 @@ function findConfigFile(startDir, explicitPath) {
|
|
|
133
149
|
dir = parent;
|
|
134
150
|
}
|
|
135
151
|
}
|
|
152
|
+
function findConfigDownward(startDir) {
|
|
153
|
+
const root = resolve(startDir);
|
|
154
|
+
const found = [];
|
|
155
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
156
|
+
while (queue.length > 0) {
|
|
157
|
+
const { dir, depth } = queue.shift();
|
|
158
|
+
const direct = join(dir, "govgate", "config.json");
|
|
159
|
+
if (existsSync(direct)) found.push(direct);
|
|
160
|
+
if (depth >= DOWNWARD_MAX_DEPTH) continue;
|
|
161
|
+
let entries;
|
|
162
|
+
try {
|
|
163
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
164
|
+
} catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (!entry.isDirectory()) continue;
|
|
169
|
+
if (entry.name === "govgate") continue;
|
|
170
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
171
|
+
queue.push({ dir: join(dir, entry.name), depth: depth + 1 });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [...new Set(found)].sort();
|
|
175
|
+
}
|
|
176
|
+
function findConfigFile(startDir, explicitPath) {
|
|
177
|
+
if (explicitPath) {
|
|
178
|
+
const p = resolve(startDir, explicitPath);
|
|
179
|
+
if (!existsSync(p)) throw new ConfigError(`Config file not found: ${p}`);
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
const upward = findConfigUpward(startDir);
|
|
183
|
+
if (upward) return upward;
|
|
184
|
+
const downward = findConfigDownward(startDir);
|
|
185
|
+
if (downward.length === 1) {
|
|
186
|
+
console.error(
|
|
187
|
+
`govgate: no govgate/config.json at or above ${resolve(startDir)}; using discovered ${downward[0]}`
|
|
188
|
+
);
|
|
189
|
+
return downward[0];
|
|
190
|
+
}
|
|
191
|
+
if (downward.length > 1) {
|
|
192
|
+
throw new ConfigError(
|
|
193
|
+
`Ambiguous config: found ${downward.length} govgate/config.json files under ${resolve(
|
|
194
|
+
startDir
|
|
195
|
+
)} and none at or above it:
|
|
196
|
+
${downward.map((p) => ` - ${p}`).join(
|
|
197
|
+
"\n"
|
|
198
|
+
)}
|
|
199
|
+
Pick one with --config <path>, pass --suite <slug>, or run govgate from the intended directory.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
136
204
|
function loadFileConfig(path) {
|
|
137
205
|
let data;
|
|
138
206
|
try {
|
|
@@ -154,6 +222,9 @@ function loadFileConfig(path) {
|
|
|
154
222
|
throw new ConfigError(`${path}: "defaultEnvironment" must be a string`);
|
|
155
223
|
out.defaultEnvironment = cfg.defaultEnvironment;
|
|
156
224
|
}
|
|
225
|
+
if (cfg.environments !== void 0) {
|
|
226
|
+
out.environments = parseEnvironments(path, cfg.environments);
|
|
227
|
+
}
|
|
157
228
|
if (cfg.mappings !== void 0) {
|
|
158
229
|
if (typeof cfg.mappings !== "object" || cfg.mappings === null)
|
|
159
230
|
throw new ConfigError(`${path}: "mappings" must be an object`);
|
|
@@ -169,6 +240,38 @@ function loadFileConfig(path) {
|
|
|
169
240
|
}
|
|
170
241
|
return out;
|
|
171
242
|
}
|
|
243
|
+
var MAX_ENVIRONMENTS = 20;
|
|
244
|
+
var SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
245
|
+
function parseEnvironments(path, raw) {
|
|
246
|
+
if (!Array.isArray(raw)) throw new ConfigError(`${path}: "environments" must be an array`);
|
|
247
|
+
if (raw.length > MAX_ENVIRONMENTS)
|
|
248
|
+
throw new ConfigError(`${path}: "environments" supports at most ${MAX_ENVIRONMENTS} entries`);
|
|
249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const [i, entry] of raw.entries()) {
|
|
252
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
253
|
+
throw new ConfigError(`${path}: environments[${i}] must be an object`);
|
|
254
|
+
const e = entry;
|
|
255
|
+
if (typeof e.slug !== "string" || !SLUG_RE.test(e.slug))
|
|
256
|
+
throw new ConfigError(
|
|
257
|
+
`${path}: environments[${i}].slug must be a lowercase slug (a-z, 0-9, hyphens)`
|
|
258
|
+
);
|
|
259
|
+
if (seen.has(e.slug))
|
|
260
|
+
throw new ConfigError(`${path}: duplicate environment slug "${e.slug}"`);
|
|
261
|
+
seen.add(e.slug);
|
|
262
|
+
if (typeof e.name !== "string" || e.name.trim() === "")
|
|
263
|
+
throw new ConfigError(`${path}: environments[${i}].name must be a non-empty string`);
|
|
264
|
+
if (e.baseUrl !== void 0 && typeof e.baseUrl !== "string")
|
|
265
|
+
throw new ConfigError(`${path}: environments[${i}].baseUrl must be a string`);
|
|
266
|
+
if (e.notes !== void 0 && typeof e.notes !== "string")
|
|
267
|
+
throw new ConfigError(`${path}: environments[${i}].notes must be a string`);
|
|
268
|
+
const decl = { slug: e.slug, name: e.name };
|
|
269
|
+
if (e.baseUrl !== void 0) decl.baseUrl = e.baseUrl;
|
|
270
|
+
if (e.notes !== void 0) decl.notes = e.notes;
|
|
271
|
+
out.push(decl);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
172
275
|
function loadFileMappings(flags, cwd = process.cwd()) {
|
|
173
276
|
const configPath = findConfigFile(cwd, flags.config);
|
|
174
277
|
const file = configPath ? loadFileConfig(configPath) : {};
|
|
@@ -197,8 +300,15 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
197
300
|
);
|
|
198
301
|
}
|
|
199
302
|
if (!suite) {
|
|
303
|
+
if (!configPath) {
|
|
304
|
+
throw new ConfigError(
|
|
305
|
+
`No govgate/config.json found (searched upward from ${resolve(
|
|
306
|
+
cwd
|
|
307
|
+
)}, then downward). Run govgate from your repo root or the artifact/drop folder that contains govgate/, pass --config <path>, or pass --suite <slug>.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
200
310
|
throw new ConfigError(
|
|
201
|
-
|
|
311
|
+
`Suite missing in ${configPath}. Add { "suite": "<slug>" } to that file or pass --suite <slug>.`
|
|
202
312
|
);
|
|
203
313
|
}
|
|
204
314
|
return {
|
|
@@ -206,6 +316,7 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
206
316
|
apiKey,
|
|
207
317
|
suite,
|
|
208
318
|
environment,
|
|
319
|
+
environments: file.environments,
|
|
209
320
|
mappings: file.mappings ?? {},
|
|
210
321
|
configPath
|
|
211
322
|
};
|
|
@@ -526,6 +637,18 @@ program.command("report").description("Parse test result files (JUnit XML), map
|
|
|
526
637
|
const ci = detectCiContext();
|
|
527
638
|
let runId = opts.runId ?? "";
|
|
528
639
|
if (!runId) {
|
|
640
|
+
if (config.environments?.length) {
|
|
641
|
+
try {
|
|
642
|
+
const sync = await api.putEnvironments(config.environments);
|
|
643
|
+
if (sync.created > 0) {
|
|
644
|
+
console.log(`Created ${sync.created} declared environment(s).`);
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
console.error(
|
|
648
|
+
`Warning: environment sync failed (${e instanceof Error ? e.message : String(e)}) \u2014 continuing.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
529
652
|
const run = await api.createRun({
|
|
530
653
|
suiteSlug: config.suite,
|
|
531
654
|
environmentSlug: config.environment,
|
|
@@ -568,6 +691,30 @@ function tryResolveConfig(flags) {
|
|
|
568
691
|
return void 0;
|
|
569
692
|
}
|
|
570
693
|
}
|
|
694
|
+
program.command("sync-env").description("Upsert the environments declared in govgate/config.json (idempotent by slug)").option("--url <url>", "tool URL (or GOVGATE_URL)").option("--api-key <key>", "API key (prefer GOVGATE_API_KEY)").option("--config <path>", "path to govgate/config.json (default: searched upward)").action(async (opts) => {
|
|
695
|
+
try {
|
|
696
|
+
const config = resolveConfig({
|
|
697
|
+
url: opts.url,
|
|
698
|
+
apiKey: opts.apiKey,
|
|
699
|
+
suite: "unused",
|
|
700
|
+
// environments sync does not involve a suite
|
|
701
|
+
config: opts.config
|
|
702
|
+
});
|
|
703
|
+
if (!config.environments?.length) {
|
|
704
|
+
throw new ConfigError(
|
|
705
|
+
`No "environments" declared in ${config.configPath ?? "govgate/config.json"}. Add e.g. { "environments": [{ "slug": "dev", "name": "DEV" }] } to that file.`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const api = new ApiClient(config.url, config.apiKey);
|
|
709
|
+
const res = await api.putEnvironments(config.environments);
|
|
710
|
+
console.log(`Environments synced: ${res.created} created, ${res.updated} updated.`);
|
|
711
|
+
for (const e of res.environments) {
|
|
712
|
+
console.log(` ${e.slug} ${e.name}${e.baseUrl ? ` ${e.baseUrl}` : ""}`);
|
|
713
|
+
}
|
|
714
|
+
} catch (e) {
|
|
715
|
+
handleError(e);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
571
718
|
program.command("complete").description("Complete a run and evaluate the gate (final job of a multi-job pipeline)").requiredOption("--run-id <uuid>", "run to complete").option("--url <url>", "tool URL (or GOVGATE_URL)").option("--api-key <key>", "API key (prefer GOVGATE_API_KEY)").option("--config <path>", "path to govgate/config.json").option("--suite <slug>", "suite slug (only used for config resolution)").option("--fail-on <list>", "statuses that fail the gate: fail | fail,blocked", "fail").option("--min-executed <pct>", "minimum executed percentage required by the gate").action(async (opts) => {
|
|
572
719
|
try {
|
|
573
720
|
const config = resolveConfig({
|
package/package.json
CHANGED