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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +77 -0
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "govgate",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Governance gate for releases: report CI test results (JUnit XML) to your testing tool and gate promotion on the outcome.",
5
5
  "license": "MIT",
6
6
  "author": "HatStack",