@automatify-au/cli 0.1.11 → 0.1.13

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 +110 -1
  2. package/dist/automatify.cjs +1726 -32
  3. package/package.json +3 -2
@@ -2894,8 +2894,8 @@ var require_utils = __commonJS({
2894
2894
  var result = transform[inputType][outputType](input);
2895
2895
  return result;
2896
2896
  };
2897
- exports2.resolve = function(path14) {
2898
- var parts = path14.split("/");
2897
+ exports2.resolve = function(path16) {
2898
+ var parts = path16.split("/");
2899
2899
  var result = [];
2900
2900
  for (var index = 0; index < parts.length; index++) {
2901
2901
  var part = parts[index];
@@ -8748,18 +8748,18 @@ var require_object = __commonJS({
8748
8748
  var object = new ZipObject(name, zipObjectContent, o);
8749
8749
  this.files[name] = object;
8750
8750
  };
8751
- var parentFolder = function(path14) {
8752
- if (path14.slice(-1) === "/") {
8753
- path14 = path14.substring(0, path14.length - 1);
8751
+ var parentFolder = function(path16) {
8752
+ if (path16.slice(-1) === "/") {
8753
+ path16 = path16.substring(0, path16.length - 1);
8754
8754
  }
8755
- var lastSlash = path14.lastIndexOf("/");
8756
- return lastSlash > 0 ? path14.substring(0, lastSlash) : "";
8755
+ var lastSlash = path16.lastIndexOf("/");
8756
+ return lastSlash > 0 ? path16.substring(0, lastSlash) : "";
8757
8757
  };
8758
- var forceTrailingSlash = function(path14) {
8759
- if (path14.slice(-1) !== "/") {
8760
- path14 += "/";
8758
+ var forceTrailingSlash = function(path16) {
8759
+ if (path16.slice(-1) !== "/") {
8760
+ path16 += "/";
8761
8761
  }
8762
- return path14;
8762
+ return path16;
8763
8763
  };
8764
8764
  var folderAdd = function(name, createFolders) {
8765
8765
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -9725,9 +9725,9 @@ var require_load = __commonJS({
9725
9725
  var require_lib3 = __commonJS({
9726
9726
  "../../node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/lib/index.js"(exports2, module2) {
9727
9727
  "use strict";
9728
- function JSZip3() {
9729
- if (!(this instanceof JSZip3)) {
9730
- return new JSZip3();
9728
+ function JSZip4() {
9729
+ if (!(this instanceof JSZip4)) {
9730
+ return new JSZip4();
9731
9731
  }
9732
9732
  if (arguments.length) {
9733
9733
  throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");
@@ -9736,7 +9736,7 @@ var require_lib3 = __commonJS({
9736
9736
  this.comment = null;
9737
9737
  this.root = "";
9738
9738
  this.clone = function() {
9739
- var newObj = new JSZip3();
9739
+ var newObj = new JSZip4();
9740
9740
  for (var i in this) {
9741
9741
  if (typeof this[i] !== "function") {
9742
9742
  newObj[i] = this[i];
@@ -9745,16 +9745,16 @@ var require_lib3 = __commonJS({
9745
9745
  return newObj;
9746
9746
  };
9747
9747
  }
9748
- JSZip3.prototype = require_object();
9749
- JSZip3.prototype.loadAsync = require_load();
9750
- JSZip3.support = require_support();
9751
- JSZip3.defaults = require_defaults();
9752
- JSZip3.version = "3.10.1";
9753
- JSZip3.loadAsync = function(content, options) {
9754
- return new JSZip3().loadAsync(content, options);
9748
+ JSZip4.prototype = require_object();
9749
+ JSZip4.prototype.loadAsync = require_load();
9750
+ JSZip4.support = require_support();
9751
+ JSZip4.defaults = require_defaults();
9752
+ JSZip4.version = "3.10.1";
9753
+ JSZip4.loadAsync = function(content, options) {
9754
+ return new JSZip4().loadAsync(content, options);
9755
9755
  };
9756
- JSZip3.external = require_external();
9757
- module2.exports = JSZip3;
9756
+ JSZip4.external = require_external();
9757
+ module2.exports = JSZip4;
9758
9758
  }
9759
9759
  });
9760
9760
 
@@ -10723,6 +10723,9 @@ function toJsonLine(data) {
10723
10723
  var import_node_fs4 = require("node:fs");
10724
10724
  var import_node_path5 = __toESM(require("node:path"), 1);
10725
10725
 
10726
+ // ../../packages/shared-types/dist/entitlements.js
10727
+ var UNLIMITED_ACTIVE_USERS = Number.MAX_SAFE_INTEGER;
10728
+
10726
10729
  // ../../packages/shared-types/dist/index.js
10727
10730
  var TestCaseStatus;
10728
10731
  (function(TestCaseStatus2) {
@@ -12450,7 +12453,7 @@ function createAutoHandler(deps = {}) {
12450
12453
  var import_node_path10 = __toESM(require("node:path"), 1);
12451
12454
  var import_node_fs9 = require("node:fs");
12452
12455
 
12453
- // ../../packages/jira-client/src/adf.ts
12456
+ // ../../packages/jira-client/dist/adf.js
12454
12457
  function paragraph(text = "") {
12455
12458
  return text ? { type: "paragraph", content: [{ type: "text", text }] } : { type: "paragraph" };
12456
12459
  }
@@ -12493,7 +12496,7 @@ function buildAllureEvidenceCommentAdf(input) {
12493
12496
  };
12494
12497
  }
12495
12498
 
12496
- // ../../packages/jira-client/src/jiraClient.ts
12499
+ // ../../packages/jira-client/dist/jiraClient.js
12497
12500
  var import_node_fs5 = require("node:fs");
12498
12501
  var import_node_path6 = __toESM(require("node:path"), 1);
12499
12502
  function normalizeSite(site) {
@@ -12619,7 +12622,7 @@ async function addIssueComment(params) {
12619
12622
  };
12620
12623
  }
12621
12624
 
12622
- // ../../packages/allure-core/src/parseAllureZip.ts
12625
+ // ../../packages/allure-core/dist/parseAllureZip.js
12623
12626
  var import_node_fs6 = require("node:fs");
12624
12627
  var import_node_path7 = __toESM(require("node:path"), 1);
12625
12628
  var import_jszip = __toESM(require_lib3(), 1);
@@ -12681,7 +12684,7 @@ async function parseAllureReportZip(zipPath) {
12681
12684
  };
12682
12685
  }
12683
12686
 
12684
- // ../../packages/allure-core/src/extractAllureZip.ts
12687
+ // ../../packages/allure-core/dist/extractAllureZip.js
12685
12688
  var import_node_fs7 = require("node:fs");
12686
12689
  var import_node_path8 = __toESM(require("node:path"), 1);
12687
12690
  var import_jszip2 = __toESM(require_lib3(), 1);
@@ -12734,6 +12737,9 @@ async function extractAllureReportZip(zipPath, extractDir) {
12734
12737
  };
12735
12738
  }
12736
12739
 
12740
+ // ../../packages/allure-core/dist/parseAllureFailedTests.js
12741
+ var import_jszip3 = __toESM(require_lib3(), 1);
12742
+
12737
12743
  // src/allureOpen.ts
12738
12744
  var import_node_fs8 = require("node:fs");
12739
12745
  var import_promises = require("node:fs/promises");
@@ -16039,6 +16045,1688 @@ function createRunsHandler(deps = {}) {
16039
16045
  };
16040
16046
  }
16041
16047
 
16048
+ // src/setup.ts
16049
+ var import_node_fs14 = require("node:fs");
16050
+ var import_node_path15 = __toESM(require("node:path"), 1);
16051
+
16052
+ // src/githubSetupProvider.ts
16053
+ var import_node_child_process4 = require("node:child_process");
16054
+
16055
+ // src/setupPlan.ts
16056
+ var import_node_crypto = require("node:crypto");
16057
+ var import_node_fs13 = require("node:fs");
16058
+ var import_node_path14 = __toESM(require("node:path"), 1);
16059
+ var SETUP_PLAN_SCHEMA_VERSION = "automatify.testops.setup/v1";
16060
+ var SETUP_PLAN_KIND = "AutomatifyTestOpsSetupPlan";
16061
+ var ACTIONS = [
16062
+ {
16063
+ id: "write-workflow-file",
16064
+ scope: "workflow-file",
16065
+ summary: "Write the approved GitHub Actions workflow into the local repository checkout.",
16066
+ mutates: "local-file"
16067
+ },
16068
+ {
16069
+ id: "set-callback-secrets",
16070
+ scope: "github-secrets",
16071
+ summary: "Create missing GitHub Actions callback secrets without exposing their values.",
16072
+ mutates: "github"
16073
+ },
16074
+ {
16075
+ id: "upsert-jira-profile",
16076
+ scope: "jira-profile",
16077
+ summary: "Create or update the Jira scenario automation profile through Forge contracts.",
16078
+ mutates: "jira"
16079
+ }
16080
+ ];
16081
+ function sha256(value) {
16082
+ return (0, import_node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
16083
+ }
16084
+ function sortJsonValue(value) {
16085
+ if (Array.isArray(value)) {
16086
+ return value.map(sortJsonValue);
16087
+ }
16088
+ if (value && typeof value === "object") {
16089
+ return Object.fromEntries(
16090
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortJsonValue(nested)])
16091
+ );
16092
+ }
16093
+ return value;
16094
+ }
16095
+ function stableJson(value) {
16096
+ return JSON.stringify(sortJsonValue(value));
16097
+ }
16098
+ function computePlanId(planWithoutId) {
16099
+ return `sha256:${sha256(stableJson(planWithoutId))}`;
16100
+ }
16101
+ function withoutPlanId(plan) {
16102
+ const { planId: _planId, ...rest } = plan;
16103
+ return rest;
16104
+ }
16105
+ function requireNonEmpty(value, field) {
16106
+ const normalized = value.trim();
16107
+ if (!normalized) {
16108
+ throw new Error(`${field} is required.`);
16109
+ }
16110
+ return normalized;
16111
+ }
16112
+ function normalizeProjectKey(value) {
16113
+ const normalized = requireNonEmpty(value, "project key").toUpperCase();
16114
+ if (!/^[A-Z][A-Z0-9_]{1,31}$/.test(normalized)) {
16115
+ throw new Error("project key must contain 2-32 uppercase letters, digits, or underscores and start with a letter.");
16116
+ }
16117
+ return normalized;
16118
+ }
16119
+ function normalizeRepoPart(value, field) {
16120
+ const normalized = requireNonEmpty(value, field);
16121
+ if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
16122
+ throw new Error(`${field} contains unsupported characters.`);
16123
+ }
16124
+ return normalized;
16125
+ }
16126
+ function normalizeWorkflowPath(value) {
16127
+ const normalized = import_node_path14.default.posix.normalize(requireNonEmpty(value, "workflow path").replaceAll("\\", "/"));
16128
+ if (import_node_path14.default.posix.isAbsolute(normalized) || normalized.startsWith("../") || !normalized.startsWith(".github/workflows/") || !/\.ya?ml$/i.test(normalized)) {
16129
+ throw new Error("workflow path must be a relative .github/workflows/*.yml or *.yaml path without '..'.");
16130
+ }
16131
+ return normalized;
16132
+ }
16133
+ function normalizeSecretName(value, field) {
16134
+ const normalized = requireNonEmpty(value, field).toUpperCase();
16135
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(normalized)) {
16136
+ throw new Error(`${field} must be a valid GitHub Actions secret name.`);
16137
+ }
16138
+ return normalized;
16139
+ }
16140
+ function buildGitHubSetupPlan(input) {
16141
+ const projectKey = normalizeProjectKey(input.projectKey);
16142
+ const owner = normalizeRepoPart(input.owner, "GitHub owner");
16143
+ const repository = normalizeRepoPart(input.repository, "GitHub repository");
16144
+ const ref = requireNonEmpty(input.ref, "Git ref");
16145
+ const workflowId = requireNonEmpty(input.workflowId, "workflow id");
16146
+ const workflowPath = normalizeWorkflowPath(input.workflowPath);
16147
+ const profileLabel = requireNonEmpty(input.profileLabel, "profile label");
16148
+ const endpointSecretName = normalizeSecretName(
16149
+ input.callbackEndpointSecretName,
16150
+ "callback endpoint secret name"
16151
+ );
16152
+ const tokenSecretName = normalizeSecretName(
16153
+ input.callbackAuthTokenSecretName,
16154
+ "callback auth token secret name"
16155
+ );
16156
+ if (endpointSecretName === tokenSecretName) {
16157
+ throw new Error("callback endpoint and auth token secret names must be different.");
16158
+ }
16159
+ const workflowContent = (0, import_node_fs13.readFileSync)(input.workflowSourcePath, "utf8");
16160
+ if (!workflowContent.trim()) {
16161
+ throw new Error("workflow source file is empty.");
16162
+ }
16163
+ if (!/\bworkflow_dispatch\s*:/m.test(workflowContent)) {
16164
+ throw new Error("workflow source must declare workflow_dispatch so Forge can address it explicitly.");
16165
+ }
16166
+ for (const secretName of [endpointSecretName, tokenSecretName]) {
16167
+ if (!workflowContent.includes(`secrets.${secretName}`)) {
16168
+ throw new Error(`workflow source must reference GitHub Actions secret secrets.${secretName}.`);
16169
+ }
16170
+ }
16171
+ const actions = input.setProjectDefault ? [
16172
+ ...ACTIONS,
16173
+ {
16174
+ id: "set-project-default",
16175
+ scope: "project-default",
16176
+ summary: "Select the approved Jira automation profile as the project default through Forge contracts.",
16177
+ mutates: "jira"
16178
+ }
16179
+ ] : [...ACTIONS];
16180
+ const planWithoutId = {
16181
+ schemaVersion: SETUP_PLAN_SCHEMA_VERSION,
16182
+ kind: SETUP_PLAN_KIND,
16183
+ provider: "github-actions",
16184
+ project: { key: projectKey },
16185
+ github: {
16186
+ owner,
16187
+ repository,
16188
+ ref,
16189
+ workflowId,
16190
+ workflowFile: {
16191
+ path: workflowPath,
16192
+ sourcePath: import_node_path14.default.resolve(input.workflowSourcePath),
16193
+ sha256: sha256(workflowContent)
16194
+ },
16195
+ requiredSecrets: [
16196
+ {
16197
+ repositorySecretName: endpointSecretName,
16198
+ valueKey: "callbackEndpoint",
16199
+ purpose: "Authenticated Forge callback endpoint used by the workflow."
16200
+ },
16201
+ {
16202
+ repositorySecretName: tokenSecretName,
16203
+ valueKey: "callbackAuthToken",
16204
+ purpose: "Project-admin CLI key used by the workflow for Forge callbacks."
16205
+ }
16206
+ ],
16207
+ guidance: {
16208
+ authentication: "Prefer a GitHub App installation token or a fine-grained token with only the repository permissions required for workflow inspection, secrets administration, and workflow dispatch from Forge.",
16209
+ permissions: [
16210
+ "Actions: read (doctor) and write only when the Forge profile must dispatch workflow runs.",
16211
+ "Secrets: read/write metadata only for the explicit github-secrets apply scope.",
16212
+ "Contents: read for doctor; this command never creates commits or pull requests."
16213
+ ],
16214
+ remoteChangesExcluded: [
16215
+ "accounts",
16216
+ "credentials",
16217
+ "commits",
16218
+ "pull requests",
16219
+ "workflow runs"
16220
+ ]
16221
+ }
16222
+ },
16223
+ jiraProfile: {
16224
+ label: profileLabel,
16225
+ enabled: input.enabled,
16226
+ provider: "githubActions",
16227
+ config: {
16228
+ owner,
16229
+ repo: repository,
16230
+ workflowId,
16231
+ ref
16232
+ },
16233
+ setProjectDefault: input.setProjectDefault
16234
+ },
16235
+ actions,
16236
+ smokeValidation: [
16237
+ {
16238
+ id: "workflow-content",
16239
+ verifies: "Local and remote workflow content/path without dispatching it.",
16240
+ triggersExternalRun: false
16241
+ },
16242
+ {
16243
+ id: "callback-secret-metadata",
16244
+ verifies: "Required GitHub repository secret names exist; secret values remain unreadable.",
16245
+ triggersExternalRun: false
16246
+ },
16247
+ {
16248
+ id: "jira-profile",
16249
+ verifies: "The Forge automation profile and optional project default match the plan.",
16250
+ triggersExternalRun: false
16251
+ }
16252
+ ],
16253
+ rollback: [
16254
+ {
16255
+ actionId: "write-workflow-file",
16256
+ strategy: "Restore the previous file through version control or delete the newly created uncommitted file.",
16257
+ automatic: false
16258
+ },
16259
+ {
16260
+ actionId: "set-callback-secrets",
16261
+ strategy: "Rotate or delete the named GitHub repository secrets manually; previous secret values cannot be read or restored.",
16262
+ automatic: false
16263
+ },
16264
+ {
16265
+ actionId: "upsert-jira-profile",
16266
+ strategy: "Restore the pre-apply profile fields captured in the apply report, or delete a newly created unused profile through the existing Forge contract.",
16267
+ automatic: false
16268
+ },
16269
+ ...input.setProjectDefault ? [
16270
+ {
16271
+ actionId: "set-project-default",
16272
+ strategy: "Restore the pre-apply default profile id captured in the apply report.",
16273
+ automatic: false
16274
+ }
16275
+ ] : []
16276
+ ],
16277
+ followUp: {
16278
+ azureDevOps: "Add an Azure DevOps provider adapter over this schema/action boundary, mapping pipeline variables and the existing azureDevops Forge profile config without changing setup apply orchestration."
16279
+ }
16280
+ };
16281
+ return {
16282
+ ...planWithoutId,
16283
+ planId: computePlanId(planWithoutId)
16284
+ };
16285
+ }
16286
+ function isRecord(value) {
16287
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16288
+ }
16289
+ function hasExactKeys(value, keys) {
16290
+ const actual = Object.keys(value).sort();
16291
+ const expected = [...keys].sort();
16292
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
16293
+ }
16294
+ function isNonEmptyString(value) {
16295
+ return typeof value === "string" && value.trim().length > 0;
16296
+ }
16297
+ function validateRequiredSecrets(value, errors) {
16298
+ if (!Array.isArray(value) || value.length !== 2) {
16299
+ errors.push("github.requiredSecrets must contain exactly the callback endpoint and callback auth token metadata.");
16300
+ return false;
16301
+ }
16302
+ const keys = /* @__PURE__ */ new Set();
16303
+ const repositoryNames = /* @__PURE__ */ new Set();
16304
+ for (const [index, secret] of value.entries()) {
16305
+ if (!isRecord(secret) || !hasExactKeys(secret, ["repositorySecretName", "valueKey", "purpose"])) {
16306
+ errors.push(`github.requiredSecrets[${index}] has an invalid shape.`);
16307
+ continue;
16308
+ }
16309
+ if (!isNonEmptyString(secret.repositorySecretName) || !/^[A-Z_][A-Z0-9_]*$/.test(secret.repositorySecretName)) {
16310
+ errors.push(`github.requiredSecrets[${index}].repositorySecretName is invalid.`);
16311
+ } else {
16312
+ repositoryNames.add(secret.repositorySecretName);
16313
+ }
16314
+ if (secret.valueKey !== "callbackEndpoint" && secret.valueKey !== "callbackAuthToken") {
16315
+ errors.push(`github.requiredSecrets[${index}].valueKey is invalid.`);
16316
+ } else {
16317
+ keys.add(secret.valueKey);
16318
+ }
16319
+ if (!isNonEmptyString(secret.purpose)) {
16320
+ errors.push(`github.requiredSecrets[${index}].purpose is required.`);
16321
+ }
16322
+ }
16323
+ if (!keys.has("callbackEndpoint") || !keys.has("callbackAuthToken")) {
16324
+ errors.push("github.requiredSecrets must map both callbackEndpoint and callbackAuthToken.");
16325
+ }
16326
+ if (repositoryNames.size !== 2) {
16327
+ errors.push("github.requiredSecrets must use two distinct repository secret names.");
16328
+ }
16329
+ return errors.length === 0;
16330
+ }
16331
+ function validateActions(value, setDefault, errors) {
16332
+ const expected = setDefault ? [
16333
+ { id: "write-workflow-file", scope: "workflow-file", mutates: "local-file" },
16334
+ { id: "set-callback-secrets", scope: "github-secrets", mutates: "github" },
16335
+ { id: "upsert-jira-profile", scope: "jira-profile", mutates: "jira" },
16336
+ { id: "set-project-default", scope: "project-default", mutates: "jira" }
16337
+ ] : [
16338
+ { id: "write-workflow-file", scope: "workflow-file", mutates: "local-file" },
16339
+ { id: "set-callback-secrets", scope: "github-secrets", mutates: "github" },
16340
+ { id: "upsert-jira-profile", scope: "jira-profile", mutates: "jira" }
16341
+ ];
16342
+ if (!Array.isArray(value) || value.length !== expected.length) {
16343
+ errors.push("actions do not match the required setup mutation scopes.");
16344
+ return false;
16345
+ }
16346
+ value.forEach((action, index) => {
16347
+ if (!isRecord(action) || !hasExactKeys(action, ["id", "scope", "summary", "mutates"])) {
16348
+ errors.push(`actions[${index}] has an invalid shape.`);
16349
+ return;
16350
+ }
16351
+ const expectedAction = expected[index];
16352
+ if (action.id !== expectedAction.id || action.scope !== expectedAction.scope || action.mutates !== expectedAction.mutates) {
16353
+ errors.push(`actions[${index}] must be ${expectedAction.id}/${expectedAction.scope}/${expectedAction.mutates}.`);
16354
+ }
16355
+ if (!isNonEmptyString(action.id) || !isNonEmptyString(action.summary)) {
16356
+ errors.push(`actions[${index}] requires id and summary.`);
16357
+ }
16358
+ if (!["local-file", "github", "jira"].includes(action.mutates)) {
16359
+ errors.push(`actions[${index}].mutates is invalid.`);
16360
+ }
16361
+ });
16362
+ return errors.length === 0;
16363
+ }
16364
+ function validateGuidance(value, errors) {
16365
+ if (!isRecord(value) || !hasExactKeys(value, ["authentication", "permissions", "remoteChangesExcluded"])) {
16366
+ errors.push("github.guidance has an invalid shape.");
16367
+ return;
16368
+ }
16369
+ if (!isNonEmptyString(value.authentication)) {
16370
+ errors.push("github.guidance.authentication is required.");
16371
+ }
16372
+ if (!Array.isArray(value.permissions) || value.permissions.length === 0 || !value.permissions.every(isNonEmptyString)) {
16373
+ errors.push("github.guidance.permissions must be a non-empty string array.");
16374
+ }
16375
+ const expectedExclusions = ["accounts", "credentials", "commits", "pull requests", "workflow runs"];
16376
+ if (!Array.isArray(value.remoteChangesExcluded) || value.remoteChangesExcluded.length !== expectedExclusions.length || !value.remoteChangesExcluded.every((item, index) => item === expectedExclusions[index])) {
16377
+ errors.push("github.guidance.remoteChangesExcluded must be a string array.");
16378
+ }
16379
+ }
16380
+ function validateSmokeAndRollback(smoke, rollback, setDefault, errors) {
16381
+ const expectedSmokeIds = ["workflow-content", "callback-secret-metadata", "jira-profile"];
16382
+ if (!Array.isArray(smoke) || smoke.length !== expectedSmokeIds.length) {
16383
+ errors.push("smokeValidation must contain the three provider checks.");
16384
+ } else {
16385
+ smoke.forEach((check, index) => {
16386
+ if (!isRecord(check) || !hasExactKeys(check, ["id", "verifies", "triggersExternalRun"]) || check.id !== expectedSmokeIds[index] || !isNonEmptyString(check.verifies) || check.triggersExternalRun !== false) {
16387
+ errors.push(`smokeValidation[${index}] has an invalid shape or could trigger an external run.`);
16388
+ }
16389
+ });
16390
+ }
16391
+ const expectedRollbackIds = setDefault ? ["write-workflow-file", "set-callback-secrets", "upsert-jira-profile", "set-project-default"] : ["write-workflow-file", "set-callback-secrets", "upsert-jira-profile"];
16392
+ if (!Array.isArray(rollback) || rollback.length !== expectedRollbackIds.length) {
16393
+ errors.push("rollback must cover every mutation action.");
16394
+ } else {
16395
+ rollback.forEach((step, index) => {
16396
+ if (!isRecord(step) || !hasExactKeys(step, ["actionId", "strategy", "automatic"]) || step.actionId !== expectedRollbackIds[index] || !isNonEmptyString(step.strategy) || step.automatic !== false) {
16397
+ errors.push(`rollback[${index}] has an invalid shape or action id.`);
16398
+ }
16399
+ });
16400
+ }
16401
+ }
16402
+ function validateSetupPlan(value) {
16403
+ const errors = [];
16404
+ if (!isRecord(value)) {
16405
+ return { ok: false, errors: ["Plan must be a JSON object."] };
16406
+ }
16407
+ if (!hasExactKeys(value, [
16408
+ "schemaVersion",
16409
+ "kind",
16410
+ "planId",
16411
+ "provider",
16412
+ "project",
16413
+ "github",
16414
+ "jiraProfile",
16415
+ "actions",
16416
+ "smokeValidation",
16417
+ "rollback",
16418
+ "followUp"
16419
+ ])) {
16420
+ errors.push("Plan has missing or unsupported top-level fields.");
16421
+ }
16422
+ if (value.schemaVersion !== SETUP_PLAN_SCHEMA_VERSION) {
16423
+ errors.push(`schemaVersion must be ${SETUP_PLAN_SCHEMA_VERSION}.`);
16424
+ }
16425
+ if (value.kind !== SETUP_PLAN_KIND) {
16426
+ errors.push(`kind must be ${SETUP_PLAN_KIND}.`);
16427
+ }
16428
+ if (value.provider !== "github-actions") {
16429
+ errors.push("provider must be github-actions for this plan version.");
16430
+ }
16431
+ if (!isNonEmptyString(value.planId) || !value.planId.startsWith("sha256:")) {
16432
+ errors.push("planId must be a sha256 identifier.");
16433
+ }
16434
+ if (!isRecord(value.project) || !hasExactKeys(value.project, ["key"]) || !isNonEmptyString(value.project.key) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(value.project.key)) {
16435
+ errors.push("project.key is required.");
16436
+ }
16437
+ const github = value.github;
16438
+ if (!isRecord(github) || !hasExactKeys(github, [
16439
+ "owner",
16440
+ "repository",
16441
+ "ref",
16442
+ "workflowId",
16443
+ "workflowFile",
16444
+ "requiredSecrets",
16445
+ "guidance"
16446
+ ])) {
16447
+ errors.push("github has an invalid shape.");
16448
+ } else {
16449
+ for (const field of ["owner", "repository", "ref", "workflowId"]) {
16450
+ if (!isNonEmptyString(github[field])) {
16451
+ errors.push(`github.${field} is required.`);
16452
+ }
16453
+ }
16454
+ if (isNonEmptyString(github.owner) && !/^[A-Za-z0-9_.-]+$/.test(github.owner)) {
16455
+ errors.push("github.owner contains unsupported characters.");
16456
+ }
16457
+ if (isNonEmptyString(github.repository) && !/^[A-Za-z0-9_.-]+$/.test(github.repository)) {
16458
+ errors.push("github.repository contains unsupported characters.");
16459
+ }
16460
+ if (!isRecord(github.workflowFile) || !hasExactKeys(github.workflowFile, ["path", "sourcePath", "sha256"])) {
16461
+ errors.push("github.workflowFile has an invalid shape.");
16462
+ } else {
16463
+ if (!isNonEmptyString(github.workflowFile.path)) {
16464
+ errors.push("github.workflowFile.path is required.");
16465
+ } else {
16466
+ try {
16467
+ normalizeWorkflowPath(github.workflowFile.path);
16468
+ } catch (error) {
16469
+ errors.push(error instanceof Error ? error.message : "github.workflowFile.path is invalid.");
16470
+ }
16471
+ }
16472
+ if (!isNonEmptyString(github.workflowFile.sourcePath)) {
16473
+ errors.push("github.workflowFile.sourcePath is required.");
16474
+ }
16475
+ if (typeof github.workflowFile.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(github.workflowFile.sha256)) {
16476
+ errors.push("github.workflowFile.sha256 must be a lowercase SHA-256 digest.");
16477
+ }
16478
+ }
16479
+ validateRequiredSecrets(github.requiredSecrets, errors);
16480
+ validateGuidance(github.guidance, errors);
16481
+ }
16482
+ const profile = value.jiraProfile;
16483
+ let setDefault = false;
16484
+ if (!isRecord(profile) || !hasExactKeys(profile, ["label", "enabled", "provider", "config", "setProjectDefault"])) {
16485
+ errors.push("jiraProfile has an invalid shape.");
16486
+ } else {
16487
+ setDefault = profile.setProjectDefault === true;
16488
+ if (!isNonEmptyString(profile.label)) {
16489
+ errors.push("jiraProfile.label is required.");
16490
+ }
16491
+ if (typeof profile.enabled !== "boolean" || typeof profile.setProjectDefault !== "boolean") {
16492
+ errors.push("jiraProfile enabled/default flags must be booleans.");
16493
+ }
16494
+ if (profile.provider !== "githubActions") {
16495
+ errors.push("jiraProfile.provider must be githubActions.");
16496
+ }
16497
+ if (!isRecord(profile.config) || !hasExactKeys(profile.config, ["owner", "repo", "workflowId", "ref"])) {
16498
+ errors.push("jiraProfile.config has an invalid shape.");
16499
+ } else if (isRecord(github)) {
16500
+ if (profile.config.owner !== github.owner || profile.config.repo !== github.repository || profile.config.workflowId !== github.workflowId || profile.config.ref !== github.ref) {
16501
+ errors.push("jiraProfile.config must match the GitHub repository/ref/workflow fields.");
16502
+ }
16503
+ }
16504
+ }
16505
+ validateActions(value.actions, setDefault, errors);
16506
+ validateSmokeAndRollback(value.smokeValidation, value.rollback, setDefault, errors);
16507
+ if (!isRecord(value.followUp) || !hasExactKeys(value.followUp, ["azureDevOps"]) || !isNonEmptyString(value.followUp.azureDevOps)) {
16508
+ errors.push("followUp.azureDevOps is required.");
16509
+ }
16510
+ if (errors.length === 0) {
16511
+ const plan = value;
16512
+ if (plan.planId !== computePlanId(withoutPlanId(plan))) {
16513
+ errors.push("planId does not match the plan contents.");
16514
+ }
16515
+ }
16516
+ return errors.length === 0 ? { ok: true, errors: [], plan: value } : { ok: false, errors };
16517
+ }
16518
+ function hashSetupContent(value) {
16519
+ return sha256(value);
16520
+ }
16521
+
16522
+ // src/githubSetupProvider.ts
16523
+ function isRecord2(value) {
16524
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16525
+ }
16526
+ function safeGitHubMessage(body, fallback) {
16527
+ if (isRecord2(body) && typeof body.message === "string") {
16528
+ return body.message.slice(0, 500);
16529
+ }
16530
+ return fallback;
16531
+ }
16532
+ async function githubApi(fetchImpl, url, token) {
16533
+ const response = await fetchImpl(url, {
16534
+ method: "GET",
16535
+ headers: {
16536
+ accept: "application/vnd.github+json",
16537
+ "x-github-api-version": "2022-11-28",
16538
+ ...token ? { authorization: `Bearer ${token}` } : {}
16539
+ }
16540
+ });
16541
+ const text = await response.text();
16542
+ let body = {};
16543
+ if (text) {
16544
+ try {
16545
+ body = JSON.parse(text);
16546
+ } catch {
16547
+ body = {};
16548
+ }
16549
+ }
16550
+ return { ok: response.ok, status: response.status, body };
16551
+ }
16552
+ async function setGitHubRepositorySecretWithCli(input) {
16553
+ await new Promise((resolve, reject) => {
16554
+ const child = (0, import_node_child_process4.spawn)(
16555
+ "gh",
16556
+ ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`],
16557
+ {
16558
+ shell: false,
16559
+ stdio: ["pipe", "ignore", "pipe"],
16560
+ env: {
16561
+ ...process.env,
16562
+ GH_TOKEN: input.adminToken
16563
+ }
16564
+ }
16565
+ );
16566
+ let stderr = "";
16567
+ child.stderr.setEncoding("utf8");
16568
+ child.stderr.on("data", (chunk) => {
16569
+ stderr += chunk;
16570
+ });
16571
+ child.on("error", (error) => {
16572
+ reject(new Error(`Unable to start GitHub CLI: ${error.message}`));
16573
+ });
16574
+ child.on("close", (code) => {
16575
+ if (code === 0) {
16576
+ resolve();
16577
+ return;
16578
+ }
16579
+ const suffix = stderr.trim() ? ` ${stderr.trim().slice(0, 500)}` : "";
16580
+ reject(new Error(`GitHub CLI could not set repository secret ${input.name}.${suffix}`));
16581
+ });
16582
+ child.stdin.end(input.value, "utf8");
16583
+ });
16584
+ }
16585
+ var GitHubSetupProvider = class {
16586
+ name = "github-actions";
16587
+ fetchImpl;
16588
+ secretSetter;
16589
+ constructor(deps = {}) {
16590
+ this.fetchImpl = deps.fetchImpl ?? fetch;
16591
+ this.secretSetter = deps.setRepositorySecret ?? setGitHubRepositorySecretWithCli;
16592
+ }
16593
+ async inspectExecutionDefinition(plan, token) {
16594
+ const { owner, repository, workflowId, ref, workflowFile } = plan.github;
16595
+ const workflowUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/workflows/${encodeURIComponent(workflowId)}`;
16596
+ const workflow = await githubApi(this.fetchImpl, workflowUrl, token);
16597
+ if (!workflow.ok) {
16598
+ if (workflow.status === 404) {
16599
+ return {
16600
+ found: false,
16601
+ message: "Workflow is not registered on GitHub. Commit the local workflow to the repository default branch before expecting workflow_dispatch to be available."
16602
+ };
16603
+ }
16604
+ return {
16605
+ found: false,
16606
+ message: `GitHub workflow metadata could not be verified (HTTP ${workflow.status}: ${safeGitHubMessage(workflow.body, "request failed")}).`
16607
+ };
16608
+ }
16609
+ const workflowPath = isRecord2(workflow.body) && typeof workflow.body.path === "string" ? workflow.body.path.replace(/^\//, "") : void 0;
16610
+ const state = isRecord2(workflow.body) && typeof workflow.body.state === "string" ? workflow.body.state : void 0;
16611
+ const contentUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${workflowFile.path.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`;
16612
+ const content = await githubApi(this.fetchImpl, contentUrl, token);
16613
+ let contentSha256;
16614
+ if (content.ok && isRecord2(content.body) && typeof content.body.content === "string") {
16615
+ try {
16616
+ const decoded = Buffer.from(content.body.content.replace(/\s/g, ""), "base64").toString("utf8");
16617
+ contentSha256 = hashSetupContent(decoded);
16618
+ } catch {
16619
+ contentSha256 = void 0;
16620
+ }
16621
+ }
16622
+ return {
16623
+ found: true,
16624
+ active: state === "active",
16625
+ path: workflowPath,
16626
+ contentSha256,
16627
+ message: state !== "active" ? `GitHub workflow is registered but its state is ${state ?? "unknown"}.` : workflowPath !== workflowFile.path ? `GitHub workflow path ${workflowPath ?? "<unknown>"} does not match ${workflowFile.path}.` : contentSha256 === workflowFile.sha256 ? "GitHub workflow is active and its content matches the approved plan." : "GitHub workflow metadata is reachable, but the approved ref content could not be confirmed as an exact plan match."
16628
+ };
16629
+ }
16630
+ async inspectCallbackSecretMetadata(plan, adminToken) {
16631
+ if (!adminToken) {
16632
+ return {
16633
+ verified: false,
16634
+ names: /* @__PURE__ */ new Set(),
16635
+ message: "Repository secret metadata was not verified because githubAdminToken was not provided. Secret values are never readable."
16636
+ };
16637
+ }
16638
+ const { owner, repository } = plan.github;
16639
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets?per_page=100`;
16640
+ const response = await githubApi(this.fetchImpl, url, adminToken);
16641
+ if (!response.ok) {
16642
+ throw new Error(
16643
+ `GitHub repository secret metadata request failed with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
16644
+ );
16645
+ }
16646
+ const names = /* @__PURE__ */ new Set();
16647
+ if (isRecord2(response.body) && Array.isArray(response.body.secrets)) {
16648
+ for (const item of response.body.secrets) {
16649
+ if (isRecord2(item) && typeof item.name === "string") {
16650
+ names.add(item.name);
16651
+ }
16652
+ }
16653
+ }
16654
+ return {
16655
+ verified: true,
16656
+ names,
16657
+ message: `Verified ${names.size} GitHub repository secret name(s); values remain unreadable.`
16658
+ };
16659
+ }
16660
+ async setCallbackSecret(plan, secret, value, adminToken) {
16661
+ await this.secretSetter({
16662
+ owner: plan.github.owner,
16663
+ repository: plan.github.repository,
16664
+ name: secret.repositorySecretName,
16665
+ value,
16666
+ adminToken
16667
+ });
16668
+ }
16669
+ };
16670
+
16671
+ // src/azureDevOpsSetupProvider.ts
16672
+ var isRecord3 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
16673
+ async function adoGet(fetchImpl, url, token) {
16674
+ const response = await fetchImpl(url, { headers: { accept: "application/json", ...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {} } });
16675
+ const text = await response.text();
16676
+ let body = {};
16677
+ try {
16678
+ body = text ? JSON.parse(text) : {};
16679
+ } catch {
16680
+ }
16681
+ return { ok: response.ok, status: response.status, body };
16682
+ }
16683
+ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
16684
+ const base = `https://dev.azure.com/${encodeURIComponent(input.organization)}/${encodeURIComponent(input.project)}/_apis/build/definitions/${encodeURIComponent(input.pipelineId)}`;
16685
+ const auth = { authorization: `Basic ${Buffer.from(`:${input.token}`).toString("base64")}` };
16686
+ const current = await adoGet(fetchImpl, `${base}?api-version=7.1`, input.token);
16687
+ if (!current.ok || !isRecord3(current.body)) throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
16688
+ const variables = isRecord3(current.body.variables) ? { ...current.body.variables } : {};
16689
+ variables[input.name] = { value: input.value, isSecret: true };
16690
+ const query = new URLSearchParams({
16691
+ "api-version": "7.1",
16692
+ secretsSourceDefinitionId: String(current.body.id),
16693
+ secretsSourceDefinitionRevision: String(current.body.revision)
16694
+ });
16695
+ const response = await fetchImpl(`${base}?${query.toString()}`, { method: "PUT", headers: { ...auth, "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ ...current.body, variables }) });
16696
+ if (!response.ok) throw new Error(`Azure DevOps pipeline variable update failed (HTTP ${response.status}).`);
16697
+ }
16698
+ var AzureDevOpsSetupProvider = class {
16699
+ name = "azure-devops";
16700
+ fetchImpl;
16701
+ setter;
16702
+ constructor(deps = {}) {
16703
+ this.fetchImpl = deps.fetchImpl ?? fetch;
16704
+ this.setter = deps.setPipelineVariable ?? ((input) => setAzurePipelineVariableWithRest(input, this.fetchImpl));
16705
+ }
16706
+ async inspectExecutionDefinition(plan, token) {
16707
+ const a = plan.azureDevOps;
16708
+ const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16709
+ const result = await adoGet(this.fetchImpl, url, token);
16710
+ if (!result.ok) return { found: false, message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).` };
16711
+ const body = isRecord3(result.body) ? result.body : {};
16712
+ const name = typeof body.name === "string" ? body.name : void 0;
16713
+ return { found: true, active: true, path: name, message: `Azure DevOps pipeline ${name ?? a.pipelineId} is reachable. Inspection never queues a run.` };
16714
+ }
16715
+ async inspectCallbackSecretMetadata(plan, token) {
16716
+ if (!token) return { verified: false, names: /* @__PURE__ */ new Set(), message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable." };
16717
+ const a = plan.azureDevOps;
16718
+ const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16719
+ const result = await adoGet(this.fetchImpl, url, token);
16720
+ if (!result.ok) throw new Error(`Azure DevOps pipeline metadata request failed with HTTP ${result.status}.`);
16721
+ const variables = isRecord3(result.body) && isRecord3(result.body.variables) ? result.body.variables : {};
16722
+ return { verified: true, names: new Set(Object.keys(variables)), message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.` };
16723
+ }
16724
+ async setCallbackSecret(plan, secret, value, adminToken) {
16725
+ if (!this.setter) throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
16726
+ await this.setter({ organization: plan.azureDevOps.organization, project: plan.azureDevOps.project, pipelineId: plan.azureDevOps.pipelineId, name: secret.repositorySecretName, value, token: adminToken });
16727
+ }
16728
+ };
16729
+
16730
+ // src/azureDevOpsSetupPlan.ts
16731
+ var import_node_crypto2 = require("node:crypto");
16732
+ var digest = (v) => `sha256:${(0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(v)).digest("hex")}`;
16733
+ var required = (value, field) => {
16734
+ const result = value.trim();
16735
+ if (!result) throw new Error(`${field} is required.`);
16736
+ return result;
16737
+ };
16738
+ var variableName = (value, field) => {
16739
+ const result = required(value, field).toUpperCase();
16740
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(result)) throw new Error(`${field} is invalid.`);
16741
+ return result;
16742
+ };
16743
+ function buildAzureDevOpsSetupPlan(input) {
16744
+ const projectKey = required(input.projectKey, "project key").toUpperCase();
16745
+ if (!/^[A-Z][A-Z0-9_]{1,31}$/.test(projectKey)) throw new Error("project key is invalid.");
16746
+ const organization = required(input.organization, "organization");
16747
+ const azureProject = required(input.azureProject, "Azure project");
16748
+ const pipelineId = required(input.pipelineId, "pipeline id");
16749
+ if (!/^\d+$/.test(pipelineId)) throw new Error("pipeline id must be numeric.");
16750
+ const ref = required(input.ref, "ref");
16751
+ const apiVersion = required(input.apiVersion ?? "7.1", "api version");
16752
+ const profileLabel = required(input.profileLabel, "profile label");
16753
+ const endpointName = variableName(input.callbackEndpointVariableName, "callback endpoint variable name");
16754
+ const tokenName = variableName(input.callbackAuthTokenVariableName, "callback auth token variable name");
16755
+ if (endpointName === tokenName) throw new Error("callback variable names must be distinct.");
16756
+ const requiredSecrets = [{ repositorySecretName: endpointName, valueKey: "callbackEndpoint", purpose: "Forge callback endpoint pipeline variable." }, { repositorySecretName: tokenName, valueKey: "callbackAuthToken", purpose: "Forge callback auth token pipeline variable." }];
16757
+ const scopes = ["provider-secrets", "jira-profile", ...input.setProjectDefault ? ["project-default"] : []];
16758
+ const actions = scopes.map((scope) => ({ id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default", scope, summary: `Apply ${scope} for Azure DevOps.`, mutates: "jira" }));
16759
+ const withoutId = { schemaVersion: "automatify.testops.setup/v1", kind: "AutomatifyTestOpsSetupPlan", provider: "azure-devops", project: { key: projectKey }, azureDevOps: { organization, project: azureProject, pipelineId, ref, apiVersion, requiredSecrets, guidance: { authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.", permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."], remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"] } }, jiraProfile: { label: profileLabel, enabled: input.enabled, provider: "azureDevops", config: { organization, project: azureProject, pipelineId, apiVersion, bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } }) }, setProjectDefault: input.setProjectDefault }, actions, smokeValidation: [{ id: "pipeline-content", verifies: "Azure DevOps pipeline metadata without queueing a run.", triggersExternalRun: false }, { id: "pipeline-variable-metadata", verifies: "Required variable names; values remain unreadable.", triggersExternalRun: false }, { id: "jira-profile", verifies: "Forge automation profile and optional default.", triggersExternalRun: false }], rollback: actions.map((a) => ({ actionId: a.id, strategy: "Restore the previous state manually; secret values cannot be read back.", automatic: false })), followUp: { azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary." } };
16760
+ return { ...withoutId, planId: digest(withoutId) };
16761
+ }
16762
+ function validateAzureDevOpsSetupPlan(value) {
16763
+ const errors = [];
16764
+ const record = value;
16765
+ const exact = (v, keys) => Boolean(v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join());
16766
+ if (!exact(value, ["schemaVersion", "kind", "planId", "provider", "project", "azureDevOps", "jiraProfile", "actions", "smokeValidation", "rollback", "followUp"])) errors.push("Plan has missing or unsupported top-level fields.");
16767
+ if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops") errors.push("Plan schema, kind, or provider is invalid.");
16768
+ if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? "")) errors.push("project.key is invalid.");
16769
+ const a = record?.azureDevOps;
16770
+ if (!exact(a, ["organization", "project", "pipelineId", "ref", "apiVersion", "requiredSecrets", "guidance"]) || ![a?.organization, a?.project, a?.ref, a?.apiVersion].every((x) => typeof x === "string" && x.trim()) || !/^\d+$/.test(a?.pipelineId ?? "")) errors.push("azureDevOps has an invalid shape or fields.");
16771
+ if (!Array.isArray(a?.requiredSecrets) || a.requiredSecrets.length !== 2 || new Set(a.requiredSecrets.map((s) => s.repositorySecretName)).size !== 2 || new Set(a.requiredSecrets.map((s) => s.valueKey)).size !== 2 || !a.requiredSecrets.some((s) => s.valueKey === "callbackEndpoint") || !a.requiredSecrets.some((s) => s.valueKey === "callbackAuthToken") || !a.requiredSecrets.every((s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose)) errors.push("azureDevOps.requiredSecrets is invalid.");
16772
+ const p = record?.jiraProfile;
16773
+ if (!exact(p, ["label", "enabled", "provider", "config", "setProjectDefault"]) || p?.provider !== "azureDevops" || !exact(p?.config, ["organization", "project", "pipelineId", "apiVersion", "bodyTemplate"]) || p.config.organization !== a?.organization || p.config.project !== a?.project || p.config.pipelineId !== a?.pipelineId || p.config.apiVersion !== a?.apiVersion) errors.push("jiraProfile/config is invalid or inconsistent.");
16774
+ const ids = p?.setProjectDefault ? ["set-provider-secrets", "upsert-jira-profile", "set-project-default"] : ["set-provider-secrets", "upsert-jira-profile"];
16775
+ const scopes = p?.setProjectDefault ? ["provider-secrets", "jira-profile", "project-default"] : ["provider-secrets", "jira-profile"];
16776
+ if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every((x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira")) errors.push("actions are invalid.");
16777
+ const smokeIds = ["pipeline-content", "pipeline-variable-metadata", "jira-profile"];
16778
+ if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every((x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false)) errors.push("smokeValidation is invalid.");
16779
+ if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every((x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false)) errors.push("rollback is invalid.");
16780
+ if (errors.length === 0) {
16781
+ const { planId, ...rest } = record;
16782
+ if (planId !== digest(rest)) errors.push("planId does not match the plan contents.");
16783
+ }
16784
+ return errors.length ? { ok: false, errors } : { ok: true, errors, plan: value };
16785
+ }
16786
+
16787
+ // src/setup.ts
16788
+ var APPLY_RESULT_SCHEMA_VERSION = "automatify.testops.setup.apply/v1";
16789
+ var DOCTOR_RESULT_SCHEMA_VERSION = "automatify.testops.setup.doctor/v1";
16790
+ var SECRET_KEYS = [
16791
+ "githubProviderToken",
16792
+ "githubAdminToken",
16793
+ "azureDevOpsProviderToken",
16794
+ "azureDevOpsAdminToken",
16795
+ "callbackEndpoint",
16796
+ "callbackAuthToken"
16797
+ ];
16798
+ var MUTATION_SCOPES = [
16799
+ "workflow-file",
16800
+ "github-secrets",
16801
+ "provider-secrets",
16802
+ "jira-profile",
16803
+ "project-default"
16804
+ ];
16805
+ var SetupCommandError = class extends Error {
16806
+ cliCode;
16807
+ exitCode;
16808
+ constructor(cliCode, message, exitCode) {
16809
+ super(message);
16810
+ this.name = "SetupCommandError";
16811
+ this.cliCode = cliCode;
16812
+ this.exitCode = exitCode;
16813
+ }
16814
+ };
16815
+ var PLAN_VALUE_FLAGS = /* @__PURE__ */ new Set([
16816
+ "--provider",
16817
+ "--project-key",
16818
+ "--owner",
16819
+ "--repo",
16820
+ "--ref",
16821
+ "--workflow-id",
16822
+ "--workflow-path",
16823
+ "--workflow-source",
16824
+ "--profile-label",
16825
+ "--callback-endpoint-secret-name",
16826
+ "--callback-auth-token-secret-name",
16827
+ "--organization",
16828
+ "--azure-project",
16829
+ "--pipeline-id",
16830
+ "--api-version",
16831
+ "--callback-endpoint-variable-name",
16832
+ "--callback-auth-token-variable-name"
16833
+ ]);
16834
+ var APPLY_VALUE_FLAGS = /* @__PURE__ */ new Set(["--plan", "--repo-root"]);
16835
+ var APPLY_BOOL_FLAGS = /* @__PURE__ */ new Set([
16836
+ "--confirm",
16837
+ "--dry-run",
16838
+ "--secrets-stdin",
16839
+ "--rotate-secrets",
16840
+ "--rotate-provider-token"
16841
+ ]);
16842
+ function defaultReadStdin() {
16843
+ return new Promise((resolve, reject) => {
16844
+ let value = "";
16845
+ process.stdin.setEncoding("utf8");
16846
+ process.stdin.on("data", (chunk) => {
16847
+ value += chunk;
16848
+ });
16849
+ process.stdin.on("end", () => resolve(value));
16850
+ process.stdin.on("error", reject);
16851
+ });
16852
+ }
16853
+ function parseArgs10(args, valueFlags, boolFlags, allowApplyCollections = false) {
16854
+ const flags = {};
16855
+ const enabled = /* @__PURE__ */ new Set();
16856
+ const approvals = [];
16857
+ const secretRefs = [];
16858
+ const errors = [];
16859
+ for (let index = 0; index < args.length; index += 1) {
16860
+ const token = args[index];
16861
+ if (!token.startsWith("--")) {
16862
+ errors.push(`Unexpected positional argument at index ${index}.`);
16863
+ continue;
16864
+ }
16865
+ if (boolFlags.has(token)) {
16866
+ enabled.add(token);
16867
+ continue;
16868
+ }
16869
+ if (allowApplyCollections && (token === "--approve" || token === "--secret-ref")) {
16870
+ const value2 = args[index + 1];
16871
+ if (!value2 || value2.startsWith("--")) {
16872
+ errors.push(`${token} requires a value.`);
16873
+ continue;
16874
+ }
16875
+ (token === "--approve" ? approvals : secretRefs).push(value2);
16876
+ index += 1;
16877
+ continue;
16878
+ }
16879
+ if (!valueFlags.has(token)) {
16880
+ errors.push(`Unknown option ${token}.`);
16881
+ continue;
16882
+ }
16883
+ const value = args[index + 1];
16884
+ if (!value || value.startsWith("--")) {
16885
+ errors.push(`${token} requires a value.`);
16886
+ continue;
16887
+ }
16888
+ flags[token] = value;
16889
+ index += 1;
16890
+ }
16891
+ return { flags, boolFlags: enabled, approvals, secretRefs, errors };
16892
+ }
16893
+ function jsonResponse(payload, exitCode = ExitCode.Success) {
16894
+ return {
16895
+ exitCode,
16896
+ stdout: [JSON.stringify(payload)]
16897
+ };
16898
+ }
16899
+ function errorPayload(code, message, diagnostics = []) {
16900
+ return {
16901
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
16902
+ status: "failed",
16903
+ error: {
16904
+ code,
16905
+ message,
16906
+ diagnostics
16907
+ }
16908
+ };
16909
+ }
16910
+ function safeErrorMessage(error, secrets = { values: {}, sources: {} }) {
16911
+ let message = error instanceof Error ? error.message : "Unknown setup error.";
16912
+ for (const secret of Object.values(secrets.values)) {
16913
+ if (secret) {
16914
+ message = message.split(secret).join("[REDACTED]");
16915
+ }
16916
+ }
16917
+ return message.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [REDACTED]").replace(/token[=:]\s*[^\s,;]+/gi, "token=[REDACTED]");
16918
+ }
16919
+ function exitCodeForError(error) {
16920
+ if (error instanceof SetupCommandError) {
16921
+ return error.exitCode;
16922
+ }
16923
+ if (error instanceof ForgeClientError) {
16924
+ return error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR" ? ExitCode.TransportError : ExitCode.RemoteError;
16925
+ }
16926
+ return ExitCode.RemoteError;
16927
+ }
16928
+ function errorCodeForError(error, fallback) {
16929
+ return error instanceof SetupCommandError ? error.cliCode : fallback;
16930
+ }
16931
+ function loadPlan(planPath, cwd) {
16932
+ const absolute = import_node_path15.default.resolve(cwd, planPath);
16933
+ let parsed;
16934
+ try {
16935
+ parsed = JSON.parse((0, import_node_fs14.readFileSync)(absolute, "utf8"));
16936
+ } catch (error) {
16937
+ throw new SetupCommandError(
16938
+ "PLAN_READ_ERROR",
16939
+ `Could not read setup plan JSON: ${error instanceof Error ? error.message : "unknown error"}`,
16940
+ ExitCode.ValidationError
16941
+ );
16942
+ }
16943
+ const validation = isAzurePlanValue(parsed) ? validateAzureDevOpsSetupPlan(parsed) : validateSetupPlan(parsed);
16944
+ if (!validation.ok || !validation.plan) {
16945
+ throw new SetupCommandError(
16946
+ "PLAN_VALIDATION_ERROR",
16947
+ `Setup plan validation failed: ${validation.errors.join(" ")}`,
16948
+ ExitCode.ValidationError
16949
+ );
16950
+ }
16951
+ return validation.plan;
16952
+ }
16953
+ function isAzurePlanValue(value) {
16954
+ return Boolean(value && typeof value === "object" && value.provider === "azure-devops");
16955
+ }
16956
+ function isSecretKey(value) {
16957
+ return SECRET_KEYS.includes(value);
16958
+ }
16959
+ async function resolveSecrets(parsed, deps) {
16960
+ const values = {};
16961
+ const sources = {};
16962
+ if (parsed.boolFlags.has("--secrets-stdin")) {
16963
+ const raw = await deps.readStdin();
16964
+ let envelope;
16965
+ try {
16966
+ envelope = JSON.parse(raw);
16967
+ } catch {
16968
+ throw new SetupCommandError(
16969
+ "SECRET_INPUT_ERROR",
16970
+ "Secret stdin must be a JSON object. Input content was not retained or printed.",
16971
+ ExitCode.ValidationError
16972
+ );
16973
+ }
16974
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
16975
+ throw new SetupCommandError("SECRET_INPUT_ERROR", "Secret stdin must be a JSON object.", ExitCode.ValidationError);
16976
+ }
16977
+ for (const [key, value] of Object.entries(envelope)) {
16978
+ if (!isSecretKey(key)) {
16979
+ throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin contains unsupported key ${key}.`, ExitCode.ValidationError);
16980
+ }
16981
+ if (typeof value !== "string" || !value.trim()) {
16982
+ throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin key ${key} must be a non-empty string.`, ExitCode.ValidationError);
16983
+ }
16984
+ values[key] = value.trim();
16985
+ sources[key] = "stdin";
16986
+ }
16987
+ }
16988
+ for (const reference of parsed.secretRefs) {
16989
+ const separator = reference.indexOf("=");
16990
+ const key = separator > 0 ? reference.slice(0, separator) : "";
16991
+ const target = separator > 0 ? reference.slice(separator + 1) : "";
16992
+ if (!isSecretKey(key) || !target.startsWith("env:")) {
16993
+ throw new SetupCommandError(
16994
+ "SECRET_REFERENCE_ERROR",
16995
+ "Secret references must use <supported-key>=env:<VARIABLE_NAME>.",
16996
+ ExitCode.ValidationError
16997
+ );
16998
+ }
16999
+ const envName = target.slice(4);
17000
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
17001
+ throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret reference for ${key} has an invalid environment variable name.`, ExitCode.ValidationError);
17002
+ }
17003
+ if (sources[key]) {
17004
+ throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret ${key} was provided more than once.`, ExitCode.ValidationError);
17005
+ }
17006
+ const value = deps.env[envName]?.trim();
17007
+ if (!value) {
17008
+ throw new SetupCommandError(
17009
+ "SECRET_REFERENCE_ERROR",
17010
+ `Secret reference for ${key} points to an empty or missing environment variable.`,
17011
+ ExitCode.ValidationError
17012
+ );
17013
+ }
17014
+ values[key] = value;
17015
+ sources[key] = "env-ref";
17016
+ }
17017
+ return { values, sources };
17018
+ }
17019
+ function approvedScopes(plan, parsed, dryRun) {
17020
+ const planScopes = plan.actions.map((action) => action.scope);
17021
+ const requested = parsed.approvals.length === 0 && dryRun ? ["all"] : parsed.approvals;
17022
+ if (requested.length === 0) {
17023
+ throw new SetupCommandError(
17024
+ "APPROVAL_REQUIRED",
17025
+ "At least one --approve <scope> is required for apply. Use --approve all to approve every planned action.",
17026
+ ExitCode.UsageError
17027
+ );
17028
+ }
17029
+ const expanded = /* @__PURE__ */ new Set();
17030
+ for (const scope of requested) {
17031
+ if (scope === "all") {
17032
+ planScopes.forEach((item) => expanded.add(item));
17033
+ continue;
17034
+ }
17035
+ if (!MUTATION_SCOPES.includes(scope)) {
17036
+ throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
17037
+ }
17038
+ if (!planScopes.includes(scope)) {
17039
+ throw new SetupCommandError("APPROVAL_ERROR", `Approval scope ${scope} is not present in this plan.`, ExitCode.UsageError);
17040
+ }
17041
+ expanded.add(scope);
17042
+ }
17043
+ return planScopes.filter((scope) => expanded.has(scope));
17044
+ }
17045
+ function requireServiceData(result, contract) {
17046
+ if (!result.ok) {
17047
+ throw new Error(`${contract} failed: ${result.error.code}: ${result.error.message}`);
17048
+ }
17049
+ return result.data;
17050
+ }
17051
+ function findMatchingProfile(profiles, plan) {
17052
+ const matches = profiles.filter((profile) => profile.label === plan.jiraProfile.label);
17053
+ if (matches.length > 1) {
17054
+ throw new SetupCommandError(
17055
+ "AMBIGUOUS_PROFILE",
17056
+ `More than one Jira automation profile has label ${plan.jiraProfile.label}; rename duplicates before applying this deterministic plan.`,
17057
+ ExitCode.ValidationError
17058
+ );
17059
+ }
17060
+ return matches[0];
17061
+ }
17062
+ function profileMatches(profile, plan) {
17063
+ const config = profile.config;
17064
+ return profile.provider === "githubActions" && profile.label === plan.jiraProfile.label && profile.enabled === plan.jiraProfile.enabled && config.owner === plan.jiraProfile.config.owner && config.repo === plan.jiraProfile.config.repo && config.workflowId === plan.jiraProfile.config.workflowId && config.ref === plan.jiraProfile.config.ref;
17065
+ }
17066
+ function previousProfileState(profile) {
17067
+ if (!profile) {
17068
+ return { existed: false };
17069
+ }
17070
+ return {
17071
+ existed: true,
17072
+ id: profile.id,
17073
+ label: profile.label,
17074
+ provider: profile.provider,
17075
+ enabled: profile.enabled,
17076
+ wasProjectDefault: Boolean(profile.isProjectDefault),
17077
+ hadSecret: profile.hasSecret
17078
+ };
17079
+ }
17080
+ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken) {
17081
+ let profiles = [];
17082
+ let matchingProfile;
17083
+ let currentDefaultProfileId;
17084
+ if (scopes.includes("jira-profile") || scopes.includes("project-default")) {
17085
+ profiles = requireServiceData(
17086
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
17087
+ context: { projectKey: plan.project.key }
17088
+ }),
17089
+ "listScenarioAutomationProfiles"
17090
+ );
17091
+ matchingProfile = findMatchingProfile(profiles, plan);
17092
+ currentDefaultProfileId = profiles.find((profile) => profile.isProjectDefault)?.id;
17093
+ const needsProviderToken = scopes.includes("jira-profile") && (!matchingProfile?.hasSecret || rotateProviderToken);
17094
+ if (needsProviderToken && !dryRun && !secrets.values.githubProviderToken) {
17095
+ throw new SetupCommandError(
17096
+ "SECRET_INPUT_REQUIRED",
17097
+ "githubProviderToken is required to create this Jira profile, restore a missing provider secret, or rotate the provider token.",
17098
+ ExitCode.ValidationError
17099
+ );
17100
+ }
17101
+ }
17102
+ let repositorySecretNames;
17103
+ if (scopes.includes("github-secrets")) {
17104
+ if (!dryRun && !secrets.values.githubAdminToken) {
17105
+ throw new SetupCommandError(
17106
+ "SECRET_INPUT_REQUIRED",
17107
+ "githubAdminToken is required for the approved github-secrets scope.",
17108
+ ExitCode.ValidationError
17109
+ );
17110
+ }
17111
+ if (secrets.values.githubAdminToken) {
17112
+ const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
17113
+ plan,
17114
+ secrets.values.githubAdminToken
17115
+ );
17116
+ repositorySecretNames = inspection.names;
17117
+ const secretsToWrite = plan.github.requiredSecrets.filter(
17118
+ (secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
17119
+ );
17120
+ if (!dryRun) {
17121
+ for (const secret of secretsToWrite) {
17122
+ if (!secrets.values[secret.valueKey]) {
17123
+ throw new SetupCommandError(
17124
+ "SECRET_INPUT_REQUIRED",
17125
+ `${secret.valueKey} is required to create or rotate ${secret.repositorySecretName}.`,
17126
+ ExitCode.ValidationError
17127
+ );
17128
+ }
17129
+ }
17130
+ }
17131
+ }
17132
+ }
17133
+ return { profiles, matchingProfile, currentDefaultProfileId, repositorySecretNames };
17134
+ }
17135
+ function rollbackFor(plan, actionId) {
17136
+ return plan.rollback.find((item) => item.actionId === actionId)?.strategy ?? "Review the plan rollback section.";
17137
+ }
17138
+ function resolveWorkflowTarget(plan, repoRoot) {
17139
+ const resolvedRoot = import_node_path15.default.resolve(repoRoot);
17140
+ const target = import_node_path15.default.resolve(resolvedRoot, plan.github.workflowFile.path);
17141
+ if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${import_node_path15.default.sep}`)) {
17142
+ throw new SetupCommandError(
17143
+ "WORKFLOW_PATH_ERROR",
17144
+ "Workflow target escapes the approved repository root.",
17145
+ ExitCode.ValidationError
17146
+ );
17147
+ }
17148
+ return target;
17149
+ }
17150
+ function applyWorkflowFile(plan, repoRoot, dryRun) {
17151
+ const action = plan.actions.find((item) => item.scope === "workflow-file");
17152
+ const target = resolveWorkflowTarget(plan, repoRoot);
17153
+ const sourceContent = (0, import_node_fs14.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
17154
+ if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
17155
+ throw new SetupCommandError(
17156
+ "WORKFLOW_SOURCE_CHANGED",
17157
+ "Workflow source content no longer matches the approved plan hash; generate a new plan before applying.",
17158
+ ExitCode.ValidationError
17159
+ );
17160
+ }
17161
+ const existed = (0, import_node_fs14.existsSync)(target);
17162
+ const previousContent = existed ? (0, import_node_fs14.readFileSync)(target, "utf8") : void 0;
17163
+ const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
17164
+ if (matches) {
17165
+ return {
17166
+ id: action.id,
17167
+ scope: action.scope,
17168
+ status: "skipped",
17169
+ message: "Local workflow file already matches the approved content hash.",
17170
+ rollback: { available: false, guidance: "No workflow file mutation occurred." }
17171
+ };
17172
+ }
17173
+ if (!dryRun) {
17174
+ (0, import_node_fs14.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
17175
+ (0, import_node_fs14.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
17176
+ }
17177
+ return {
17178
+ id: action.id,
17179
+ scope: action.scope,
17180
+ status: dryRun ? "planned" : existed ? "updated" : "created",
17181
+ message: dryRun ? `${existed ? "Update" : "Create"} ${plan.github.workflowFile.path} with sha256 ${plan.github.workflowFile.sha256}.` : `${existed ? "Updated" : "Created"} ${plan.github.workflowFile.path}.`,
17182
+ rollback: {
17183
+ available: !dryRun,
17184
+ guidance: rollbackFor(plan, action.id),
17185
+ previousState: {
17186
+ existed,
17187
+ sha256: previousContent === void 0 ? null : hashSetupContent(previousContent)
17188
+ }
17189
+ }
17190
+ };
17191
+ }
17192
+ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets) {
17193
+ const action = plan.actions.find((item) => item.scope === "github-secrets");
17194
+ const names = state.repositorySecretNames;
17195
+ const pending = plan.github.requiredSecrets.filter(
17196
+ (secret) => rotateSecrets || !names?.has(secret.repositorySecretName)
17197
+ );
17198
+ if (names && pending.length === 0) {
17199
+ return {
17200
+ id: action.id,
17201
+ scope: action.scope,
17202
+ status: "skipped",
17203
+ message: "All required GitHub repository secret names already exist; values remain unreadable and were not overwritten.",
17204
+ rollback: { available: false, guidance: "No GitHub secret mutation occurred." }
17205
+ };
17206
+ }
17207
+ if (dryRun) {
17208
+ return {
17209
+ id: action.id,
17210
+ scope: action.scope,
17211
+ status: "planned",
17212
+ message: names ? `Set ${pending.map((secret) => secret.repositorySecretName).join(", ")}; values will be read only from approved secret input.` : "Inspect required GitHub secret metadata, then create missing names from approved secret input.",
17213
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17214
+ };
17215
+ }
17216
+ const adminToken = secrets.values.githubAdminToken;
17217
+ for (const secret of pending) {
17218
+ await deps.githubProvider.setCallbackSecret(
17219
+ plan,
17220
+ secret,
17221
+ secrets.values[secret.valueKey],
17222
+ adminToken
17223
+ );
17224
+ }
17225
+ return {
17226
+ id: action.id,
17227
+ scope: action.scope,
17228
+ status: "updated",
17229
+ message: `Set ${pending.length} GitHub repository secret(s). Values were not printed or placed in process arguments.`,
17230
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17231
+ };
17232
+ }
17233
+ async function applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken) {
17234
+ const action = plan.actions.find((item) => item.scope === "jira-profile");
17235
+ const current = state.matchingProfile;
17236
+ const matches = current ? profileMatches(current, plan) : false;
17237
+ if (current && matches && current.hasSecret && !rotateProviderToken) {
17238
+ return {
17239
+ profile: current,
17240
+ result: {
17241
+ id: action.id,
17242
+ scope: action.scope,
17243
+ status: "skipped",
17244
+ message: "Jira automation profile already matches the plan and has a stored provider secret.",
17245
+ rollback: { available: false, guidance: "No Jira profile mutation occurred." }
17246
+ }
17247
+ };
17248
+ }
17249
+ if (dryRun) {
17250
+ const plannedProfile = current ?? {
17251
+ id: "<created-by-forge>",
17252
+ projectKey: plan.project.key,
17253
+ label: plan.jiraProfile.label,
17254
+ provider: "githubActions",
17255
+ authType: "bearer",
17256
+ method: "POST",
17257
+ endpointSummary: "<computed-by-forge>",
17258
+ config: plan.jiraProfile.config,
17259
+ enabled: plan.jiraProfile.enabled,
17260
+ hasSecret: Boolean(secrets.values.githubProviderToken),
17261
+ createdAt: "",
17262
+ updatedAt: ""
17263
+ };
17264
+ return {
17265
+ profile: plannedProfile,
17266
+ result: {
17267
+ id: action.id,
17268
+ scope: action.scope,
17269
+ status: "planned",
17270
+ message: `${current ? "Update" : "Create"} the Jira automation profile through upsertScenarioAutomationProfile.`,
17271
+ rollback: {
17272
+ available: false,
17273
+ guidance: rollbackFor(plan, action.id),
17274
+ previousState: previousProfileState(current)
17275
+ }
17276
+ }
17277
+ };
17278
+ }
17279
+ const secretToken = !current?.hasSecret || rotateProviderToken ? secrets.values.githubProviderToken : void 0;
17280
+ const profile = requireServiceData(
17281
+ await context.invokeForgeContract("upsertScenarioAutomationProfile", {
17282
+ context: { projectKey: plan.project.key },
17283
+ input: {
17284
+ profileId: current?.id,
17285
+ label: plan.jiraProfile.label,
17286
+ provider: "githubActions",
17287
+ config: plan.jiraProfile.config,
17288
+ enabled: plan.jiraProfile.enabled,
17289
+ secretToken
17290
+ }
17291
+ }),
17292
+ "upsertScenarioAutomationProfile"
17293
+ );
17294
+ return {
17295
+ profile,
17296
+ result: {
17297
+ id: action.id,
17298
+ scope: action.scope,
17299
+ status: current ? "updated" : "created",
17300
+ message: `${current ? "Updated" : "Created"} Jira automation profile ${plan.jiraProfile.label} through the existing Forge contract.`,
17301
+ rollback: {
17302
+ available: true,
17303
+ guidance: rollbackFor(plan, action.id),
17304
+ previousState: previousProfileState(current)
17305
+ }
17306
+ }
17307
+ };
17308
+ }
17309
+ async function applyProjectDefault(plan, state, profile, context, dryRun) {
17310
+ const action = plan.actions.find((item) => item.scope === "project-default");
17311
+ if (state.currentDefaultProfileId === profile.id) {
17312
+ return {
17313
+ id: action.id,
17314
+ scope: action.scope,
17315
+ status: "skipped",
17316
+ message: "Jira automation profile is already the project default.",
17317
+ rollback: { available: false, guidance: "No project-default mutation occurred." }
17318
+ };
17319
+ }
17320
+ if (!dryRun) {
17321
+ requireServiceData(
17322
+ await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
17323
+ context: { projectKey: plan.project.key },
17324
+ profileId: profile.id
17325
+ }),
17326
+ "setScenarioAutomationDefaultProfile"
17327
+ );
17328
+ }
17329
+ return {
17330
+ id: action.id,
17331
+ scope: action.scope,
17332
+ status: dryRun ? "planned" : "updated",
17333
+ message: dryRun ? "Set the planned Jira automation profile as project default through setScenarioAutomationDefaultProfile." : "Set the Jira automation profile as project default through the existing Forge contract.",
17334
+ rollback: {
17335
+ available: !dryRun,
17336
+ guidance: rollbackFor(plan, action.id),
17337
+ previousState: { profileId: state.currentDefaultProfileId ?? null }
17338
+ }
17339
+ };
17340
+ }
17341
+ function doctorStatus(checks) {
17342
+ if (checks.some((check) => check.status === "fail")) return "fail";
17343
+ if (checks.some((check) => check.status === "warn")) return "warn";
17344
+ return "pass";
17345
+ }
17346
+ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17347
+ const checks = [];
17348
+ let forgeTransportFailure = false;
17349
+ const target = resolveWorkflowTarget(plan, repoRoot);
17350
+ if (!(0, import_node_fs14.existsSync)(target)) {
17351
+ checks.push({
17352
+ id: "workflow-local",
17353
+ status: "fail",
17354
+ message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
17355
+ });
17356
+ } else {
17357
+ const localHash = hashSetupContent((0, import_node_fs14.readFileSync)(target, "utf8"));
17358
+ checks.push({
17359
+ id: "workflow-local",
17360
+ status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
17361
+ message: localHash === plan.github.workflowFile.sha256 ? "Local workflow content matches the approved plan hash." : "Local workflow content differs from the approved plan hash."
17362
+ });
17363
+ }
17364
+ try {
17365
+ const workflow = await deps.githubProvider.inspectExecutionDefinition(
17366
+ plan,
17367
+ secrets.values.githubProviderToken ?? secrets.values.githubAdminToken
17368
+ );
17369
+ const exact = workflow.found && workflow.active && workflow.path === plan.github.workflowFile.path && workflow.contentSha256 === plan.github.workflowFile.sha256;
17370
+ checks.push({
17371
+ id: "workflow-github",
17372
+ status: exact ? "pass" : workflow.found && (workflow.path !== plan.github.workflowFile.path || workflow.active === false) ? "fail" : "warn",
17373
+ message: workflow.message
17374
+ });
17375
+ } catch (error) {
17376
+ checks.push({
17377
+ id: "workflow-github",
17378
+ status: "warn",
17379
+ message: `GitHub workflow could not be verified: ${safeErrorMessage(error, secrets)}`
17380
+ });
17381
+ }
17382
+ try {
17383
+ const secretInspection = await deps.githubProvider.inspectCallbackSecretMetadata(
17384
+ plan,
17385
+ secrets.values.githubAdminToken
17386
+ );
17387
+ if (!secretInspection.verified) {
17388
+ checks.push({ id: "callback-secret-metadata", status: "warn", message: secretInspection.message });
17389
+ } else {
17390
+ const missing = plan.github.requiredSecrets.filter(
17391
+ (secret) => !secretInspection.names.has(secret.repositorySecretName)
17392
+ );
17393
+ checks.push({
17394
+ id: "callback-secret-metadata",
17395
+ status: missing.length === 0 ? "pass" : "fail",
17396
+ message: missing.length === 0 ? "All required GitHub repository secret names exist; values remain unreadable and were not printed." : `Missing GitHub repository secret name(s): ${missing.map((item) => item.repositorySecretName).join(", ")}.`
17397
+ });
17398
+ }
17399
+ } catch (error) {
17400
+ checks.push({
17401
+ id: "callback-secret-metadata",
17402
+ status: "warn",
17403
+ message: `GitHub repository secret metadata could not be verified: ${safeErrorMessage(error, secrets)}`
17404
+ });
17405
+ }
17406
+ try {
17407
+ const profiles = requireServiceData(
17408
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
17409
+ context: { projectKey: plan.project.key }
17410
+ }),
17411
+ "listScenarioAutomationProfiles"
17412
+ );
17413
+ const profile = findMatchingProfile(profiles, plan);
17414
+ const profileOk = Boolean(profile && profileMatches(profile, plan) && profile.hasSecret);
17415
+ checks.push({
17416
+ id: "jira-profile",
17417
+ status: profileOk ? "pass" : "fail",
17418
+ message: profileOk ? "Jira automation profile fields match the plan and Forge reports a stored provider secret." : "Jira automation profile is missing, differs from the plan, or lacks a provider secret."
17419
+ });
17420
+ if (plan.jiraProfile.setProjectDefault) {
17421
+ checks.push({
17422
+ id: "jira-project-default",
17423
+ status: profile?.isProjectDefault ? "pass" : "fail",
17424
+ message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
17425
+ });
17426
+ }
17427
+ } catch (error) {
17428
+ forgeTransportFailure = error instanceof ForgeClientError && (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR");
17429
+ checks.push({
17430
+ id: "jira-profile",
17431
+ status: "fail",
17432
+ message: `Jira automation profile could not be verified: ${safeErrorMessage(error, secrets)}`
17433
+ });
17434
+ }
17435
+ const status = doctorStatus(checks);
17436
+ return {
17437
+ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
17438
+ planId: plan.planId,
17439
+ provider: "github-actions",
17440
+ status,
17441
+ exitCode: forgeTransportFailure ? ExitCode.TransportError : status === "fail" ? ExitCode.ValidationError : ExitCode.Success,
17442
+ externalRunTriggered: false,
17443
+ checks
17444
+ };
17445
+ }
17446
+ function planCommand(args, deps) {
17447
+ const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
17448
+ if (parsed.errors.length > 0) {
17449
+ return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors), ExitCode.UsageError);
17450
+ }
17451
+ const provider = parsed.flags["--provider"] ?? "github-actions";
17452
+ if (provider === "azure-devops") {
17453
+ try {
17454
+ return jsonResponse(buildAzureDevOpsSetupPlan({
17455
+ projectKey: parsed.flags["--project-key"] ?? "",
17456
+ organization: parsed.flags["--organization"] ?? "",
17457
+ azureProject: parsed.flags["--azure-project"] ?? "",
17458
+ pipelineId: parsed.flags["--pipeline-id"] ?? "",
17459
+ ref: parsed.flags["--ref"] ?? "main",
17460
+ apiVersion: parsed.flags["--api-version"],
17461
+ profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
17462
+ enabled: !parsed.boolFlags.has("--disabled"),
17463
+ setProjectDefault: parsed.boolFlags.has("--set-default"),
17464
+ callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17465
+ callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17466
+ }));
17467
+ } catch (error) {
17468
+ return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17469
+ }
17470
+ }
17471
+ if (provider !== "github-actions") {
17472
+ return jsonResponse(
17473
+ errorPayload("VALIDATION_ERROR", "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."),
17474
+ ExitCode.ValidationError
17475
+ );
17476
+ }
17477
+ try {
17478
+ const plan = buildGitHubSetupPlan({
17479
+ projectKey: parsed.flags["--project-key"] ?? "",
17480
+ owner: parsed.flags["--owner"] ?? "",
17481
+ repository: parsed.flags["--repo"] ?? "",
17482
+ ref: parsed.flags["--ref"] ?? "main",
17483
+ workflowId: parsed.flags["--workflow-id"] ?? "scenario-dispatch.yml",
17484
+ workflowPath: parsed.flags["--workflow-path"] ?? ".github/workflows/scenario-dispatch.yml",
17485
+ workflowSourcePath: import_node_path15.default.resolve(deps.cwd, parsed.flags["--workflow-source"] ?? ""),
17486
+ profileLabel: parsed.flags["--profile-label"] ?? "GitHub Actions",
17487
+ enabled: !parsed.boolFlags.has("--disabled"),
17488
+ setProjectDefault: parsed.boolFlags.has("--set-default"),
17489
+ callbackEndpointSecretName: parsed.flags["--callback-endpoint-secret-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17490
+ callbackAuthTokenSecretName: parsed.flags["--callback-auth-token-secret-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17491
+ });
17492
+ return jsonResponse(plan);
17493
+ } catch (error) {
17494
+ return jsonResponse(
17495
+ errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
17496
+ ExitCode.ValidationError
17497
+ );
17498
+ }
17499
+ }
17500
+ async function applyCommand(args, context, deps) {
17501
+ const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
17502
+ if (parsed.errors.length > 0) {
17503
+ return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors), ExitCode.UsageError);
17504
+ }
17505
+ const planPath = parsed.flags["--plan"];
17506
+ if (!planPath) {
17507
+ return jsonResponse(errorPayload("USAGE_ERROR", "--plan <file> is required."), ExitCode.UsageError);
17508
+ }
17509
+ const dryRun = parsed.boolFlags.has("--dry-run");
17510
+ if (!dryRun && !parsed.boolFlags.has("--confirm")) {
17511
+ return jsonResponse(
17512
+ errorPayload("CONFIRMATION_REQUIRED", "Mutating apply requires --confirm plus explicit --approve scopes."),
17513
+ ExitCode.UsageError
17514
+ );
17515
+ }
17516
+ let secrets = { values: {}, sources: {} };
17517
+ const actionResults = [];
17518
+ try {
17519
+ const plan = loadPlan(planPath, deps.cwd);
17520
+ secrets = await resolveSecrets(parsed, deps);
17521
+ if (plan.provider === "azure-devops") {
17522
+ return await applyAzureCommand(plan, parsed, context, deps, secrets);
17523
+ }
17524
+ const scopes = approvedScopes(plan, parsed, dryRun);
17525
+ const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17526
+ const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
17527
+ const rotateProviderToken = parsed.boolFlags.has("--rotate-provider-token");
17528
+ const state = await prepareState(
17529
+ plan,
17530
+ scopes,
17531
+ secrets,
17532
+ context,
17533
+ deps,
17534
+ dryRun,
17535
+ rotateSecrets,
17536
+ rotateProviderToken
17537
+ );
17538
+ let appliedProfile = state.matchingProfile;
17539
+ for (const action of plan.actions) {
17540
+ if (!scopes.includes(action.scope)) {
17541
+ actionResults.push({
17542
+ id: action.id,
17543
+ scope: action.scope,
17544
+ status: "skipped",
17545
+ message: "Action was not in the explicit approval scope.",
17546
+ rollback: { available: false, guidance: "No mutation occurred." }
17547
+ });
17548
+ continue;
17549
+ }
17550
+ try {
17551
+ if (action.scope === "workflow-file") {
17552
+ actionResults.push(applyWorkflowFile(plan, repoRoot, dryRun));
17553
+ } else if (action.scope === "github-secrets") {
17554
+ actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
17555
+ } else if (action.scope === "jira-profile") {
17556
+ const applied = await applyJiraProfile(
17557
+ plan,
17558
+ state,
17559
+ secrets,
17560
+ context,
17561
+ dryRun,
17562
+ rotateProviderToken
17563
+ );
17564
+ appliedProfile = applied.profile;
17565
+ actionResults.push(applied.result);
17566
+ } else if (action.scope === "project-default") {
17567
+ if (!appliedProfile) {
17568
+ throw new SetupCommandError(
17569
+ "PROFILE_REQUIRED",
17570
+ "Project-default apply requires an existing or approved Jira automation profile.",
17571
+ ExitCode.ValidationError
17572
+ );
17573
+ }
17574
+ actionResults.push(await applyProjectDefault(plan, state, appliedProfile, context, dryRun));
17575
+ }
17576
+ } catch (error) {
17577
+ actionResults.push({
17578
+ id: action.id,
17579
+ scope: action.scope,
17580
+ status: "failed",
17581
+ message: safeErrorMessage(error, secrets),
17582
+ rollback: {
17583
+ available: false,
17584
+ guidance: rollbackFor(plan, action.id)
17585
+ }
17586
+ });
17587
+ throw error;
17588
+ }
17589
+ }
17590
+ const doctor = dryRun ? void 0 : await inspectPlan(plan, repoRoot, secrets, context, deps);
17591
+ return jsonResponse(
17592
+ {
17593
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
17594
+ planId: plan.planId,
17595
+ provider: plan.provider,
17596
+ status: dryRun ? "dry-run" : doctor?.status === "fail" ? "applied-with-validation-failures" : doctor?.status === "warn" ? "applied-with-warnings" : "applied",
17597
+ dryRun,
17598
+ confirmed: !dryRun,
17599
+ approvedScopes: scopes,
17600
+ secretInputs: Object.entries(secrets.sources).map(([key, source]) => ({ key, source })),
17601
+ actions: actionResults,
17602
+ smokeValidation: doctor ?? {
17603
+ status: "not-run",
17604
+ externalRunTriggered: false,
17605
+ message: "Dry-run performs no mutations and does not require remote smoke validation. Run setup doctor for read-only verification."
17606
+ }
17607
+ },
17608
+ doctor?.exitCode ?? ExitCode.Success
17609
+ );
17610
+ } catch (error) {
17611
+ const message = safeErrorMessage(error, secrets);
17612
+ return jsonResponse(
17613
+ {
17614
+ ...errorPayload(errorCodeForError(error, "APPLY_FAILED"), message, [
17615
+ "Review each completed action and its rollback guidance; GitHub secret values cannot be restored automatically."
17616
+ ]),
17617
+ actions: actionResults
17618
+ },
17619
+ exitCodeForError(error)
17620
+ );
17621
+ }
17622
+ }
17623
+ async function applyAzureCommand(plan, parsed, context, deps, secrets) {
17624
+ const dryRun = parsed.boolFlags.has("--dry-run");
17625
+ const scopes = approvedScopes(plan, parsed, dryRun);
17626
+ const profiles = requireServiceData(await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }), "listScenarioAutomationProfiles");
17627
+ const current = profiles.find((item) => item.label === plan.jiraProfile.label);
17628
+ if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.", ExitCode.ValidationError);
17629
+ const results = [];
17630
+ const secretNames = scopes.includes("provider-secrets") && secrets.values.azureDevOpsAdminToken ? (await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken)).names : void 0;
17631
+ let appliedProfile = current;
17632
+ for (const action of plan.actions) {
17633
+ if (!scopes.includes(action.scope)) {
17634
+ results.push({ id: action.id, scope: action.scope, status: "skipped", message: "Action was not in the explicit approval scope.", rollback: { available: false, guidance: "No mutation occurred." } });
17635
+ continue;
17636
+ }
17637
+ if (action.scope === "provider-secrets") {
17638
+ const pending = plan.azureDevOps.requiredSecrets.filter((item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName));
17639
+ if (!dryRun && pending.some((item) => !secrets.values[item.valueKey])) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "Azure DevOps callback secrets are required for approved pipeline variable changes.", ExitCode.ValidationError);
17640
+ if (!dryRun) for (const item of pending) await deps.azureProvider.setCallbackSecret(plan, item, secrets.values[item.valueKey], secrets.values.azureDevOpsAdminToken);
17641
+ results.push({ id: action.id, scope: action.scope, status: dryRun ? "planned" : pending.length ? "updated" : "skipped", message: "Azure DevOps pipeline variable names are managed without printing secret values.", rollback: { available: false, guidance: rollbackFor(plan, action.id) } });
17642
+ } else if (action.scope === "jira-profile") {
17643
+ const rotateProvider = parsed.boolFlags.has("--rotate-provider-token");
17644
+ if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.", ExitCode.ValidationError);
17645
+ const profile = dryRun ? current ?? { id: "<created-by-forge>", projectKey: plan.project.key, label: plan.jiraProfile.label, provider: "azureDevops", authType: "basicPat", method: "POST", endpointSummary: "<computed-by-forge>", config: plan.jiraProfile.config, enabled: plan.jiraProfile.enabled, hasSecret: false, createdAt: "", updatedAt: "" } : requireServiceData(await context.invokeForgeContract("upsertScenarioAutomationProfile", { context: { projectKey: plan.project.key }, input: { profileId: current?.id, label: plan.jiraProfile.label, provider: "azureDevops", config: plan.jiraProfile.config, enabled: plan.jiraProfile.enabled, secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0 } }), "upsertScenarioAutomationProfile");
17646
+ appliedProfile = profile;
17647
+ results.push({ id: action.id, scope: action.scope, status: dryRun ? "planned" : current ? "updated" : "created", message: "Azure DevOps Jira automation profile applied through the existing Forge contract.", rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) } });
17648
+ } else if (action.scope === "project-default" && appliedProfile && !dryRun) {
17649
+ requireServiceData(await context.invokeForgeContract("setScenarioAutomationDefaultProfile", { context: { projectKey: plan.project.key }, profileId: appliedProfile.id }), "setScenarioAutomationDefaultProfile");
17650
+ results.push({ id: action.id, scope: action.scope, status: "updated", message: "Set the Azure DevOps Jira automation profile as project default.", rollback: { available: true, guidance: rollbackFor(plan, action.id) } });
17651
+ } else if (action.scope === "project-default") {
17652
+ results.push({ id: action.id, scope: action.scope, status: "planned", message: "Set the Azure DevOps Jira automation profile as project default.", rollback: { available: false, guidance: rollbackFor(plan, action.id) } });
17653
+ }
17654
+ }
17655
+ return jsonResponse({ schemaVersion: APPLY_RESULT_SCHEMA_VERSION, planId: plan.planId, provider: plan.provider, status: dryRun ? "dry-run" : "applied", dryRun, confirmed: !dryRun, approvedScopes: scopes, actions: results, smokeValidation: { status: "not-run", externalRunTriggered: false } });
17656
+ }
17657
+ async function doctorCommand(args, context, deps) {
17658
+ const parsed = parseArgs10(
17659
+ args,
17660
+ APPLY_VALUE_FLAGS,
17661
+ /* @__PURE__ */ new Set(["--secrets-stdin"]),
17662
+ true
17663
+ );
17664
+ if (parsed.approvals.length > 0) {
17665
+ return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
17666
+ }
17667
+ if (parsed.errors.length > 0) {
17668
+ return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors), ExitCode.UsageError);
17669
+ }
17670
+ const planPath = parsed.flags["--plan"];
17671
+ if (!planPath) {
17672
+ return jsonResponse(errorPayload("USAGE_ERROR", "--plan <file> is required."), ExitCode.UsageError);
17673
+ }
17674
+ let secrets = { values: {}, sources: {} };
17675
+ try {
17676
+ const plan = loadPlan(planPath, deps.cwd);
17677
+ secrets = await resolveSecrets(parsed, deps);
17678
+ if (plan.provider === "azure-devops") {
17679
+ const checks = [];
17680
+ const inspection = await deps.azureProvider.inspectExecutionDefinition(plan, secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken);
17681
+ checks.push({ id: "pipeline-azure", status: inspection.found ? "pass" : "fail", message: inspection.message });
17682
+ const variables = await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken);
17683
+ const missing = plan.azureDevOps.requiredSecrets.filter((item) => !variables.names.has(item.repositorySecretName));
17684
+ checks.push({ id: "callback-secret-metadata", status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn", message: variables.message });
17685
+ const profiles = requireServiceData(await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }), "listScenarioAutomationProfiles");
17686
+ const profile = profiles.find((item) => item.label === plan.jiraProfile.label);
17687
+ const config = profile?.config;
17688
+ const matches = profile?.provider === "azureDevops" && profile.enabled === plan.jiraProfile.enabled && profile.hasSecret && config?.organization === plan.jiraProfile.config.organization && config?.project === plan.jiraProfile.config.project && config?.pipelineId === plan.jiraProfile.config.pipelineId && config?.apiVersion === plan.jiraProfile.config.apiVersion && config?.bodyTemplate === plan.jiraProfile.config.bodyTemplate;
17689
+ checks.push({ id: "jira-profile", status: matches ? "pass" : "fail", message: matches ? "Jira automation profile fields match and Forge reports a stored provider secret." : "Jira automation profile is missing, differs, or lacks provider secret metadata." });
17690
+ if (plan.jiraProfile.setProjectDefault) checks.push({ id: "jira-project-default", status: profile?.isProjectDefault ? "pass" : "fail", message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default." });
17691
+ const status = doctorStatus(checks);
17692
+ return jsonResponse({ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION, planId: plan.planId, provider: plan.provider, status, exitCode: status === "fail" ? ExitCode.ValidationError : ExitCode.Success, externalRunTriggered: false, checks });
17693
+ }
17694
+ const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17695
+ const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
17696
+ return jsonResponse(summary, summary.exitCode);
17697
+ } catch (error) {
17698
+ return jsonResponse(
17699
+ errorPayload(errorCodeForError(error, "DOCTOR_FAILED"), safeErrorMessage(error, secrets)),
17700
+ exitCodeForError(error)
17701
+ );
17702
+ }
17703
+ }
17704
+ function createSetupHandler(overrides = {}) {
17705
+ const deps = {
17706
+ cwd: overrides.cwd ?? process.cwd(),
17707
+ env: overrides.env ?? process.env,
17708
+ readStdin: overrides.readStdin ?? defaultReadStdin,
17709
+ githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps),
17710
+ azureProvider: overrides.azureProvider ?? new AzureDevOpsSetupProvider()
17711
+ };
17712
+ return async (request, context) => {
17713
+ const [subcommand, ...args] = request.args;
17714
+ if (subcommand === "plan") {
17715
+ return planCommand(args, deps);
17716
+ }
17717
+ if (subcommand === "apply") {
17718
+ return applyCommand(args, context, deps);
17719
+ }
17720
+ if (subcommand === "doctor") {
17721
+ return doctorCommand(args, context, deps);
17722
+ }
17723
+ return jsonResponse(
17724
+ errorPayload("USAGE_ERROR", `Unsupported setup subcommand ${subcommand ?? "<missing>"}.`),
17725
+ ExitCode.UsageError
17726
+ );
17727
+ };
17728
+ }
17729
+
16042
17730
  // src/suites.ts
16043
17731
  var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
16044
17732
  "--config",
@@ -16049,7 +17737,7 @@ var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
16049
17737
  "--jira-email",
16050
17738
  "--jira-api-token"
16051
17739
  ]);
16052
- function parseArgs10(args) {
17740
+ function parseArgs11(args) {
16053
17741
  const flags = {};
16054
17742
  const boolFlags = /* @__PURE__ */ new Set();
16055
17743
  const unknownFlags = [];
@@ -16169,7 +17857,7 @@ function createSuitesHandler(deps = {}) {
16169
17857
  const env = deps.env ?? process.env;
16170
17858
  return async (request, context) => {
16171
17859
  const [subcommand, ...restArgs] = request.args;
16172
- const parsed = parseArgs10(restArgs);
17860
+ const parsed = parseArgs11(restArgs);
16173
17861
  const useJson = parsed.boolFlags.has("--json");
16174
17862
  const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
16175
17863
  const projectKey = config.values.projectKey;
@@ -16343,7 +18031,7 @@ var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
16343
18031
  "--jira-api-token"
16344
18032
  ]);
16345
18033
  var RECONCILE_CONFIRM_TOKEN = "RECONCILE";
16346
- function parseArgs11(args) {
18034
+ function parseArgs12(args) {
16347
18035
  const flags = {};
16348
18036
  const boolFlags = /* @__PURE__ */ new Set();
16349
18037
  const unknownFlags = [];
@@ -16432,7 +18120,7 @@ function createSyncHandler(deps = {}) {
16432
18120
  const env = deps.env ?? process.env;
16433
18121
  return async (request, context) => {
16434
18122
  const [subcommand, ...restArgs] = request.args;
16435
- const parsed = parseArgs11(restArgs);
18123
+ const parsed = parseArgs12(restArgs);
16436
18124
  const useJson = parsed.boolFlags.has("--json");
16437
18125
  const config = resolveCliConfig(pickConfigArgs10(parsed), env, cwd);
16438
18126
  const projectKey = config.values.projectKey;
@@ -16592,6 +18280,12 @@ var COMMAND_REGISTRY = [
16592
18280
  subcommands: ["show"],
16593
18281
  handler: createRunsHandler()
16594
18282
  },
18283
+ {
18284
+ name: "setup",
18285
+ description: "Plan, apply, and verify provider onboarding without duplicating Forge domain logic",
18286
+ subcommands: ["plan", "apply", "doctor"],
18287
+ handler: createSetupHandler()
18288
+ },
16595
18289
  {
16596
18290
  name: "suites",
16597
18291
  description: "Hierarchy-aware suite browse commands",