@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.
Files changed (66) hide show
  1. package/README.md +485 -0
  2. package/config.d.ts +39 -0
  3. package/dist/actions/core/assign_project_members_action.cjs.js +227 -0
  4. package/dist/actions/core/assign_project_members_action.cjs.js.map +1 -0
  5. package/dist/actions/core/create_appstack_action.cjs.js +953 -0
  6. package/dist/actions/core/create_appstack_action.cjs.js.map +1 -0
  7. package/dist/actions/core/create_project_action.cjs.js +243 -0
  8. package/dist/actions/core/create_project_action.cjs.js.map +1 -0
  9. package/dist/actions/core/download_iac_action.cjs.js +133 -0
  10. package/dist/actions/core/download_iac_action.cjs.js.map +1 -0
  11. package/dist/actions/core/set_environment_variable_values_action.cjs.js +121 -0
  12. package/dist/actions/core/set_environment_variable_values_action.cjs.js.map +1 -0
  13. package/dist/actions/core/set_state_backend_action.cjs.js +174 -0
  14. package/dist/actions/core/set_state_backend_action.cjs.js.map +1 -0
  15. package/dist/actions/exporter/export_iac_to_git_action.cjs.js +290 -0
  16. package/dist/actions/exporter/export_iac_to_git_action.cjs.js.map +1 -0
  17. package/dist/actions/exporter/list_vault_secrets_action.cjs.js +39 -0
  18. package/dist/actions/exporter/list_vault_secrets_action.cjs.js.map +1 -0
  19. package/dist/config/constants.cjs.js +23 -0
  20. package/dist/config/constants.cjs.js.map +1 -0
  21. package/dist/exporter/exportOverrides.cjs.js +27 -0
  22. package/dist/exporter/exportOverrides.cjs.js.map +1 -0
  23. package/dist/exporter/gitExporterConfigHttp.cjs.js +105 -0
  24. package/dist/exporter/gitExporterConfigHttp.cjs.js.map +1 -0
  25. package/dist/exporter/http.cjs.js +14 -0
  26. package/dist/exporter/http.cjs.js.map +1 -0
  27. package/dist/exporter/paths.cjs.js +25 -0
  28. package/dist/exporter/paths.cjs.js.map +1 -0
  29. package/dist/exporter/projectGitExporterTemplate.cjs.js +62 -0
  30. package/dist/exporter/projectGitExporterTemplate.cjs.js.map +1 -0
  31. package/dist/exporter/topologyResolve.cjs.js +71 -0
  32. package/dist/exporter/topologyResolve.cjs.js.map +1 -0
  33. package/dist/index.cjs.js +31 -0
  34. package/dist/index.cjs.js.map +1 -0
  35. package/dist/index.d.ts +224 -0
  36. package/dist/lib/extractArchive.cjs.js +62 -0
  37. package/dist/lib/extractArchive.cjs.js.map +1 -0
  38. package/dist/modules/stackgenExporterScaffolderModule.cjs.js +29 -0
  39. package/dist/modules/stackgenExporterScaffolderModule.cjs.js.map +1 -0
  40. package/dist/modules/stackgenUiProxiesModule.cjs.js +366 -0
  41. package/dist/modules/stackgenUiProxiesModule.cjs.js.map +1 -0
  42. package/dist/plugin/plugin.cjs.js +136 -0
  43. package/dist/plugin/plugin.cjs.js.map +1 -0
  44. package/dist/plugin/router.cjs.js +341 -0
  45. package/dist/plugin/router.cjs.js.map +1 -0
  46. package/dist/services/MappingsService/mappingsService.cjs.js +149 -0
  47. package/dist/services/MappingsService/mappingsService.cjs.js.map +1 -0
  48. package/dist/services/StackgenService/stackgenService.cjs.js +267 -0
  49. package/dist/services/StackgenService/stackgenService.cjs.js.map +1 -0
  50. package/dist/utils/api-objects.cjs.js +205 -0
  51. package/dist/utils/api-objects.cjs.js.map +1 -0
  52. package/dist/vendor/stackgen-api-client/index.cjs.js +47681 -0
  53. package/dist/vendor/stackgen-api-client/index.cjs.js.map +1 -0
  54. package/migrations/20250131055302_init.js +24 -0
  55. package/migrations/20250305123456_alter_type_associations.js +23 -0
  56. package/package.json +86 -0
  57. package/templates/appstack-configure-environment.yaml +81 -0
  58. package/templates/appstack-from-template.yaml +76 -0
  59. package/templates/appstack-with-modules.yaml +69 -0
  60. package/templates/appstack-with-resources.yaml +81 -0
  61. package/templates/export-iac.yaml +97 -0
  62. package/templates/full-infra-setup.yaml +156 -0
  63. package/templates/git_export_exporter_multi_scm.yaml +123 -0
  64. package/templates/project-advanced.yaml +77 -0
  65. package/templates/project-basic.yaml +39 -0
  66. 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