@stackgenhq/backstage-plugin-stackgen-backend 0.3.6-beta.1
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 +485 -0
- package/config.d.ts +39 -0
- package/dist/actions/core/assign_project_members_action.cjs.js +227 -0
- package/dist/actions/core/assign_project_members_action.cjs.js.map +1 -0
- package/dist/actions/core/create_appstack_action.cjs.js +953 -0
- package/dist/actions/core/create_appstack_action.cjs.js.map +1 -0
- package/dist/actions/core/create_project_action.cjs.js +243 -0
- package/dist/actions/core/create_project_action.cjs.js.map +1 -0
- package/dist/actions/core/download_iac_action.cjs.js +133 -0
- package/dist/actions/core/download_iac_action.cjs.js.map +1 -0
- package/dist/actions/core/set_environment_variable_values_action.cjs.js +121 -0
- package/dist/actions/core/set_environment_variable_values_action.cjs.js.map +1 -0
- package/dist/actions/core/set_state_backend_action.cjs.js +174 -0
- package/dist/actions/core/set_state_backend_action.cjs.js.map +1 -0
- package/dist/actions/exporter/export_iac_to_git_action.cjs.js +290 -0
- package/dist/actions/exporter/export_iac_to_git_action.cjs.js.map +1 -0
- package/dist/actions/exporter/list_vault_secrets_action.cjs.js +39 -0
- package/dist/actions/exporter/list_vault_secrets_action.cjs.js.map +1 -0
- package/dist/config/constants.cjs.js +23 -0
- package/dist/config/constants.cjs.js.map +1 -0
- package/dist/exporter/exportOverrides.cjs.js +27 -0
- package/dist/exporter/exportOverrides.cjs.js.map +1 -0
- package/dist/exporter/gitExporterConfigHttp.cjs.js +105 -0
- package/dist/exporter/gitExporterConfigHttp.cjs.js.map +1 -0
- package/dist/exporter/http.cjs.js +14 -0
- package/dist/exporter/http.cjs.js.map +1 -0
- package/dist/exporter/paths.cjs.js +25 -0
- package/dist/exporter/paths.cjs.js.map +1 -0
- package/dist/exporter/projectGitExporterTemplate.cjs.js +62 -0
- package/dist/exporter/projectGitExporterTemplate.cjs.js.map +1 -0
- package/dist/exporter/topologyResolve.cjs.js +71 -0
- package/dist/exporter/topologyResolve.cjs.js.map +1 -0
- package/dist/index.cjs.js +31 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +224 -0
- package/dist/lib/extractArchive.cjs.js +62 -0
- package/dist/lib/extractArchive.cjs.js.map +1 -0
- package/dist/modules/stackgenExporterScaffolderModule.cjs.js +29 -0
- package/dist/modules/stackgenExporterScaffolderModule.cjs.js.map +1 -0
- package/dist/modules/stackgenUiProxiesModule.cjs.js +366 -0
- package/dist/modules/stackgenUiProxiesModule.cjs.js.map +1 -0
- package/dist/plugin/plugin.cjs.js +136 -0
- package/dist/plugin/plugin.cjs.js.map +1 -0
- package/dist/plugin/router.cjs.js +341 -0
- package/dist/plugin/router.cjs.js.map +1 -0
- package/dist/services/MappingsService/mappingsService.cjs.js +149 -0
- package/dist/services/MappingsService/mappingsService.cjs.js.map +1 -0
- package/dist/services/StackgenService/stackgenService.cjs.js +267 -0
- package/dist/services/StackgenService/stackgenService.cjs.js.map +1 -0
- package/dist/utils/api-objects.cjs.js +205 -0
- package/dist/utils/api-objects.cjs.js.map +1 -0
- package/dist/vendor/stackgen-api-client/index.cjs.js +47681 -0
- package/dist/vendor/stackgen-api-client/index.cjs.js.map +1 -0
- package/migrations/20250131055302_init.js +24 -0
- package/migrations/20250305123456_alter_type_associations.js +23 -0
- package/package.json +86 -0
- package/templates/appstack-configure-environment.yaml +81 -0
- package/templates/appstack-from-template.yaml +76 -0
- package/templates/appstack-with-modules.yaml +69 -0
- package/templates/appstack-with-resources.yaml +81 -0
- package/templates/export-iac.yaml +97 -0
- package/templates/full-infra-setup.yaml +156 -0
- package/templates/git_export_exporter_multi_scm.yaml +123 -0
- package/templates/project-advanced.yaml +77 -0
- package/templates/project-basic.yaml +39 -0
- package/templates/project-with-members.yaml +86 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var topologyResolve = require('../../exporter/topologyResolve.cjs.js');
|
|
6
|
+
var apiObjects = require('../../utils/api-objects.cjs.js');
|
|
7
|
+
|
|
8
|
+
const setEnvironmentVariableValuesInputSchema = zod.z.object({
|
|
9
|
+
teamId: zod.z.string().min(1, "teamId is required"),
|
|
10
|
+
environmentId: zod.z.string().min(1, "environmentId is required"),
|
|
11
|
+
topologyId: zod.z.string().optional(),
|
|
12
|
+
appstackName: zod.z.string().optional(),
|
|
13
|
+
variableValues: zod.z.array(
|
|
14
|
+
zod.z.object({
|
|
15
|
+
name: zod.z.string().min(1),
|
|
16
|
+
value: zod.z.string()
|
|
17
|
+
})
|
|
18
|
+
).default([])
|
|
19
|
+
}).superRefine((value, ctx) => {
|
|
20
|
+
if (!value.topologyId?.trim() && !value.appstackName?.trim()) {
|
|
21
|
+
ctx.addIssue({
|
|
22
|
+
code: zod.z.ZodIssueCode.custom,
|
|
23
|
+
message: "topologyId or appstackName is required",
|
|
24
|
+
path: ["topologyId"]
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const setEnvironmentVariableValuesOutputSchema = zod.z.object({
|
|
29
|
+
updatedCount: zod.z.number(),
|
|
30
|
+
updatedVariablesText: zod.z.string(),
|
|
31
|
+
environmentId: zod.z.string(),
|
|
32
|
+
topologyId: zod.z.string()
|
|
33
|
+
});
|
|
34
|
+
function parseVariableValue(raw) {
|
|
35
|
+
const trimmed = raw.trim();
|
|
36
|
+
if (trimmed === "") {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(trimmed);
|
|
41
|
+
} catch {
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const setEnvironmentVariableValuesAction = (config, logger) => {
|
|
46
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
47
|
+
id: "stackGen:setEnvironmentVariableValues",
|
|
48
|
+
description: "Sets Terraform variable values on an existing StackGen environment profile",
|
|
49
|
+
schema: {
|
|
50
|
+
input: () => setEnvironmentVariableValuesInputSchema,
|
|
51
|
+
output: () => setEnvironmentVariableValuesOutputSchema
|
|
52
|
+
},
|
|
53
|
+
async handler(ctx) {
|
|
54
|
+
const stackgenApis = apiObjects.getStackgenApiObjects(config);
|
|
55
|
+
const teamId = ctx.input.teamId.trim();
|
|
56
|
+
const environmentId = ctx.input.environmentId.trim();
|
|
57
|
+
const variableValues = ctx.input.variableValues ?? [];
|
|
58
|
+
let topologyId = ctx.input.topologyId?.trim() || "";
|
|
59
|
+
if (!topologyId) {
|
|
60
|
+
topologyId = await topologyResolve.resolveTopologyId({
|
|
61
|
+
config,
|
|
62
|
+
teamId,
|
|
63
|
+
appstackName: ctx.input.appstackName?.trim()});
|
|
64
|
+
}
|
|
65
|
+
if (variableValues.length === 0) {
|
|
66
|
+
ctx.logger.info("No variable values provided; skipping updates");
|
|
67
|
+
ctx.output("updatedCount", 0);
|
|
68
|
+
ctx.output("updatedVariablesText", "No variables updated");
|
|
69
|
+
ctx.output("environmentId", environmentId);
|
|
70
|
+
ctx.output("topologyId", topologyId);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const updated = [];
|
|
74
|
+
for (const pair of variableValues) {
|
|
75
|
+
const variableIdentifier = pair.name.trim();
|
|
76
|
+
const value = parseVariableValue(pair.value);
|
|
77
|
+
ctx.logger.info(
|
|
78
|
+
`Setting ${variableIdentifier} on env ${environmentId} (topology=${topologyId})`
|
|
79
|
+
);
|
|
80
|
+
try {
|
|
81
|
+
await stackgenApis.v1TfBlocksApiObj.updateTFVariableEnvironmentProfileValue(
|
|
82
|
+
{
|
|
83
|
+
topologyId,
|
|
84
|
+
orgId: teamId,
|
|
85
|
+
variableIdentifier,
|
|
86
|
+
envProfileId: environmentId,
|
|
87
|
+
tFVariableEnvironmentProfileValue: {
|
|
88
|
+
envProfileId: environmentId,
|
|
89
|
+
value
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
updated.push(`${variableIdentifier} = ${pair.value}`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const parsedError = await apiObjects.readClientError(error);
|
|
96
|
+
const detail = apiObjects.formatApiFailure(
|
|
97
|
+
parsedError.status,
|
|
98
|
+
parsedError.body,
|
|
99
|
+
parsedError.text,
|
|
100
|
+
parsedError.requestId
|
|
101
|
+
);
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Failed to set ${variableIdentifier} on environment ${environmentId}: ${detail}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
logger.info(
|
|
108
|
+
`stackGen:setEnvironmentVariableValues updated=${updated.length} env=${environmentId}`
|
|
109
|
+
);
|
|
110
|
+
ctx.output("updatedCount", updated.length);
|
|
111
|
+
ctx.output("updatedVariablesText", updated.join("\n"));
|
|
112
|
+
ctx.output("environmentId", environmentId);
|
|
113
|
+
ctx.output("topologyId", topologyId);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
exports.setEnvironmentVariableValuesAction = setEnvironmentVariableValuesAction;
|
|
119
|
+
exports.setEnvironmentVariableValuesInputSchema = setEnvironmentVariableValuesInputSchema;
|
|
120
|
+
exports.setEnvironmentVariableValuesOutputSchema = setEnvironmentVariableValuesOutputSchema;
|
|
121
|
+
//# sourceMappingURL=set_environment_variable_values_action.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set_environment_variable_values_action.cjs.js","sources":["../../../src/actions/core/set_environment_variable_values_action.ts"],"sourcesContent":["import { createTemplateAction } from \"@backstage/plugin-scaffolder-node\";\nimport { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { Config } from \"@backstage/config\";\nimport { z } from \"zod\";\nimport { resolveTopologyId } from \"../../exporter/topologyResolve\";\nimport {\n formatApiFailure,\n getStackgenApiObjects,\n readClientError,\n} from \"../../utils/api-objects\";\n\nexport const setEnvironmentVariableValuesInputSchema = z\n .object({\n teamId: z.string().min(1, \"teamId is required\"),\n environmentId: z.string().min(1, \"environmentId is required\"),\n topologyId: z.string().optional(),\n appstackName: z.string().optional(),\n variableValues: z\n .array(\n z.object({\n name: z.string().min(1),\n value: z.string(),\n }),\n )\n .default([]),\n })\n .superRefine((value, ctx) => {\n if (!value.topologyId?.trim() && !value.appstackName?.trim()) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"topologyId or appstackName is required\",\n path: [\"topologyId\"],\n });\n }\n });\n\nexport const setEnvironmentVariableValuesOutputSchema = z.object({\n updatedCount: z.number(),\n updatedVariablesText: z.string(),\n environmentId: z.string(),\n topologyId: z.string(),\n});\n\nfunction parseVariableValue(raw: string): unknown {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n return \"\";\n }\n try {\n return JSON.parse(trimmed);\n } catch {\n return raw;\n }\n}\n\n/**\n * Applies VariableSelect values onto an existing environment profile\n * (consumption / override), matching appcd-ui TF env value updates.\n */\nexport const setEnvironmentVariableValuesAction = (\n config: Config,\n logger: LoggerService,\n) => {\n return createTemplateAction({\n id: \"stackGen:setEnvironmentVariableValues\",\n description:\n \"Sets Terraform variable values on an existing StackGen environment profile\",\n schema: {\n input: () => setEnvironmentVariableValuesInputSchema,\n output: () => setEnvironmentVariableValuesOutputSchema,\n },\n async handler(ctx) {\n const stackgenApis = getStackgenApiObjects(config);\n const teamId = ctx.input.teamId.trim();\n const environmentId = ctx.input.environmentId.trim();\n const variableValues = ctx.input.variableValues ?? [];\n\n let topologyId = ctx.input.topologyId?.trim() || \"\";\n if (!topologyId) {\n topologyId = await resolveTopologyId({\n config,\n teamId,\n appstackName: ctx.input.appstackName?.trim(),\n logger,\n });\n }\n\n if (variableValues.length === 0) {\n ctx.logger.info(\"No variable values provided; skipping updates\");\n ctx.output(\"updatedCount\", 0);\n ctx.output(\"updatedVariablesText\", \"No variables updated\");\n ctx.output(\"environmentId\", environmentId);\n ctx.output(\"topologyId\", topologyId);\n return;\n }\n\n const updated: string[] = [];\n for (const pair of variableValues) {\n const variableIdentifier = pair.name.trim();\n const value = parseVariableValue(pair.value);\n ctx.logger.info(\n `Setting ${variableIdentifier} on env ${environmentId} (topology=${topologyId})`,\n );\n try {\n await stackgenApis.v1TfBlocksApiObj.updateTFVariableEnvironmentProfileValue(\n {\n topologyId,\n orgId: teamId,\n variableIdentifier,\n envProfileId: environmentId,\n tFVariableEnvironmentProfileValue: {\n envProfileId: environmentId,\n value,\n } as never,\n },\n );\n updated.push(`${variableIdentifier} = ${pair.value}`);\n } catch (error) {\n const parsedError = await readClientError(error);\n const detail = formatApiFailure(\n parsedError.status,\n parsedError.body,\n parsedError.text,\n parsedError.requestId,\n );\n throw new Error(\n `Failed to set ${variableIdentifier} on environment ${environmentId}: ${detail}`,\n );\n }\n }\n\n logger.info(\n `stackGen:setEnvironmentVariableValues updated=${updated.length} env=${environmentId}`,\n );\n ctx.output(\"updatedCount\", updated.length);\n ctx.output(\"updatedVariablesText\", updated.join(\"\\n\"));\n ctx.output(\"environmentId\", environmentId);\n ctx.output(\"topologyId\", topologyId);\n },\n });\n};\n"],"names":["z","createTemplateAction","getStackgenApiObjects","resolveTopologyId","readClientError","formatApiFailure"],"mappings":"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"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var topologyResolve = require('../../exporter/topologyResolve.cjs.js');
|
|
6
|
+
var apiObjects = require('../../utils/api-objects.cjs.js');
|
|
7
|
+
|
|
8
|
+
const stateBackendConfigSchema = zod.z.record(zod.z.string(), zod.z.string());
|
|
9
|
+
const stateBackendSchema = zod.z.object({
|
|
10
|
+
type: zod.z.enum(["S3", "GCS", "AZURERM", "LOCAL", "POSTGRES", "REMOTE", "CLOUD"]),
|
|
11
|
+
config: stateBackendConfigSchema
|
|
12
|
+
});
|
|
13
|
+
const setStateBackendInputSchema = zod.z.object({
|
|
14
|
+
teamId: zod.z.string().min(1, "teamId is required"),
|
|
15
|
+
topologyId: zod.z.string().optional(),
|
|
16
|
+
appstackName: zod.z.string().optional(),
|
|
17
|
+
environmentName: zod.z.string().min(1, "environmentName is required"),
|
|
18
|
+
color: zod.z.string().optional().default("#000000"),
|
|
19
|
+
variables: zod.z.array(
|
|
20
|
+
zod.z.object({
|
|
21
|
+
name: zod.z.string(),
|
|
22
|
+
type: zod.z.string().optional().default("string"),
|
|
23
|
+
defaultValueJson: zod.z.string().optional(),
|
|
24
|
+
description: zod.z.string().optional()
|
|
25
|
+
})
|
|
26
|
+
).optional().default([]),
|
|
27
|
+
/**
|
|
28
|
+
* Convenience input from VariableSelect: [{ name, value }] → string variables.
|
|
29
|
+
* Merged with `variables` when both are set.
|
|
30
|
+
*/
|
|
31
|
+
variableValues: zod.z.array(
|
|
32
|
+
zod.z.object({
|
|
33
|
+
name: zod.z.string(),
|
|
34
|
+
value: zod.z.string()
|
|
35
|
+
})
|
|
36
|
+
).optional().default([]),
|
|
37
|
+
stateBackend: stateBackendSchema
|
|
38
|
+
}).superRefine((value, ctx) => {
|
|
39
|
+
if (!value.topologyId?.trim() && !value.appstackName?.trim()) {
|
|
40
|
+
ctx.addIssue({
|
|
41
|
+
code: zod.z.ZodIssueCode.custom,
|
|
42
|
+
message: "topologyId or appstackName is required",
|
|
43
|
+
path: ["topologyId"]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const setStateBackendOutputSchema = zod.z.object({
|
|
48
|
+
success: zod.z.boolean(),
|
|
49
|
+
environmentId: zod.z.string().optional()
|
|
50
|
+
});
|
|
51
|
+
const DEFAULT_PROJECT_MEMBER_ROLE_ID = "00000000-0000-0000-0000-000000000002";
|
|
52
|
+
async function ensureProjectMembership(stackgenApis, teamId, logger) {
|
|
53
|
+
logger.info("Resolving PAT user via appcd auth API");
|
|
54
|
+
const meBody = await stackgenApis.authenticationApiObject.getMe();
|
|
55
|
+
const userId = String(meBody?.userId || "").trim();
|
|
56
|
+
if (!userId) {
|
|
57
|
+
throw new Error("auth/me response did not include userId");
|
|
58
|
+
}
|
|
59
|
+
logger.info(`Resolved PAT user: ${userId}`);
|
|
60
|
+
try {
|
|
61
|
+
await stackgenApis.projectApiObject.assignUsersToOrg({
|
|
62
|
+
orgId: teamId,
|
|
63
|
+
assignUsersToOrgRequest: {
|
|
64
|
+
userAssignments: [
|
|
65
|
+
{ userId, roleId: DEFAULT_PROJECT_MEMBER_ROLE_ID }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const parsedError = await apiObjects.readClientError(error);
|
|
71
|
+
const detail = apiObjects.formatApiFailure(
|
|
72
|
+
parsedError.status,
|
|
73
|
+
parsedError.body,
|
|
74
|
+
parsedError.text,
|
|
75
|
+
parsedError.requestId
|
|
76
|
+
);
|
|
77
|
+
if (detail.toLowerCase().includes("already") || parsedError.text.toLowerCase().includes("already") || parsedError.status === 409) {
|
|
78
|
+
logger.info(`PAT user ${userId} already a member of project ${teamId}`);
|
|
79
|
+
return { userId, wasAdded: false };
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Failed to add PAT user to project: ${detail}`);
|
|
82
|
+
}
|
|
83
|
+
logger.info(`Added PAT user ${userId} to project ${teamId}`);
|
|
84
|
+
return { userId, wasAdded: true };
|
|
85
|
+
}
|
|
86
|
+
const setStateBackendAction = (config, logger) => {
|
|
87
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
88
|
+
id: "stackGen:setStateBackend",
|
|
89
|
+
description: "Creates an environment profile with a Terraform state backend (e.g. S3) on a StackGen topology",
|
|
90
|
+
schema: {
|
|
91
|
+
// Backstage parseSchemas only accepts (z) => ZodType or per-field callbacks.
|
|
92
|
+
input: () => setStateBackendInputSchema,
|
|
93
|
+
output: () => setStateBackendOutputSchema
|
|
94
|
+
},
|
|
95
|
+
async handler(ctx) {
|
|
96
|
+
const stackgenApis = apiObjects.getStackgenApiObjects(config);
|
|
97
|
+
const {
|
|
98
|
+
teamId,
|
|
99
|
+
topologyId: topologyIdInput,
|
|
100
|
+
appstackName,
|
|
101
|
+
environmentName,
|
|
102
|
+
color,
|
|
103
|
+
variables,
|
|
104
|
+
variableValues,
|
|
105
|
+
stateBackend
|
|
106
|
+
} = ctx.input;
|
|
107
|
+
let topologyId = topologyIdInput?.trim() || "";
|
|
108
|
+
if (!topologyId) {
|
|
109
|
+
topologyId = await topologyResolve.resolveTopologyId({
|
|
110
|
+
config,
|
|
111
|
+
teamId,
|
|
112
|
+
appstackName: appstackName?.trim()});
|
|
113
|
+
}
|
|
114
|
+
const fromSelect = (variableValues || []).map((pair) => ({
|
|
115
|
+
name: pair.name,
|
|
116
|
+
type: "string",
|
|
117
|
+
defaultValueJson: JSON.stringify(pair.value),
|
|
118
|
+
description: `Set via VariableSelect (${pair.name})`
|
|
119
|
+
}));
|
|
120
|
+
const mergedVariables = [...variables || [], ...fromSelect];
|
|
121
|
+
ctx.logger.info(`Ensuring PAT user has access to project ${teamId}`);
|
|
122
|
+
await ensureProjectMembership(stackgenApis, teamId, logger);
|
|
123
|
+
const payload = {
|
|
124
|
+
profileName: environmentName,
|
|
125
|
+
color,
|
|
126
|
+
topologyId,
|
|
127
|
+
variables: mergedVariables,
|
|
128
|
+
stateBackend: {
|
|
129
|
+
id: "",
|
|
130
|
+
type: stateBackend.type,
|
|
131
|
+
config: stateBackend.config,
|
|
132
|
+
editorMode: false
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
ctx.logger.info(
|
|
136
|
+
`Creating environment '${environmentName}' with ${stateBackend.type} state backend`
|
|
137
|
+
);
|
|
138
|
+
logger.info(
|
|
139
|
+
`stackGen:setStateBackend via generated client (env=${environmentName}, type=${stateBackend.type})`
|
|
140
|
+
);
|
|
141
|
+
try {
|
|
142
|
+
const body = await stackgenApis.v1EnvironmentProfileApiObj.createEnvProfile({
|
|
143
|
+
orgId: teamId,
|
|
144
|
+
envProfileRequest: payload
|
|
145
|
+
});
|
|
146
|
+
const envId = body?.id || body?.environmentId || "";
|
|
147
|
+
ctx.logger.info(
|
|
148
|
+
`\u2713 Environment '${environmentName}' created with ${stateBackend.type} state backend (ID: ${envId})`
|
|
149
|
+
);
|
|
150
|
+
ctx.output("success", true);
|
|
151
|
+
ctx.output("environmentId", envId);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const parsedError = await apiObjects.readClientError(error);
|
|
154
|
+
const detail = apiObjects.formatApiFailure(
|
|
155
|
+
parsedError.status,
|
|
156
|
+
parsedError.body,
|
|
157
|
+
parsedError.text,
|
|
158
|
+
parsedError.requestId
|
|
159
|
+
);
|
|
160
|
+
ctx.logger.error(
|
|
161
|
+
`Failed to create environment with state backend: ${detail}`
|
|
162
|
+
);
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Failed to create environment '${environmentName}' with state backend: ${detail}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
exports.setStateBackendAction = setStateBackendAction;
|
|
172
|
+
exports.setStateBackendInputSchema = setStateBackendInputSchema;
|
|
173
|
+
exports.setStateBackendOutputSchema = setStateBackendOutputSchema;
|
|
174
|
+
//# sourceMappingURL=set_state_backend_action.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set_state_backend_action.cjs.js","sources":["../../../src/actions/core/set_state_backend_action.ts"],"sourcesContent":["import { createTemplateAction } from \"@backstage/plugin-scaffolder-node\";\nimport { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { Config } from \"@backstage/config\";\nimport { z } from \"zod\";\nimport { resolveTopologyId } from \"../../exporter/topologyResolve\";\nimport {\n formatApiFailure,\n getStackgenApiObjects,\n readClientError,\n} from \"../../utils/api-objects\";\n\n// --- Schemas ---\n\nconst stateBackendConfigSchema = z.record(z.string(), z.string());\n\nconst stateBackendSchema = z.object({\n type: z.enum([\"S3\", \"GCS\", \"AZURERM\", \"LOCAL\", \"POSTGRES\", \"REMOTE\", \"CLOUD\"]),\n config: stateBackendConfigSchema,\n});\n\nexport const setStateBackendInputSchema = z\n .object({\n teamId: z.string().min(1, \"teamId is required\"),\n topologyId: z.string().optional(),\n appstackName: z.string().optional(),\n environmentName: z.string().min(1, \"environmentName is required\"),\n color: z.string().optional().default(\"#000000\"),\n variables: z\n .array(\n z.object({\n name: z.string(),\n type: z.string().optional().default(\"string\"),\n defaultValueJson: z.string().optional(),\n description: z.string().optional(),\n }),\n )\n .optional()\n .default([]),\n /**\n * Convenience input from VariableSelect: [{ name, value }] → string variables.\n * Merged with `variables` when both are set.\n */\n variableValues: z\n .array(\n z.object({\n name: z.string(),\n value: z.string(),\n }),\n )\n .optional()\n .default([]),\n stateBackend: stateBackendSchema,\n })\n .superRefine((value, ctx) => {\n if (!value.topologyId?.trim() && !value.appstackName?.trim()) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"topologyId or appstackName is required\",\n path: [\"topologyId\"],\n });\n }\n });\n\nexport const setStateBackendOutputSchema = z.object({\n success: z.boolean(),\n environmentId: z.string().optional(),\n});\n\n// --- Helpers ---\n\nconst DEFAULT_PROJECT_MEMBER_ROLE_ID = \"00000000-0000-0000-0000-000000000002\";\n\n/**\n * Resolves the PAT user and ensures they are a member of the project,\n * matching the pattern used in createAppStackAction.\n */\nasync function ensureProjectMembership(\n stackgenApis: ReturnType<typeof getStackgenApiObjects>,\n teamId: string,\n logger: LoggerService,\n): Promise<{ userId: string; wasAdded: boolean }> {\n // Step 1: Resolve PAT user\n logger.info(\"Resolving PAT user via appcd auth API\");\n const meBody = await stackgenApis.authenticationApiObject.getMe();\n const userId = String(meBody?.userId || \"\").trim();\n if (!userId) {\n throw new Error(\"auth/me response did not include userId\");\n }\n logger.info(`Resolved PAT user: ${userId}`);\n\n // Step 2: Add user to project\n try {\n await stackgenApis.projectApiObject.assignUsersToOrg({\n orgId: teamId,\n assignUsersToOrgRequest: {\n userAssignments: [\n { userId, roleId: DEFAULT_PROJECT_MEMBER_ROLE_ID },\n ],\n } as never,\n });\n } catch (error) {\n const parsedError = await readClientError(error);\n const detail = formatApiFailure(\n parsedError.status,\n parsedError.body,\n parsedError.text,\n parsedError.requestId,\n );\n // \"already a member\" is fine\n if (\n detail.toLowerCase().includes(\"already\") ||\n parsedError.text.toLowerCase().includes(\"already\") ||\n parsedError.status === 409\n ) {\n logger.info(`PAT user ${userId} already a member of project ${teamId}`);\n return { userId, wasAdded: false };\n }\n throw new Error(`Failed to add PAT user to project: ${detail}`);\n }\n\n logger.info(`Added PAT user ${userId} to project ${teamId}`);\n return { userId, wasAdded: true };\n}\n\n// --- Action ---\n// Creates an environment profile on a topology with a state backend configuration.\n// Mirrors the functional test flow:\n// 1. Resolve PAT user & add to project (for iac-gen API access)\n// 2. POST /iac-gen/v1/environment-profile?orgId={teamId}\n// body: { profileName, color, topologyId, variables, stateBackend }\n\nexport const setStateBackendAction = (config: Config, logger: LoggerService) => {\n return createTemplateAction({\n id: \"stackGen:setStateBackend\",\n description:\n \"Creates an environment profile with a Terraform state backend (e.g. S3) on a StackGen topology\",\n schema: {\n // Backstage parseSchemas only accepts (z) => ZodType or per-field callbacks.\n input: () => setStateBackendInputSchema,\n output: () => setStateBackendOutputSchema,\n },\n async handler(ctx) {\n const stackgenApis = getStackgenApiObjects(config);\n\n const {\n teamId,\n topologyId: topologyIdInput,\n appstackName,\n environmentName,\n color,\n variables,\n variableValues,\n stateBackend,\n } = ctx.input;\n\n let topologyId = topologyIdInput?.trim() || \"\";\n if (!topologyId) {\n topologyId = await resolveTopologyId({\n config,\n teamId,\n appstackName: appstackName?.trim(),\n logger,\n });\n }\n\n const fromSelect = (variableValues || []).map((pair) => ({\n name: pair.name,\n type: \"string\",\n defaultValueJson: JSON.stringify(pair.value),\n description: `Set via VariableSelect (${pair.name})`,\n }));\n const mergedVariables = [...(variables || []), ...fromSelect];\n\n // Step 0: Ensure PAT user has access to project\n ctx.logger.info(`Ensuring PAT user has access to project ${teamId}`);\n await ensureProjectMembership(stackgenApis, teamId, logger);\n\n // Step 1: POST to create environment profile with state backend\n const payload = {\n profileName: environmentName,\n color,\n topologyId,\n variables: mergedVariables,\n stateBackend: {\n id: \"\",\n type: stateBackend.type,\n config: stateBackend.config,\n editorMode: false,\n },\n };\n\n ctx.logger.info(\n `Creating environment '${environmentName}' with ${stateBackend.type} state backend`,\n );\n logger.info(\n `stackGen:setStateBackend via generated client (env=${environmentName}, type=${stateBackend.type})`,\n );\n\n try {\n const body = (await stackgenApis.v1EnvironmentProfileApiObj.createEnvProfile({\n orgId: teamId,\n envProfileRequest: payload as never,\n })) as { id?: string; environmentId?: string };\n const envId = body?.id || body?.environmentId || \"\";\n ctx.logger.info(\n `✓ Environment '${environmentName}' created with ${stateBackend.type} state backend (ID: ${envId})`,\n );\n ctx.output(\"success\", true);\n ctx.output(\"environmentId\", envId);\n } catch (error) {\n const parsedError = await readClientError(error);\n const detail = formatApiFailure(\n parsedError.status,\n parsedError.body,\n parsedError.text,\n parsedError.requestId,\n );\n ctx.logger.error(\n `Failed to create environment with state backend: ${detail}`,\n );\n throw new Error(\n `Failed to create environment '${environmentName}' with state backend: ${detail}`,\n );\n }\n },\n });\n};\n"],"names":["z","readClientError","formatApiFailure","createTemplateAction","getStackgenApiObjects","resolveTopologyId"],"mappings":"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"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var topologyResolve = require('../../exporter/topologyResolve.cjs.js');
|
|
6
|
+
var gitExporterConfigHttp = require('../../exporter/gitExporterConfigHttp.cjs.js');
|
|
7
|
+
var projectGitExporterTemplate = require('../../exporter/projectGitExporterTemplate.cjs.js');
|
|
8
|
+
var exportOverrides = require('../../exporter/exportOverrides.cjs.js');
|
|
9
|
+
var apiObjects = require('../../utils/api-objects.cjs.js');
|
|
10
|
+
|
|
11
|
+
const exportBootstrapGitConfigSchema = zod.z.object({
|
|
12
|
+
name: zod.z.string().optional(),
|
|
13
|
+
type: zod.z.string().optional(),
|
|
14
|
+
parameters: zod.z.object({
|
|
15
|
+
scmType: zod.z.string().min(1),
|
|
16
|
+
repoUrl: zod.z.string().min(1),
|
|
17
|
+
targetBranch: zod.z.string().optional(),
|
|
18
|
+
path: zod.z.string().optional()
|
|
19
|
+
}),
|
|
20
|
+
vaultReference: zod.z.string().min(1),
|
|
21
|
+
default: zod.z.boolean().optional()
|
|
22
|
+
});
|
|
23
|
+
const exportToGitInputSchema = zod.z.object({
|
|
24
|
+
appstackName: zod.z.string().min(1),
|
|
25
|
+
teamId: zod.z.string().min(1),
|
|
26
|
+
appstackId: zod.z.string().optional(),
|
|
27
|
+
topologyId: zod.z.string().optional(),
|
|
28
|
+
/**
|
|
29
|
+
* When set (non-empty), used as stack-exporter git `config_id`; skips listing and bootstrap.
|
|
30
|
+
* Precedence: configId overrides gitConfig.
|
|
31
|
+
*/
|
|
32
|
+
configId: zod.z.string().optional(),
|
|
33
|
+
/**
|
|
34
|
+
* Optional bootstrap override. If omitted and no usable appstack config exists, the action loads
|
|
35
|
+
* the project default git template and POSTs an appstack-scoped git config.
|
|
36
|
+
* Ignored when configId is set.
|
|
37
|
+
*/
|
|
38
|
+
gitConfig: exportBootstrapGitConfigSchema.optional(),
|
|
39
|
+
/**
|
|
40
|
+
* First-class git export controls (parity with the StackGen UI). Each maps to a
|
|
41
|
+
* stack-exporter override key via `buildExportOverrides`; explicit `overrides`
|
|
42
|
+
* entries for the same key win.
|
|
43
|
+
*/
|
|
44
|
+
// Branch to base the change on (PR base / commit destination) -> `target_branch`.
|
|
45
|
+
baseBranch: zod.z.string().optional(),
|
|
46
|
+
// Branch StackGen pushes changes to (source/head branch) -> `branch_format`.
|
|
47
|
+
pushBranch: zod.z.string().optional(),
|
|
48
|
+
/**
|
|
49
|
+
* When true, reuse the previous export push branch (`use_same_branch`).
|
|
50
|
+
* Set by ExporterPushBranch when the field was prefilled from history and left unchanged.
|
|
51
|
+
*/
|
|
52
|
+
useSameBranch: zod.z.boolean().optional(),
|
|
53
|
+
// When false, commit directly to the branch without opening a PR (`commit_only`). Defaults to creating a PR.
|
|
54
|
+
createPr: zod.z.boolean().optional(),
|
|
55
|
+
overrides: zod.z.array(
|
|
56
|
+
zod.z.object({
|
|
57
|
+
key: zod.z.string(),
|
|
58
|
+
value: zod.z.string()
|
|
59
|
+
})
|
|
60
|
+
).optional()
|
|
61
|
+
});
|
|
62
|
+
const exportToGitOutputSchema = zod.z.object({
|
|
63
|
+
// Optional because a commit-only export (createPr=false) does not open a pull request.
|
|
64
|
+
pullRequestUrl: zod.z.string().url().optional(),
|
|
65
|
+
externalLink: zod.z.string().url().optional(),
|
|
66
|
+
// Populated for commit-only exports: the branch StackGen committed the changes to.
|
|
67
|
+
committedBranch: zod.z.string().optional(),
|
|
68
|
+
// Human-readable notes from stack-exporter (e.g. "committed to branch X").
|
|
69
|
+
notes: zod.z.string().optional()
|
|
70
|
+
});
|
|
71
|
+
function normalizeConfig(raw) {
|
|
72
|
+
if (!raw || typeof raw !== "object") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
77
|
+
function chooseConfig(configs, appstackName) {
|
|
78
|
+
const gitOnly = configs.filter((c) => (c.type ?? "").toLowerCase() === "git");
|
|
79
|
+
const defaults = gitOnly.filter((c) => c.default);
|
|
80
|
+
if (defaults.length === 1) {
|
|
81
|
+
return defaults[0];
|
|
82
|
+
}
|
|
83
|
+
if (defaults.length > 1) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Multiple default git exporter configs found for "${appstackName}". Pass configId (ExporterGitConfigSelect) or gitConfig on stackGen:exportToGit, or reduce configs in stack-exporter.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (gitOnly.length === 1) {
|
|
89
|
+
return gitOnly[0];
|
|
90
|
+
}
|
|
91
|
+
if (gitOnly.length > 1) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Multiple git exporter configs found for "${appstackName}". Pass configId (ExporterGitConfigSelect) or gitConfig on stackGen:exportToGit, or mark one config as default in stack-exporter.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
function isCompleteGitConfig(gc) {
|
|
99
|
+
return Boolean(gc?.parameters?.scmType && gc?.parameters?.repoUrl && gc?.vaultReference);
|
|
100
|
+
}
|
|
101
|
+
function asExportResponse(raw) {
|
|
102
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
103
|
+
}
|
|
104
|
+
function parsePrUrl(raw) {
|
|
105
|
+
const data = asExportResponse(raw);
|
|
106
|
+
const direct = typeof data.externalLink === "string" ? data.externalLink.trim() : "";
|
|
107
|
+
const notes = typeof data.notes === "string" ? data.notes : "";
|
|
108
|
+
const fromNotes = notes.match(/https?:\/\/[^\s"',)>\]]+/i)?.[0] ?? "";
|
|
109
|
+
const url = (direct || fromNotes).trim();
|
|
110
|
+
return url || void 0;
|
|
111
|
+
}
|
|
112
|
+
function parseCommittedBranch(raw) {
|
|
113
|
+
const data = asExportResponse(raw);
|
|
114
|
+
const head = typeof data.metadata?.head === "string" ? data.metadata.head.trim() : "";
|
|
115
|
+
return head || void 0;
|
|
116
|
+
}
|
|
117
|
+
function parseNotes(raw) {
|
|
118
|
+
const data = asExportResponse(raw);
|
|
119
|
+
const notes = typeof data.notes === "string" ? data.notes.trim() : "";
|
|
120
|
+
return notes || void 0;
|
|
121
|
+
}
|
|
122
|
+
async function bootstrapGitConfig(params) {
|
|
123
|
+
const { config, teamId, appstackId, appstackName, gitConfig: gc, logger } = params;
|
|
124
|
+
let bootstrap;
|
|
125
|
+
if (isCompleteGitConfig(gc)) {
|
|
126
|
+
bootstrap = {
|
|
127
|
+
name: gc.name?.trim() || "default-git-config",
|
|
128
|
+
scmType: gc.parameters.scmType,
|
|
129
|
+
repoUrl: gc.parameters.repoUrl,
|
|
130
|
+
targetBranch: gc.parameters.targetBranch ?? "main",
|
|
131
|
+
path: gc.parameters.path ?? "/",
|
|
132
|
+
tokenRef: gc.vaultReference
|
|
133
|
+
};
|
|
134
|
+
} else {
|
|
135
|
+
const templateBody = await projectGitExporterTemplate.fetchProjectGitTemplates(config, teamId, logger);
|
|
136
|
+
bootstrap = projectGitExporterTemplate.chooseGitTemplateFromTemplatesResponse(templateBody);
|
|
137
|
+
}
|
|
138
|
+
if (!bootstrap) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`No git exporter config found for appstack "${appstackName}" and team "${teamId}", and no usable project git template (GET exporter /config/template). Ensure createProject included gitConfig, or pass gitConfig / configId on this action.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!appstackId?.trim()) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Cannot create git exporter config for "${appstackName}": appstackId is required so the config is bound to the AppStack.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const createdId = await gitExporterConfigHttp.createGitExporterConfiguration(
|
|
149
|
+
config,
|
|
150
|
+
{
|
|
151
|
+
orgId: teamId,
|
|
152
|
+
name: bootstrap.name,
|
|
153
|
+
appstackId,
|
|
154
|
+
scmType: bootstrap.scmType,
|
|
155
|
+
repoUrl: bootstrap.repoUrl,
|
|
156
|
+
targetBranch: bootstrap.targetBranch,
|
|
157
|
+
path: bootstrap.path,
|
|
158
|
+
secrets: { token_ref: bootstrap.tokenRef }
|
|
159
|
+
},
|
|
160
|
+
logger
|
|
161
|
+
);
|
|
162
|
+
const usedExplicitGitConfig = isCompleteGitConfig(gc);
|
|
163
|
+
logger.info(
|
|
164
|
+
`Created git exporter config "${createdId}" for appstack "${appstackId}" (${usedExplicitGitConfig ? "explicit gitConfig input" : "project default git template"})`
|
|
165
|
+
);
|
|
166
|
+
return createdId;
|
|
167
|
+
}
|
|
168
|
+
const exportAppStackToGitAction = (config, _logger) => {
|
|
169
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
170
|
+
id: "stackGen:exportToGit",
|
|
171
|
+
description: "Exports an AppStack via stack-exporter (resolve topology, select or bootstrap appstack-scoped git config, run export)",
|
|
172
|
+
schema: {
|
|
173
|
+
// Backstage parseSchemas only accepts (z) => ZodType or per-field callbacks.
|
|
174
|
+
input: () => exportToGitInputSchema,
|
|
175
|
+
output: () => exportToGitOutputSchema
|
|
176
|
+
},
|
|
177
|
+
async handler(ctx) {
|
|
178
|
+
try {
|
|
179
|
+
const { appstackName, teamId } = ctx.input;
|
|
180
|
+
const stackgenApis = apiObjects.getStackgenApiObjects(config);
|
|
181
|
+
const resolvedAppstackId = await topologyResolve.resolveExporterAppstackId({
|
|
182
|
+
config,
|
|
183
|
+
teamId,
|
|
184
|
+
appstackId: ctx.input.appstackId,
|
|
185
|
+
appstackName,
|
|
186
|
+
logger: ctx.logger
|
|
187
|
+
});
|
|
188
|
+
const topologyId = ctx.input.topologyId?.trim() || await topologyResolve.resolveTopologyId({
|
|
189
|
+
config,
|
|
190
|
+
teamId,
|
|
191
|
+
appstackId: ctx.input.appstackId,
|
|
192
|
+
appstackName,
|
|
193
|
+
logger: ctx.logger
|
|
194
|
+
});
|
|
195
|
+
if (!topologyId) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Could not resolve topologyId for appstack "${appstackName}" in team "${teamId}". Pass topologyId on stackGen:exportToGit or ensure the appstack exists in appcd /appstacks.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const explicitConfigId = ctx.input.configId?.trim();
|
|
201
|
+
let selectedConfigId = explicitConfigId || void 0;
|
|
202
|
+
if (!selectedConfigId && isCompleteGitConfig(ctx.input.gitConfig)) {
|
|
203
|
+
selectedConfigId = await bootstrapGitConfig({
|
|
204
|
+
config,
|
|
205
|
+
teamId,
|
|
206
|
+
appstackId: resolvedAppstackId,
|
|
207
|
+
appstackName,
|
|
208
|
+
gitConfig: ctx.input.gitConfig,
|
|
209
|
+
logger: ctx.logger
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (!selectedConfigId) {
|
|
213
|
+
const list = await gitExporterConfigHttp.listGitExporterConfigs(
|
|
214
|
+
config,
|
|
215
|
+
{
|
|
216
|
+
orgId: teamId,
|
|
217
|
+
type: "git",
|
|
218
|
+
appstackId: resolvedAppstackId
|
|
219
|
+
},
|
|
220
|
+
ctx.logger
|
|
221
|
+
);
|
|
222
|
+
const configs = list.map(normalizeConfig).filter((c) => Boolean(c?.id));
|
|
223
|
+
const selected = chooseConfig(configs, appstackName);
|
|
224
|
+
if (selected?.id) {
|
|
225
|
+
selectedConfigId = selected.id;
|
|
226
|
+
} else {
|
|
227
|
+
selectedConfigId = await bootstrapGitConfig({
|
|
228
|
+
config,
|
|
229
|
+
teamId,
|
|
230
|
+
appstackId: resolvedAppstackId,
|
|
231
|
+
appstackName,
|
|
232
|
+
gitConfig: ctx.input.gitConfig,
|
|
233
|
+
logger: ctx.logger
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const overrides = exportOverrides.buildExportOverrides({
|
|
238
|
+
baseBranch: ctx.input.baseBranch,
|
|
239
|
+
pushBranch: ctx.input.pushBranch,
|
|
240
|
+
useSameBranch: ctx.input.useSameBranch,
|
|
241
|
+
createPr: ctx.input.createPr,
|
|
242
|
+
overrides: ctx.input.overrides
|
|
243
|
+
});
|
|
244
|
+
const exportRes = await stackgenApis.exporterApiObj.exportIAC({
|
|
245
|
+
orgId: teamId,
|
|
246
|
+
exportRequest: {
|
|
247
|
+
topologyId,
|
|
248
|
+
appstackName,
|
|
249
|
+
configId: selectedConfigId,
|
|
250
|
+
overrides
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
const prUrl = parsePrUrl(exportRes);
|
|
254
|
+
const committedBranch = parseCommittedBranch(exportRes);
|
|
255
|
+
const notes = parseNotes(exportRes);
|
|
256
|
+
if (ctx.input.createPr === false) {
|
|
257
|
+
if (committedBranch) {
|
|
258
|
+
ctx.output("committedBranch", committedBranch);
|
|
259
|
+
}
|
|
260
|
+
if (notes) {
|
|
261
|
+
ctx.output("notes", notes);
|
|
262
|
+
}
|
|
263
|
+
if (prUrl) {
|
|
264
|
+
ctx.output("pullRequestUrl", prUrl);
|
|
265
|
+
ctx.output("externalLink", prUrl);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!prUrl) {
|
|
270
|
+
throw new Error("Export succeeded but no pull request URL was returned by stack-exporter");
|
|
271
|
+
}
|
|
272
|
+
ctx.output("pullRequestUrl", prUrl);
|
|
273
|
+
ctx.output("externalLink", prUrl);
|
|
274
|
+
if (committedBranch) {
|
|
275
|
+
ctx.output("committedBranch", committedBranch);
|
|
276
|
+
}
|
|
277
|
+
if (notes) {
|
|
278
|
+
ctx.output("notes", notes);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const detail = await apiObjects.formatClientErrorMessage(error);
|
|
282
|
+
ctx.logger.error(`Export to Git failed: ${detail}`);
|
|
283
|
+
throw new Error(`Export to Git failed: ${detail}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
exports.exportAppStackToGitAction = exportAppStackToGitAction;
|
|
290
|
+
//# sourceMappingURL=export_iac_to_git_action.cjs.js.map
|