govgate 0.3.3 → 0.5.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 +37 -1
- package/dist/index.js +121 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ standard reporter.
|
|
|
23
23
|
| `--api-key <key>` | `GOVGATE_API_KEY` | API key (env var strongly preferred) |
|
|
24
24
|
| `--config <path>` | `govgate/config.json` searched upward | Config file |
|
|
25
25
|
| `--suite <slug>` | config `suite` | Target suite |
|
|
26
|
-
| `--env <slug>` | config `defaultEnvironment` | Environment for the run |
|
|
26
|
+
| `--env <slug>` | inferred, else config `defaultEnvironment` | Environment for the run (overrides inference) |
|
|
27
|
+
| `--base-url <url>` | `API_BASEURL` | Live app base URL; matched against the declared environments' `baseUrl`s to infer the environment |
|
|
27
28
|
| `--name <name>` | derived from CI context | Run name |
|
|
28
29
|
| `--build-version <v>` | short commit SHA from CI | Build version |
|
|
29
30
|
| `--external-url <url>` | CI build URL | "Build ↗" link on the run page |
|
|
@@ -40,6 +41,32 @@ standard reporter.
|
|
|
40
41
|
Completes a run left open by `--no-complete` and evaluates the gate over **all** results
|
|
41
42
|
in the run (including those posted by other jobs).
|
|
42
43
|
|
|
44
|
+
### `govgate sync-env`
|
|
45
|
+
|
|
46
|
+
Upserts the environments declared in `govgate/config.json` to the tool. Idempotent by
|
|
47
|
+
slug: an existing `dev` is updated in place, never duplicated. Takes `--url`,
|
|
48
|
+
`--api-key`, `--config` (same resolution as `report`). `report` also runs this sync
|
|
49
|
+
automatically before creating a run when the block is present, so a brand-new stage
|
|
50
|
+
can never fail on an unknown environment — the explicit command exists for
|
|
51
|
+
onboarding-time provisioning and for verifying the catalog without reporting.
|
|
52
|
+
|
|
53
|
+
## Environment inference (v0.5.0+)
|
|
54
|
+
|
|
55
|
+
When `--env` is absent, `report` resolves the environment **from the deployment
|
|
56
|
+
under test**: the base URL (from `--base-url` or the `API_BASEURL` env var — the
|
|
57
|
+
same variable your smoke tests already use) is matched against the declared
|
|
58
|
+
environments' `baseUrl`s, and the matching slug wins. Falls back to
|
|
59
|
+
`defaultEnvironment` when nothing matches. Matching ignores scheme, casing, and
|
|
60
|
+
trailing slashes.
|
|
61
|
+
|
|
62
|
+
This makes one identical report step correct in every stage — no per-stage
|
|
63
|
+
`--env` variable to configure or mis-copy. The chosen slug is always printed to
|
|
64
|
+
the CI log.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
npx govgate report "test-results/*.xml" --fail-on fail # env inferred from API_BASEURL
|
|
68
|
+
```
|
|
69
|
+
|
|
43
70
|
## Exit codes
|
|
44
71
|
|
|
45
72
|
| Code | Meaning |
|
|
@@ -67,6 +94,15 @@ Resolution per test (results are unioned):
|
|
|
67
94
|
{
|
|
68
95
|
"suite": "checkout-regression",
|
|
69
96
|
"defaultEnvironment": "dev",
|
|
97
|
+
// Code-first environment catalog (v0.4.0+): synced to the tool by slug
|
|
98
|
+
// (existing slugs update in place — never duplicated). Slugs are scoped per
|
|
99
|
+
// application, so the plain dev/uat/prod trio is safe for every service; add
|
|
100
|
+
// more (test, pre-prod, demo, …) as needed, up to 20.
|
|
101
|
+
"environments": [
|
|
102
|
+
{ "slug": "dev", "name": "DEV", "baseUrl": "https://dev.example.com" },
|
|
103
|
+
{ "slug": "uat", "name": "UAT" },
|
|
104
|
+
{ "slug": "prod", "name": "PROD", "baseUrl": "https://www.example.com", "notes": "Live." }
|
|
105
|
+
],
|
|
70
106
|
"mappings": {
|
|
71
107
|
"4": ["checkout.spec.ts::pays with saved card"],
|
|
72
108
|
"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,60 @@ 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
|
+
}
|
|
275
|
+
function normalizeBaseUrl(raw) {
|
|
276
|
+
const trimmed = raw.trim();
|
|
277
|
+
if (trimmed === "") return void 0;
|
|
278
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
279
|
+
let url;
|
|
280
|
+
try {
|
|
281
|
+
url = new URL(withScheme);
|
|
282
|
+
} catch {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
286
|
+
return `${url.host}${path}`.toLowerCase();
|
|
287
|
+
}
|
|
288
|
+
function inferEnvironment(environments, baseUrl) {
|
|
289
|
+
if (!baseUrl || !environments?.length) return void 0;
|
|
290
|
+
const target = normalizeBaseUrl(baseUrl);
|
|
291
|
+
if (!target) return void 0;
|
|
292
|
+
for (const env of environments) {
|
|
293
|
+
if (env.baseUrl && normalizeBaseUrl(env.baseUrl) === target) return env.slug;
|
|
294
|
+
}
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
235
297
|
function loadFileMappings(flags, cwd = process.cwd()) {
|
|
236
298
|
const configPath = findConfigFile(cwd, flags.config);
|
|
237
299
|
const file = configPath ? loadFileConfig(configPath) : {};
|
|
@@ -248,7 +310,22 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
248
310
|
const url = flags.url ?? process.env.GOVGATE_URL;
|
|
249
311
|
const apiKey = flags.apiKey ?? process.env.GOVGATE_API_KEY;
|
|
250
312
|
const suite = flags.suite ?? file.suite;
|
|
251
|
-
|
|
313
|
+
let environment = flags.env;
|
|
314
|
+
if (!environment) {
|
|
315
|
+
const liveBaseUrl = flags.baseUrl ?? process.env.API_BASEURL;
|
|
316
|
+
const inferred = inferEnvironment(file.environments, liveBaseUrl);
|
|
317
|
+
if (inferred) {
|
|
318
|
+
console.error(`govgate: environment '${inferred}' inferred from base URL ${liveBaseUrl}`);
|
|
319
|
+
environment = inferred;
|
|
320
|
+
} else {
|
|
321
|
+
if (liveBaseUrl && file.environments?.some((e) => e.baseUrl)) {
|
|
322
|
+
console.error(
|
|
323
|
+
`govgate: base URL ${liveBaseUrl} matches no declared environment baseUrl` + (file.defaultEnvironment ? `; falling back to defaultEnvironment '${file.defaultEnvironment}'` : " and no defaultEnvironment is set")
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
environment = file.defaultEnvironment;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
252
329
|
if (!url) {
|
|
253
330
|
throw new ConfigError(
|
|
254
331
|
"Tool URL missing. Set GOVGATE_URL or pass --url https://<your-deployment>"
|
|
@@ -276,6 +353,7 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
276
353
|
apiKey,
|
|
277
354
|
suite,
|
|
278
355
|
environment,
|
|
356
|
+
environments: file.environments,
|
|
279
357
|
mappings: file.mappings ?? {},
|
|
280
358
|
configPath
|
|
281
359
|
};
|
|
@@ -561,14 +639,18 @@ async function completeAndGate(api, url, runId, gate) {
|
|
|
561
639
|
if (!verdict.passed) process.exit(EXIT_GATE_FAILED);
|
|
562
640
|
}
|
|
563
641
|
var program = new Command().name("govgate").description("Report CI test results to your testing tool and gate releases on the outcome.");
|
|
564
|
-
program.command("report").description("Parse test result files (JUnit XML), map tests to cases, and report a run").argument("<files...>", "result files or globs, e.g. test-results/*.xml").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)").option("--suite <slug>", "suite slug (overrides config)").option("--env <slug>", "environment slug (overrides config
|
|
642
|
+
program.command("report").description("Parse test result files (JUnit XML), map tests to cases, and report a run").argument("<files...>", "result files or globs, e.g. test-results/*.xml").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)").option("--suite <slug>", "suite slug (overrides config)").option("--env <slug>", "environment slug (overrides config and inference)").option(
|
|
643
|
+
"--base-url <url>",
|
|
644
|
+
"base URL of the app under test; matched against the declared environments' baseUrls to infer the environment (default: API_BASEURL)"
|
|
645
|
+
).option("--name <name>", "run name (default: derived from CI context)").option("--build-version <v>", "build version (default: short commit SHA from CI)").option("--external-url <url>", "link to the build/PR (default: from CI context)").option("--format <format>", "input format", "junit").option("--run-id <uuid>", "append results to an existing run instead of creating one").option("--no-complete", "leave the run in progress (multi-job pipelines); skips the gate").option("--fail-on <list>", "statuses that fail the gate: fail | fail,blocked", "fail").option("--min-executed <pct>", "minimum executed percentage required by the gate").option("--actor <label>", "tested-by label on results", "govgate").option("--dry-run", "parse and map only; post nothing").action(async (files, opts) => {
|
|
565
646
|
try {
|
|
566
647
|
const flags = {
|
|
567
648
|
url: opts.url,
|
|
568
649
|
apiKey: opts.apiKey,
|
|
569
650
|
suite: opts.suite,
|
|
570
651
|
env: opts.env,
|
|
571
|
-
config: opts.config
|
|
652
|
+
config: opts.config,
|
|
653
|
+
baseUrl: opts.baseUrl
|
|
572
654
|
};
|
|
573
655
|
const tests = await parseFiles(files, opts.format);
|
|
574
656
|
const config = opts.dryRun ? tryResolveConfig(flags) : resolveConfig(flags);
|
|
@@ -596,6 +678,18 @@ program.command("report").description("Parse test result files (JUnit XML), map
|
|
|
596
678
|
const ci = detectCiContext();
|
|
597
679
|
let runId = opts.runId ?? "";
|
|
598
680
|
if (!runId) {
|
|
681
|
+
if (config.environments?.length) {
|
|
682
|
+
try {
|
|
683
|
+
const sync = await api.putEnvironments(config.environments);
|
|
684
|
+
if (sync.created > 0) {
|
|
685
|
+
console.log(`Created ${sync.created} declared environment(s).`);
|
|
686
|
+
}
|
|
687
|
+
} catch (e) {
|
|
688
|
+
console.error(
|
|
689
|
+
`Warning: environment sync failed (${e instanceof Error ? e.message : String(e)}) \u2014 continuing.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
599
693
|
const run = await api.createRun({
|
|
600
694
|
suiteSlug: config.suite,
|
|
601
695
|
environmentSlug: config.environment,
|
|
@@ -638,6 +732,30 @@ function tryResolveConfig(flags) {
|
|
|
638
732
|
return void 0;
|
|
639
733
|
}
|
|
640
734
|
}
|
|
735
|
+
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) => {
|
|
736
|
+
try {
|
|
737
|
+
const config = resolveConfig({
|
|
738
|
+
url: opts.url,
|
|
739
|
+
apiKey: opts.apiKey,
|
|
740
|
+
suite: "unused",
|
|
741
|
+
// environments sync does not involve a suite
|
|
742
|
+
config: opts.config
|
|
743
|
+
});
|
|
744
|
+
if (!config.environments?.length) {
|
|
745
|
+
throw new ConfigError(
|
|
746
|
+
`No "environments" declared in ${config.configPath ?? "govgate/config.json"}. Add e.g. { "environments": [{ "slug": "dev", "name": "DEV" }] } to that file.`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const api = new ApiClient(config.url, config.apiKey);
|
|
750
|
+
const res = await api.putEnvironments(config.environments);
|
|
751
|
+
console.log(`Environments synced: ${res.created} created, ${res.updated} updated.`);
|
|
752
|
+
for (const e of res.environments) {
|
|
753
|
+
console.log(` ${e.slug} ${e.name}${e.baseUrl ? ` ${e.baseUrl}` : ""}`);
|
|
754
|
+
}
|
|
755
|
+
} catch (e) {
|
|
756
|
+
handleError(e);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
641
759
|
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
760
|
try {
|
|
643
761
|
const config = resolveConfig({
|
package/package.json
CHANGED