@automatify-au/cli 0.1.10 → 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 +155 -1
- package/dist/automatify.cjs +2365 -275
- 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) {
|
|
@@ -12447,10 +12450,10 @@ function createAutoHandler(deps = {}) {
|
|
|
12447
12450
|
}
|
|
12448
12451
|
|
|
12449
12452
|
// src/allure.ts
|
|
12450
|
-
var
|
|
12451
|
-
var
|
|
12453
|
+
var import_node_path10 = __toESM(require("node:path"), 1);
|
|
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,6 +12684,441 @@ async function parseAllureReportZip(zipPath) {
|
|
|
12681
12684
|
};
|
|
12682
12685
|
}
|
|
12683
12686
|
|
|
12687
|
+
// ../../packages/allure-core/dist/extractAllureZip.js
|
|
12688
|
+
var import_node_fs7 = require("node:fs");
|
|
12689
|
+
var import_node_path8 = __toESM(require("node:path"), 1);
|
|
12690
|
+
var import_jszip2 = __toESM(require_lib3(), 1);
|
|
12691
|
+
var INDEX_HTML = "index.html";
|
|
12692
|
+
var MISSING_INDEX_ERROR = "Could not find index.html. Please provide a generated Allure HTML report ZIP, not raw allure-results.";
|
|
12693
|
+
function findIndexEntry(paths) {
|
|
12694
|
+
return paths.find((entryPath) => entryPath === INDEX_HTML) ?? paths.find((entryPath) => entryPath === `allure-report/${INDEX_HTML}`) ?? paths.find((entryPath) => entryPath.endsWith(`/${INDEX_HTML}`));
|
|
12695
|
+
}
|
|
12696
|
+
function ensureInsideRoot(rootDir, candidate) {
|
|
12697
|
+
const resolved = import_node_path8.default.resolve(rootDir, candidate);
|
|
12698
|
+
const rootWithSep = rootDir.endsWith(import_node_path8.default.sep) ? rootDir : rootDir + import_node_path8.default.sep;
|
|
12699
|
+
if (resolved !== rootDir && !resolved.startsWith(rootWithSep)) {
|
|
12700
|
+
throw new Error(`Refusing to write zip entry outside extract directory: ${candidate}`);
|
|
12701
|
+
}
|
|
12702
|
+
return resolved;
|
|
12703
|
+
}
|
|
12704
|
+
async function extractAllureReportZip(zipPath, extractDir) {
|
|
12705
|
+
if (!(0, import_node_fs7.existsSync)(zipPath)) {
|
|
12706
|
+
throw new Error(`Allure report ZIP not found: ${zipPath}`);
|
|
12707
|
+
}
|
|
12708
|
+
if (import_node_path8.default.extname(zipPath).toLowerCase() !== ".zip") {
|
|
12709
|
+
throw new Error("Allure report must be a .zip file.");
|
|
12710
|
+
}
|
|
12711
|
+
const zip = await import_jszip2.default.loadAsync((0, import_node_fs7.readFileSync)(zipPath));
|
|
12712
|
+
const entries = Object.entries(zip.files);
|
|
12713
|
+
const filePaths = entries.filter(([, file]) => !file.dir).map(([entryPath]) => entryPath);
|
|
12714
|
+
const indexEntry = findIndexEntry(filePaths);
|
|
12715
|
+
if (!indexEntry) {
|
|
12716
|
+
throw new Error(MISSING_INDEX_ERROR);
|
|
12717
|
+
}
|
|
12718
|
+
const resolvedExtract = import_node_path8.default.resolve(extractDir);
|
|
12719
|
+
(0, import_node_fs7.mkdirSync)(resolvedExtract, { recursive: true });
|
|
12720
|
+
for (const [entryPath, file] of entries) {
|
|
12721
|
+
const target = ensureInsideRoot(resolvedExtract, entryPath);
|
|
12722
|
+
if (file.dir) {
|
|
12723
|
+
(0, import_node_fs7.mkdirSync)(target, { recursive: true });
|
|
12724
|
+
continue;
|
|
12725
|
+
}
|
|
12726
|
+
(0, import_node_fs7.mkdirSync)(import_node_path8.default.dirname(target), { recursive: true });
|
|
12727
|
+
const buffer = await file.async("nodebuffer");
|
|
12728
|
+
(0, import_node_fs7.writeFileSync)(target, buffer);
|
|
12729
|
+
}
|
|
12730
|
+
const indexAbsolute = import_node_path8.default.join(resolvedExtract, indexEntry);
|
|
12731
|
+
const rootDir = import_node_path8.default.dirname(indexAbsolute);
|
|
12732
|
+
return {
|
|
12733
|
+
zipPath,
|
|
12734
|
+
extractDir: resolvedExtract,
|
|
12735
|
+
rootDir,
|
|
12736
|
+
indexPath: indexAbsolute
|
|
12737
|
+
};
|
|
12738
|
+
}
|
|
12739
|
+
|
|
12740
|
+
// ../../packages/allure-core/dist/parseAllureFailedTests.js
|
|
12741
|
+
var import_jszip3 = __toESM(require_lib3(), 1);
|
|
12742
|
+
|
|
12743
|
+
// src/allureOpen.ts
|
|
12744
|
+
var import_node_fs8 = require("node:fs");
|
|
12745
|
+
var import_promises = require("node:fs/promises");
|
|
12746
|
+
var import_node_http = __toESM(require("node:http"), 1);
|
|
12747
|
+
var import_node_os = require("node:os");
|
|
12748
|
+
var import_node_path9 = __toESM(require("node:path"), 1);
|
|
12749
|
+
var import_node_child_process2 = require("node:child_process");
|
|
12750
|
+
function parseArgs2(args) {
|
|
12751
|
+
const positionals = [];
|
|
12752
|
+
const flags = {};
|
|
12753
|
+
const boolFlags = /* @__PURE__ */ new Set();
|
|
12754
|
+
const unknownFlags = [];
|
|
12755
|
+
const valuedFlags = /* @__PURE__ */ new Set(["--port", "--host", "--extract-to"]);
|
|
12756
|
+
const supportedBoolFlags = /* @__PURE__ */ new Set(["--keep", "--no-open", "--json", "--dry-run", "--verbose"]);
|
|
12757
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
12758
|
+
const token = args[i];
|
|
12759
|
+
if (!token.startsWith("--")) {
|
|
12760
|
+
positionals.push(token);
|
|
12761
|
+
continue;
|
|
12762
|
+
}
|
|
12763
|
+
if (supportedBoolFlags.has(token)) {
|
|
12764
|
+
boolFlags.add(token);
|
|
12765
|
+
continue;
|
|
12766
|
+
}
|
|
12767
|
+
if (!valuedFlags.has(token)) {
|
|
12768
|
+
unknownFlags.push(token);
|
|
12769
|
+
continue;
|
|
12770
|
+
}
|
|
12771
|
+
const value = args[i + 1];
|
|
12772
|
+
if (!value || value.startsWith("--")) {
|
|
12773
|
+
unknownFlags.push(token);
|
|
12774
|
+
continue;
|
|
12775
|
+
}
|
|
12776
|
+
flags[token] = value;
|
|
12777
|
+
i += 1;
|
|
12778
|
+
}
|
|
12779
|
+
return { positionals, flags, boolFlags, unknownFlags };
|
|
12780
|
+
}
|
|
12781
|
+
function parsePort(raw) {
|
|
12782
|
+
if (raw === void 0) {
|
|
12783
|
+
return { value: 0 };
|
|
12784
|
+
}
|
|
12785
|
+
const parsed = Number(raw);
|
|
12786
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
12787
|
+
return { value: 0, error: `Invalid --port value: ${raw}. Must be an integer between 0 and 65535.` };
|
|
12788
|
+
}
|
|
12789
|
+
return { value: parsed };
|
|
12790
|
+
}
|
|
12791
|
+
function normalizeError(error) {
|
|
12792
|
+
return error instanceof Error ? error.message : "Unknown Allure open error.";
|
|
12793
|
+
}
|
|
12794
|
+
var MIME_TYPES = {
|
|
12795
|
+
".html": "text/html; charset=utf-8",
|
|
12796
|
+
".htm": "text/html; charset=utf-8",
|
|
12797
|
+
".js": "application/javascript; charset=utf-8",
|
|
12798
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
12799
|
+
".css": "text/css; charset=utf-8",
|
|
12800
|
+
".json": "application/json; charset=utf-8",
|
|
12801
|
+
".svg": "image/svg+xml",
|
|
12802
|
+
".png": "image/png",
|
|
12803
|
+
".jpg": "image/jpeg",
|
|
12804
|
+
".jpeg": "image/jpeg",
|
|
12805
|
+
".gif": "image/gif",
|
|
12806
|
+
".webp": "image/webp",
|
|
12807
|
+
".ico": "image/x-icon",
|
|
12808
|
+
".woff": "font/woff",
|
|
12809
|
+
".woff2": "font/woff2",
|
|
12810
|
+
".ttf": "font/ttf",
|
|
12811
|
+
".otf": "font/otf",
|
|
12812
|
+
".eot": "application/vnd.ms-fontobject",
|
|
12813
|
+
".map": "application/json; charset=utf-8",
|
|
12814
|
+
".txt": "text/plain; charset=utf-8"
|
|
12815
|
+
};
|
|
12816
|
+
function contentTypeFor(filePath) {
|
|
12817
|
+
return MIME_TYPES[import_node_path9.default.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
12818
|
+
}
|
|
12819
|
+
function resolveRequestedPath(rootDir, urlPath) {
|
|
12820
|
+
let decoded;
|
|
12821
|
+
try {
|
|
12822
|
+
decoded = decodeURIComponent(urlPath.split("?")[0].split("#")[0]);
|
|
12823
|
+
} catch {
|
|
12824
|
+
return void 0;
|
|
12825
|
+
}
|
|
12826
|
+
const normalized = import_node_path9.default.normalize(decoded);
|
|
12827
|
+
const trimmed = normalized.replace(/^[/\\]+/, "");
|
|
12828
|
+
const candidate = import_node_path9.default.resolve(rootDir, trimmed);
|
|
12829
|
+
const rootWithSep = rootDir.endsWith(import_node_path9.default.sep) ? rootDir : rootDir + import_node_path9.default.sep;
|
|
12830
|
+
if (candidate !== rootDir && !candidate.startsWith(rootWithSep)) {
|
|
12831
|
+
return void 0;
|
|
12832
|
+
}
|
|
12833
|
+
return candidate;
|
|
12834
|
+
}
|
|
12835
|
+
function createStaticServerStarter() {
|
|
12836
|
+
return async ({ rootDir, host, port }) => {
|
|
12837
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
12838
|
+
if (!req.url || req.method !== "GET" && req.method !== "HEAD") {
|
|
12839
|
+
res.statusCode = 405;
|
|
12840
|
+
res.end("Method Not Allowed");
|
|
12841
|
+
return;
|
|
12842
|
+
}
|
|
12843
|
+
const resolved = resolveRequestedPath(rootDir, req.url);
|
|
12844
|
+
if (!resolved) {
|
|
12845
|
+
res.statusCode = 400;
|
|
12846
|
+
res.end("Bad Request");
|
|
12847
|
+
return;
|
|
12848
|
+
}
|
|
12849
|
+
let target = resolved;
|
|
12850
|
+
try {
|
|
12851
|
+
const stat = (0, import_node_fs8.statSync)(target);
|
|
12852
|
+
if (stat.isDirectory()) {
|
|
12853
|
+
target = import_node_path9.default.join(target, "index.html");
|
|
12854
|
+
}
|
|
12855
|
+
} catch {
|
|
12856
|
+
res.statusCode = 404;
|
|
12857
|
+
res.end("Not Found");
|
|
12858
|
+
return;
|
|
12859
|
+
}
|
|
12860
|
+
if (!(0, import_node_fs8.existsSync)(target)) {
|
|
12861
|
+
res.statusCode = 404;
|
|
12862
|
+
res.end("Not Found");
|
|
12863
|
+
return;
|
|
12864
|
+
}
|
|
12865
|
+
res.setHeader("Content-Type", contentTypeFor(target));
|
|
12866
|
+
if (req.method === "HEAD") {
|
|
12867
|
+
res.statusCode = 200;
|
|
12868
|
+
res.end();
|
|
12869
|
+
return;
|
|
12870
|
+
}
|
|
12871
|
+
const stream = (0, import_node_fs8.createReadStream)(target);
|
|
12872
|
+
stream.on("error", () => {
|
|
12873
|
+
res.statusCode = 500;
|
|
12874
|
+
res.end("Internal Server Error");
|
|
12875
|
+
});
|
|
12876
|
+
stream.pipe(res);
|
|
12877
|
+
});
|
|
12878
|
+
await new Promise((resolve, reject) => {
|
|
12879
|
+
const onError = (err) => {
|
|
12880
|
+
server.off("listening", onListening);
|
|
12881
|
+
reject(err);
|
|
12882
|
+
};
|
|
12883
|
+
const onListening = () => {
|
|
12884
|
+
server.off("error", onError);
|
|
12885
|
+
resolve();
|
|
12886
|
+
};
|
|
12887
|
+
server.once("error", onError);
|
|
12888
|
+
server.once("listening", onListening);
|
|
12889
|
+
server.listen(port, host);
|
|
12890
|
+
});
|
|
12891
|
+
const address = server.address();
|
|
12892
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
12893
|
+
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
12894
|
+
const url = `http://${displayHost}:${actualPort}/`;
|
|
12895
|
+
return {
|
|
12896
|
+
url,
|
|
12897
|
+
port: actualPort,
|
|
12898
|
+
host,
|
|
12899
|
+
close: () => new Promise((resolve, reject) => {
|
|
12900
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
12901
|
+
})
|
|
12902
|
+
};
|
|
12903
|
+
};
|
|
12904
|
+
}
|
|
12905
|
+
function createOsBrowserOpener() {
|
|
12906
|
+
return async (url) => {
|
|
12907
|
+
const platform = process.platform;
|
|
12908
|
+
let command;
|
|
12909
|
+
let args;
|
|
12910
|
+
if (platform === "darwin") {
|
|
12911
|
+
command = "open";
|
|
12912
|
+
args = [url];
|
|
12913
|
+
} else if (platform === "win32") {
|
|
12914
|
+
command = "cmd";
|
|
12915
|
+
args = ["/c", "start", "", url];
|
|
12916
|
+
} else {
|
|
12917
|
+
command = "xdg-open";
|
|
12918
|
+
args = [url];
|
|
12919
|
+
}
|
|
12920
|
+
try {
|
|
12921
|
+
const child = (0, import_node_child_process2.spawn)(command, args, { stdio: "ignore", detached: true });
|
|
12922
|
+
child.on("error", () => void 0);
|
|
12923
|
+
child.unref();
|
|
12924
|
+
} catch {
|
|
12925
|
+
}
|
|
12926
|
+
};
|
|
12927
|
+
}
|
|
12928
|
+
function waitForProcessSignals() {
|
|
12929
|
+
return new Promise((resolve) => {
|
|
12930
|
+
const cleanup = () => {
|
|
12931
|
+
process.off("SIGINT", onSignal);
|
|
12932
|
+
process.off("SIGTERM", onSignal);
|
|
12933
|
+
resolve();
|
|
12934
|
+
};
|
|
12935
|
+
const onSignal = () => {
|
|
12936
|
+
cleanup();
|
|
12937
|
+
};
|
|
12938
|
+
process.once("SIGINT", onSignal);
|
|
12939
|
+
process.once("SIGTERM", onSignal);
|
|
12940
|
+
});
|
|
12941
|
+
}
|
|
12942
|
+
function buildJsonOutput(params) {
|
|
12943
|
+
const output = {
|
|
12944
|
+
zipPath: params.zipPath,
|
|
12945
|
+
dryRun: params.dryRun,
|
|
12946
|
+
served: params.served,
|
|
12947
|
+
opened: params.opened,
|
|
12948
|
+
keep: params.keep,
|
|
12949
|
+
extractDir: params.extractDir,
|
|
12950
|
+
rootDir: params.rootDir,
|
|
12951
|
+
indexPath: params.indexPath,
|
|
12952
|
+
summary: {
|
|
12953
|
+
total: params.summary.counts.total,
|
|
12954
|
+
passed: params.summary.counts.passed,
|
|
12955
|
+
failed: params.summary.counts.failed,
|
|
12956
|
+
broken: params.summary.counts.broken,
|
|
12957
|
+
skipped: params.summary.counts.skipped,
|
|
12958
|
+
unknown: params.summary.counts.unknown
|
|
12959
|
+
}
|
|
12960
|
+
};
|
|
12961
|
+
if (params.url) {
|
|
12962
|
+
output.url = params.url;
|
|
12963
|
+
}
|
|
12964
|
+
if (params.port !== void 0) {
|
|
12965
|
+
output.port = params.port;
|
|
12966
|
+
}
|
|
12967
|
+
if (params.host !== void 0) {
|
|
12968
|
+
output.host = params.host;
|
|
12969
|
+
}
|
|
12970
|
+
return output;
|
|
12971
|
+
}
|
|
12972
|
+
function createAllureOpenHandler(deps = {}) {
|
|
12973
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
12974
|
+
const parseZip = deps.parseAllureReportZip ?? parseAllureReportZip;
|
|
12975
|
+
const extractZip = deps.extractAllureReportZip ?? extractAllureReportZip;
|
|
12976
|
+
const createTempDir = deps.createTempDir ?? (() => (0, import_promises.mkdtemp)(import_node_path9.default.join((0, import_node_os.tmpdir)(), "automatify-allure-open-")));
|
|
12977
|
+
const startServer = deps.startServer ?? createStaticServerStarter();
|
|
12978
|
+
const openInBrowser = deps.openInBrowser ?? createOsBrowserOpener();
|
|
12979
|
+
const waitForShutdown = deps.waitForShutdown ?? waitForProcessSignals;
|
|
12980
|
+
const removeDir = deps.removeDir ?? ((dir) => (0, import_promises.rm)(dir, { recursive: true, force: true }));
|
|
12981
|
+
const logger = deps.logger ?? console;
|
|
12982
|
+
return async (request) => {
|
|
12983
|
+
const parsed = parseArgs2(request.args);
|
|
12984
|
+
const useJson = parsed.boolFlags.has("--json");
|
|
12985
|
+
const dryRun = parsed.boolFlags.has("--dry-run");
|
|
12986
|
+
const verbose = parsed.boolFlags.has("--verbose");
|
|
12987
|
+
const keep = parsed.boolFlags.has("--keep");
|
|
12988
|
+
const noOpen = parsed.boolFlags.has("--no-open");
|
|
12989
|
+
const userExtractDir = parsed.flags["--extract-to"];
|
|
12990
|
+
if (parsed.unknownFlags.length > 0) {
|
|
12991
|
+
return {
|
|
12992
|
+
exitCode: ExitCode.UsageError,
|
|
12993
|
+
stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
|
|
12994
|
+
};
|
|
12995
|
+
}
|
|
12996
|
+
const [zipPathInput] = parsed.positionals;
|
|
12997
|
+
if (!zipPathInput || parsed.positionals.length > 1) {
|
|
12998
|
+
return {
|
|
12999
|
+
exitCode: ExitCode.UsageError,
|
|
13000
|
+
stderr: ["Usage: automatify testops allure open <zipPath> [options]"]
|
|
13001
|
+
};
|
|
13002
|
+
}
|
|
13003
|
+
const portResult = parsePort(parsed.flags["--port"]);
|
|
13004
|
+
if (portResult.error) {
|
|
13005
|
+
return {
|
|
13006
|
+
exitCode: ExitCode.UsageError,
|
|
13007
|
+
stderr: [`ERROR: ${portResult.error}`]
|
|
13008
|
+
};
|
|
13009
|
+
}
|
|
13010
|
+
const host = parsed.flags["--host"] ?? "127.0.0.1";
|
|
13011
|
+
const zipPath = import_node_path9.default.resolve(cwd, zipPathInput);
|
|
13012
|
+
let summary;
|
|
13013
|
+
try {
|
|
13014
|
+
summary = await parseZip(zipPath);
|
|
13015
|
+
} catch (error) {
|
|
13016
|
+
return {
|
|
13017
|
+
exitCode: ExitCode.ValidationError,
|
|
13018
|
+
stderr: [`ERROR: ${normalizeError(error)}`]
|
|
13019
|
+
};
|
|
13020
|
+
}
|
|
13021
|
+
const extractDirRaw = userExtractDir ? import_node_path9.default.resolve(cwd, userExtractDir) : await createTempDir();
|
|
13022
|
+
if (dryRun) {
|
|
13023
|
+
const indexPath = import_node_path9.default.join(extractDirRaw, "index.html");
|
|
13024
|
+
if (useJson) {
|
|
13025
|
+
return {
|
|
13026
|
+
exitCode: ExitCode.Success,
|
|
13027
|
+
stdout: toJsonLine(buildJsonOutput({
|
|
13028
|
+
zipPath,
|
|
13029
|
+
dryRun: true,
|
|
13030
|
+
served: false,
|
|
13031
|
+
opened: false,
|
|
13032
|
+
keep,
|
|
13033
|
+
extractDir: extractDirRaw,
|
|
13034
|
+
rootDir: extractDirRaw,
|
|
13035
|
+
indexPath,
|
|
13036
|
+
summary
|
|
13037
|
+
}))
|
|
13038
|
+
};
|
|
13039
|
+
}
|
|
13040
|
+
return {
|
|
13041
|
+
exitCode: ExitCode.Success,
|
|
13042
|
+
stdout: [
|
|
13043
|
+
`Allure report ZIP: ${zipPath}`,
|
|
13044
|
+
`Total: ${summary.counts.total} (passed ${summary.counts.passed}, failed ${summary.counts.failed}, broken ${summary.counts.broken}, skipped ${summary.counts.skipped}, unknown ${summary.counts.unknown})`,
|
|
13045
|
+
`Dry run: would extract to ${extractDirRaw} and serve on http://${host}:${portResult.value === 0 ? "<auto>" : portResult.value}/`
|
|
13046
|
+
]
|
|
13047
|
+
};
|
|
13048
|
+
}
|
|
13049
|
+
let extractResult;
|
|
13050
|
+
try {
|
|
13051
|
+
extractResult = await extractZip(zipPath, extractDirRaw);
|
|
13052
|
+
} catch (error) {
|
|
13053
|
+
if (!userExtractDir) {
|
|
13054
|
+
await removeDir(extractDirRaw).catch(() => void 0);
|
|
13055
|
+
}
|
|
13056
|
+
return {
|
|
13057
|
+
exitCode: ExitCode.ValidationError,
|
|
13058
|
+
stderr: [`ERROR: ${normalizeError(error)}`]
|
|
13059
|
+
};
|
|
13060
|
+
}
|
|
13061
|
+
let serverHandle;
|
|
13062
|
+
try {
|
|
13063
|
+
serverHandle = await startServer({
|
|
13064
|
+
rootDir: extractResult.rootDir,
|
|
13065
|
+
host,
|
|
13066
|
+
port: portResult.value
|
|
13067
|
+
});
|
|
13068
|
+
} catch (error) {
|
|
13069
|
+
if (!keep && !userExtractDir) {
|
|
13070
|
+
await removeDir(extractResult.extractDir).catch(() => void 0);
|
|
13071
|
+
}
|
|
13072
|
+
return {
|
|
13073
|
+
exitCode: ExitCode.TransportError,
|
|
13074
|
+
stderr: [`ERROR: Could not start static server: ${normalizeError(error)}`]
|
|
13075
|
+
};
|
|
13076
|
+
}
|
|
13077
|
+
let opened = false;
|
|
13078
|
+
if (!noOpen) {
|
|
13079
|
+
try {
|
|
13080
|
+
await openInBrowser(serverHandle.url);
|
|
13081
|
+
opened = true;
|
|
13082
|
+
} catch {
|
|
13083
|
+
opened = false;
|
|
13084
|
+
}
|
|
13085
|
+
}
|
|
13086
|
+
if (useJson) {
|
|
13087
|
+
logger.log(JSON.stringify(buildJsonOutput({
|
|
13088
|
+
zipPath,
|
|
13089
|
+
dryRun: false,
|
|
13090
|
+
served: true,
|
|
13091
|
+
opened,
|
|
13092
|
+
keep,
|
|
13093
|
+
extractDir: extractResult.extractDir,
|
|
13094
|
+
rootDir: extractResult.rootDir,
|
|
13095
|
+
indexPath: extractResult.indexPath,
|
|
13096
|
+
url: serverHandle.url,
|
|
13097
|
+
port: serverHandle.port,
|
|
13098
|
+
host: serverHandle.host,
|
|
13099
|
+
summary
|
|
13100
|
+
})));
|
|
13101
|
+
} else {
|
|
13102
|
+
logger.log(`Allure report extracted to ${extractResult.extractDir}.`);
|
|
13103
|
+
if (verbose) {
|
|
13104
|
+
logger.log(`Index: ${extractResult.indexPath}`);
|
|
13105
|
+
}
|
|
13106
|
+
logger.log(`Serving at ${serverHandle.url}`);
|
|
13107
|
+
logger.log(opened ? "Opened in your default browser." : "Browser open skipped.");
|
|
13108
|
+
logger.log("Press Ctrl+C to stop the server.");
|
|
13109
|
+
}
|
|
13110
|
+
await waitForShutdown();
|
|
13111
|
+
try {
|
|
13112
|
+
await serverHandle.close();
|
|
13113
|
+
} catch {
|
|
13114
|
+
}
|
|
13115
|
+
if (!keep && !userExtractDir) {
|
|
13116
|
+
await removeDir(extractResult.extractDir).catch(() => void 0);
|
|
13117
|
+
}
|
|
13118
|
+
return { exitCode: ExitCode.Success };
|
|
13119
|
+
};
|
|
13120
|
+
}
|
|
13121
|
+
|
|
12684
13122
|
// src/allure.ts
|
|
12685
13123
|
var CONFIG_FLAGS2 = /* @__PURE__ */ new Set([
|
|
12686
13124
|
"--config",
|
|
@@ -12691,7 +13129,7 @@ var CONFIG_FLAGS2 = /* @__PURE__ */ new Set([
|
|
|
12691
13129
|
"--jira-email",
|
|
12692
13130
|
"--jira-api-token"
|
|
12693
13131
|
]);
|
|
12694
|
-
function
|
|
13132
|
+
function parseArgs3(args) {
|
|
12695
13133
|
const positionals = [];
|
|
12696
13134
|
const flags = {};
|
|
12697
13135
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
@@ -12742,7 +13180,7 @@ function pickConfigArgs2(parsed) {
|
|
|
12742
13180
|
function normalizeSite2(value) {
|
|
12743
13181
|
return value.trim().replace(/\/+$/, "");
|
|
12744
13182
|
}
|
|
12745
|
-
function
|
|
13183
|
+
function normalizeError2(error) {
|
|
12746
13184
|
return error instanceof Error ? error.message : "Unknown Allure upload error.";
|
|
12747
13185
|
}
|
|
12748
13186
|
function hasCredential(value) {
|
|
@@ -12847,7 +13285,7 @@ function buildDownloadJsonOutput(params) {
|
|
|
12847
13285
|
outputPath: params.outputPath
|
|
12848
13286
|
};
|
|
12849
13287
|
}
|
|
12850
|
-
function
|
|
13288
|
+
function buildJsonOutput2(params) {
|
|
12851
13289
|
const attachment = {
|
|
12852
13290
|
filename: params.attachment.filename
|
|
12853
13291
|
};
|
|
@@ -12887,19 +13325,24 @@ function createAllureHandler(deps = {}) {
|
|
|
12887
13325
|
const createComment = deps.addIssueComment ?? addIssueComment;
|
|
12888
13326
|
const listAttachments = deps.listIssueAttachments ?? listIssueAttachments;
|
|
12889
13327
|
const downloadAttachment = deps.downloadIssueAttachment ?? downloadIssueAttachment;
|
|
12890
|
-
|
|
13328
|
+
const openHandler = createAllureOpenHandler({ cwd });
|
|
13329
|
+
return async (request, context) => {
|
|
12891
13330
|
const [subcommand, ...subArgs] = request.args;
|
|
13331
|
+
if (subcommand === "open") {
|
|
13332
|
+
return openHandler({ ...request, args: subArgs }, context);
|
|
13333
|
+
}
|
|
12892
13334
|
if (subcommand !== "upload" && subcommand !== "download") {
|
|
12893
13335
|
return {
|
|
12894
13336
|
exitCode: ExitCode.UsageError,
|
|
12895
13337
|
stderr: [
|
|
12896
13338
|
"Usage:",
|
|
12897
13339
|
" automatify testops allure upload <issueKey> <zipPath> [options]",
|
|
12898
|
-
" automatify testops allure download <issueKey> [options]"
|
|
13340
|
+
" automatify testops allure download <issueKey> [options]",
|
|
13341
|
+
" automatify testops allure open <zipPath> [options]"
|
|
12899
13342
|
]
|
|
12900
13343
|
};
|
|
12901
13344
|
}
|
|
12902
|
-
const parsed =
|
|
13345
|
+
const parsed = parseArgs3(subArgs);
|
|
12903
13346
|
const useJson = parsed.boolFlags.has("--json");
|
|
12904
13347
|
const dryRun = parsed.boolFlags.has("--dry-run");
|
|
12905
13348
|
const verbose = parsed.boolFlags.has("--verbose");
|
|
@@ -12936,8 +13379,8 @@ function createAllureHandler(deps = {}) {
|
|
|
12936
13379
|
attachmentId: parsed.flags["--attachment-id"],
|
|
12937
13380
|
filename: parsed.flags["--filename"]
|
|
12938
13381
|
});
|
|
12939
|
-
const outputDir =
|
|
12940
|
-
const outputPath =
|
|
13382
|
+
const outputDir = import_node_path10.default.resolve(cwd, parsed.flags["--output-dir"] ?? ".");
|
|
13383
|
+
const outputPath = import_node_path10.default.join(outputDir, selected.filename);
|
|
12941
13384
|
if (dryRun) {
|
|
12942
13385
|
if (useJson) {
|
|
12943
13386
|
return {
|
|
@@ -12960,8 +13403,8 @@ function createAllureHandler(deps = {}) {
|
|
|
12960
13403
|
]
|
|
12961
13404
|
};
|
|
12962
13405
|
}
|
|
12963
|
-
(0,
|
|
12964
|
-
if ((0,
|
|
13406
|
+
(0, import_node_fs9.mkdirSync)(outputDir, { recursive: true });
|
|
13407
|
+
if ((0, import_node_fs9.existsSync)(outputPath) && !force) {
|
|
12965
13408
|
return {
|
|
12966
13409
|
exitCode: ExitCode.ValidationError,
|
|
12967
13410
|
stderr: [`ERROR: Output file already exists: ${outputPath}. Pass --force to overwrite.`]
|
|
@@ -12996,7 +13439,7 @@ function createAllureHandler(deps = {}) {
|
|
|
12996
13439
|
} catch (error) {
|
|
12997
13440
|
return {
|
|
12998
13441
|
exitCode: ExitCode.RemoteError,
|
|
12999
|
-
stderr: [`ERROR: ${
|
|
13442
|
+
stderr: [`ERROR: ${normalizeError2(error)}`]
|
|
13000
13443
|
};
|
|
13001
13444
|
}
|
|
13002
13445
|
}
|
|
@@ -13008,8 +13451,8 @@ function createAllureHandler(deps = {}) {
|
|
|
13008
13451
|
};
|
|
13009
13452
|
}
|
|
13010
13453
|
const resolved = resolveJiraConfig(parsed, env, cwd);
|
|
13011
|
-
const zipPath =
|
|
13012
|
-
const attachmentFilename =
|
|
13454
|
+
const zipPath = import_node_path10.default.resolve(cwd, zipPathInput);
|
|
13455
|
+
const attachmentFilename = import_node_path10.default.basename(zipPath);
|
|
13013
13456
|
if (!resolved.config) {
|
|
13014
13457
|
return {
|
|
13015
13458
|
exitCode: ExitCode.ValidationError,
|
|
@@ -13022,14 +13465,14 @@ function createAllureHandler(deps = {}) {
|
|
|
13022
13465
|
} catch (error) {
|
|
13023
13466
|
return {
|
|
13024
13467
|
exitCode: ExitCode.ValidationError,
|
|
13025
|
-
stderr: [`ERROR: ${
|
|
13468
|
+
stderr: [`ERROR: ${normalizeError2(error)}`]
|
|
13026
13469
|
};
|
|
13027
13470
|
}
|
|
13028
13471
|
if (dryRun) {
|
|
13029
13472
|
if (useJson) {
|
|
13030
13473
|
return {
|
|
13031
13474
|
exitCode: ExitCode.Success,
|
|
13032
|
-
stdout: toJsonLine(
|
|
13475
|
+
stdout: toJsonLine(buildJsonOutput2({
|
|
13033
13476
|
issueKey,
|
|
13034
13477
|
uploaded: false,
|
|
13035
13478
|
commentAdded: false,
|
|
@@ -13072,7 +13515,7 @@ function createAllureHandler(deps = {}) {
|
|
|
13072
13515
|
if (useJson) {
|
|
13073
13516
|
return {
|
|
13074
13517
|
exitCode: ExitCode.Success,
|
|
13075
|
-
stdout: toJsonLine(
|
|
13518
|
+
stdout: toJsonLine(buildJsonOutput2({
|
|
13076
13519
|
issueKey,
|
|
13077
13520
|
uploaded: true,
|
|
13078
13521
|
commentAdded,
|
|
@@ -13096,15 +13539,15 @@ function createAllureHandler(deps = {}) {
|
|
|
13096
13539
|
} catch (error) {
|
|
13097
13540
|
return {
|
|
13098
13541
|
exitCode: ExitCode.RemoteError,
|
|
13099
|
-
stderr: [`ERROR: ${
|
|
13542
|
+
stderr: [`ERROR: ${normalizeError2(error)}`]
|
|
13100
13543
|
};
|
|
13101
13544
|
}
|
|
13102
13545
|
};
|
|
13103
13546
|
}
|
|
13104
13547
|
|
|
13105
13548
|
// src/bdd.ts
|
|
13106
|
-
var
|
|
13107
|
-
var
|
|
13549
|
+
var import_node_fs10 = require("node:fs");
|
|
13550
|
+
var import_node_path11 = __toESM(require("node:path"), 1);
|
|
13108
13551
|
var import_yazl = __toESM(require_yazl(), 1);
|
|
13109
13552
|
|
|
13110
13553
|
// src/forgeClient.ts
|
|
@@ -13269,7 +13712,7 @@ var CONFIG_FLAGS3 = /* @__PURE__ */ new Set([
|
|
|
13269
13712
|
"--jira-email",
|
|
13270
13713
|
"--jira-api-token"
|
|
13271
13714
|
]);
|
|
13272
|
-
function
|
|
13715
|
+
function parseArgs4(args) {
|
|
13273
13716
|
const flags = {};
|
|
13274
13717
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
13275
13718
|
const unknownFlags = [];
|
|
@@ -13316,7 +13759,7 @@ function pickConfigArgs3(parsed) {
|
|
|
13316
13759
|
}
|
|
13317
13760
|
return args;
|
|
13318
13761
|
}
|
|
13319
|
-
function
|
|
13762
|
+
function normalizeError3(error) {
|
|
13320
13763
|
if (error instanceof ForgeClientError) {
|
|
13321
13764
|
return `${error.code}: ${error.message}`;
|
|
13322
13765
|
}
|
|
@@ -13408,7 +13851,7 @@ function parseScenarioFeatureFile(raw, filePath) {
|
|
|
13408
13851
|
}
|
|
13409
13852
|
const scenarioTag = tags.map((tag) => tag.match(SCENARIO_METADATA_PATTERN)).find((match) => Boolean(match));
|
|
13410
13853
|
const scenarioKeyFromTag = scenarioTag?.[1]?.trim().toUpperCase() ?? "";
|
|
13411
|
-
const fileKeyMatch =
|
|
13854
|
+
const fileKeyMatch = import_node_path11.default.basename(filePath ?? "").match(/^(SC-\d+)\.feature$/i);
|
|
13412
13855
|
const scenarioKeyFromFile = fileKeyMatch?.[1]?.trim().toUpperCase() ?? "";
|
|
13413
13856
|
const scenarioKey = scenarioKeyFromTag || scenarioKeyFromFile;
|
|
13414
13857
|
if (scenarioKeyFromTag && scenarioKeyFromFile && scenarioKeyFromTag !== scenarioKeyFromFile) {
|
|
@@ -13526,7 +13969,7 @@ function toJsonExportManifestItems(items) {
|
|
|
13526
13969
|
async function defaultCreateZipArchive(archivePath, entries) {
|
|
13527
13970
|
await new Promise((resolve, reject) => {
|
|
13528
13971
|
const zip = new import_yazl.ZipFile();
|
|
13529
|
-
const output = zip.outputStream.pipe((0,
|
|
13972
|
+
const output = zip.outputStream.pipe((0, import_node_fs10.createWriteStream)(archivePath));
|
|
13530
13973
|
output.on("close", () => resolve());
|
|
13531
13974
|
output.on("error", reject);
|
|
13532
13975
|
zip.outputStream.on("error", reject);
|
|
@@ -13556,14 +13999,14 @@ function missingProjectResponse() {
|
|
|
13556
13999
|
function createBddHandler(deps = {}) {
|
|
13557
14000
|
const cwd = deps.cwd ?? process.cwd();
|
|
13558
14001
|
const env = deps.env ?? process.env;
|
|
13559
|
-
const mkdir = deps.mkdir ?? ((targetPath, options) => (0,
|
|
13560
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
13561
|
-
const readDir = deps.readDir ?? ((dirPath) => (0,
|
|
13562
|
-
const writeFile = deps.writeFile ?? ((filePath, content) => (0,
|
|
14002
|
+
const mkdir = deps.mkdir ?? ((targetPath, options) => (0, import_node_fs10.mkdirSync)(targetPath, options));
|
|
14003
|
+
const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs10.readFileSync)(filePath, "utf8"));
|
|
14004
|
+
const readDir = deps.readDir ?? ((dirPath) => (0, import_node_fs10.readdirSync)(dirPath));
|
|
14005
|
+
const writeFile = deps.writeFile ?? ((filePath, content) => (0, import_node_fs10.writeFileSync)(filePath, content, "utf8"));
|
|
13563
14006
|
const createZipArchive = deps.createZipArchive ?? defaultCreateZipArchive;
|
|
13564
14007
|
return async (request, context) => {
|
|
13565
14008
|
const [subcommand, ...restArgs] = request.args;
|
|
13566
|
-
const parsed =
|
|
14009
|
+
const parsed = parseArgs4(restArgs);
|
|
13567
14010
|
const requestedFormat = (parsed.flags["--format"] ?? "").trim().toLowerCase();
|
|
13568
14011
|
const outputFormat = parsed.boolFlags.has("--json") ? "json" : parsed.boolFlags.has("--feature") ? "feature" : requestedFormat || "text";
|
|
13569
14012
|
const config = resolveCliConfig(pickConfigArgs3(parsed), env, cwd);
|
|
@@ -13603,7 +14046,7 @@ function createBddHandler(deps = {}) {
|
|
|
13603
14046
|
stderr: ["ERROR: Missing required --output-dir for testops bdd features export."]
|
|
13604
14047
|
};
|
|
13605
14048
|
}
|
|
13606
|
-
const outputDir =
|
|
14049
|
+
const outputDir = import_node_path11.default.resolve(cwd, outputDirFlag);
|
|
13607
14050
|
try {
|
|
13608
14051
|
const result = await context.invokeForgeContract("listBddFeatures", {
|
|
13609
14052
|
context: {
|
|
@@ -13639,7 +14082,7 @@ function createBddHandler(deps = {}) {
|
|
|
13639
14082
|
`
|
|
13640
14083
|
}));
|
|
13641
14084
|
for (let index = 0; index < features.length; index += 1) {
|
|
13642
|
-
writeFile(
|
|
14085
|
+
writeFile(import_node_path11.default.join(outputDir, fileNames[index]), exportEntries[index].content);
|
|
13643
14086
|
}
|
|
13644
14087
|
const manifest = {
|
|
13645
14088
|
action: "bdd-features-export",
|
|
@@ -13652,7 +14095,7 @@ function createBddHandler(deps = {}) {
|
|
|
13652
14095
|
};
|
|
13653
14096
|
const manifestContent = `${JSON.stringify(manifest, null, 2)}
|
|
13654
14097
|
`;
|
|
13655
|
-
writeFile(
|
|
14098
|
+
writeFile(import_node_path11.default.join(outputDir, "manifest.json"), manifestContent);
|
|
13656
14099
|
const archivePath = `${outputDir}.zip`;
|
|
13657
14100
|
if (zipRequested) {
|
|
13658
14101
|
await createZipArchive(archivePath, [
|
|
@@ -13691,7 +14134,7 @@ function createBddHandler(deps = {}) {
|
|
|
13691
14134
|
} catch (error) {
|
|
13692
14135
|
return {
|
|
13693
14136
|
exitCode: ExitCode.TransportError,
|
|
13694
|
-
stderr: [`ERROR: ${
|
|
14137
|
+
stderr: [`ERROR: ${normalizeError3(error)}`]
|
|
13695
14138
|
};
|
|
13696
14139
|
}
|
|
13697
14140
|
}
|
|
@@ -13722,7 +14165,7 @@ function createBddHandler(deps = {}) {
|
|
|
13722
14165
|
stderr: ["ERROR: Missing required --output-dir for testops bdd scenarios export."]
|
|
13723
14166
|
};
|
|
13724
14167
|
}
|
|
13725
|
-
const outputDir =
|
|
14168
|
+
const outputDir = import_node_path11.default.resolve(cwd, outputDirFlag);
|
|
13726
14169
|
const zipRequested = parsed.boolFlags.has("--zip");
|
|
13727
14170
|
try {
|
|
13728
14171
|
const scenariosResult = await context.invokeForgeContract("listBddScenarios", {
|
|
@@ -13757,7 +14200,7 @@ function createBddHandler(deps = {}) {
|
|
|
13757
14200
|
const fileName = scenarioToExportFileName(scenario);
|
|
13758
14201
|
const fileContent = `${scenarioToFeatureLines(scenario).join("\n")}
|
|
13759
14202
|
`;
|
|
13760
|
-
writeFile(
|
|
14203
|
+
writeFile(import_node_path11.default.join(outputDir, fileName), fileContent);
|
|
13761
14204
|
return {
|
|
13762
14205
|
scenario,
|
|
13763
14206
|
fileName,
|
|
@@ -13783,7 +14226,7 @@ function createBddHandler(deps = {}) {
|
|
|
13783
14226
|
};
|
|
13784
14227
|
const manifestContent = `${JSON.stringify(manifest, null, 2)}
|
|
13785
14228
|
`;
|
|
13786
|
-
writeFile(
|
|
14229
|
+
writeFile(import_node_path11.default.join(outputDir, "manifest.json"), manifestContent);
|
|
13787
14230
|
const archivePath = `${outputDir}.zip`;
|
|
13788
14231
|
if (zipRequested) {
|
|
13789
14232
|
await createZipArchive(archivePath, [
|
|
@@ -13826,13 +14269,13 @@ function createBddHandler(deps = {}) {
|
|
|
13826
14269
|
} catch (error) {
|
|
13827
14270
|
return {
|
|
13828
14271
|
exitCode: ExitCode.TransportError,
|
|
13829
|
-
stderr: [`ERROR: ${
|
|
14272
|
+
stderr: [`ERROR: ${normalizeError3(error)}`]
|
|
13830
14273
|
};
|
|
13831
14274
|
}
|
|
13832
14275
|
}
|
|
13833
14276
|
if (nested === "import") {
|
|
13834
|
-
const sourceFile = parsed.flags["--file"] ?
|
|
13835
|
-
const inputDir = parsed.flags["--input-dir"] ?
|
|
14277
|
+
const sourceFile = parsed.flags["--file"] ? import_node_path11.default.resolve(cwd, parsed.flags["--file"]) : "";
|
|
14278
|
+
const inputDir = parsed.flags["--input-dir"] ? import_node_path11.default.resolve(cwd, parsed.flags["--input-dir"]) : "";
|
|
13836
14279
|
const dryRun = parsed.boolFlags.has("--dry-run");
|
|
13837
14280
|
if (!sourceFile && !inputDir) {
|
|
13838
14281
|
return {
|
|
@@ -13841,7 +14284,7 @@ function createBddHandler(deps = {}) {
|
|
|
13841
14284
|
};
|
|
13842
14285
|
}
|
|
13843
14286
|
try {
|
|
13844
|
-
const scenarioFiles = sourceFile ? [sourceFile] : readDir(inputDir).filter((entry) => entry.toLowerCase().endsWith(".feature")).map((entry) =>
|
|
14287
|
+
const scenarioFiles = sourceFile ? [sourceFile] : readDir(inputDir).filter((entry) => entry.toLowerCase().endsWith(".feature")).map((entry) => import_node_path11.default.join(inputDir, entry)).sort((left, right) => left.localeCompare(right));
|
|
13845
14288
|
if (scenarioFiles.length === 0) {
|
|
13846
14289
|
return {
|
|
13847
14290
|
exitCode: ExitCode.ValidationError,
|
|
@@ -13980,7 +14423,7 @@ function createBddHandler(deps = {}) {
|
|
|
13980
14423
|
} catch (error) {
|
|
13981
14424
|
return {
|
|
13982
14425
|
exitCode: ExitCode.TransportError,
|
|
13983
|
-
stderr: [`ERROR: ${
|
|
14426
|
+
stderr: [`ERROR: ${normalizeError3(error)}`]
|
|
13984
14427
|
};
|
|
13985
14428
|
}
|
|
13986
14429
|
}
|
|
@@ -14083,7 +14526,7 @@ function createBddHandler(deps = {}) {
|
|
|
14083
14526
|
} catch (error) {
|
|
14084
14527
|
return {
|
|
14085
14528
|
exitCode: ExitCode.TransportError,
|
|
14086
|
-
stderr: [`ERROR: ${
|
|
14529
|
+
stderr: [`ERROR: ${normalizeError3(error)}`]
|
|
14087
14530
|
};
|
|
14088
14531
|
}
|
|
14089
14532
|
}
|
|
@@ -14136,7 +14579,7 @@ function createBddHandler(deps = {}) {
|
|
|
14136
14579
|
} catch (error) {
|
|
14137
14580
|
return {
|
|
14138
14581
|
exitCode: ExitCode.TransportError,
|
|
14139
|
-
stderr: [`ERROR: ${
|
|
14582
|
+
stderr: [`ERROR: ${normalizeError3(error)}`]
|
|
14140
14583
|
};
|
|
14141
14584
|
}
|
|
14142
14585
|
}
|
|
@@ -14157,7 +14600,7 @@ var CONFIG_FLAGS4 = /* @__PURE__ */ new Set([
|
|
|
14157
14600
|
"--jira-email",
|
|
14158
14601
|
"--jira-api-token"
|
|
14159
14602
|
]);
|
|
14160
|
-
function
|
|
14603
|
+
function parseArgs5(args) {
|
|
14161
14604
|
const flags = {};
|
|
14162
14605
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
14163
14606
|
const unknownFlags = [];
|
|
@@ -14195,7 +14638,7 @@ function pickConfigArgs4(parsed) {
|
|
|
14195
14638
|
}
|
|
14196
14639
|
return args;
|
|
14197
14640
|
}
|
|
14198
|
-
function
|
|
14641
|
+
function normalizeError4(error) {
|
|
14199
14642
|
if (error instanceof ForgeClientError) {
|
|
14200
14643
|
return `${error.code}: ${error.message}`;
|
|
14201
14644
|
}
|
|
@@ -14329,7 +14772,7 @@ function createCasesHandler(deps = {}) {
|
|
|
14329
14772
|
const env = deps.env ?? process.env;
|
|
14330
14773
|
return async (request, context) => {
|
|
14331
14774
|
const [subcommand, ...restArgs] = request.args;
|
|
14332
|
-
const parsed =
|
|
14775
|
+
const parsed = parseArgs5(restArgs);
|
|
14333
14776
|
const useJson = parsed.boolFlags.has("--json");
|
|
14334
14777
|
const config = resolveCliConfig(pickConfigArgs4(parsed), env, cwd);
|
|
14335
14778
|
const projectKey = config.values.projectKey;
|
|
@@ -14376,7 +14819,7 @@ function createCasesHandler(deps = {}) {
|
|
|
14376
14819
|
} catch (error) {
|
|
14377
14820
|
return {
|
|
14378
14821
|
exitCode: ExitCode.TransportError,
|
|
14379
|
-
stderr: [`ERROR: ${
|
|
14822
|
+
stderr: [`ERROR: ${normalizeError4(error)}`]
|
|
14380
14823
|
};
|
|
14381
14824
|
}
|
|
14382
14825
|
}
|
|
@@ -14418,7 +14861,7 @@ function createCasesHandler(deps = {}) {
|
|
|
14418
14861
|
} catch (error) {
|
|
14419
14862
|
return {
|
|
14420
14863
|
exitCode: ExitCode.TransportError,
|
|
14421
|
-
stderr: [`ERROR: ${
|
|
14864
|
+
stderr: [`ERROR: ${normalizeError4(error)}`]
|
|
14422
14865
|
};
|
|
14423
14866
|
}
|
|
14424
14867
|
}
|
|
@@ -14481,7 +14924,7 @@ function createCasesHandler(deps = {}) {
|
|
|
14481
14924
|
} catch (error) {
|
|
14482
14925
|
return {
|
|
14483
14926
|
exitCode: ExitCode.TransportError,
|
|
14484
|
-
stderr: [`ERROR: ${
|
|
14927
|
+
stderr: [`ERROR: ${normalizeError4(error)}`]
|
|
14485
14928
|
};
|
|
14486
14929
|
}
|
|
14487
14930
|
}
|
|
@@ -14544,7 +14987,7 @@ function createCasesHandler(deps = {}) {
|
|
|
14544
14987
|
} catch (error) {
|
|
14545
14988
|
return {
|
|
14546
14989
|
exitCode: ExitCode.TransportError,
|
|
14547
|
-
stderr: [`ERROR: ${
|
|
14990
|
+
stderr: [`ERROR: ${normalizeError4(error)}`]
|
|
14548
14991
|
};
|
|
14549
14992
|
}
|
|
14550
14993
|
}
|
|
@@ -14556,6 +14999,7 @@ function createCasesHandler(deps = {}) {
|
|
|
14556
14999
|
}
|
|
14557
15000
|
|
|
14558
15001
|
// src/doctor.ts
|
|
15002
|
+
var import_node_child_process3 = require("node:child_process");
|
|
14559
15003
|
var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
|
|
14560
15004
|
"--config",
|
|
14561
15005
|
"--base-url",
|
|
@@ -14565,11 +15009,16 @@ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
14565
15009
|
"--jira-email",
|
|
14566
15010
|
"--jira-api-token"
|
|
14567
15011
|
]);
|
|
14568
|
-
|
|
15012
|
+
var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set([
|
|
15013
|
+
"--site",
|
|
15014
|
+
"--email",
|
|
15015
|
+
"--api-token"
|
|
15016
|
+
]);
|
|
15017
|
+
function parseArgs6(args) {
|
|
14569
15018
|
const flags = {};
|
|
14570
15019
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
14571
15020
|
const unknownFlags = [];
|
|
14572
|
-
const valuedFlags = /* @__PURE__ */ new Set([...CONFIG_FLAGS5]);
|
|
15021
|
+
const valuedFlags = /* @__PURE__ */ new Set([...CONFIG_FLAGS5, ...DIRECT_JIRA_FLAGS]);
|
|
14573
15022
|
const supportedBoolFlags = /* @__PURE__ */ new Set(["--json", "--check-context"]);
|
|
14574
15023
|
for (let i = 0; i < args.length; i += 1) {
|
|
14575
15024
|
const token = args[i];
|
|
@@ -14635,6 +15084,49 @@ function checkFromServiceResult(result) {
|
|
|
14635
15084
|
message: `Forge endpoint reachable (service returned ${result.error.code}).`
|
|
14636
15085
|
};
|
|
14637
15086
|
}
|
|
15087
|
+
function normalizeSite3(value) {
|
|
15088
|
+
return value.trim().replace(/\/+$/, "");
|
|
15089
|
+
}
|
|
15090
|
+
function hasValue(value) {
|
|
15091
|
+
return value.trim().length > 0;
|
|
15092
|
+
}
|
|
15093
|
+
function resolveAllureJiraConfig(parsed, env, cwd) {
|
|
15094
|
+
const resolution = resolveCliConfig(pickConfigArgs5(parsed), env, cwd);
|
|
15095
|
+
const site = normalizeSite3(parsed.flags["--site"] ?? env.JIRA_SITE ?? resolution.values.baseUrl);
|
|
15096
|
+
const email = (parsed.flags["--email"] ?? resolution.values.jiraEmail).trim();
|
|
15097
|
+
const apiToken = (parsed.flags["--api-token"] ?? resolution.values.jiraApiToken).trim();
|
|
15098
|
+
const issueKey = (parsed.flags["--issue-key"] ?? "").trim();
|
|
15099
|
+
const errors = [];
|
|
15100
|
+
if (!hasValue(site)) {
|
|
15101
|
+
errors.push("Missing Jira site. Pass --site, set JIRA_SITE/JIRA_BASE_URL, or configure baseUrl.");
|
|
15102
|
+
}
|
|
15103
|
+
if (!hasValue(email)) {
|
|
15104
|
+
errors.push("Missing Jira email. Pass --email or set JIRA_EMAIL.");
|
|
15105
|
+
}
|
|
15106
|
+
if (!hasValue(apiToken)) {
|
|
15107
|
+
errors.push("Missing Jira API token. Pass --api-token or set JIRA_API_TOKEN.");
|
|
15108
|
+
}
|
|
15109
|
+
return { site, email, apiToken, issueKey, errors };
|
|
15110
|
+
}
|
|
15111
|
+
function commandCheck(deps, command, args, passMessage, warnMessage) {
|
|
15112
|
+
try {
|
|
15113
|
+
deps.execFileSync(command, [...args], {
|
|
15114
|
+
encoding: "utf8",
|
|
15115
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
15116
|
+
});
|
|
15117
|
+
return {
|
|
15118
|
+
name: command,
|
|
15119
|
+
status: "pass",
|
|
15120
|
+
message: passMessage
|
|
15121
|
+
};
|
|
15122
|
+
} catch {
|
|
15123
|
+
return {
|
|
15124
|
+
name: command,
|
|
15125
|
+
status: "warn",
|
|
15126
|
+
message: warnMessage
|
|
15127
|
+
};
|
|
15128
|
+
}
|
|
15129
|
+
}
|
|
14638
15130
|
function toJsonPayload(summary) {
|
|
14639
15131
|
return {
|
|
14640
15132
|
status: summary.status,
|
|
@@ -14667,9 +15159,11 @@ function toHumanLines2(summary) {
|
|
|
14667
15159
|
function createDoctorHandler(deps = {}) {
|
|
14668
15160
|
const cwd = deps.cwd ?? process.cwd();
|
|
14669
15161
|
const env = deps.env ?? process.env;
|
|
15162
|
+
const runCommand = deps.execFileSync ?? import_node_child_process3.execFileSync;
|
|
15163
|
+
const listAttachments = deps.listIssueAttachments ?? listIssueAttachments;
|
|
14670
15164
|
return async (request, context) => {
|
|
14671
|
-
const [, ...subArgs] = request.args;
|
|
14672
|
-
const parsed =
|
|
15165
|
+
const [subcommand, ...subArgs] = request.args;
|
|
15166
|
+
const parsed = parseArgs6(subArgs);
|
|
14673
15167
|
const useJson = parsed.boolFlags.has("--json");
|
|
14674
15168
|
const enforceContextCheck = parsed.boolFlags.has("--check-context");
|
|
14675
15169
|
const checks = [];
|
|
@@ -14685,10 +15179,74 @@ function createDoctorHandler(deps = {}) {
|
|
|
14685
15179
|
stdout: useJson ? toJsonLine(toJsonPayload(summary2)) : toHumanLines2(summary2)
|
|
14686
15180
|
};
|
|
14687
15181
|
}
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
15182
|
+
if (subcommand === "allure") {
|
|
15183
|
+
const config = resolveAllureJiraConfig(parsed, env, cwd);
|
|
15184
|
+
checks.push({
|
|
15185
|
+
name: "allure_jira_credentials",
|
|
15186
|
+
status: config.errors.length === 0 ? "pass" : "fail",
|
|
15187
|
+
message: config.errors.length === 0 ? "Jira site, email, and API token are configured for Allure upload/download." : config.errors.join(" ")
|
|
15188
|
+
});
|
|
15189
|
+
if (config.issueKey && config.errors.length === 0) {
|
|
15190
|
+
try {
|
|
15191
|
+
const attachments = await listAttachments({
|
|
15192
|
+
site: config.site,
|
|
15193
|
+
email: config.email,
|
|
15194
|
+
apiToken: config.apiToken,
|
|
15195
|
+
issueKey: config.issueKey
|
|
15196
|
+
});
|
|
15197
|
+
checks.push({
|
|
15198
|
+
name: "allure_jira_issue_access",
|
|
15199
|
+
status: "pass",
|
|
15200
|
+
message: `Jira issue ${config.issueKey} is reachable; ${attachments.length} attachment(s) visible.`
|
|
15201
|
+
});
|
|
15202
|
+
} catch (error) {
|
|
15203
|
+
checks.push({
|
|
15204
|
+
name: "allure_jira_issue_access",
|
|
15205
|
+
status: "fail",
|
|
15206
|
+
message: normalizeConnectivityError(error)
|
|
15207
|
+
});
|
|
15208
|
+
}
|
|
15209
|
+
} else if (!config.issueKey) {
|
|
15210
|
+
checks.push({
|
|
15211
|
+
name: "allure_jira_issue_access",
|
|
15212
|
+
status: "skip",
|
|
15213
|
+
message: "Issue access check skipped. Pass --issue-key to verify Jira read/download access for a specific issue."
|
|
15214
|
+
});
|
|
15215
|
+
}
|
|
15216
|
+
checks.push(commandCheck(
|
|
15217
|
+
{ execFileSync: runCommand },
|
|
15218
|
+
"unzip",
|
|
15219
|
+
["-v"],
|
|
15220
|
+
"unzip is available for manual ZIP inspection.",
|
|
15221
|
+
"unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
|
|
15222
|
+
));
|
|
15223
|
+
checks.push(commandCheck(
|
|
15224
|
+
{ execFileSync: runCommand },
|
|
15225
|
+
"java",
|
|
15226
|
+
["-version"],
|
|
15227
|
+
"Java is available for optional raw allure-results workflows.",
|
|
15228
|
+
"Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
|
|
15229
|
+
));
|
|
15230
|
+
checks.push(commandCheck(
|
|
15231
|
+
{ execFileSync: runCommand },
|
|
15232
|
+
"allure",
|
|
15233
|
+
["--version"],
|
|
15234
|
+
"Allure CLI is available for optional raw allure-results workflows.",
|
|
15235
|
+
"Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
|
|
15236
|
+
));
|
|
15237
|
+
const summary2 = {
|
|
15238
|
+
status: computeOverallStatus(checks),
|
|
15239
|
+
checks
|
|
15240
|
+
};
|
|
15241
|
+
return {
|
|
15242
|
+
exitCode: pickDoctorExitCode(summary2),
|
|
15243
|
+
stdout: useJson ? toJsonLine(toJsonPayload(summary2)) : toHumanLines2(summary2)
|
|
15244
|
+
};
|
|
15245
|
+
}
|
|
15246
|
+
const configArgs = pickConfigArgs5(parsed);
|
|
15247
|
+
const resolution = resolveCliConfig(configArgs, env, cwd);
|
|
15248
|
+
const configValidation = validateCliConfigResolution(resolution);
|
|
15249
|
+
checks.push({
|
|
14692
15250
|
name: "config_completeness",
|
|
14693
15251
|
status: configValidation.ok ? "pass" : "fail",
|
|
14694
15252
|
message: configValidation.ok ? "Required config is present." : configValidation.errors.join(" ")
|
|
@@ -14773,8 +15331,8 @@ function createDoctorHandler(deps = {}) {
|
|
|
14773
15331
|
}
|
|
14774
15332
|
|
|
14775
15333
|
// src/ingestFeature.ts
|
|
14776
|
-
var
|
|
14777
|
-
var
|
|
15334
|
+
var import_node_fs11 = require("node:fs");
|
|
15335
|
+
var import_node_path12 = __toESM(require("node:path"), 1);
|
|
14778
15336
|
var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
|
|
14779
15337
|
"--config",
|
|
14780
15338
|
"--base-url",
|
|
@@ -14784,7 +15342,7 @@ var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
|
|
|
14784
15342
|
"--jira-email",
|
|
14785
15343
|
"--jira-api-token"
|
|
14786
15344
|
]);
|
|
14787
|
-
function
|
|
15345
|
+
function parseArgs7(args) {
|
|
14788
15346
|
const flags = {};
|
|
14789
15347
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
14790
15348
|
const unknownFlags = [];
|
|
@@ -14850,7 +15408,7 @@ function summarizeSuccess(result) {
|
|
|
14850
15408
|
}
|
|
14851
15409
|
return lines;
|
|
14852
15410
|
}
|
|
14853
|
-
function
|
|
15411
|
+
function normalizeError5(error) {
|
|
14854
15412
|
if (error instanceof ForgeClientError) {
|
|
14855
15413
|
return `${error.code}: ${error.message}`;
|
|
14856
15414
|
}
|
|
@@ -14860,18 +15418,18 @@ function normalizeError4(error) {
|
|
|
14860
15418
|
return "Unknown ingest error.";
|
|
14861
15419
|
}
|
|
14862
15420
|
function createIngestFeatureHandler(deps = {}) {
|
|
14863
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
14864
|
-
const readStdin = deps.readStdin ?? (() => (0,
|
|
15421
|
+
const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs11.readFileSync)(filePath, "utf8"));
|
|
15422
|
+
const readStdin = deps.readStdin ?? (() => (0, import_node_fs11.readFileSync)(0, "utf8"));
|
|
14865
15423
|
const cwd = deps.cwd ?? process.cwd();
|
|
14866
15424
|
const env = deps.env ?? process.env;
|
|
14867
15425
|
return async (request, context) => {
|
|
14868
15426
|
const [, ...subArgs] = request.args;
|
|
14869
|
-
const parsed =
|
|
15427
|
+
const parsed = parseArgs7(subArgs);
|
|
14870
15428
|
const configArgs = pickConfigArgs6(parsed);
|
|
14871
15429
|
const config = resolveCliConfig(configArgs, env, cwd);
|
|
14872
15430
|
const projectKey = config.values.projectKey;
|
|
14873
15431
|
const issueKey = config.values.issueKey;
|
|
14874
|
-
const sourceFile = parsed.flags["--file"] ?
|
|
15432
|
+
const sourceFile = parsed.flags["--file"] ? import_node_path12.default.resolve(cwd, parsed.flags["--file"]) : "";
|
|
14875
15433
|
const useStdin = parsed.boolFlags.has("--stdin");
|
|
14876
15434
|
const useJson = parsed.boolFlags.has("--json");
|
|
14877
15435
|
const sourceCount = Number(Boolean(sourceFile)) + Number(useStdin);
|
|
@@ -14887,7 +15445,7 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
14887
15445
|
} catch (error) {
|
|
14888
15446
|
return {
|
|
14889
15447
|
exitCode: ExitCode.InternalError,
|
|
14890
|
-
stderr: [`ERROR: Failed to read feature source: ${
|
|
15448
|
+
stderr: [`ERROR: Failed to read feature source: ${normalizeError5(error)}`]
|
|
14891
15449
|
};
|
|
14892
15450
|
}
|
|
14893
15451
|
const name = (parsed.flags["--name"] ?? deriveNameFromGherkin(gherkin)).trim();
|
|
@@ -14982,7 +15540,7 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
14982
15540
|
status: "failed",
|
|
14983
15541
|
featureName: name,
|
|
14984
15542
|
source: useStdin ? "stdin" : sourceFile,
|
|
14985
|
-
errorMessage:
|
|
15543
|
+
errorMessage: normalizeError5(error)
|
|
14986
15544
|
})
|
|
14987
15545
|
};
|
|
14988
15546
|
}
|
|
@@ -14993,15 +15551,15 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
14993
15551
|
`Feature: ${name}`,
|
|
14994
15552
|
`Source: ${useStdin ? "stdin" : sourceFile}`
|
|
14995
15553
|
],
|
|
14996
|
-
stderr: [`ERROR: ${
|
|
15554
|
+
stderr: [`ERROR: ${normalizeError5(error)}`]
|
|
14997
15555
|
};
|
|
14998
15556
|
}
|
|
14999
15557
|
};
|
|
15000
15558
|
}
|
|
15001
15559
|
|
|
15002
15560
|
// src/runUpload.ts
|
|
15003
|
-
var
|
|
15004
|
-
var
|
|
15561
|
+
var import_node_fs12 = require("node:fs");
|
|
15562
|
+
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
15005
15563
|
var STEP_RESULTS = [
|
|
15006
15564
|
StepResult.Passed,
|
|
15007
15565
|
StepResult.Failed,
|
|
@@ -15017,7 +15575,7 @@ var CONFIG_FLAGS7 = /* @__PURE__ */ new Set([
|
|
|
15017
15575
|
"--jira-email",
|
|
15018
15576
|
"--jira-api-token"
|
|
15019
15577
|
]);
|
|
15020
|
-
function
|
|
15578
|
+
function parseArgs8(args) {
|
|
15021
15579
|
const flags = {};
|
|
15022
15580
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
15023
15581
|
const unknownFlags = [];
|
|
@@ -15063,7 +15621,7 @@ function pickConfigArgs7(parsed) {
|
|
|
15063
15621
|
}
|
|
15064
15622
|
return configArgs;
|
|
15065
15623
|
}
|
|
15066
|
-
function
|
|
15624
|
+
function normalizeError6(error) {
|
|
15067
15625
|
if (error instanceof ForgeClientError) {
|
|
15068
15626
|
return `${error.code}: ${error.message}`;
|
|
15069
15627
|
}
|
|
@@ -15142,19 +15700,19 @@ function summarizeRunResult(result) {
|
|
|
15142
15700
|
return lines;
|
|
15143
15701
|
}
|
|
15144
15702
|
function createRunUploadHandler(deps = {}) {
|
|
15145
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
15146
|
-
const readStdin = deps.readStdin ?? (() => (0,
|
|
15703
|
+
const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs12.readFileSync)(filePath, "utf8"));
|
|
15704
|
+
const readStdin = deps.readStdin ?? (() => (0, import_node_fs12.readFileSync)(0, "utf8"));
|
|
15147
15705
|
const cwd = deps.cwd ?? process.cwd();
|
|
15148
15706
|
const env = deps.env ?? process.env;
|
|
15149
15707
|
return async (request, context) => {
|
|
15150
15708
|
const [, ...subArgs] = request.args;
|
|
15151
|
-
const parsed =
|
|
15709
|
+
const parsed = parseArgs8(subArgs);
|
|
15152
15710
|
const configArgs = pickConfigArgs7(parsed);
|
|
15153
15711
|
const config = resolveCliConfig(configArgs, env, cwd);
|
|
15154
15712
|
const projectKey = config.values.projectKey;
|
|
15155
15713
|
const issueKey = config.values.issueKey;
|
|
15156
15714
|
const testCaseKey = parsed.flags["--test-case-key"]?.trim().toUpperCase() ?? "";
|
|
15157
|
-
const sourceFile = parsed.flags["--file"] ?
|
|
15715
|
+
const sourceFile = parsed.flags["--file"] ? import_node_path13.default.resolve(cwd, parsed.flags["--file"]) : "";
|
|
15158
15716
|
const useStdin = parsed.boolFlags.has("--stdin");
|
|
15159
15717
|
const useJson = parsed.boolFlags.has("--json");
|
|
15160
15718
|
const sourceCount = Number(Boolean(sourceFile)) + Number(useStdin);
|
|
@@ -15182,7 +15740,7 @@ function createRunUploadHandler(deps = {}) {
|
|
|
15182
15740
|
} catch (error) {
|
|
15183
15741
|
return {
|
|
15184
15742
|
exitCode: ExitCode.InternalError,
|
|
15185
|
-
stderr: [`ERROR: Failed to read run payload source: ${
|
|
15743
|
+
stderr: [`ERROR: Failed to read run payload source: ${normalizeError6(error)}`]
|
|
15186
15744
|
};
|
|
15187
15745
|
}
|
|
15188
15746
|
const payloadFromSource = parseRunPayload(raw);
|
|
@@ -15212,7 +15770,7 @@ function createRunUploadHandler(deps = {}) {
|
|
|
15212
15770
|
} catch (error) {
|
|
15213
15771
|
return {
|
|
15214
15772
|
exitCode: ExitCode.TransportError,
|
|
15215
|
-
stderr: [`ERROR: ${
|
|
15773
|
+
stderr: [`ERROR: ${normalizeError6(error)}`]
|
|
15216
15774
|
};
|
|
15217
15775
|
}
|
|
15218
15776
|
}
|
|
@@ -15292,7 +15850,7 @@ function createRunUploadHandler(deps = {}) {
|
|
|
15292
15850
|
featureName: runInput.featureName,
|
|
15293
15851
|
scenarioName: runInput.scenarioName,
|
|
15294
15852
|
executedAt: runInput.executedAt,
|
|
15295
|
-
errorMessage:
|
|
15853
|
+
errorMessage: normalizeError6(error)
|
|
15296
15854
|
})
|
|
15297
15855
|
};
|
|
15298
15856
|
}
|
|
@@ -15304,7 +15862,7 @@ function createRunUploadHandler(deps = {}) {
|
|
|
15304
15862
|
`Scenario: ${runInput.scenarioName}`,
|
|
15305
15863
|
`ExecutedAt: ${runInput.executedAt}`
|
|
15306
15864
|
],
|
|
15307
|
-
stderr: [`ERROR: ${
|
|
15865
|
+
stderr: [`ERROR: ${normalizeError6(error)}`]
|
|
15308
15866
|
};
|
|
15309
15867
|
}
|
|
15310
15868
|
};
|
|
@@ -15320,7 +15878,7 @@ var CONFIG_FLAGS8 = /* @__PURE__ */ new Set([
|
|
|
15320
15878
|
"--jira-email",
|
|
15321
15879
|
"--jira-api-token"
|
|
15322
15880
|
]);
|
|
15323
|
-
function
|
|
15881
|
+
function parseArgs9(args) {
|
|
15324
15882
|
const flags = {};
|
|
15325
15883
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
15326
15884
|
const unknownFlags = [];
|
|
@@ -15358,7 +15916,7 @@ function pickConfigArgs8(parsed) {
|
|
|
15358
15916
|
}
|
|
15359
15917
|
return args;
|
|
15360
15918
|
}
|
|
15361
|
-
function
|
|
15919
|
+
function normalizeError7(error) {
|
|
15362
15920
|
if (error instanceof ForgeClientError) {
|
|
15363
15921
|
return `${error.code}: ${error.message}`;
|
|
15364
15922
|
}
|
|
@@ -15422,7 +15980,7 @@ function createRunsHandler(deps = {}) {
|
|
|
15422
15980
|
const env = deps.env ?? process.env;
|
|
15423
15981
|
return async (request, context) => {
|
|
15424
15982
|
const [subcommand, ...restArgs] = request.args;
|
|
15425
|
-
const parsed =
|
|
15983
|
+
const parsed = parseArgs9(restArgs);
|
|
15426
15984
|
const useJson = parsed.boolFlags.has("--json");
|
|
15427
15985
|
const config = resolveCliConfig(pickConfigArgs8(parsed), env, cwd);
|
|
15428
15986
|
const projectKey = config.values.projectKey;
|
|
@@ -15476,7 +16034,7 @@ function createRunsHandler(deps = {}) {
|
|
|
15476
16034
|
} catch (error) {
|
|
15477
16035
|
return {
|
|
15478
16036
|
exitCode: ExitCode.TransportError,
|
|
15479
|
-
stderr: [`ERROR: ${
|
|
16037
|
+
stderr: [`ERROR: ${normalizeError7(error)}`]
|
|
15480
16038
|
};
|
|
15481
16039
|
}
|
|
15482
16040
|
}
|
|
@@ -15487,175 +16045,1655 @@ function createRunsHandler(deps = {}) {
|
|
|
15487
16045
|
};
|
|
15488
16046
|
}
|
|
15489
16047
|
|
|
15490
|
-
// src/
|
|
15491
|
-
var
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
|
|
15509
|
-
|
|
15510
|
-
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
unknownFlags.push(token);
|
|
15522
|
-
continue;
|
|
15523
|
-
}
|
|
15524
|
-
flags[token] = value;
|
|
15525
|
-
index += 1;
|
|
15526
|
-
}
|
|
15527
|
-
return { flags, boolFlags, unknownFlags };
|
|
15528
|
-
}
|
|
15529
|
-
function pickConfigArgs9(parsed) {
|
|
15530
|
-
const args = [];
|
|
15531
|
-
for (const [flag, value] of Object.entries(parsed.flags)) {
|
|
15532
|
-
if (CONFIG_FLAGS9.has(flag)) {
|
|
15533
|
-
args.push(flag, value);
|
|
15534
|
-
}
|
|
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"
|
|
15535
16079
|
}
|
|
15536
|
-
|
|
16080
|
+
];
|
|
16081
|
+
function sha256(value) {
|
|
16082
|
+
return (0, import_node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
15537
16083
|
}
|
|
15538
|
-
function
|
|
15539
|
-
if (
|
|
15540
|
-
return
|
|
16084
|
+
function sortJsonValue(value) {
|
|
16085
|
+
if (Array.isArray(value)) {
|
|
16086
|
+
return value.map(sortJsonValue);
|
|
15541
16087
|
}
|
|
15542
|
-
if (
|
|
15543
|
-
return
|
|
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
|
+
);
|
|
15544
16092
|
}
|
|
15545
|
-
return
|
|
16093
|
+
return value;
|
|
15546
16094
|
}
|
|
15547
|
-
function
|
|
15548
|
-
return
|
|
15549
|
-
id: suite.id,
|
|
15550
|
-
key: suite.key,
|
|
15551
|
-
name: suite.name,
|
|
15552
|
-
description: suite.description ?? null,
|
|
15553
|
-
linkedIssueKeys: [...suite.linkedIssueKeys ?? []],
|
|
15554
|
-
createdAt: suite.createdAt,
|
|
15555
|
-
updatedAt: suite.updatedAt
|
|
15556
|
-
};
|
|
16095
|
+
function stableJson(value) {
|
|
16096
|
+
return JSON.stringify(sortJsonValue(value));
|
|
15557
16097
|
}
|
|
15558
|
-
function
|
|
15559
|
-
return {
|
|
15560
|
-
id: testCase.id,
|
|
15561
|
-
key: testCase.key,
|
|
15562
|
-
title: testCase.title,
|
|
15563
|
-
status: testCase.status,
|
|
15564
|
-
description: testCase.description ?? null,
|
|
15565
|
-
linkedIssueKeys: [...testCase.linkedIssueKeys ?? []],
|
|
15566
|
-
createdAt: testCase.createdAt,
|
|
15567
|
-
updatedAt: testCase.updatedAt
|
|
15568
|
-
};
|
|
16098
|
+
function computePlanId(planWithoutId) {
|
|
16099
|
+
return `sha256:${sha256(stableJson(planWithoutId))}`;
|
|
15569
16100
|
}
|
|
15570
|
-
function
|
|
15571
|
-
|
|
15572
|
-
|
|
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.`);
|
|
15573
16109
|
}
|
|
15574
|
-
return
|
|
15575
|
-
`Test suites: ${items.length}`,
|
|
15576
|
-
...items.map((item) => `- ${item.key} ${item.name}`)
|
|
15577
|
-
];
|
|
16110
|
+
return normalized;
|
|
15578
16111
|
}
|
|
15579
|
-
function
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
`Created: ${suite.createdAt}`,
|
|
15586
|
-
`Updated: ${suite.updatedAt}`
|
|
15587
|
-
];
|
|
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;
|
|
15588
16118
|
}
|
|
15589
|
-
function
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
|
|
15593
|
-
`Suite: ${suite.key} ${suite.name}`,
|
|
15594
|
-
"No test cases are currently linked to this suite."
|
|
15595
|
-
];
|
|
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.`);
|
|
15596
16123
|
}
|
|
15597
|
-
return
|
|
15598
|
-
`Suite test cases: ${items.length}`,
|
|
15599
|
-
`Suite: ${suite.key} ${suite.name}`,
|
|
15600
|
-
...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
|
|
15601
|
-
];
|
|
16124
|
+
return normalized;
|
|
15602
16125
|
}
|
|
15603
|
-
function
|
|
15604
|
-
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
}
|
|
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;
|
|
15608
16132
|
}
|
|
15609
|
-
function
|
|
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
|
+
};
|
|
15610
16281
|
return {
|
|
15611
|
-
|
|
15612
|
-
|
|
16282
|
+
...planWithoutId,
|
|
16283
|
+
planId: computePlanId(planWithoutId)
|
|
15613
16284
|
};
|
|
15614
16285
|
}
|
|
15615
|
-
function
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
|
|
15625
|
-
|
|
15626
|
-
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
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;
|
|
15630
16308
|
}
|
|
15631
|
-
if (!
|
|
15632
|
-
|
|
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);
|
|
15633
16313
|
}
|
|
15634
|
-
if (
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15643
|
-
|
|
15644
|
-
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
|
|
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
|
+
|
|
17528
|
+
// src/suites.ts
|
|
17529
|
+
var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
|
|
17530
|
+
"--config",
|
|
17531
|
+
"--base-url",
|
|
17532
|
+
"--project-key",
|
|
17533
|
+
"--issue-key",
|
|
17534
|
+
"--auth-mode",
|
|
17535
|
+
"--jira-email",
|
|
17536
|
+
"--jira-api-token"
|
|
17537
|
+
]);
|
|
17538
|
+
function parseArgs11(args) {
|
|
17539
|
+
const flags = {};
|
|
17540
|
+
const boolFlags = /* @__PURE__ */ new Set();
|
|
17541
|
+
const unknownFlags = [];
|
|
17542
|
+
const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS9]);
|
|
17543
|
+
const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
|
|
17544
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
17545
|
+
const token = args[index];
|
|
17546
|
+
if (!token.startsWith("--")) {
|
|
17547
|
+
continue;
|
|
17548
|
+
}
|
|
17549
|
+
if (supportedBoolFlags.has(token)) {
|
|
17550
|
+
boolFlags.add(token);
|
|
17551
|
+
continue;
|
|
17552
|
+
}
|
|
17553
|
+
if (!supportedValueFlags.has(token)) {
|
|
17554
|
+
unknownFlags.push(token);
|
|
17555
|
+
continue;
|
|
17556
|
+
}
|
|
17557
|
+
const value = args[index + 1];
|
|
17558
|
+
if (!value || value.startsWith("--")) {
|
|
17559
|
+
unknownFlags.push(token);
|
|
17560
|
+
continue;
|
|
17561
|
+
}
|
|
17562
|
+
flags[token] = value;
|
|
17563
|
+
index += 1;
|
|
17564
|
+
}
|
|
17565
|
+
return { flags, boolFlags, unknownFlags };
|
|
17566
|
+
}
|
|
17567
|
+
function pickConfigArgs9(parsed) {
|
|
17568
|
+
const args = [];
|
|
17569
|
+
for (const [flag, value] of Object.entries(parsed.flags)) {
|
|
17570
|
+
if (CONFIG_FLAGS9.has(flag)) {
|
|
17571
|
+
args.push(flag, value);
|
|
17572
|
+
}
|
|
17573
|
+
}
|
|
17574
|
+
return args;
|
|
17575
|
+
}
|
|
17576
|
+
function normalizeError8(error) {
|
|
17577
|
+
if (error instanceof ForgeClientError) {
|
|
17578
|
+
return `${error.code}: ${error.message}`;
|
|
17579
|
+
}
|
|
17580
|
+
if (error instanceof Error) {
|
|
17581
|
+
return error.message;
|
|
17582
|
+
}
|
|
17583
|
+
return "Unknown suites command error.";
|
|
17584
|
+
}
|
|
17585
|
+
function toJsonSuite(suite) {
|
|
17586
|
+
return {
|
|
17587
|
+
id: suite.id,
|
|
17588
|
+
key: suite.key,
|
|
17589
|
+
name: suite.name,
|
|
17590
|
+
description: suite.description ?? null,
|
|
17591
|
+
linkedIssueKeys: [...suite.linkedIssueKeys ?? []],
|
|
17592
|
+
createdAt: suite.createdAt,
|
|
17593
|
+
updatedAt: suite.updatedAt
|
|
17594
|
+
};
|
|
17595
|
+
}
|
|
17596
|
+
function toJsonCase2(testCase) {
|
|
17597
|
+
return {
|
|
17598
|
+
id: testCase.id,
|
|
17599
|
+
key: testCase.key,
|
|
17600
|
+
title: testCase.title,
|
|
17601
|
+
status: testCase.status,
|
|
17602
|
+
description: testCase.description ?? null,
|
|
17603
|
+
linkedIssueKeys: [...testCase.linkedIssueKeys ?? []],
|
|
17604
|
+
createdAt: testCase.createdAt,
|
|
17605
|
+
updatedAt: testCase.updatedAt
|
|
17606
|
+
};
|
|
17607
|
+
}
|
|
17608
|
+
function suitesToLines(items) {
|
|
17609
|
+
if (items.length === 0) {
|
|
17610
|
+
return ["Test suites: 0", "No test suites found for the selected context."];
|
|
17611
|
+
}
|
|
17612
|
+
return [
|
|
17613
|
+
`Test suites: ${items.length}`,
|
|
17614
|
+
...items.map((item) => `- ${item.key} ${item.name}`)
|
|
17615
|
+
];
|
|
17616
|
+
}
|
|
17617
|
+
function suiteToLines(suite) {
|
|
17618
|
+
return [
|
|
17619
|
+
`Test suite: ${suite.key}`,
|
|
17620
|
+
`Name: ${suite.name}`,
|
|
17621
|
+
`Description: ${suite.description ?? "none"}`,
|
|
17622
|
+
`Linked issues: ${(suite.linkedIssueKeys ?? []).join(", ") || "none"}`,
|
|
17623
|
+
`Created: ${suite.createdAt}`,
|
|
17624
|
+
`Updated: ${suite.updatedAt}`
|
|
17625
|
+
];
|
|
17626
|
+
}
|
|
17627
|
+
function suiteCasesToLines(suite, items) {
|
|
17628
|
+
if (items.length === 0) {
|
|
17629
|
+
return [
|
|
17630
|
+
`Suite test cases: 0`,
|
|
17631
|
+
`Suite: ${suite.key} ${suite.name}`,
|
|
17632
|
+
"No test cases are currently linked to this suite."
|
|
17633
|
+
];
|
|
17634
|
+
}
|
|
17635
|
+
return [
|
|
17636
|
+
`Suite test cases: ${items.length}`,
|
|
17637
|
+
`Suite: ${suite.key} ${suite.name}`,
|
|
17638
|
+
...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
|
|
17639
|
+
];
|
|
17640
|
+
}
|
|
17641
|
+
function missingProjectResponse4() {
|
|
17642
|
+
return {
|
|
17643
|
+
exitCode: ExitCode.ValidationError,
|
|
17644
|
+
stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
|
|
17645
|
+
};
|
|
17646
|
+
}
|
|
17647
|
+
function missingKeyResponse2(commandPath) {
|
|
17648
|
+
return {
|
|
17649
|
+
exitCode: ExitCode.ValidationError,
|
|
17650
|
+
stderr: [`ERROR: Missing required --key for ${commandPath}.`]
|
|
17651
|
+
};
|
|
17652
|
+
}
|
|
17653
|
+
function createSuitesHandler(deps = {}) {
|
|
17654
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
17655
|
+
const env = deps.env ?? process.env;
|
|
17656
|
+
return async (request, context) => {
|
|
17657
|
+
const [subcommand, ...restArgs] = request.args;
|
|
17658
|
+
const parsed = parseArgs11(restArgs);
|
|
17659
|
+
const useJson = parsed.boolFlags.has("--json");
|
|
17660
|
+
const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
|
|
17661
|
+
const projectKey = config.values.projectKey;
|
|
17662
|
+
const issueKey = config.values.issueKey;
|
|
17663
|
+
if (parsed.unknownFlags.length > 0) {
|
|
17664
|
+
return {
|
|
17665
|
+
exitCode: ExitCode.UsageError,
|
|
17666
|
+
stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
|
|
17667
|
+
};
|
|
17668
|
+
}
|
|
17669
|
+
if (!projectKey) {
|
|
17670
|
+
return missingProjectResponse4();
|
|
17671
|
+
}
|
|
17672
|
+
if (subcommand === "list") {
|
|
17673
|
+
try {
|
|
17674
|
+
const result = await context.invokeForgeContract("listTestSuites", {
|
|
17675
|
+
context: {
|
|
17676
|
+
projectKey,
|
|
17677
|
+
issueKey: issueKey || void 0
|
|
17678
|
+
}
|
|
17679
|
+
});
|
|
17680
|
+
if (!result.ok) {
|
|
17681
|
+
return {
|
|
17682
|
+
exitCode: ExitCode.RemoteError,
|
|
17683
|
+
stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
|
|
17684
|
+
};
|
|
17685
|
+
}
|
|
17686
|
+
if (useJson) {
|
|
17687
|
+
return {
|
|
17688
|
+
exitCode: ExitCode.Success,
|
|
17689
|
+
stdout: toJsonLine({
|
|
17690
|
+
action: "suites-list",
|
|
17691
|
+
projectKey,
|
|
17692
|
+
issueKey: issueKey || null,
|
|
17693
|
+
count: result.data.length,
|
|
17694
|
+
items: result.data.map((item) => toJsonSuite(item))
|
|
17695
|
+
})
|
|
17696
|
+
};
|
|
15659
17697
|
}
|
|
15660
17698
|
return {
|
|
15661
17699
|
exitCode: ExitCode.Success,
|
|
@@ -15664,7 +17702,7 @@ function createSuitesHandler(deps = {}) {
|
|
|
15664
17702
|
} catch (error) {
|
|
15665
17703
|
return {
|
|
15666
17704
|
exitCode: ExitCode.TransportError,
|
|
15667
|
-
stderr: [`ERROR: ${
|
|
17705
|
+
stderr: [`ERROR: ${normalizeError8(error)}`]
|
|
15668
17706
|
};
|
|
15669
17707
|
}
|
|
15670
17708
|
}
|
|
@@ -15705,7 +17743,7 @@ function createSuitesHandler(deps = {}) {
|
|
|
15705
17743
|
} catch (error) {
|
|
15706
17744
|
return {
|
|
15707
17745
|
exitCode: ExitCode.TransportError,
|
|
15708
|
-
stderr: [`ERROR: ${
|
|
17746
|
+
stderr: [`ERROR: ${normalizeError8(error)}`]
|
|
15709
17747
|
};
|
|
15710
17748
|
}
|
|
15711
17749
|
}
|
|
@@ -15769,7 +17807,7 @@ function createSuitesHandler(deps = {}) {
|
|
|
15769
17807
|
} catch (error) {
|
|
15770
17808
|
return {
|
|
15771
17809
|
exitCode: ExitCode.TransportError,
|
|
15772
|
-
stderr: [`ERROR: ${
|
|
17810
|
+
stderr: [`ERROR: ${normalizeError8(error)}`]
|
|
15773
17811
|
};
|
|
15774
17812
|
}
|
|
15775
17813
|
}
|
|
@@ -15791,7 +17829,7 @@ var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
|
|
|
15791
17829
|
"--jira-api-token"
|
|
15792
17830
|
]);
|
|
15793
17831
|
var RECONCILE_CONFIRM_TOKEN = "RECONCILE";
|
|
15794
|
-
function
|
|
17832
|
+
function parseArgs12(args) {
|
|
15795
17833
|
const flags = {};
|
|
15796
17834
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
15797
17835
|
const unknownFlags = [];
|
|
@@ -15829,7 +17867,7 @@ function pickConfigArgs10(parsed) {
|
|
|
15829
17867
|
}
|
|
15830
17868
|
return args;
|
|
15831
17869
|
}
|
|
15832
|
-
function
|
|
17870
|
+
function normalizeError9(error) {
|
|
15833
17871
|
if (error instanceof ForgeClientError) {
|
|
15834
17872
|
return `${error.code}: ${error.message}`;
|
|
15835
17873
|
}
|
|
@@ -15880,7 +17918,7 @@ function createSyncHandler(deps = {}) {
|
|
|
15880
17918
|
const env = deps.env ?? process.env;
|
|
15881
17919
|
return async (request, context) => {
|
|
15882
17920
|
const [subcommand, ...restArgs] = request.args;
|
|
15883
|
-
const parsed =
|
|
17921
|
+
const parsed = parseArgs12(restArgs);
|
|
15884
17922
|
const useJson = parsed.boolFlags.has("--json");
|
|
15885
17923
|
const config = resolveCliConfig(pickConfigArgs10(parsed), env, cwd);
|
|
15886
17924
|
const projectKey = config.values.projectKey;
|
|
@@ -15932,7 +17970,7 @@ function createSyncHandler(deps = {}) {
|
|
|
15932
17970
|
} catch (error) {
|
|
15933
17971
|
return {
|
|
15934
17972
|
exitCode: ExitCode.TransportError,
|
|
15935
|
-
stderr: [`ERROR: ${
|
|
17973
|
+
stderr: [`ERROR: ${normalizeError9(error)}`]
|
|
15936
17974
|
};
|
|
15937
17975
|
}
|
|
15938
17976
|
}
|
|
@@ -15978,7 +18016,7 @@ function createSyncHandler(deps = {}) {
|
|
|
15978
18016
|
} catch (error) {
|
|
15979
18017
|
return {
|
|
15980
18018
|
exitCode: ExitCode.TransportError,
|
|
15981
|
-
stderr: [`ERROR: ${
|
|
18019
|
+
stderr: [`ERROR: ${normalizeError9(error)}`]
|
|
15982
18020
|
};
|
|
15983
18021
|
}
|
|
15984
18022
|
}
|
|
@@ -15993,8 +18031,8 @@ function createSyncHandler(deps = {}) {
|
|
|
15993
18031
|
var COMMAND_REGISTRY = [
|
|
15994
18032
|
{
|
|
15995
18033
|
name: "allure",
|
|
15996
|
-
description: "Allure evidence upload/download commands for Jira issues",
|
|
15997
|
-
subcommands: ["upload", "download"],
|
|
18034
|
+
description: "Allure evidence upload/download/open commands for Jira issues",
|
|
18035
|
+
subcommands: ["upload", "download", "open"],
|
|
15998
18036
|
handler: createAllureHandler()
|
|
15999
18037
|
},
|
|
16000
18038
|
{
|
|
@@ -16040,6 +18078,12 @@ var COMMAND_REGISTRY = [
|
|
|
16040
18078
|
subcommands: ["show"],
|
|
16041
18079
|
handler: createRunsHandler()
|
|
16042
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
|
+
},
|
|
16043
18087
|
{
|
|
16044
18088
|
name: "suites",
|
|
16045
18089
|
description: "Hierarchy-aware suite browse commands",
|
|
@@ -16049,7 +18093,7 @@ var COMMAND_REGISTRY = [
|
|
|
16049
18093
|
{
|
|
16050
18094
|
name: "doctor",
|
|
16051
18095
|
description: "Preflight and health checks",
|
|
16052
|
-
subcommands: ["check"],
|
|
18096
|
+
subcommands: ["check", "allure"],
|
|
16053
18097
|
handler: createDoctorHandler()
|
|
16054
18098
|
},
|
|
16055
18099
|
{
|
|
@@ -16234,6 +18278,7 @@ function showTopLevelHelp() {
|
|
|
16234
18278
|
"",
|
|
16235
18279
|
"Available products:",
|
|
16236
18280
|
" testops TestOps \u2014 test management, BDD, execution, reporting",
|
|
18281
|
+
" allure Allure evidence diagnostics and local report workflows (doctor, open)",
|
|
16237
18282
|
"",
|
|
16238
18283
|
"Boundary:",
|
|
16239
18284
|
" Thin client over Forge contracts or Jira Cloud APIs.",
|
|
@@ -16243,6 +18288,29 @@ function showTopLevelHelp() {
|
|
|
16243
18288
|
console.log(line);
|
|
16244
18289
|
}
|
|
16245
18290
|
}
|
|
18291
|
+
function showAllureHelp() {
|
|
18292
|
+
const lines = [
|
|
18293
|
+
"Automatify Allure",
|
|
18294
|
+
"",
|
|
18295
|
+
"Usage:",
|
|
18296
|
+
" automatify allure doctor [options]",
|
|
18297
|
+
" automatify allure open <zipPath> [options]",
|
|
18298
|
+
"",
|
|
18299
|
+
"Commands:",
|
|
18300
|
+
" doctor Check local Allure/Jira readiness",
|
|
18301
|
+
" open Extract a generated Allure HTML report ZIP and serve it locally",
|
|
18302
|
+
"",
|
|
18303
|
+
"Examples:",
|
|
18304
|
+
" automatify allure doctor",
|
|
18305
|
+
" automatify allure doctor --json",
|
|
18306
|
+
" automatify allure doctor --issue-key ABC-123",
|
|
18307
|
+
" automatify allure open ./allure-report.zip",
|
|
18308
|
+
" automatify allure open ./allure-report.zip --port 8080 --no-open"
|
|
18309
|
+
];
|
|
18310
|
+
for (const line of lines) {
|
|
18311
|
+
console.log(line);
|
|
18312
|
+
}
|
|
18313
|
+
}
|
|
16246
18314
|
async function main() {
|
|
16247
18315
|
const args = process.argv.slice(2);
|
|
16248
18316
|
const [productArg, ...restArgs] = args;
|
|
@@ -16251,6 +18319,28 @@ async function main() {
|
|
|
16251
18319
|
process.exitCode = 0;
|
|
16252
18320
|
return;
|
|
16253
18321
|
}
|
|
18322
|
+
if (productArg === "allure") {
|
|
18323
|
+
const [commandArg, ...commandRest] = restArgs;
|
|
18324
|
+
if (!commandArg || commandArg === "--help" || commandArg === "-h" || commandArg === "help") {
|
|
18325
|
+
showAllureHelp();
|
|
18326
|
+
process.exitCode = 0;
|
|
18327
|
+
return;
|
|
18328
|
+
}
|
|
18329
|
+
if (commandArg === "doctor") {
|
|
18330
|
+
const exitCode2 = await runCli(["doctor", "allure", ...commandRest]);
|
|
18331
|
+
process.exitCode = exitCode2;
|
|
18332
|
+
return;
|
|
18333
|
+
}
|
|
18334
|
+
if (commandArg === "open") {
|
|
18335
|
+
const exitCode2 = await runCli(["allure", "open", ...commandRest]);
|
|
18336
|
+
process.exitCode = exitCode2;
|
|
18337
|
+
return;
|
|
18338
|
+
}
|
|
18339
|
+
console.error(`Unknown allure command: ${commandArg}`);
|
|
18340
|
+
console.error("Run `automatify allure --help` to see available commands.");
|
|
18341
|
+
process.exitCode = 1;
|
|
18342
|
+
return;
|
|
18343
|
+
}
|
|
16254
18344
|
if (productArg !== "testops") {
|
|
16255
18345
|
console.error(`Unknown product: ${productArg}`);
|
|
16256
18346
|
console.error("Run `automatify --help` to see available products.");
|