@trayai/tray-sync-cli 1.0.7 → 1.0.9
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/dist/commands/promote.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readdir as fsReaddir, readFile as fsReadFile, rm as fsRm } from "node:fs/promises";
|
|
3
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
3
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
4
4
|
import { createTrayClientForRegion } from "../api/trayClient.js";
|
|
5
5
|
import { readCredentials } from "../config/credentials.js";
|
|
6
6
|
import { readEnvironments } from "../config/environments.js";
|
|
@@ -12,14 +12,16 @@ import { readTrayYaml } from "../config/trayYaml.js";
|
|
|
12
12
|
import { resolveContainedSafe } from "../lib/pathContainment.js";
|
|
13
13
|
import { entityDisplayName } from "../lib/exportStructure.js";
|
|
14
14
|
import { importProject, previewImport } from "../lib/promoteExecute.js";
|
|
15
|
-
import { toProjectImportImpact } from "../lib/promoteReport.js";
|
|
16
|
-
import { isFullyResolved, resolveProjectRequirements, scaffoldUnresolvedAuthentications, } from "../lib/promoteTarget.js";
|
|
15
|
+
import { summarizeImpactForVersionDescription, toProjectImportImpact, } from "../lib/promoteReport.js";
|
|
16
|
+
import { computeNextVersionNumber, createProjectVersion, getLatestProjectVersion, isFullyResolved, resolveProjectRequirements, scaffoldUnresolvedAuthentications, } from "../lib/promoteTarget.js";
|
|
17
17
|
import { resolveToken } from "../lib/tokenResolution.js";
|
|
18
18
|
import { isUuid } from "../lib/uuid.js";
|
|
19
19
|
import { OutputFormats, parseFormat } from "../lib/outputFormat.js";
|
|
20
|
+
import { parseVersionNumber } from "../lib/versionNumber.js";
|
|
20
21
|
import { checkProjectDrift, DriftStatuses } from "./status.js";
|
|
21
|
-
import { printError, printInvalidAuthMappings, printLocalDrift, printMutuallyExclusiveResumeAndProject, printNoTargetProjectMapped, printProjectError, printProjectImportImpact, printProjectNoStateFile, printProjectNotPulledLocally, printPromoted, printTargetEnvNotFound, printTrayYamlNotFound, printUnresolvedRequirements, printWouldBePromoted, } from "../views/promote/promote.v2.js";
|
|
22
|
+
import { printError, printInvalidAuthMappings, printLocalDrift, printMutuallyExclusiveMajorAndVersion, printMutuallyExclusiveResumeAndProject, printNoTargetProjectMapped, printProjectError, printProjectImportImpact, printProjectNoStateFile, printProjectNotPulledLocally, printPromoted, printTargetEnvNotFound, printTrayYamlNotFound, printUnresolvedRequirements, printVersionCreationFailed, printWouldBePromoted, } from "../views/promote/promote.v2.js";
|
|
22
23
|
import { printPromoteResultJson } from "../views/promote/shared.js";
|
|
24
|
+
const DEFAULT_VERSION_TITLE = "Promoted via tray-sync-cli";
|
|
23
25
|
function findProjectDir(existingDirs, projectId) {
|
|
24
26
|
return existingDirs.find((name) => name.split("--")[0] === projectId);
|
|
25
27
|
}
|
|
@@ -50,6 +52,12 @@ export async function runPromote(deps, options) {
|
|
|
50
52
|
printMutuallyExclusiveResumeAndProject(stderr);
|
|
51
53
|
return 1;
|
|
52
54
|
}
|
|
55
|
+
if (options.major && options.versionNumber) {
|
|
56
|
+
if (asJson)
|
|
57
|
+
return fail({ reason: "mutually_exclusive_major_and_version" });
|
|
58
|
+
printMutuallyExclusiveMajorAndVersion(stderr);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
53
61
|
const trayYaml = await readTrayYaml(cwd);
|
|
54
62
|
if (!trayYaml) {
|
|
55
63
|
if (asJson)
|
|
@@ -208,6 +216,34 @@ export async function runPromote(deps, options) {
|
|
|
208
216
|
continue;
|
|
209
217
|
}
|
|
210
218
|
await importProject(targetClient, targetProjectId, payload);
|
|
219
|
+
if (options.version ?? true) {
|
|
220
|
+
try {
|
|
221
|
+
const mode = options.major ? "major" : "minor";
|
|
222
|
+
const versionNumber = options.versionNumber ??
|
|
223
|
+
computeNextVersionNumber(await getLatestProjectVersion(targetClient, targetProjectId), mode);
|
|
224
|
+
const title = options.versionTitle ?? DEFAULT_VERSION_TITLE;
|
|
225
|
+
const description = options.versionDescription ?? summarizeImpactForVersionDescription(impact);
|
|
226
|
+
const created = await createProjectVersion(targetClient, targetProjectId, versionNumber, title, description);
|
|
227
|
+
impact.createdVersion = {
|
|
228
|
+
versionNumber: created.versionNumber,
|
|
229
|
+
title: created.title,
|
|
230
|
+
description: created.description,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (asJson) {
|
|
235
|
+
return fail({
|
|
236
|
+
reason: "version_creation_failed",
|
|
237
|
+
project_id: projectId,
|
|
238
|
+
source_project_name: sourceProjectName,
|
|
239
|
+
message: err.message,
|
|
240
|
+
target_project_id: targetProjectId,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
printVersionCreationFailed(stderr, projectId, sourceProjectName, err.message, targetProjectId);
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
211
247
|
if (!asJson) {
|
|
212
248
|
printPromoted(stdout, projectId, sourceProjectName, options.to);
|
|
213
249
|
printProjectImportImpact(stdout, impact);
|
|
@@ -252,6 +288,13 @@ export const promoteCommand = new Command("promote")
|
|
|
252
288
|
.option("--resume", "resume a previously interrupted promote")
|
|
253
289
|
.option("--dry-run", "report what would change without creating projects or importing")
|
|
254
290
|
.option("--auto-create-projects", "create the target project when no mapping exists yet")
|
|
291
|
+
.addOption(new Option("--no-version", "skip creating a target project version after import").helpGroup("Version options:"))
|
|
292
|
+
.addOption(new Option("--major", "bump the major version instead of minor").helpGroup("Version options:"))
|
|
293
|
+
.addOption(new Option("--version-number <major.minor>", "explicit target version number (overrides --major)")
|
|
294
|
+
.argParser(parseVersionNumber)
|
|
295
|
+
.helpGroup("Version options:"))
|
|
296
|
+
.addOption(new Option("--version-title <title>", "title for the created version").helpGroup("Version options:"))
|
|
297
|
+
.addOption(new Option("--version-description <description>", "description for the created version (auto-generated from promotion impact if omitted)").helpGroup("Version options:"))
|
|
255
298
|
.option("-t, --token <token>", "Tray API token (used for both source and target)")
|
|
256
299
|
.option("--format <format>", "human or json", parseFormat, OutputFormats.HUMAN)
|
|
257
300
|
.action(async (opts) => {
|
|
@@ -274,6 +317,11 @@ export const promoteCommand = new Command("promote")
|
|
|
274
317
|
dryRun: Boolean(opts.dryRun),
|
|
275
318
|
autoCreateProjects: Boolean(opts.autoCreateProjects),
|
|
276
319
|
format: opts.format,
|
|
320
|
+
version: opts.version,
|
|
321
|
+
major: Boolean(opts.major),
|
|
322
|
+
versionNumber: opts.versionNumber,
|
|
323
|
+
versionTitle: opts.versionTitle,
|
|
324
|
+
versionDescription: opts.versionDescription,
|
|
277
325
|
});
|
|
278
326
|
process.exitCode = exitCode;
|
|
279
327
|
});
|
|
@@ -20,4 +20,22 @@ export function toProjectImportImpact(projectName, projectId, impact) {
|
|
|
20
20
|
],
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
export function summarizeImpactForVersionDescription(impact) {
|
|
24
|
+
function countsByKind(entities) {
|
|
25
|
+
const counts = { created: 0, updated: 0, removed: 0 };
|
|
26
|
+
for (const entity of entities)
|
|
27
|
+
counts[entity.kind]++;
|
|
28
|
+
return Object.entries(counts)
|
|
29
|
+
.filter(([, count]) => count > 0)
|
|
30
|
+
.map(([kind, count]) => `${count} ${kind}`);
|
|
31
|
+
}
|
|
32
|
+
const parts = [];
|
|
33
|
+
const workflowCounts = countsByKind(impact.workflows);
|
|
34
|
+
if (workflowCounts.length > 0)
|
|
35
|
+
parts.push(`${workflowCounts.join(", ")} workflows`);
|
|
36
|
+
const configCounts = countsByKind(impact.config);
|
|
37
|
+
if (configCounts.length > 0)
|
|
38
|
+
parts.push(`${configCounts.join(", ")} config keys`);
|
|
39
|
+
return parts.length > 0 ? parts.join("; ") : "No changes detected";
|
|
40
|
+
}
|
|
23
41
|
//# sourceMappingURL=promoteReport.js.map
|
|
@@ -96,6 +96,30 @@ export function validateAuthenticationMappings(mapping) {
|
|
|
96
96
|
.filter(([, authenticationId]) => !isUuid(authenticationId))
|
|
97
97
|
.map(([authExportId, value]) => ({ authExportId, value }));
|
|
98
98
|
}
|
|
99
|
+
export async function getLatestProjectVersion(client, targetProjectId) {
|
|
100
|
+
const result = (await client.request("GET", `/v1/projects/${targetProjectId}/versions?last=1`));
|
|
101
|
+
const latest = result.elements?.[0];
|
|
102
|
+
if (!latest)
|
|
103
|
+
return null;
|
|
104
|
+
const [major, minor] = latest.versionNumber.split(".").map(Number);
|
|
105
|
+
return { major, minor };
|
|
106
|
+
}
|
|
107
|
+
export function computeNextVersionNumber(current, mode) {
|
|
108
|
+
if (!current)
|
|
109
|
+
return "1.0";
|
|
110
|
+
if (mode === "major")
|
|
111
|
+
return `${current.major + 1}.0`;
|
|
112
|
+
return `${current.major}.${current.minor + 1}`;
|
|
113
|
+
}
|
|
114
|
+
export async function createProjectVersion(client, targetProjectId, versionNumber, title, description) {
|
|
115
|
+
const result = (await client.request("POST", `/v1/projects/${targetProjectId}/versions/${versionNumber}`, { title, description }));
|
|
116
|
+
return {
|
|
117
|
+
versionNumber: result.versionNumber,
|
|
118
|
+
title: result.title,
|
|
119
|
+
description: result.description,
|
|
120
|
+
created: result.created,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
99
123
|
export async function resolveProjectRequirements(client, deps, options) {
|
|
100
124
|
const targetResolution = await resolveTargetProjectId(client, {
|
|
101
125
|
projectDirPath: options.projectDirPath,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { InvalidArgumentError } from "commander";
|
|
2
|
+
const VERSION_NUMBER_RE = /^\d+\.\d+$/;
|
|
3
|
+
export function isVersionNumber(value) {
|
|
4
|
+
return VERSION_NUMBER_RE.test(value);
|
|
5
|
+
}
|
|
6
|
+
export function parseVersionNumber(value) {
|
|
7
|
+
if (!isVersionNumber(value)) {
|
|
8
|
+
throw new InvalidArgumentError("version must be in major.minor format (e.g. 1.0)");
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=versionNumber.js.map
|
|
@@ -4,6 +4,9 @@ import { printIssueBlock } from "../shared.js";
|
|
|
4
4
|
export function printMutuallyExclusiveResumeAndProject(stderr) {
|
|
5
5
|
stderr(`${FAILURE_ICON} ${ansis.bold("--resume")} and ${ansis.bold("--project")} are mutually exclusive.`);
|
|
6
6
|
}
|
|
7
|
+
export function printMutuallyExclusiveMajorAndVersion(stderr) {
|
|
8
|
+
stderr(`${FAILURE_ICON} ${ansis.bold("--major")} and ${ansis.bold("--version-number")} are mutually exclusive.`);
|
|
9
|
+
}
|
|
7
10
|
export function printTrayYamlNotFound(stderr) {
|
|
8
11
|
stderr(`${FAILURE_ICON} tray.yaml not found. Run \`${ansis.bold("tray init")}\` first.`);
|
|
9
12
|
}
|
|
@@ -73,6 +76,12 @@ export function printProjectImportImpact(stdout, impact) {
|
|
|
73
76
|
stdout(` ${ansis.bold(c.kind)}: ${c.name}`);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
if (impact.createdVersion) {
|
|
80
|
+
stdout(` version: ${ansis.bold(impact.createdVersion.versionNumber)} — ${impact.createdVersion.title}`);
|
|
81
|
+
}
|
|
76
82
|
stdout("");
|
|
77
83
|
}
|
|
84
|
+
export function printVersionCreationFailed(stderr, projectId, sourceProjectName, message, targetProjectId) {
|
|
85
|
+
stderr(`${FAILURE_ICON} Project ${ansis.bold(`${projectId} (${sourceProjectName})`)} was promoted to target project ${targetProjectId}, but creating a version failed: ${message}`);
|
|
86
|
+
}
|
|
78
87
|
//# sourceMappingURL=promote.v2.js.map
|
package/package.json
CHANGED