@automatify-au/cli 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -1
- package/dist/automatify.cjs +1524 -32
- package/package.json +3 -2
package/dist/automatify.cjs
CHANGED
|
@@ -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(
|
|
2898
|
-
var parts =
|
|
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(
|
|
8752
|
-
if (
|
|
8753
|
-
|
|
8751
|
+
var parentFolder = function(path16) {
|
|
8752
|
+
if (path16.slice(-1) === "/") {
|
|
8753
|
+
path16 = path16.substring(0, path16.length - 1);
|
|
8754
8754
|
}
|
|
8755
|
-
var lastSlash =
|
|
8756
|
-
return lastSlash > 0 ?
|
|
8755
|
+
var lastSlash = path16.lastIndexOf("/");
|
|
8756
|
+
return lastSlash > 0 ? path16.substring(0, lastSlash) : "";
|
|
8757
8757
|
};
|
|
8758
|
-
var forceTrailingSlash = function(
|
|
8759
|
-
if (
|
|
8760
|
-
|
|
8758
|
+
var forceTrailingSlash = function(path16) {
|
|
8759
|
+
if (path16.slice(-1) !== "/") {
|
|
8760
|
+
path16 += "/";
|
|
8761
8761
|
}
|
|
8762
|
-
return
|
|
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
|
|
9729
|
-
if (!(this instanceof
|
|
9730
|
-
return new
|
|
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
|
|
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
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
return new
|
|
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
|
-
|
|
9757
|
-
module2.exports =
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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,1486 @@ 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/setup.ts
|
|
16672
|
+
var APPLY_RESULT_SCHEMA_VERSION = "automatify.testops.setup.apply/v1";
|
|
16673
|
+
var DOCTOR_RESULT_SCHEMA_VERSION = "automatify.testops.setup.doctor/v1";
|
|
16674
|
+
var SECRET_KEYS = [
|
|
16675
|
+
"githubProviderToken",
|
|
16676
|
+
"githubAdminToken",
|
|
16677
|
+
"callbackEndpoint",
|
|
16678
|
+
"callbackAuthToken"
|
|
16679
|
+
];
|
|
16680
|
+
var MUTATION_SCOPES = [
|
|
16681
|
+
"workflow-file",
|
|
16682
|
+
"github-secrets",
|
|
16683
|
+
"jira-profile",
|
|
16684
|
+
"project-default"
|
|
16685
|
+
];
|
|
16686
|
+
var SetupCommandError = class extends Error {
|
|
16687
|
+
cliCode;
|
|
16688
|
+
exitCode;
|
|
16689
|
+
constructor(cliCode, message, exitCode) {
|
|
16690
|
+
super(message);
|
|
16691
|
+
this.name = "SetupCommandError";
|
|
16692
|
+
this.cliCode = cliCode;
|
|
16693
|
+
this.exitCode = exitCode;
|
|
16694
|
+
}
|
|
16695
|
+
};
|
|
16696
|
+
var PLAN_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
16697
|
+
"--provider",
|
|
16698
|
+
"--project-key",
|
|
16699
|
+
"--owner",
|
|
16700
|
+
"--repo",
|
|
16701
|
+
"--ref",
|
|
16702
|
+
"--workflow-id",
|
|
16703
|
+
"--workflow-path",
|
|
16704
|
+
"--workflow-source",
|
|
16705
|
+
"--profile-label",
|
|
16706
|
+
"--callback-endpoint-secret-name",
|
|
16707
|
+
"--callback-auth-token-secret-name"
|
|
16708
|
+
]);
|
|
16709
|
+
var APPLY_VALUE_FLAGS = /* @__PURE__ */ new Set(["--plan", "--repo-root"]);
|
|
16710
|
+
var APPLY_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
16711
|
+
"--confirm",
|
|
16712
|
+
"--dry-run",
|
|
16713
|
+
"--secrets-stdin",
|
|
16714
|
+
"--rotate-secrets",
|
|
16715
|
+
"--rotate-provider-token"
|
|
16716
|
+
]);
|
|
16717
|
+
function defaultReadStdin() {
|
|
16718
|
+
return new Promise((resolve, reject) => {
|
|
16719
|
+
let value = "";
|
|
16720
|
+
process.stdin.setEncoding("utf8");
|
|
16721
|
+
process.stdin.on("data", (chunk) => {
|
|
16722
|
+
value += chunk;
|
|
16723
|
+
});
|
|
16724
|
+
process.stdin.on("end", () => resolve(value));
|
|
16725
|
+
process.stdin.on("error", reject);
|
|
16726
|
+
});
|
|
16727
|
+
}
|
|
16728
|
+
function parseArgs10(args, valueFlags, boolFlags, allowApplyCollections = false) {
|
|
16729
|
+
const flags = {};
|
|
16730
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
16731
|
+
const approvals = [];
|
|
16732
|
+
const secretRefs = [];
|
|
16733
|
+
const errors = [];
|
|
16734
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
16735
|
+
const token = args[index];
|
|
16736
|
+
if (!token.startsWith("--")) {
|
|
16737
|
+
errors.push(`Unexpected positional argument at index ${index}.`);
|
|
16738
|
+
continue;
|
|
16739
|
+
}
|
|
16740
|
+
if (boolFlags.has(token)) {
|
|
16741
|
+
enabled.add(token);
|
|
16742
|
+
continue;
|
|
16743
|
+
}
|
|
16744
|
+
if (allowApplyCollections && (token === "--approve" || token === "--secret-ref")) {
|
|
16745
|
+
const value2 = args[index + 1];
|
|
16746
|
+
if (!value2 || value2.startsWith("--")) {
|
|
16747
|
+
errors.push(`${token} requires a value.`);
|
|
16748
|
+
continue;
|
|
16749
|
+
}
|
|
16750
|
+
(token === "--approve" ? approvals : secretRefs).push(value2);
|
|
16751
|
+
index += 1;
|
|
16752
|
+
continue;
|
|
16753
|
+
}
|
|
16754
|
+
if (!valueFlags.has(token)) {
|
|
16755
|
+
errors.push(`Unknown option ${token}.`);
|
|
16756
|
+
continue;
|
|
16757
|
+
}
|
|
16758
|
+
const value = args[index + 1];
|
|
16759
|
+
if (!value || value.startsWith("--")) {
|
|
16760
|
+
errors.push(`${token} requires a value.`);
|
|
16761
|
+
continue;
|
|
16762
|
+
}
|
|
16763
|
+
flags[token] = value;
|
|
16764
|
+
index += 1;
|
|
16765
|
+
}
|
|
16766
|
+
return { flags, boolFlags: enabled, approvals, secretRefs, errors };
|
|
16767
|
+
}
|
|
16768
|
+
function jsonResponse(payload, exitCode = ExitCode.Success) {
|
|
16769
|
+
return {
|
|
16770
|
+
exitCode,
|
|
16771
|
+
stdout: [JSON.stringify(payload)]
|
|
16772
|
+
};
|
|
16773
|
+
}
|
|
16774
|
+
function errorPayload(code, message, diagnostics = []) {
|
|
16775
|
+
return {
|
|
16776
|
+
schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
|
|
16777
|
+
status: "failed",
|
|
16778
|
+
error: {
|
|
16779
|
+
code,
|
|
16780
|
+
message,
|
|
16781
|
+
diagnostics
|
|
16782
|
+
}
|
|
16783
|
+
};
|
|
16784
|
+
}
|
|
16785
|
+
function safeErrorMessage(error, secrets = { values: {}, sources: {} }) {
|
|
16786
|
+
let message = error instanceof Error ? error.message : "Unknown setup error.";
|
|
16787
|
+
for (const secret of Object.values(secrets.values)) {
|
|
16788
|
+
if (secret) {
|
|
16789
|
+
message = message.split(secret).join("[REDACTED]");
|
|
16790
|
+
}
|
|
16791
|
+
}
|
|
16792
|
+
return message.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [REDACTED]").replace(/token[=:]\s*[^\s,;]+/gi, "token=[REDACTED]");
|
|
16793
|
+
}
|
|
16794
|
+
function exitCodeForError(error) {
|
|
16795
|
+
if (error instanceof SetupCommandError) {
|
|
16796
|
+
return error.exitCode;
|
|
16797
|
+
}
|
|
16798
|
+
if (error instanceof ForgeClientError) {
|
|
16799
|
+
return error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR" ? ExitCode.TransportError : ExitCode.RemoteError;
|
|
16800
|
+
}
|
|
16801
|
+
return ExitCode.RemoteError;
|
|
16802
|
+
}
|
|
16803
|
+
function errorCodeForError(error, fallback) {
|
|
16804
|
+
return error instanceof SetupCommandError ? error.cliCode : fallback;
|
|
16805
|
+
}
|
|
16806
|
+
function loadPlan(planPath, cwd) {
|
|
16807
|
+
const absolute = import_node_path15.default.resolve(cwd, planPath);
|
|
16808
|
+
let parsed;
|
|
16809
|
+
try {
|
|
16810
|
+
parsed = JSON.parse((0, import_node_fs14.readFileSync)(absolute, "utf8"));
|
|
16811
|
+
} catch (error) {
|
|
16812
|
+
throw new SetupCommandError(
|
|
16813
|
+
"PLAN_READ_ERROR",
|
|
16814
|
+
`Could not read setup plan JSON: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
16815
|
+
ExitCode.ValidationError
|
|
16816
|
+
);
|
|
16817
|
+
}
|
|
16818
|
+
const validation = validateSetupPlan(parsed);
|
|
16819
|
+
if (!validation.ok || !validation.plan) {
|
|
16820
|
+
throw new SetupCommandError(
|
|
16821
|
+
"PLAN_VALIDATION_ERROR",
|
|
16822
|
+
`Setup plan validation failed: ${validation.errors.join(" ")}`,
|
|
16823
|
+
ExitCode.ValidationError
|
|
16824
|
+
);
|
|
16825
|
+
}
|
|
16826
|
+
return validation.plan;
|
|
16827
|
+
}
|
|
16828
|
+
function isSecretKey(value) {
|
|
16829
|
+
return SECRET_KEYS.includes(value);
|
|
16830
|
+
}
|
|
16831
|
+
async function resolveSecrets(parsed, deps) {
|
|
16832
|
+
const values = {};
|
|
16833
|
+
const sources = {};
|
|
16834
|
+
if (parsed.boolFlags.has("--secrets-stdin")) {
|
|
16835
|
+
const raw = await deps.readStdin();
|
|
16836
|
+
let envelope;
|
|
16837
|
+
try {
|
|
16838
|
+
envelope = JSON.parse(raw);
|
|
16839
|
+
} catch {
|
|
16840
|
+
throw new SetupCommandError(
|
|
16841
|
+
"SECRET_INPUT_ERROR",
|
|
16842
|
+
"Secret stdin must be a JSON object. Input content was not retained or printed.",
|
|
16843
|
+
ExitCode.ValidationError
|
|
16844
|
+
);
|
|
16845
|
+
}
|
|
16846
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
16847
|
+
throw new SetupCommandError("SECRET_INPUT_ERROR", "Secret stdin must be a JSON object.", ExitCode.ValidationError);
|
|
16848
|
+
}
|
|
16849
|
+
for (const [key, value] of Object.entries(envelope)) {
|
|
16850
|
+
if (!isSecretKey(key)) {
|
|
16851
|
+
throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin contains unsupported key ${key}.`, ExitCode.ValidationError);
|
|
16852
|
+
}
|
|
16853
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
16854
|
+
throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin key ${key} must be a non-empty string.`, ExitCode.ValidationError);
|
|
16855
|
+
}
|
|
16856
|
+
values[key] = value.trim();
|
|
16857
|
+
sources[key] = "stdin";
|
|
16858
|
+
}
|
|
16859
|
+
}
|
|
16860
|
+
for (const reference of parsed.secretRefs) {
|
|
16861
|
+
const separator = reference.indexOf("=");
|
|
16862
|
+
const key = separator > 0 ? reference.slice(0, separator) : "";
|
|
16863
|
+
const target = separator > 0 ? reference.slice(separator + 1) : "";
|
|
16864
|
+
if (!isSecretKey(key) || !target.startsWith("env:")) {
|
|
16865
|
+
throw new SetupCommandError(
|
|
16866
|
+
"SECRET_REFERENCE_ERROR",
|
|
16867
|
+
"Secret references must use <supported-key>=env:<VARIABLE_NAME>.",
|
|
16868
|
+
ExitCode.ValidationError
|
|
16869
|
+
);
|
|
16870
|
+
}
|
|
16871
|
+
const envName = target.slice(4);
|
|
16872
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
|
|
16873
|
+
throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret reference for ${key} has an invalid environment variable name.`, ExitCode.ValidationError);
|
|
16874
|
+
}
|
|
16875
|
+
if (sources[key]) {
|
|
16876
|
+
throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret ${key} was provided more than once.`, ExitCode.ValidationError);
|
|
16877
|
+
}
|
|
16878
|
+
const value = deps.env[envName]?.trim();
|
|
16879
|
+
if (!value) {
|
|
16880
|
+
throw new SetupCommandError(
|
|
16881
|
+
"SECRET_REFERENCE_ERROR",
|
|
16882
|
+
`Secret reference for ${key} points to an empty or missing environment variable.`,
|
|
16883
|
+
ExitCode.ValidationError
|
|
16884
|
+
);
|
|
16885
|
+
}
|
|
16886
|
+
values[key] = value;
|
|
16887
|
+
sources[key] = "env-ref";
|
|
16888
|
+
}
|
|
16889
|
+
return { values, sources };
|
|
16890
|
+
}
|
|
16891
|
+
function approvedScopes(plan, parsed, dryRun) {
|
|
16892
|
+
const planScopes = plan.actions.map((action) => action.scope);
|
|
16893
|
+
const requested = parsed.approvals.length === 0 && dryRun ? ["all"] : parsed.approvals;
|
|
16894
|
+
if (requested.length === 0) {
|
|
16895
|
+
throw new SetupCommandError(
|
|
16896
|
+
"APPROVAL_REQUIRED",
|
|
16897
|
+
"At least one --approve <scope> is required for apply. Use --approve all to approve every planned action.",
|
|
16898
|
+
ExitCode.UsageError
|
|
16899
|
+
);
|
|
16900
|
+
}
|
|
16901
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
16902
|
+
for (const scope of requested) {
|
|
16903
|
+
if (scope === "all") {
|
|
16904
|
+
planScopes.forEach((item) => expanded.add(item));
|
|
16905
|
+
continue;
|
|
16906
|
+
}
|
|
16907
|
+
if (!MUTATION_SCOPES.includes(scope)) {
|
|
16908
|
+
throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
|
|
16909
|
+
}
|
|
16910
|
+
if (!planScopes.includes(scope)) {
|
|
16911
|
+
throw new SetupCommandError("APPROVAL_ERROR", `Approval scope ${scope} is not present in this plan.`, ExitCode.UsageError);
|
|
16912
|
+
}
|
|
16913
|
+
expanded.add(scope);
|
|
16914
|
+
}
|
|
16915
|
+
return planScopes.filter((scope) => expanded.has(scope));
|
|
16916
|
+
}
|
|
16917
|
+
function requireServiceData(result, contract) {
|
|
16918
|
+
if (!result.ok) {
|
|
16919
|
+
throw new Error(`${contract} failed: ${result.error.code}: ${result.error.message}`);
|
|
16920
|
+
}
|
|
16921
|
+
return result.data;
|
|
16922
|
+
}
|
|
16923
|
+
function findMatchingProfile(profiles, plan) {
|
|
16924
|
+
const matches = profiles.filter((profile) => profile.label === plan.jiraProfile.label);
|
|
16925
|
+
if (matches.length > 1) {
|
|
16926
|
+
throw new SetupCommandError(
|
|
16927
|
+
"AMBIGUOUS_PROFILE",
|
|
16928
|
+
`More than one Jira automation profile has label ${plan.jiraProfile.label}; rename duplicates before applying this deterministic plan.`,
|
|
16929
|
+
ExitCode.ValidationError
|
|
16930
|
+
);
|
|
16931
|
+
}
|
|
16932
|
+
return matches[0];
|
|
16933
|
+
}
|
|
16934
|
+
function profileMatches(profile, plan) {
|
|
16935
|
+
const config = profile.config;
|
|
16936
|
+
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;
|
|
16937
|
+
}
|
|
16938
|
+
function previousProfileState(profile) {
|
|
16939
|
+
if (!profile) {
|
|
16940
|
+
return { existed: false };
|
|
16941
|
+
}
|
|
16942
|
+
return {
|
|
16943
|
+
existed: true,
|
|
16944
|
+
id: profile.id,
|
|
16945
|
+
label: profile.label,
|
|
16946
|
+
provider: profile.provider,
|
|
16947
|
+
enabled: profile.enabled,
|
|
16948
|
+
wasProjectDefault: Boolean(profile.isProjectDefault),
|
|
16949
|
+
hadSecret: profile.hasSecret
|
|
16950
|
+
};
|
|
16951
|
+
}
|
|
16952
|
+
async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken) {
|
|
16953
|
+
let profiles = [];
|
|
16954
|
+
let matchingProfile;
|
|
16955
|
+
let currentDefaultProfileId;
|
|
16956
|
+
if (scopes.includes("jira-profile") || scopes.includes("project-default")) {
|
|
16957
|
+
profiles = requireServiceData(
|
|
16958
|
+
await context.invokeForgeContract("listScenarioAutomationProfiles", {
|
|
16959
|
+
context: { projectKey: plan.project.key }
|
|
16960
|
+
}),
|
|
16961
|
+
"listScenarioAutomationProfiles"
|
|
16962
|
+
);
|
|
16963
|
+
matchingProfile = findMatchingProfile(profiles, plan);
|
|
16964
|
+
currentDefaultProfileId = profiles.find((profile) => profile.isProjectDefault)?.id;
|
|
16965
|
+
const needsProviderToken = scopes.includes("jira-profile") && (!matchingProfile?.hasSecret || rotateProviderToken);
|
|
16966
|
+
if (needsProviderToken && !dryRun && !secrets.values.githubProviderToken) {
|
|
16967
|
+
throw new SetupCommandError(
|
|
16968
|
+
"SECRET_INPUT_REQUIRED",
|
|
16969
|
+
"githubProviderToken is required to create this Jira profile, restore a missing provider secret, or rotate the provider token.",
|
|
16970
|
+
ExitCode.ValidationError
|
|
16971
|
+
);
|
|
16972
|
+
}
|
|
16973
|
+
}
|
|
16974
|
+
let repositorySecretNames;
|
|
16975
|
+
if (scopes.includes("github-secrets")) {
|
|
16976
|
+
if (!dryRun && !secrets.values.githubAdminToken) {
|
|
16977
|
+
throw new SetupCommandError(
|
|
16978
|
+
"SECRET_INPUT_REQUIRED",
|
|
16979
|
+
"githubAdminToken is required for the approved github-secrets scope.",
|
|
16980
|
+
ExitCode.ValidationError
|
|
16981
|
+
);
|
|
16982
|
+
}
|
|
16983
|
+
if (secrets.values.githubAdminToken) {
|
|
16984
|
+
const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
|
|
16985
|
+
plan,
|
|
16986
|
+
secrets.values.githubAdminToken
|
|
16987
|
+
);
|
|
16988
|
+
repositorySecretNames = inspection.names;
|
|
16989
|
+
const secretsToWrite = plan.github.requiredSecrets.filter(
|
|
16990
|
+
(secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
|
|
16991
|
+
);
|
|
16992
|
+
if (!dryRun) {
|
|
16993
|
+
for (const secret of secretsToWrite) {
|
|
16994
|
+
if (!secrets.values[secret.valueKey]) {
|
|
16995
|
+
throw new SetupCommandError(
|
|
16996
|
+
"SECRET_INPUT_REQUIRED",
|
|
16997
|
+
`${secret.valueKey} is required to create or rotate ${secret.repositorySecretName}.`,
|
|
16998
|
+
ExitCode.ValidationError
|
|
16999
|
+
);
|
|
17000
|
+
}
|
|
17001
|
+
}
|
|
17002
|
+
}
|
|
17003
|
+
}
|
|
17004
|
+
}
|
|
17005
|
+
return { profiles, matchingProfile, currentDefaultProfileId, repositorySecretNames };
|
|
17006
|
+
}
|
|
17007
|
+
function rollbackFor(plan, actionId) {
|
|
17008
|
+
return plan.rollback.find((item) => item.actionId === actionId)?.strategy ?? "Review the plan rollback section.";
|
|
17009
|
+
}
|
|
17010
|
+
function resolveWorkflowTarget(plan, repoRoot) {
|
|
17011
|
+
const resolvedRoot = import_node_path15.default.resolve(repoRoot);
|
|
17012
|
+
const target = import_node_path15.default.resolve(resolvedRoot, plan.github.workflowFile.path);
|
|
17013
|
+
if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${import_node_path15.default.sep}`)) {
|
|
17014
|
+
throw new SetupCommandError(
|
|
17015
|
+
"WORKFLOW_PATH_ERROR",
|
|
17016
|
+
"Workflow target escapes the approved repository root.",
|
|
17017
|
+
ExitCode.ValidationError
|
|
17018
|
+
);
|
|
17019
|
+
}
|
|
17020
|
+
return target;
|
|
17021
|
+
}
|
|
17022
|
+
function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
17023
|
+
const action = plan.actions.find((item) => item.scope === "workflow-file");
|
|
17024
|
+
const target = resolveWorkflowTarget(plan, repoRoot);
|
|
17025
|
+
const sourceContent = (0, import_node_fs14.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
|
|
17026
|
+
if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
|
|
17027
|
+
throw new SetupCommandError(
|
|
17028
|
+
"WORKFLOW_SOURCE_CHANGED",
|
|
17029
|
+
"Workflow source content no longer matches the approved plan hash; generate a new plan before applying.",
|
|
17030
|
+
ExitCode.ValidationError
|
|
17031
|
+
);
|
|
17032
|
+
}
|
|
17033
|
+
const existed = (0, import_node_fs14.existsSync)(target);
|
|
17034
|
+
const previousContent = existed ? (0, import_node_fs14.readFileSync)(target, "utf8") : void 0;
|
|
17035
|
+
const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
|
|
17036
|
+
if (matches) {
|
|
17037
|
+
return {
|
|
17038
|
+
id: action.id,
|
|
17039
|
+
scope: action.scope,
|
|
17040
|
+
status: "skipped",
|
|
17041
|
+
message: "Local workflow file already matches the approved content hash.",
|
|
17042
|
+
rollback: { available: false, guidance: "No workflow file mutation occurred." }
|
|
17043
|
+
};
|
|
17044
|
+
}
|
|
17045
|
+
if (!dryRun) {
|
|
17046
|
+
(0, import_node_fs14.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
|
|
17047
|
+
(0, import_node_fs14.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
|
|
17048
|
+
}
|
|
17049
|
+
return {
|
|
17050
|
+
id: action.id,
|
|
17051
|
+
scope: action.scope,
|
|
17052
|
+
status: dryRun ? "planned" : existed ? "updated" : "created",
|
|
17053
|
+
message: dryRun ? `${existed ? "Update" : "Create"} ${plan.github.workflowFile.path} with sha256 ${plan.github.workflowFile.sha256}.` : `${existed ? "Updated" : "Created"} ${plan.github.workflowFile.path}.`,
|
|
17054
|
+
rollback: {
|
|
17055
|
+
available: !dryRun,
|
|
17056
|
+
guidance: rollbackFor(plan, action.id),
|
|
17057
|
+
previousState: {
|
|
17058
|
+
existed,
|
|
17059
|
+
sha256: previousContent === void 0 ? null : hashSetupContent(previousContent)
|
|
17060
|
+
}
|
|
17061
|
+
}
|
|
17062
|
+
};
|
|
17063
|
+
}
|
|
17064
|
+
async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets) {
|
|
17065
|
+
const action = plan.actions.find((item) => item.scope === "github-secrets");
|
|
17066
|
+
const names = state.repositorySecretNames;
|
|
17067
|
+
const pending = plan.github.requiredSecrets.filter(
|
|
17068
|
+
(secret) => rotateSecrets || !names?.has(secret.repositorySecretName)
|
|
17069
|
+
);
|
|
17070
|
+
if (names && pending.length === 0) {
|
|
17071
|
+
return {
|
|
17072
|
+
id: action.id,
|
|
17073
|
+
scope: action.scope,
|
|
17074
|
+
status: "skipped",
|
|
17075
|
+
message: "All required GitHub repository secret names already exist; values remain unreadable and were not overwritten.",
|
|
17076
|
+
rollback: { available: false, guidance: "No GitHub secret mutation occurred." }
|
|
17077
|
+
};
|
|
17078
|
+
}
|
|
17079
|
+
if (dryRun) {
|
|
17080
|
+
return {
|
|
17081
|
+
id: action.id,
|
|
17082
|
+
scope: action.scope,
|
|
17083
|
+
status: "planned",
|
|
17084
|
+
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.",
|
|
17085
|
+
rollback: { available: false, guidance: rollbackFor(plan, action.id) }
|
|
17086
|
+
};
|
|
17087
|
+
}
|
|
17088
|
+
const adminToken = secrets.values.githubAdminToken;
|
|
17089
|
+
for (const secret of pending) {
|
|
17090
|
+
await deps.githubProvider.setCallbackSecret(
|
|
17091
|
+
plan,
|
|
17092
|
+
secret,
|
|
17093
|
+
secrets.values[secret.valueKey],
|
|
17094
|
+
adminToken
|
|
17095
|
+
);
|
|
17096
|
+
}
|
|
17097
|
+
return {
|
|
17098
|
+
id: action.id,
|
|
17099
|
+
scope: action.scope,
|
|
17100
|
+
status: "updated",
|
|
17101
|
+
message: `Set ${pending.length} GitHub repository secret(s). Values were not printed or placed in process arguments.`,
|
|
17102
|
+
rollback: { available: false, guidance: rollbackFor(plan, action.id) }
|
|
17103
|
+
};
|
|
17104
|
+
}
|
|
17105
|
+
async function applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken) {
|
|
17106
|
+
const action = plan.actions.find((item) => item.scope === "jira-profile");
|
|
17107
|
+
const current = state.matchingProfile;
|
|
17108
|
+
const matches = current ? profileMatches(current, plan) : false;
|
|
17109
|
+
if (current && matches && current.hasSecret && !rotateProviderToken) {
|
|
17110
|
+
return {
|
|
17111
|
+
profile: current,
|
|
17112
|
+
result: {
|
|
17113
|
+
id: action.id,
|
|
17114
|
+
scope: action.scope,
|
|
17115
|
+
status: "skipped",
|
|
17116
|
+
message: "Jira automation profile already matches the plan and has a stored provider secret.",
|
|
17117
|
+
rollback: { available: false, guidance: "No Jira profile mutation occurred." }
|
|
17118
|
+
}
|
|
17119
|
+
};
|
|
17120
|
+
}
|
|
17121
|
+
if (dryRun) {
|
|
17122
|
+
const plannedProfile = current ?? {
|
|
17123
|
+
id: "<created-by-forge>",
|
|
17124
|
+
projectKey: plan.project.key,
|
|
17125
|
+
label: plan.jiraProfile.label,
|
|
17126
|
+
provider: "githubActions",
|
|
17127
|
+
authType: "bearer",
|
|
17128
|
+
method: "POST",
|
|
17129
|
+
endpointSummary: "<computed-by-forge>",
|
|
17130
|
+
config: plan.jiraProfile.config,
|
|
17131
|
+
enabled: plan.jiraProfile.enabled,
|
|
17132
|
+
hasSecret: Boolean(secrets.values.githubProviderToken),
|
|
17133
|
+
createdAt: "",
|
|
17134
|
+
updatedAt: ""
|
|
17135
|
+
};
|
|
17136
|
+
return {
|
|
17137
|
+
profile: plannedProfile,
|
|
17138
|
+
result: {
|
|
17139
|
+
id: action.id,
|
|
17140
|
+
scope: action.scope,
|
|
17141
|
+
status: "planned",
|
|
17142
|
+
message: `${current ? "Update" : "Create"} the Jira automation profile through upsertScenarioAutomationProfile.`,
|
|
17143
|
+
rollback: {
|
|
17144
|
+
available: false,
|
|
17145
|
+
guidance: rollbackFor(plan, action.id),
|
|
17146
|
+
previousState: previousProfileState(current)
|
|
17147
|
+
}
|
|
17148
|
+
}
|
|
17149
|
+
};
|
|
17150
|
+
}
|
|
17151
|
+
const secretToken = !current?.hasSecret || rotateProviderToken ? secrets.values.githubProviderToken : void 0;
|
|
17152
|
+
const profile = requireServiceData(
|
|
17153
|
+
await context.invokeForgeContract("upsertScenarioAutomationProfile", {
|
|
17154
|
+
context: { projectKey: plan.project.key },
|
|
17155
|
+
input: {
|
|
17156
|
+
profileId: current?.id,
|
|
17157
|
+
label: plan.jiraProfile.label,
|
|
17158
|
+
provider: "githubActions",
|
|
17159
|
+
config: plan.jiraProfile.config,
|
|
17160
|
+
enabled: plan.jiraProfile.enabled,
|
|
17161
|
+
secretToken
|
|
17162
|
+
}
|
|
17163
|
+
}),
|
|
17164
|
+
"upsertScenarioAutomationProfile"
|
|
17165
|
+
);
|
|
17166
|
+
return {
|
|
17167
|
+
profile,
|
|
17168
|
+
result: {
|
|
17169
|
+
id: action.id,
|
|
17170
|
+
scope: action.scope,
|
|
17171
|
+
status: current ? "updated" : "created",
|
|
17172
|
+
message: `${current ? "Updated" : "Created"} Jira automation profile ${plan.jiraProfile.label} through the existing Forge contract.`,
|
|
17173
|
+
rollback: {
|
|
17174
|
+
available: true,
|
|
17175
|
+
guidance: rollbackFor(plan, action.id),
|
|
17176
|
+
previousState: previousProfileState(current)
|
|
17177
|
+
}
|
|
17178
|
+
}
|
|
17179
|
+
};
|
|
17180
|
+
}
|
|
17181
|
+
async function applyProjectDefault(plan, state, profile, context, dryRun) {
|
|
17182
|
+
const action = plan.actions.find((item) => item.scope === "project-default");
|
|
17183
|
+
if (state.currentDefaultProfileId === profile.id) {
|
|
17184
|
+
return {
|
|
17185
|
+
id: action.id,
|
|
17186
|
+
scope: action.scope,
|
|
17187
|
+
status: "skipped",
|
|
17188
|
+
message: "Jira automation profile is already the project default.",
|
|
17189
|
+
rollback: { available: false, guidance: "No project-default mutation occurred." }
|
|
17190
|
+
};
|
|
17191
|
+
}
|
|
17192
|
+
if (!dryRun) {
|
|
17193
|
+
requireServiceData(
|
|
17194
|
+
await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
|
|
17195
|
+
context: { projectKey: plan.project.key },
|
|
17196
|
+
profileId: profile.id
|
|
17197
|
+
}),
|
|
17198
|
+
"setScenarioAutomationDefaultProfile"
|
|
17199
|
+
);
|
|
17200
|
+
}
|
|
17201
|
+
return {
|
|
17202
|
+
id: action.id,
|
|
17203
|
+
scope: action.scope,
|
|
17204
|
+
status: dryRun ? "planned" : "updated",
|
|
17205
|
+
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.",
|
|
17206
|
+
rollback: {
|
|
17207
|
+
available: !dryRun,
|
|
17208
|
+
guidance: rollbackFor(plan, action.id),
|
|
17209
|
+
previousState: { profileId: state.currentDefaultProfileId ?? null }
|
|
17210
|
+
}
|
|
17211
|
+
};
|
|
17212
|
+
}
|
|
17213
|
+
function doctorStatus(checks) {
|
|
17214
|
+
if (checks.some((check) => check.status === "fail")) return "fail";
|
|
17215
|
+
if (checks.some((check) => check.status === "warn")) return "warn";
|
|
17216
|
+
return "pass";
|
|
17217
|
+
}
|
|
17218
|
+
async function inspectPlan(plan, repoRoot, secrets, context, deps) {
|
|
17219
|
+
const checks = [];
|
|
17220
|
+
let forgeTransportFailure = false;
|
|
17221
|
+
const target = resolveWorkflowTarget(plan, repoRoot);
|
|
17222
|
+
if (!(0, import_node_fs14.existsSync)(target)) {
|
|
17223
|
+
checks.push({
|
|
17224
|
+
id: "workflow-local",
|
|
17225
|
+
status: "fail",
|
|
17226
|
+
message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
|
|
17227
|
+
});
|
|
17228
|
+
} else {
|
|
17229
|
+
const localHash = hashSetupContent((0, import_node_fs14.readFileSync)(target, "utf8"));
|
|
17230
|
+
checks.push({
|
|
17231
|
+
id: "workflow-local",
|
|
17232
|
+
status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
|
|
17233
|
+
message: localHash === plan.github.workflowFile.sha256 ? "Local workflow content matches the approved plan hash." : "Local workflow content differs from the approved plan hash."
|
|
17234
|
+
});
|
|
17235
|
+
}
|
|
17236
|
+
try {
|
|
17237
|
+
const workflow = await deps.githubProvider.inspectExecutionDefinition(
|
|
17238
|
+
plan,
|
|
17239
|
+
secrets.values.githubProviderToken ?? secrets.values.githubAdminToken
|
|
17240
|
+
);
|
|
17241
|
+
const exact = workflow.found && workflow.active && workflow.path === plan.github.workflowFile.path && workflow.contentSha256 === plan.github.workflowFile.sha256;
|
|
17242
|
+
checks.push({
|
|
17243
|
+
id: "workflow-github",
|
|
17244
|
+
status: exact ? "pass" : workflow.found && (workflow.path !== plan.github.workflowFile.path || workflow.active === false) ? "fail" : "warn",
|
|
17245
|
+
message: workflow.message
|
|
17246
|
+
});
|
|
17247
|
+
} catch (error) {
|
|
17248
|
+
checks.push({
|
|
17249
|
+
id: "workflow-github",
|
|
17250
|
+
status: "warn",
|
|
17251
|
+
message: `GitHub workflow could not be verified: ${safeErrorMessage(error, secrets)}`
|
|
17252
|
+
});
|
|
17253
|
+
}
|
|
17254
|
+
try {
|
|
17255
|
+
const secretInspection = await deps.githubProvider.inspectCallbackSecretMetadata(
|
|
17256
|
+
plan,
|
|
17257
|
+
secrets.values.githubAdminToken
|
|
17258
|
+
);
|
|
17259
|
+
if (!secretInspection.verified) {
|
|
17260
|
+
checks.push({ id: "callback-secret-metadata", status: "warn", message: secretInspection.message });
|
|
17261
|
+
} else {
|
|
17262
|
+
const missing = plan.github.requiredSecrets.filter(
|
|
17263
|
+
(secret) => !secretInspection.names.has(secret.repositorySecretName)
|
|
17264
|
+
);
|
|
17265
|
+
checks.push({
|
|
17266
|
+
id: "callback-secret-metadata",
|
|
17267
|
+
status: missing.length === 0 ? "pass" : "fail",
|
|
17268
|
+
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(", ")}.`
|
|
17269
|
+
});
|
|
17270
|
+
}
|
|
17271
|
+
} catch (error) {
|
|
17272
|
+
checks.push({
|
|
17273
|
+
id: "callback-secret-metadata",
|
|
17274
|
+
status: "warn",
|
|
17275
|
+
message: `GitHub repository secret metadata could not be verified: ${safeErrorMessage(error, secrets)}`
|
|
17276
|
+
});
|
|
17277
|
+
}
|
|
17278
|
+
try {
|
|
17279
|
+
const profiles = requireServiceData(
|
|
17280
|
+
await context.invokeForgeContract("listScenarioAutomationProfiles", {
|
|
17281
|
+
context: { projectKey: plan.project.key }
|
|
17282
|
+
}),
|
|
17283
|
+
"listScenarioAutomationProfiles"
|
|
17284
|
+
);
|
|
17285
|
+
const profile = findMatchingProfile(profiles, plan);
|
|
17286
|
+
const profileOk = Boolean(profile && profileMatches(profile, plan) && profile.hasSecret);
|
|
17287
|
+
checks.push({
|
|
17288
|
+
id: "jira-profile",
|
|
17289
|
+
status: profileOk ? "pass" : "fail",
|
|
17290
|
+
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."
|
|
17291
|
+
});
|
|
17292
|
+
if (plan.jiraProfile.setProjectDefault) {
|
|
17293
|
+
checks.push({
|
|
17294
|
+
id: "jira-project-default",
|
|
17295
|
+
status: profile?.isProjectDefault ? "pass" : "fail",
|
|
17296
|
+
message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
|
|
17297
|
+
});
|
|
17298
|
+
}
|
|
17299
|
+
} catch (error) {
|
|
17300
|
+
forgeTransportFailure = error instanceof ForgeClientError && (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR");
|
|
17301
|
+
checks.push({
|
|
17302
|
+
id: "jira-profile",
|
|
17303
|
+
status: "fail",
|
|
17304
|
+
message: `Jira automation profile could not be verified: ${safeErrorMessage(error, secrets)}`
|
|
17305
|
+
});
|
|
17306
|
+
}
|
|
17307
|
+
const status = doctorStatus(checks);
|
|
17308
|
+
return {
|
|
17309
|
+
schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
|
|
17310
|
+
planId: plan.planId,
|
|
17311
|
+
provider: "github-actions",
|
|
17312
|
+
status,
|
|
17313
|
+
exitCode: forgeTransportFailure ? ExitCode.TransportError : status === "fail" ? ExitCode.ValidationError : ExitCode.Success,
|
|
17314
|
+
externalRunTriggered: false,
|
|
17315
|
+
checks
|
|
17316
|
+
};
|
|
17317
|
+
}
|
|
17318
|
+
function planCommand(args, deps) {
|
|
17319
|
+
const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
|
|
17320
|
+
if (parsed.errors.length > 0) {
|
|
17321
|
+
return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors), ExitCode.UsageError);
|
|
17322
|
+
}
|
|
17323
|
+
if ((parsed.flags["--provider"] ?? "github-actions") !== "github-actions") {
|
|
17324
|
+
return jsonResponse(
|
|
17325
|
+
errorPayload("VALIDATION_ERROR", "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."),
|
|
17326
|
+
ExitCode.ValidationError
|
|
17327
|
+
);
|
|
17328
|
+
}
|
|
17329
|
+
try {
|
|
17330
|
+
const plan = buildGitHubSetupPlan({
|
|
17331
|
+
projectKey: parsed.flags["--project-key"] ?? "",
|
|
17332
|
+
owner: parsed.flags["--owner"] ?? "",
|
|
17333
|
+
repository: parsed.flags["--repo"] ?? "",
|
|
17334
|
+
ref: parsed.flags["--ref"] ?? "main",
|
|
17335
|
+
workflowId: parsed.flags["--workflow-id"] ?? "scenario-dispatch.yml",
|
|
17336
|
+
workflowPath: parsed.flags["--workflow-path"] ?? ".github/workflows/scenario-dispatch.yml",
|
|
17337
|
+
workflowSourcePath: import_node_path15.default.resolve(deps.cwd, parsed.flags["--workflow-source"] ?? ""),
|
|
17338
|
+
profileLabel: parsed.flags["--profile-label"] ?? "GitHub Actions",
|
|
17339
|
+
enabled: !parsed.boolFlags.has("--disabled"),
|
|
17340
|
+
setProjectDefault: parsed.boolFlags.has("--set-default"),
|
|
17341
|
+
callbackEndpointSecretName: parsed.flags["--callback-endpoint-secret-name"] ?? "TESTOPS_FORGE_ENDPOINT",
|
|
17342
|
+
callbackAuthTokenSecretName: parsed.flags["--callback-auth-token-secret-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
|
|
17343
|
+
});
|
|
17344
|
+
return jsonResponse(plan);
|
|
17345
|
+
} catch (error) {
|
|
17346
|
+
return jsonResponse(
|
|
17347
|
+
errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
|
|
17348
|
+
ExitCode.ValidationError
|
|
17349
|
+
);
|
|
17350
|
+
}
|
|
17351
|
+
}
|
|
17352
|
+
async function applyCommand(args, context, deps) {
|
|
17353
|
+
const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
|
|
17354
|
+
if (parsed.errors.length > 0) {
|
|
17355
|
+
return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors), ExitCode.UsageError);
|
|
17356
|
+
}
|
|
17357
|
+
const planPath = parsed.flags["--plan"];
|
|
17358
|
+
if (!planPath) {
|
|
17359
|
+
return jsonResponse(errorPayload("USAGE_ERROR", "--plan <file> is required."), ExitCode.UsageError);
|
|
17360
|
+
}
|
|
17361
|
+
const dryRun = parsed.boolFlags.has("--dry-run");
|
|
17362
|
+
if (!dryRun && !parsed.boolFlags.has("--confirm")) {
|
|
17363
|
+
return jsonResponse(
|
|
17364
|
+
errorPayload("CONFIRMATION_REQUIRED", "Mutating apply requires --confirm plus explicit --approve scopes."),
|
|
17365
|
+
ExitCode.UsageError
|
|
17366
|
+
);
|
|
17367
|
+
}
|
|
17368
|
+
let secrets = { values: {}, sources: {} };
|
|
17369
|
+
const actionResults = [];
|
|
17370
|
+
try {
|
|
17371
|
+
const plan = loadPlan(planPath, deps.cwd);
|
|
17372
|
+
const scopes = approvedScopes(plan, parsed, dryRun);
|
|
17373
|
+
secrets = await resolveSecrets(parsed, deps);
|
|
17374
|
+
const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
|
|
17375
|
+
const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
|
|
17376
|
+
const rotateProviderToken = parsed.boolFlags.has("--rotate-provider-token");
|
|
17377
|
+
const state = await prepareState(
|
|
17378
|
+
plan,
|
|
17379
|
+
scopes,
|
|
17380
|
+
secrets,
|
|
17381
|
+
context,
|
|
17382
|
+
deps,
|
|
17383
|
+
dryRun,
|
|
17384
|
+
rotateSecrets,
|
|
17385
|
+
rotateProviderToken
|
|
17386
|
+
);
|
|
17387
|
+
let appliedProfile = state.matchingProfile;
|
|
17388
|
+
for (const action of plan.actions) {
|
|
17389
|
+
if (!scopes.includes(action.scope)) {
|
|
17390
|
+
actionResults.push({
|
|
17391
|
+
id: action.id,
|
|
17392
|
+
scope: action.scope,
|
|
17393
|
+
status: "skipped",
|
|
17394
|
+
message: "Action was not in the explicit approval scope.",
|
|
17395
|
+
rollback: { available: false, guidance: "No mutation occurred." }
|
|
17396
|
+
});
|
|
17397
|
+
continue;
|
|
17398
|
+
}
|
|
17399
|
+
try {
|
|
17400
|
+
if (action.scope === "workflow-file") {
|
|
17401
|
+
actionResults.push(applyWorkflowFile(plan, repoRoot, dryRun));
|
|
17402
|
+
} else if (action.scope === "github-secrets") {
|
|
17403
|
+
actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
|
|
17404
|
+
} else if (action.scope === "jira-profile") {
|
|
17405
|
+
const applied = await applyJiraProfile(
|
|
17406
|
+
plan,
|
|
17407
|
+
state,
|
|
17408
|
+
secrets,
|
|
17409
|
+
context,
|
|
17410
|
+
dryRun,
|
|
17411
|
+
rotateProviderToken
|
|
17412
|
+
);
|
|
17413
|
+
appliedProfile = applied.profile;
|
|
17414
|
+
actionResults.push(applied.result);
|
|
17415
|
+
} else if (action.scope === "project-default") {
|
|
17416
|
+
if (!appliedProfile) {
|
|
17417
|
+
throw new SetupCommandError(
|
|
17418
|
+
"PROFILE_REQUIRED",
|
|
17419
|
+
"Project-default apply requires an existing or approved Jira automation profile.",
|
|
17420
|
+
ExitCode.ValidationError
|
|
17421
|
+
);
|
|
17422
|
+
}
|
|
17423
|
+
actionResults.push(await applyProjectDefault(plan, state, appliedProfile, context, dryRun));
|
|
17424
|
+
}
|
|
17425
|
+
} catch (error) {
|
|
17426
|
+
actionResults.push({
|
|
17427
|
+
id: action.id,
|
|
17428
|
+
scope: action.scope,
|
|
17429
|
+
status: "failed",
|
|
17430
|
+
message: safeErrorMessage(error, secrets),
|
|
17431
|
+
rollback: {
|
|
17432
|
+
available: false,
|
|
17433
|
+
guidance: rollbackFor(plan, action.id)
|
|
17434
|
+
}
|
|
17435
|
+
});
|
|
17436
|
+
throw error;
|
|
17437
|
+
}
|
|
17438
|
+
}
|
|
17439
|
+
const doctor = dryRun ? void 0 : await inspectPlan(plan, repoRoot, secrets, context, deps);
|
|
17440
|
+
return jsonResponse(
|
|
17441
|
+
{
|
|
17442
|
+
schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
|
|
17443
|
+
planId: plan.planId,
|
|
17444
|
+
provider: plan.provider,
|
|
17445
|
+
status: dryRun ? "dry-run" : doctor?.status === "fail" ? "applied-with-validation-failures" : doctor?.status === "warn" ? "applied-with-warnings" : "applied",
|
|
17446
|
+
dryRun,
|
|
17447
|
+
confirmed: !dryRun,
|
|
17448
|
+
approvedScopes: scopes,
|
|
17449
|
+
secretInputs: Object.entries(secrets.sources).map(([key, source]) => ({ key, source })),
|
|
17450
|
+
actions: actionResults,
|
|
17451
|
+
smokeValidation: doctor ?? {
|
|
17452
|
+
status: "not-run",
|
|
17453
|
+
externalRunTriggered: false,
|
|
17454
|
+
message: "Dry-run performs no mutations and does not require remote smoke validation. Run setup doctor for read-only verification."
|
|
17455
|
+
}
|
|
17456
|
+
},
|
|
17457
|
+
doctor?.exitCode ?? ExitCode.Success
|
|
17458
|
+
);
|
|
17459
|
+
} catch (error) {
|
|
17460
|
+
const message = safeErrorMessage(error, secrets);
|
|
17461
|
+
return jsonResponse(
|
|
17462
|
+
{
|
|
17463
|
+
...errorPayload(errorCodeForError(error, "APPLY_FAILED"), message, [
|
|
17464
|
+
"Review each completed action and its rollback guidance; GitHub secret values cannot be restored automatically."
|
|
17465
|
+
]),
|
|
17466
|
+
actions: actionResults
|
|
17467
|
+
},
|
|
17468
|
+
exitCodeForError(error)
|
|
17469
|
+
);
|
|
17470
|
+
}
|
|
17471
|
+
}
|
|
17472
|
+
async function doctorCommand(args, context, deps) {
|
|
17473
|
+
const parsed = parseArgs10(
|
|
17474
|
+
args,
|
|
17475
|
+
APPLY_VALUE_FLAGS,
|
|
17476
|
+
/* @__PURE__ */ new Set(["--secrets-stdin"]),
|
|
17477
|
+
true
|
|
17478
|
+
);
|
|
17479
|
+
if (parsed.approvals.length > 0) {
|
|
17480
|
+
return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
|
|
17481
|
+
}
|
|
17482
|
+
if (parsed.errors.length > 0) {
|
|
17483
|
+
return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors), ExitCode.UsageError);
|
|
17484
|
+
}
|
|
17485
|
+
const planPath = parsed.flags["--plan"];
|
|
17486
|
+
if (!planPath) {
|
|
17487
|
+
return jsonResponse(errorPayload("USAGE_ERROR", "--plan <file> is required."), ExitCode.UsageError);
|
|
17488
|
+
}
|
|
17489
|
+
let secrets = { values: {}, sources: {} };
|
|
17490
|
+
try {
|
|
17491
|
+
const plan = loadPlan(planPath, deps.cwd);
|
|
17492
|
+
secrets = await resolveSecrets(parsed, deps);
|
|
17493
|
+
const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
|
|
17494
|
+
const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
|
|
17495
|
+
return jsonResponse(summary, summary.exitCode);
|
|
17496
|
+
} catch (error) {
|
|
17497
|
+
return jsonResponse(
|
|
17498
|
+
errorPayload(errorCodeForError(error, "DOCTOR_FAILED"), safeErrorMessage(error, secrets)),
|
|
17499
|
+
exitCodeForError(error)
|
|
17500
|
+
);
|
|
17501
|
+
}
|
|
17502
|
+
}
|
|
17503
|
+
function createSetupHandler(overrides = {}) {
|
|
17504
|
+
const deps = {
|
|
17505
|
+
cwd: overrides.cwd ?? process.cwd(),
|
|
17506
|
+
env: overrides.env ?? process.env,
|
|
17507
|
+
readStdin: overrides.readStdin ?? defaultReadStdin,
|
|
17508
|
+
githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps)
|
|
17509
|
+
};
|
|
17510
|
+
return async (request, context) => {
|
|
17511
|
+
const [subcommand, ...args] = request.args;
|
|
17512
|
+
if (subcommand === "plan") {
|
|
17513
|
+
return planCommand(args, deps);
|
|
17514
|
+
}
|
|
17515
|
+
if (subcommand === "apply") {
|
|
17516
|
+
return applyCommand(args, context, deps);
|
|
17517
|
+
}
|
|
17518
|
+
if (subcommand === "doctor") {
|
|
17519
|
+
return doctorCommand(args, context, deps);
|
|
17520
|
+
}
|
|
17521
|
+
return jsonResponse(
|
|
17522
|
+
errorPayload("USAGE_ERROR", `Unsupported setup subcommand ${subcommand ?? "<missing>"}.`),
|
|
17523
|
+
ExitCode.UsageError
|
|
17524
|
+
);
|
|
17525
|
+
};
|
|
17526
|
+
}
|
|
17527
|
+
|
|
16042
17528
|
// src/suites.ts
|
|
16043
17529
|
var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
|
|
16044
17530
|
"--config",
|
|
@@ -16049,7 +17535,7 @@ var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
|
|
|
16049
17535
|
"--jira-email",
|
|
16050
17536
|
"--jira-api-token"
|
|
16051
17537
|
]);
|
|
16052
|
-
function
|
|
17538
|
+
function parseArgs11(args) {
|
|
16053
17539
|
const flags = {};
|
|
16054
17540
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
16055
17541
|
const unknownFlags = [];
|
|
@@ -16169,7 +17655,7 @@ function createSuitesHandler(deps = {}) {
|
|
|
16169
17655
|
const env = deps.env ?? process.env;
|
|
16170
17656
|
return async (request, context) => {
|
|
16171
17657
|
const [subcommand, ...restArgs] = request.args;
|
|
16172
|
-
const parsed =
|
|
17658
|
+
const parsed = parseArgs11(restArgs);
|
|
16173
17659
|
const useJson = parsed.boolFlags.has("--json");
|
|
16174
17660
|
const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
|
|
16175
17661
|
const projectKey = config.values.projectKey;
|
|
@@ -16343,7 +17829,7 @@ var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
|
|
|
16343
17829
|
"--jira-api-token"
|
|
16344
17830
|
]);
|
|
16345
17831
|
var RECONCILE_CONFIRM_TOKEN = "RECONCILE";
|
|
16346
|
-
function
|
|
17832
|
+
function parseArgs12(args) {
|
|
16347
17833
|
const flags = {};
|
|
16348
17834
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
16349
17835
|
const unknownFlags = [];
|
|
@@ -16432,7 +17918,7 @@ function createSyncHandler(deps = {}) {
|
|
|
16432
17918
|
const env = deps.env ?? process.env;
|
|
16433
17919
|
return async (request, context) => {
|
|
16434
17920
|
const [subcommand, ...restArgs] = request.args;
|
|
16435
|
-
const parsed =
|
|
17921
|
+
const parsed = parseArgs12(restArgs);
|
|
16436
17922
|
const useJson = parsed.boolFlags.has("--json");
|
|
16437
17923
|
const config = resolveCliConfig(pickConfigArgs10(parsed), env, cwd);
|
|
16438
17924
|
const projectKey = config.values.projectKey;
|
|
@@ -16592,6 +18078,12 @@ var COMMAND_REGISTRY = [
|
|
|
16592
18078
|
subcommands: ["show"],
|
|
16593
18079
|
handler: createRunsHandler()
|
|
16594
18080
|
},
|
|
18081
|
+
{
|
|
18082
|
+
name: "setup",
|
|
18083
|
+
description: "Plan, apply, and verify provider onboarding without duplicating Forge domain logic",
|
|
18084
|
+
subcommands: ["plan", "apply", "doctor"],
|
|
18085
|
+
handler: createSetupHandler()
|
|
18086
|
+
},
|
|
16595
18087
|
{
|
|
16596
18088
|
name: "suites",
|
|
16597
18089
|
description: "Hierarchy-aware suite browse commands",
|