govgate 0.3.3 → 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 +77 -0
- 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
|
}
|
|
@@ -217,6 +222,9 @@ function loadFileConfig(path) {
|
|
|
217
222
|
throw new ConfigError(`${path}: "defaultEnvironment" must be a string`);
|
|
218
223
|
out.defaultEnvironment = cfg.defaultEnvironment;
|
|
219
224
|
}
|
|
225
|
+
if (cfg.environments !== void 0) {
|
|
226
|
+
out.environments = parseEnvironments(path, cfg.environments);
|
|
227
|
+
}
|
|
220
228
|
if (cfg.mappings !== void 0) {
|
|
221
229
|
if (typeof cfg.mappings !== "object" || cfg.mappings === null)
|
|
222
230
|
throw new ConfigError(`${path}: "mappings" must be an object`);
|
|
@@ -232,6 +240,38 @@ function loadFileConfig(path) {
|
|
|
232
240
|
}
|
|
233
241
|
return out;
|
|
234
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
|
+
}
|
|
235
275
|
function loadFileMappings(flags, cwd = process.cwd()) {
|
|
236
276
|
const configPath = findConfigFile(cwd, flags.config);
|
|
237
277
|
const file = configPath ? loadFileConfig(configPath) : {};
|
|
@@ -276,6 +316,7 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
276
316
|
apiKey,
|
|
277
317
|
suite,
|
|
278
318
|
environment,
|
|
319
|
+
environments: file.environments,
|
|
279
320
|
mappings: file.mappings ?? {},
|
|
280
321
|
configPath
|
|
281
322
|
};
|
|
@@ -596,6 +637,18 @@ program.command("report").description("Parse test result files (JUnit XML), map
|
|
|
596
637
|
const ci = detectCiContext();
|
|
597
638
|
let runId = opts.runId ?? "";
|
|
598
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
|
+
}
|
|
599
652
|
const run = await api.createRun({
|
|
600
653
|
suiteSlug: config.suite,
|
|
601
654
|
environmentSlug: config.environment,
|
|
@@ -638,6 +691,30 @@ function tryResolveConfig(flags) {
|
|
|
638
691
|
return void 0;
|
|
639
692
|
}
|
|
640
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
|
+
});
|
|
641
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) => {
|
|
642
719
|
try {
|
|
643
720
|
const config = resolveConfig({
|
package/package.json
CHANGED