@tailor-platform/sdk 1.62.0 → 1.65.0
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/CHANGELOG.md +69 -0
- package/dist/{actor-J2gJ0eK5.d.mts → actor-D_2aJjYO.d.mts} +2 -2
- package/dist/{application-BezXGbrU.mjs → application-76hhIhnJ.mjs} +42 -5
- package/dist/application-76hhIhnJ.mjs.map +1 -0
- package/dist/application-av2raLs6.mjs +4 -0
- package/dist/cli/index.mjs +505 -121
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lib.d.mts +24 -31
- package/dist/cli/lib.mjs +2 -2
- package/dist/configure/index.d.mts +5 -5
- package/dist/configure/index.mjs.map +1 -1
- package/dist/{index-CfRFkXIO.d.mts → index-Bhjep8cS.d.mts} +2 -2
- package/dist/{index-DUupuPhZ.d.mts → index-CZ2r3qiO.d.mts} +2 -2
- package/dist/{index-CPRnOjjt.d.mts → index-Db2RvnEH.d.mts} +2 -2
- package/dist/{index-CLxubakC.d.mts → index-DcXIjt9F.d.mts} +5 -5
- package/dist/{index-CQZVJ5SX.d.mts → index-QpC0TNbH.d.mts} +2 -2
- package/dist/plugin/builtin/enum-constants/index.d.mts +1 -1
- package/dist/plugin/builtin/file-utils/index.d.mts +1 -1
- package/dist/plugin/builtin/kysely-type/index.d.mts +1 -1
- package/dist/plugin/builtin/seed/index.d.mts +1 -1
- package/dist/plugin/index.d.mts +2 -2
- package/dist/{plugin-C_FyVSdl.d.mts → plugin-DylAsA4Z.d.mts} +2 -2
- package/dist/{runtime-C6o4hiYq.mjs → runtime-C7qTBDD2.mjs} +565 -99
- package/dist/runtime-C7qTBDD2.mjs.map +1 -0
- package/dist/{tailordb-BlBGmQK-.d.mts → tailordb-C-ar4XCX.d.mts} +4 -4
- package/dist/utils/test/index.d.mts +3 -3
- package/dist/{workflow.generated-Bf1tWylx.d.mts → workflow.generated-CCDsY0ce.d.mts} +42 -6
- package/docs/cli/auth.md +4 -4
- package/docs/cli/function.md +8 -8
- package/docs/cli/query.md +1 -1
- package/docs/cli/setup.md +18 -12
- package/docs/cli/workflow.md +10 -10
- package/docs/cli/workspace.md +14 -10
- package/docs/cli-reference.md +4 -4
- package/docs/github-actions.md +337 -0
- package/docs/services/auth.md +19 -0
- package/docs/services/idp.md +96 -0
- package/docs/services/tailordb-migration.md +17 -6
- package/package.json +12 -12
- package/dist/application-BezXGbrU.mjs.map +0 -1
- package/dist/application-DSXntqnV.mjs +0 -4
- package/dist/runtime-C6o4hiYq.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { Et as AuthInvokerSchema, L as CustomDomainStatus, Nt as PATScope, Q as FunctionExecution_Type, _ as userAgent, a as fetchAll, c as fetchPlatformMachineUserToken, d as initOAuth2Client, f as initOperatorClient, l as fetchUserInfo, r as closeConnectionPool, s as fetchPaged } from "../client-CobIRHl-.mjs";
|
|
4
4
|
import { t as assertDefined } from "../assert-CKfwrmCV.mjs";
|
|
5
5
|
import { n as logger, r as styles } from "../logger-DpJyJvNz.mjs";
|
|
6
|
-
import { $ as listCommand$10, $t as INITIAL_SCHEMA_NUMBER, An as
|
|
7
|
-
import { A as
|
|
6
|
+
import { $ as listCommand$10, $t as INITIAL_SCHEMA_NUMBER, An as commonArgs, At as startCommand, B as logBetaWarning, C as listCommand$13, Cn as PluginManager, Dt as jobsCommand, E as resumeCommand, En as apiCommand, F as writeDbTypesFile, Fn as pagedLogArgs, Gt as MIGRATION_LABEL_KEY, H as removeCommand$1, Ht as executeScript, I as getConfiguredEditorCommand, In as paginationArgs, Jt as compareSnapshotWithRemote, K as treeCommand, Kt as handleOptionalToRequiredError, L as openInConfiguredEditor, Ln as toPageDirection, Lt as functionExecutionStatusToString, Mn as confirmationArgs, Mt as getCommand$6, N as generateCommand$1, Nn as deploymentArgs, O as listCommand$12, On as assertWritable, P as generateMigrationScript, Pn as isVerbose, Pt as executionsCommand, Rn as workspaceArgs, Rt as formatKeyValueTable, Sn as sdkNameLabelKey, St as triggerCommand, T as healthCommand, Tn as prompt, U as updateCommand$3, Vt as deploy, Xt as protoGqlPermission, Y as getCommand$5, Yt as generateAllTypeManifestsFromSnapshot, Z as updateCommand$2, _n as formatMigrationDiff, an as createSnapshotFromLocalTypes, at as createCommand$3, b as createCommand$4, bn as ensureConfigId, c as listCommand$14, cn as getMigrationFilePath, dn as isValidMigrationNumber, f as restoreCommand, fn as loadDiff, ft as tokenCommand, g as getCommand$7, gt as listCommand$7, hn as parseMigrationNumberArg, ht as generate, i as updateCommand$4, j as truncateCommand, jn as configArg, kn as defineAppCommand, ln as getMigrationFiles, lt as getCommand$3, m as listCommand$15, mn as formatMigrationNumber, nn as assertValidMigrationFiles, o as removeCommand, on as getLatestMigrationNumber, pn as reconstructSnapshotFromMigrations, pt as listCommand$8, q as listCommand$11, qt as parseMigrationLabelNumber, r as queryCommand, rn as compareLocalTypesWithSnapshot, rt as deleteCommand$3, st as listCommand$9, t as isNativeTypeScriptRuntime, tt as getCommand$4, u as inviteCommand, v as deleteCommand$4, vn as hasChanges, vt as getCommand$2, wn as generateUserTypes, wt as listCommand$6, xn as resourceTrn, xt as webhookCommand, yn as getNamespacesWithMigrations, z as showCommand, zt as getCommand$1 } from "../runtime-C7qTBDD2.mjs";
|
|
7
|
+
import { A as readPlatformConfig, C as loadConfig, E as loadAccessToken, M as saveUserTokens, N as writePlatformConfig, O as loadMachineUserName, T as fetchLatestToken, _ as createLogLevelTreeshakeOptions, a as WorkflowJobSchema, b as getDistDir, c as INVOKER_EXPR, g as composeFunctionTreeshakeOptions, h as platformBundleDefinePlugin, i as resolveInlineSourcemap, j as resolveTokens, k as loadWorkspaceId, o as ResolverSchema, t as defineApplication, v as resolveBundleLogLevel, w as deleteUserTokens, x as hashContent$1 } from "../application-76hhIhnJ.mjs";
|
|
8
8
|
import { n as ExecutorSchema } from "../service-wI3Hvrgx.mjs";
|
|
9
9
|
import { t as multiline } from "../multiline-Cf9ODpr1.mjs";
|
|
10
10
|
import { r as isPluginGeneratedType } from "../seed-BH2FbrPV.mjs";
|
|
@@ -24,6 +24,7 @@ import { generateCodeVerifier } from "@badgateway/oauth2-client";
|
|
|
24
24
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
25
25
|
import { resolvePackageJSON, resolveTSConfig } from "pkg-types";
|
|
26
26
|
import * as crypto from "node:crypto";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
27
28
|
import * as http from "node:http";
|
|
28
29
|
import open from "open";
|
|
29
30
|
import * as rolldown from "rolldown";
|
|
@@ -1399,7 +1400,7 @@ const testRunCommand = defineAppCommand({
|
|
|
1399
1400
|
}),
|
|
1400
1401
|
"machine-user": arg(z.string().optional(), {
|
|
1401
1402
|
alias: "m",
|
|
1402
|
-
description: "Machine user name for authentication",
|
|
1403
|
+
description: "Machine user name for authentication. Falls back to the active profile's default machine user.",
|
|
1403
1404
|
env: "TAILOR_PLATFORM_MACHINE_USER_NAME"
|
|
1404
1405
|
}),
|
|
1405
1406
|
config: arg(z.string().default("tailor.config.ts"), {
|
|
@@ -1433,7 +1434,11 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file
|
|
|
1433
1434
|
if (!fs$1.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
1434
1435
|
const { config } = await loadConfig(args.config);
|
|
1435
1436
|
const authNamespace = resolveAuthNamespace(config.auth);
|
|
1436
|
-
const machineUserName = resolveMachineUserName(
|
|
1437
|
+
const machineUserName = await resolveMachineUserName({
|
|
1438
|
+
cliMachineUser: args["machine-user"],
|
|
1439
|
+
profile: args.profile,
|
|
1440
|
+
authConfig: config.auth
|
|
1441
|
+
});
|
|
1437
1442
|
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
1438
1443
|
const workspaceId = await loadWorkspaceId({
|
|
1439
1444
|
workspaceId: args["workspace-id"],
|
|
@@ -1541,13 +1546,18 @@ function resolveAuthNamespace(authConfig) {
|
|
|
1541
1546
|
throw new Error("Auth namespace is required. Ensure tailor.config.ts has an auth config.");
|
|
1542
1547
|
}
|
|
1543
1548
|
/**
|
|
1544
|
-
* Resolve machine user name from CLI args
|
|
1545
|
-
*
|
|
1546
|
-
* @param
|
|
1549
|
+
* Resolve machine user name from CLI args, profile default, or config.
|
|
1550
|
+
* Priority: --machine-user / env > profile default > first key of config.auth.machineUsers > error
|
|
1551
|
+
* @param options - Resolution options
|
|
1547
1552
|
* @returns Resolved machine user name
|
|
1548
1553
|
*/
|
|
1549
|
-
function resolveMachineUserName(
|
|
1550
|
-
|
|
1554
|
+
async function resolveMachineUserName(options) {
|
|
1555
|
+
const { cliMachineUser, profile, authConfig } = options;
|
|
1556
|
+
const resolved = await loadMachineUserName({
|
|
1557
|
+
machineUser: cliMachineUser,
|
|
1558
|
+
profile
|
|
1559
|
+
});
|
|
1560
|
+
if (resolved) return resolved;
|
|
1551
1561
|
if (authConfig && !("external" in authConfig && authConfig.external)) {
|
|
1552
1562
|
const machineUsers = authConfig.machineUsers;
|
|
1553
1563
|
if (machineUsers) {
|
|
@@ -1555,7 +1565,7 @@ function resolveMachineUserName(cliMachineUser, authConfig) {
|
|
|
1555
1565
|
if (keys.length > 0) return assertDefined(keys[0], "machine user key missing");
|
|
1556
1566
|
}
|
|
1557
1567
|
}
|
|
1558
|
-
throw new Error("Machine user is required. Provide --machine-user or ensure tailor.config.ts has machine users configured.");
|
|
1568
|
+
throw new Error("Machine user is required. Provide --machine-user, set TAILOR_PLATFORM_MACHINE_USER_NAME, set a profile default with 'tailor-sdk profile update <profile> --machine-user <name>', or ensure tailor.config.ts has machine users configured.");
|
|
1559
1569
|
}
|
|
1560
1570
|
/**
|
|
1561
1571
|
* Resolve full machine user info: name, id (from API), and attributes (from config).
|
|
@@ -1973,9 +1983,15 @@ const createCommand$2 = defineAppCommand({
|
|
|
1973
1983
|
alias: "w",
|
|
1974
1984
|
description: "Workspace ID"
|
|
1975
1985
|
}),
|
|
1976
|
-
permission: arg(z.enum(["write", "read"]).default("write"), { description: "Profile permission. 'read' blocks all write commands while the profile is active." })
|
|
1986
|
+
permission: arg(z.enum(["write", "read"]).default("write"), { description: "Profile permission. 'read' blocks all write commands while the profile is active." }),
|
|
1987
|
+
"machine-user": arg(z.string().optional(), {
|
|
1988
|
+
alias: "m",
|
|
1989
|
+
description: "Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token)."
|
|
1990
|
+
}),
|
|
1991
|
+
"machine-user-override": arg(z.enum(["allow", "deny"]).optional(), { description: "Whether the command line or TAILOR_PLATFORM_MACHINE_USER_NAME may override the profile's machine user. 'deny' requires --machine-user." })
|
|
1977
1992
|
}).strict(),
|
|
1978
1993
|
run: async (args) => {
|
|
1994
|
+
if (args["machine-user-override"] === "deny" && !args["machine-user"]) throw new Error("--machine-user-override deny requires --machine-user.");
|
|
1979
1995
|
const config = await readPlatformConfig();
|
|
1980
1996
|
if (config.profiles[args.name]) throw new Error(`Profile "${args.name}" already exists.`);
|
|
1981
1997
|
const client = await initOperatorClient(await fetchLatestToken(config, args.user));
|
|
@@ -1989,7 +2005,9 @@ const createCommand$2 = defineAppCommand({
|
|
|
1989
2005
|
config.profiles[args.name] = {
|
|
1990
2006
|
user: args.user,
|
|
1991
2007
|
workspace_id: args["workspace-id"],
|
|
1992
|
-
...args.permission === "read" ? { readonly: true } : {}
|
|
2008
|
+
...args.permission === "read" ? { readonly: true } : {},
|
|
2009
|
+
...args["machine-user"] ? { machine_user: args["machine-user"] } : {},
|
|
2010
|
+
...args["machine-user-override"] === "deny" ? { machine_user_override: "deny" } : {}
|
|
1993
2011
|
};
|
|
1994
2012
|
writePlatformConfig(config);
|
|
1995
2013
|
if (!args.json) logger.success(`Profile "${args.name}" created successfully.`);
|
|
@@ -1997,7 +2015,11 @@ const createCommand$2 = defineAppCommand({
|
|
|
1997
2015
|
name: args.name,
|
|
1998
2016
|
user: args.user,
|
|
1999
2017
|
workspaceId: args["workspace-id"],
|
|
2000
|
-
permission: args.permission
|
|
2018
|
+
permission: args.permission,
|
|
2019
|
+
...args["machine-user"] ? {
|
|
2020
|
+
machineUser: args["machine-user"],
|
|
2021
|
+
machineUserOverride: args["machine-user-override"] ?? "allow"
|
|
2022
|
+
} : {}
|
|
2001
2023
|
};
|
|
2002
2024
|
logger.out(profileInfo);
|
|
2003
2025
|
}
|
|
@@ -2045,7 +2067,11 @@ const listCommand$4 = defineAppCommand({
|
|
|
2045
2067
|
name,
|
|
2046
2068
|
user: p.user,
|
|
2047
2069
|
workspaceId: p.workspace_id,
|
|
2048
|
-
permission: p.readonly === true ? "read" : "write"
|
|
2070
|
+
permission: p.readonly === true ? "read" : "write",
|
|
2071
|
+
...p.machine_user ? {
|
|
2072
|
+
machineUser: p.machine_user,
|
|
2073
|
+
machineUserOverride: p.machine_user_override ?? "allow"
|
|
2074
|
+
} : {}
|
|
2049
2075
|
};
|
|
2050
2076
|
});
|
|
2051
2077
|
logger.out(profileInfos);
|
|
@@ -2070,17 +2096,28 @@ const updateCommand$1 = defineAppCommand({
|
|
|
2070
2096
|
alias: "w",
|
|
2071
2097
|
description: "New workspace ID"
|
|
2072
2098
|
}),
|
|
2073
|
-
permission: arg(z.enum(["write", "read"]).optional(), { description: "Profile permission. 'read' blocks all write commands; 'write' lifts the restriction." })
|
|
2099
|
+
permission: arg(z.enum(["write", "read"]).optional(), { description: "Profile permission. 'read' blocks all write commands; 'write' lifts the restriction." }),
|
|
2100
|
+
"machine-user": arg(z.string().optional(), {
|
|
2101
|
+
alias: "m",
|
|
2102
|
+
description: "Default machine user name for application-data commands (query, workflow start, function test-run, machineuser token). Pass an empty string to clear."
|
|
2103
|
+
}),
|
|
2104
|
+
"machine-user-override": arg(z.enum(["allow", "deny"]).optional(), { description: "Whether the command line or TAILOR_PLATFORM_MACHINE_USER_NAME may override the profile's machine user. 'deny' requires --machine-user; 'allow' lifts the restriction." })
|
|
2074
2105
|
}).strict(),
|
|
2075
2106
|
run: async (args) => {
|
|
2076
2107
|
const config = await readPlatformConfig();
|
|
2077
2108
|
const profile = config.profiles[args.name];
|
|
2078
2109
|
if (!profile) throw new Error(`Profile "${args.name}" not found.`);
|
|
2079
|
-
if (!args.user && !args["workspace-id"] && args.permission === void 0) throw new Error("Please provide at least one property to update.");
|
|
2110
|
+
if (!args.user && !args["workspace-id"] && args.permission === void 0 && args["machine-user"] === void 0 && args["machine-user-override"] === void 0) throw new Error("Please provide at least one property to update.");
|
|
2080
2111
|
const oldUser = profile.user;
|
|
2081
2112
|
const newUser = args.user || oldUser;
|
|
2082
2113
|
const oldWorkspaceId = profile.workspace_id;
|
|
2083
2114
|
const newWorkspaceId = args["workspace-id"] || oldWorkspaceId;
|
|
2115
|
+
const finalMachineUser = args["machine-user"] === "" ? void 0 : args["machine-user"] ?? profile.machine_user;
|
|
2116
|
+
const finalOverride = args["machine-user-override"] === "allow" ? void 0 : args["machine-user-override"] ?? profile.machine_user_override;
|
|
2117
|
+
if ((args["machine-user"] !== void 0 || args["machine-user-override"] !== void 0) && finalOverride === "deny" && !finalMachineUser) {
|
|
2118
|
+
if (args["machine-user-override"] === "deny") throw new Error("--machine-user-override deny requires --machine-user.");
|
|
2119
|
+
throw new Error(`Cannot clear the machine user while machine-user-override is "deny". Also pass --machine-user-override allow.`);
|
|
2120
|
+
}
|
|
2084
2121
|
if (args.user !== void 0 || args["workspace-id"] !== void 0) {
|
|
2085
2122
|
const client = await initOperatorClient(await fetchLatestToken(config, newUser));
|
|
2086
2123
|
if (!(await fetchAll(async (pageToken, maxPageSize) => {
|
|
@@ -2095,13 +2132,21 @@ const updateCommand$1 = defineAppCommand({
|
|
|
2095
2132
|
profile.workspace_id = newWorkspaceId;
|
|
2096
2133
|
if (args.permission === "read") profile.readonly = true;
|
|
2097
2134
|
else if (args.permission === "write") delete profile.readonly;
|
|
2135
|
+
if (args["machine-user"] !== void 0) if (args["machine-user"] === "") delete profile.machine_user;
|
|
2136
|
+
else profile.machine_user = args["machine-user"];
|
|
2137
|
+
if (args["machine-user-override"] === "deny") profile.machine_user_override = "deny";
|
|
2138
|
+
else if (args["machine-user-override"] === "allow") delete profile.machine_user_override;
|
|
2098
2139
|
writePlatformConfig(config);
|
|
2099
2140
|
if (!args.json) logger.success(`Profile "${args.name}" updated successfully`);
|
|
2100
2141
|
const profileInfo = {
|
|
2101
2142
|
name: args.name,
|
|
2102
2143
|
user: newUser,
|
|
2103
2144
|
workspaceId: newWorkspaceId,
|
|
2104
|
-
permission: profile.readonly === true ? "read" : "write"
|
|
2145
|
+
permission: profile.readonly === true ? "read" : "write",
|
|
2146
|
+
...profile.machine_user ? {
|
|
2147
|
+
machineUser: profile.machine_user,
|
|
2148
|
+
machineUserOverride: profile.machine_user_override ?? "allow"
|
|
2149
|
+
} : {}
|
|
2105
2150
|
};
|
|
2106
2151
|
logger.out(profileInfo);
|
|
2107
2152
|
}
|
|
@@ -2564,33 +2609,159 @@ const secretCommand = defineCommand({
|
|
|
2564
2609
|
});
|
|
2565
2610
|
|
|
2566
2611
|
//#endregion
|
|
2567
|
-
//#region src/cli/commands/setup/github/
|
|
2568
|
-
|
|
2612
|
+
//#region src/cli/commands/setup/github/git.ts
|
|
2613
|
+
const defaultGitRunner = (args, cwd) => {
|
|
2614
|
+
const result = spawnSync("git", args, {
|
|
2615
|
+
cwd,
|
|
2616
|
+
encoding: "utf-8"
|
|
2617
|
+
});
|
|
2618
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return null;
|
|
2619
|
+
return result.stdout.trim();
|
|
2620
|
+
};
|
|
2621
|
+
/**
|
|
2622
|
+
* Detect the remote default branch by reading `refs/remotes/origin/HEAD`.
|
|
2623
|
+
*
|
|
2624
|
+
* Throws an AI-first error (with a remediation hint) when the symbolic ref is
|
|
2625
|
+
* not set, so the caller can surface a clear next step instead of a silent
|
|
2626
|
+
* fallback.
|
|
2627
|
+
* @param cwd - Repository directory to inspect
|
|
2628
|
+
* @param run - Git runner, injectable for testing
|
|
2629
|
+
* @returns The default branch name (e.g. `main`)
|
|
2630
|
+
*/
|
|
2631
|
+
function detectDefaultBranch(cwd, run = defaultGitRunner) {
|
|
2632
|
+
const ref = run([
|
|
2633
|
+
"symbolic-ref",
|
|
2634
|
+
"--short",
|
|
2635
|
+
"refs/remotes/origin/HEAD"
|
|
2636
|
+
], cwd);
|
|
2637
|
+
const branch = ref?.startsWith("origin/") ? ref.slice(7) : ref;
|
|
2638
|
+
if (!branch) throw new Error("Could not detect the default branch from git. Pass --branch <name>, or run 'git remote set-head origin --auto' to record it.");
|
|
2639
|
+
return branch;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
//#endregion
|
|
2643
|
+
//#region src/cli/commands/setup/github/lock.ts
|
|
2644
|
+
/** Current lock schema version. Bumped only on breaking lock-format changes. */
|
|
2645
|
+
const LOCK_VERSION = 1;
|
|
2646
|
+
/** Lock file path, relative to the repository root. */
|
|
2647
|
+
const LOCK_FILENAME = ".github/tailor-sdk.lock";
|
|
2648
|
+
/**
|
|
2649
|
+
* Compute the lock content hash for a rendered workflow file.
|
|
2650
|
+
* @param content - File content to hash
|
|
2651
|
+
* @returns `sha256:<hex>` digest string
|
|
2652
|
+
*/
|
|
2653
|
+
function hashContent(content) {
|
|
2654
|
+
return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex")}`;
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Resolve the absolute lock file path for an output directory.
|
|
2658
|
+
* @param outputDir - Repository root where `.github` lives
|
|
2659
|
+
* @returns Absolute path to the lock file
|
|
2660
|
+
*/
|
|
2661
|
+
function lockPath(outputDir) {
|
|
2662
|
+
return path.join(outputDir, LOCK_FILENAME);
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Read and validate the lock file from disk.
|
|
2666
|
+
*
|
|
2667
|
+
* Returns null when no lock exists. Throws when the lock was written by a
|
|
2668
|
+
* newer SDK (forward-compatibility guard).
|
|
2669
|
+
* @param outputDir - Repository root where `.github` lives
|
|
2670
|
+
* @returns Parsed lock file, or null when absent
|
|
2671
|
+
*/
|
|
2672
|
+
function readLock(outputDir) {
|
|
2673
|
+
const file = lockPath(outputDir);
|
|
2674
|
+
if (!fs$1.existsSync(file)) return null;
|
|
2675
|
+
let parsed;
|
|
2676
|
+
try {
|
|
2677
|
+
parsed = JSON.parse(fs$1.readFileSync(file, "utf-8"));
|
|
2678
|
+
} catch (cause) {
|
|
2679
|
+
throw new Error(`${LOCK_FILENAME} is not valid JSON. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.`, { cause });
|
|
2680
|
+
}
|
|
2681
|
+
if (typeof parsed.version !== "number") throw new Error(`${LOCK_FILENAME} has no valid 'version' field. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.`);
|
|
2682
|
+
if (parsed.version > 1) throw new Error(`${LOCK_FILENAME} was written by a newer SDK (lock version ${String(parsed.version)}). Update @tailor-platform/sdk to continue (e.g. pnpm update @tailor-platform/sdk).`);
|
|
2683
|
+
if (!Array.isArray(parsed.targets)) throw new Error(`${LOCK_FILENAME} has no valid 'targets' array. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.`);
|
|
2684
|
+
return parsed;
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Write the lock file to disk (2-space JSON, trailing newline).
|
|
2688
|
+
* @param outputDir - Repository root where `.github` lives
|
|
2689
|
+
* @param lock - Lock file contents to serialize
|
|
2690
|
+
*/
|
|
2691
|
+
function writeLock(outputDir, lock) {
|
|
2692
|
+
const file = lockPath(outputDir);
|
|
2693
|
+
fs$1.mkdirSync(path.dirname(file), { recursive: true });
|
|
2694
|
+
fs$1.writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Find a lock target by identity. Targets are identified by (kind,
|
|
2698
|
+
* workspaceName); the full trigger/path cross-check is deferred to P2.
|
|
2699
|
+
* @param lock - Lock file to search, or null
|
|
2700
|
+
* @param kind - Target kind
|
|
2701
|
+
* @param workspaceName - Workspace name
|
|
2702
|
+
* @returns Matching target, or undefined
|
|
2703
|
+
*/
|
|
2704
|
+
function findTarget(lock, kind, workspaceName) {
|
|
2705
|
+
return lock?.targets.find((t) => t.kind === kind && t.workspaceName === workspaceName);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
//#endregion
|
|
2709
|
+
//#region src/cli/commands/setup/github/branch.workflow.yml
|
|
2710
|
+
var branch_workflow_default = "# __HEADER__\nname: Tailor (__WORKSPACE_NAME__)\n\non:\n # __PULL_REQUEST_START__\n pull_request:\n branches: [\"__BRANCH__\"]\n # __PATHS__\n # __PULL_REQUEST_END__\n push:\n branches: [\"__BRANCH__\"]\n # __PATHS__\n workflow_dispatch:\n # __DISPATCH_INPUTS_START__\n inputs:\n dry-run:\n description: Preview changes without deploying\n type: boolean\n default: false\n # __DISPATCH_INPUTS_END__\n\npermissions:\n contents: read\n\njobs:\n # __PLAN_JOB_START__\n tailor-plan:\n if: >-\n github.event_name == 'pull_request' ||\n (github.event_name == 'workflow_dispatch' && inputs['dry-run'])\n runs-on: ubuntu-latest\n timeout-minutes: 30\n environment: __ENVIRONMENT__\n permissions:\n contents: read\n pull-requests: write\n concurrency:\n group: tailor-plan-__WORKSPACE_NAME__-${{ github.event.pull_request.number || github.run_id }}\n cancel-in-progress: true\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-generate\n # __WORKING_DIRECTORY__\n run: __PM_EXEC__ tailor-sdk generate\n - id: tailor-generate-check\n run: |\n git add -A\n if ! git diff --cached --quiet; then\n git --no-pager diff --cached --stat\n echo \"::error::Generated files are out of date. Run 'tailor-sdk generate' locally and commit the result.\"\n exit 1\n fi\n - id: tailor-plan\n # Fork PRs cannot read secrets; the checks above still run for them.\n if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork\n uses: tailor-platform/actions/plan@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n label: __WORKSPACE_NAME__\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n github-token: ${{ secrets.GITHUB_TOKEN }}\n # __PLAN_JOB_END__\n tailor-deploy:\n # __DEPLOY_IF__\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n contents: read\n environment: __ENVIRONMENT__\n concurrency:\n group: tailor-deploy-__WORKSPACE_NAME__\n cancel-in-progress: false\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n";
|
|
2569
2711
|
|
|
2570
2712
|
//#endregion
|
|
2571
2713
|
//#region src/cli/commands/setup/github/setup-bun.yml
|
|
2572
|
-
var setup_bun_default = "- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0\n- run: bun install --frozen-lockfile\n";
|
|
2714
|
+
var setup_bun_default = "- id: tailor-setup-bun\n uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0\n- id: tailor-install\n run: bun install --frozen-lockfile\n";
|
|
2573
2715
|
|
|
2574
2716
|
//#endregion
|
|
2575
2717
|
//#region src/cli/commands/setup/github/setup-npm.yml
|
|
2576
|
-
var setup_npm_default = "- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: npm\n- run: npm ci\n";
|
|
2718
|
+
var setup_npm_default = "- id: tailor-setup-node\n uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: npm\n- id: tailor-install\n run: npm ci\n";
|
|
2577
2719
|
|
|
2578
2720
|
//#endregion
|
|
2579
2721
|
//#region src/cli/commands/setup/github/setup-pnpm.yml
|
|
2580
|
-
var setup_pnpm_default = "- uses: pnpm/action-setup@
|
|
2722
|
+
var setup_pnpm_default = "- id: tailor-setup-pnpm\n uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9\n- id: tailor-setup-node\n uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: pnpm\n- id: tailor-install\n run: pnpm install --frozen-lockfile\n";
|
|
2581
2723
|
|
|
2582
2724
|
//#endregion
|
|
2583
2725
|
//#region src/cli/commands/setup/github/setup-yarn.yml
|
|
2584
|
-
var setup_yarn_default = "- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: yarn\n- run: yarn install --frozen-lockfile\n";
|
|
2726
|
+
var setup_yarn_default = "- id: tailor-setup-node\n uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: yarn\n- id: tailor-install\n run: yarn install --frozen-lockfile\n";
|
|
2727
|
+
|
|
2728
|
+
//#endregion
|
|
2729
|
+
//#region src/cli/commands/setup/github/tag.workflow.yml
|
|
2730
|
+
var tag_workflow_default = "# __HEADER__\nname: Tailor (__WORKSPACE_NAME__)\n\non:\n push:\n tags: [\"__TAG_PATTERN__\"]\n workflow_dispatch:\n inputs:\n dry-run:\n description: Preview changes without deploying\n type: boolean\n default: false\n\npermissions:\n contents: read\n\njobs:\n # __TAG_GUARD_JOB_START__\n tailor-tag-guard:\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n outputs:\n on-branch: ${{ steps.tailor-tag-guard.outputs.on-branch }}\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n env:\n TARGET_BRANCH: \"__BRANCH__\"\n run: |\n git fetch origin \"$TARGET_BRANCH\"\n if git merge-base --is-ancestor \"$GITHUB_SHA\" \"origin/$TARGET_BRANCH\"; then\n echo \"on-branch=true\" >> \"$GITHUB_OUTPUT\"\n else\n # A tag outside the target branch is not an error — just skip.\n echo \"on-branch=false\" >> \"$GITHUB_OUTPUT\"\n echo \"::notice::Tag $GITHUB_REF_NAME is not reachable from $TARGET_BRANCH; skipping deploy.\"\n fi\n # __TAG_GUARD_JOB_END__\n tailor-plan:\n # __PLAN_NEEDS__\n # __PLAN_IF__\n runs-on: ubuntu-latest\n timeout-minutes: 30\n environment: __ENVIRONMENT__\n permissions:\n contents: read\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-generate\n # __WORKING_DIRECTORY__\n run: __PM_EXEC__ tailor-sdk generate\n - id: tailor-generate-check\n run: |\n git add -A\n if ! git diff --cached --quiet; then\n git --no-pager diff --cached --stat\n echo \"::error::Generated files are out of date. Run 'tailor-sdk generate' locally and commit the result.\"\n exit 1\n fi\n - id: tailor-plan\n uses: tailor-platform/actions/plan@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n label: __WORKSPACE_NAME__\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n\n tailor-deploy:\n needs: tailor-plan\n if: ${{ !(github.event_name == 'workflow_dispatch' && inputs['dry-run']) }}\n environment: __ENVIRONMENT__\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n contents: read\n concurrency:\n group: tailor-deploy-__WORKSPACE_NAME__\n cancel-in-progress: false\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n";
|
|
2585
2731
|
|
|
2586
2732
|
//#endregion
|
|
2587
|
-
//#region src/cli/commands/setup/github/
|
|
2733
|
+
//#region src/cli/commands/setup/github/templates.ts
|
|
2734
|
+
const setupStepIds = {
|
|
2735
|
+
pnpm: [
|
|
2736
|
+
"tailor-setup-pnpm",
|
|
2737
|
+
"tailor-setup-node",
|
|
2738
|
+
"tailor-install"
|
|
2739
|
+
],
|
|
2740
|
+
yarn: ["tailor-setup-node", "tailor-install"],
|
|
2741
|
+
npm: ["tailor-setup-node", "tailor-install"],
|
|
2742
|
+
bun: ["tailor-setup-bun", "tailor-install"]
|
|
2743
|
+
};
|
|
2588
2744
|
const setupSteps = {
|
|
2589
2745
|
pnpm: setup_pnpm_default,
|
|
2590
2746
|
yarn: setup_yarn_default,
|
|
2591
2747
|
npm: setup_npm_default,
|
|
2592
2748
|
bun: setup_bun_default
|
|
2593
2749
|
};
|
|
2750
|
+
const execPrefix = {
|
|
2751
|
+
npm: "npx",
|
|
2752
|
+
pnpm: "pnpm exec",
|
|
2753
|
+
yarn: "yarn",
|
|
2754
|
+
bun: "bunx"
|
|
2755
|
+
};
|
|
2756
|
+
const HEADER = `# Generated by \`tailor-sdk setup github\` — managed by the Tailor SDK.
|
|
2757
|
+
#
|
|
2758
|
+
# - Jobs and steps whose id starts with \`tailor-\` are managed by the SDK.
|
|
2759
|
+
# Do not edit or rename them.
|
|
2760
|
+
# - State is tracked in .github/tailor-sdk.lock (machine-owned: commit it, never edit it).
|
|
2761
|
+
# - Re-running \`tailor-sdk setup github\` regenerates this file. If you have
|
|
2762
|
+
# edited it by hand, regeneration stops and asks for --force (which discards
|
|
2763
|
+
# your edits), so prefer keeping customizations in your own jobs/steps and
|
|
2764
|
+
# re-running setup after SDK updates.`;
|
|
2594
2765
|
function indentSnippet(snippet, spaces) {
|
|
2595
2766
|
const indent = " ".repeat(spaces);
|
|
2596
2767
|
return snippet.trimEnd().split("\n").map((line) => line ? indent + line : line).join("\n");
|
|
@@ -2607,139 +2778,352 @@ function detectPackageManager(dir) {
|
|
|
2607
2778
|
if (fs$1.existsSync(path.join(dir, "package-lock.json"))) return "npm";
|
|
2608
2779
|
return "npm";
|
|
2609
2780
|
}
|
|
2781
|
+
function block(content, name, keep) {
|
|
2782
|
+
if (keep) return content.replace(new RegExp(`^ *# __${name}_(?:START|END)__\\n`, "gm"), "");
|
|
2783
|
+
return content.replace(new RegExp(`^ *# __${name}_START__\\n[\\s\\S]*?^ *# __${name}_END__\\n`, "m"), "");
|
|
2784
|
+
}
|
|
2785
|
+
function line(content, name, replacement) {
|
|
2786
|
+
const re = new RegExp(`^([ \\t]*)# __${name}__\\n`, "gm");
|
|
2787
|
+
return content.replace(re, (_match, indent) => {
|
|
2788
|
+
if (replacement === void 0) return "";
|
|
2789
|
+
return replacement.split("\n").map((l) => l ? indent + l : l).join("\n") + "\n";
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
function applyCommon(content, params) {
|
|
2793
|
+
const { workingDirectory, environment, packageManager } = params;
|
|
2794
|
+
let out = line(content, "HEADER", HEADER);
|
|
2795
|
+
out = line(out, "WORKING_DIRECTORY", workingDirectory ? `working-directory: ${workingDirectory}` : void 0);
|
|
2796
|
+
out = out.replace(/^ *# __SETUP_STEPS__$/gm, () => indentSnippet(setupSteps[packageManager], 6));
|
|
2797
|
+
return out.replaceAll("__WORKSPACE_NAME__", () => params.workspaceName).replaceAll("__ENVIRONMENT__", () => environment).replaceAll("__PM_EXEC__", () => execPrefix[packageManager]);
|
|
2798
|
+
}
|
|
2799
|
+
function setupIds(job, packageManager) {
|
|
2800
|
+
return setupStepIds[packageManager].map((id) => `${job}/${id}`);
|
|
2801
|
+
}
|
|
2610
2802
|
/**
|
|
2611
|
-
* Render the deploy workflow
|
|
2803
|
+
* Render the branch-target deploy workflow.
|
|
2612
2804
|
*
|
|
2613
|
-
*
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
2805
|
+
* When `plan` is false the plan job, the pull_request trigger, and the
|
|
2806
|
+
* workflow_dispatch dry-run input are all removed (a plan-less workflow has no
|
|
2807
|
+
* meaningful dry-run), and the deploy job runs unconditionally.
|
|
2808
|
+
* @param params - Workspace and rendering configuration
|
|
2809
|
+
* @returns Rendered YAML and the list of managed job/step ids
|
|
2810
|
+
*/
|
|
2811
|
+
function renderBranchWorkflow(params) {
|
|
2812
|
+
const { branch, plan, packageManager } = params;
|
|
2813
|
+
let out = branch_workflow_default;
|
|
2814
|
+
out = block(out, "PLAN_JOB", plan);
|
|
2815
|
+
out = block(out, "PULL_REQUEST", plan);
|
|
2816
|
+
out = block(out, "DISPATCH_INPUTS", plan);
|
|
2817
|
+
out = line(out, "DEPLOY_IF", plan ? `if: >-\n github.event_name == 'push' ||\n (github.event_name == 'workflow_dispatch' && !inputs['dry-run'])` : void 0);
|
|
2818
|
+
out = line(out, "PATHS", params.workingDirectory ? `paths: ["${params.workingDirectory}/**"]` : void 0);
|
|
2819
|
+
out = applyCommon(out, params).replaceAll("__BRANCH__", () => branch);
|
|
2820
|
+
const generatedIds = [];
|
|
2821
|
+
if (plan) generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-plan");
|
|
2822
|
+
generatedIds.push("tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), "tailor-deploy/tailor-apply");
|
|
2823
|
+
return {
|
|
2824
|
+
content: out,
|
|
2825
|
+
generatedIds
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Render the tag-target deploy workflow.
|
|
2617
2830
|
*
|
|
2618
|
-
*
|
|
2619
|
-
*
|
|
2620
|
-
*
|
|
2621
|
-
* @
|
|
2622
|
-
* @returns Workflow YAML content
|
|
2831
|
+
* When `branch` is set, a tag-reachability guard job is generated and the plan
|
|
2832
|
+
* job gates on it; otherwise no guard is emitted and tags deploy unconditionally.
|
|
2833
|
+
* @param params - Workspace and rendering configuration
|
|
2834
|
+
* @returns Rendered YAML and the list of managed job/step ids
|
|
2623
2835
|
*/
|
|
2624
|
-
function
|
|
2625
|
-
const {
|
|
2626
|
-
const
|
|
2627
|
-
|
|
2628
|
-
|
|
2836
|
+
function renderTagWorkflow(params) {
|
|
2837
|
+
const { tagPattern, branch, packageManager } = params;
|
|
2838
|
+
const hasGuard = branch !== void 0;
|
|
2839
|
+
let out = tag_workflow_default;
|
|
2840
|
+
out = block(out, "TAG_GUARD_JOB", hasGuard);
|
|
2841
|
+
out = line(out, "PLAN_NEEDS", hasGuard ? "needs: tailor-tag-guard" : void 0);
|
|
2842
|
+
out = line(out, "PLAN_IF", hasGuard ? `if: >-\n github.event_name == 'workflow_dispatch' ||\n needs.tailor-tag-guard.outputs.on-branch == 'true'` : void 0);
|
|
2843
|
+
out = applyCommon(out, params).replaceAll("__TAG_PATTERN__", () => tagPattern);
|
|
2844
|
+
if (hasGuard) out = out.replaceAll("__BRANCH__", () => branch);
|
|
2845
|
+
const generatedIds = [];
|
|
2846
|
+
if (hasGuard) generatedIds.push("tailor-tag-guard", "tailor-tag-guard/tailor-checkout", "tailor-tag-guard/tailor-tag-guard");
|
|
2847
|
+
generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-plan", "tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), "tailor-deploy/tailor-apply");
|
|
2848
|
+
return {
|
|
2849
|
+
content: out,
|
|
2850
|
+
generatedIds
|
|
2851
|
+
};
|
|
2629
2852
|
}
|
|
2630
2853
|
|
|
2631
2854
|
//#endregion
|
|
2632
2855
|
//#region src/cli/commands/setup/github/github.ts
|
|
2856
|
+
async function defaultLoadConfigName(configPath) {
|
|
2857
|
+
const { config } = await loadConfig(configPath);
|
|
2858
|
+
return config.name;
|
|
2859
|
+
}
|
|
2860
|
+
const WORKSPACE_NAME_RE = /^[a-z0-9-]+$/;
|
|
2861
|
+
function validateWorkspaceName(name) {
|
|
2862
|
+
if (name.length < 3 || name.length > 63 || !WORKSPACE_NAME_RE.test(name) || name.startsWith("-") || name.endsWith("-")) throw new Error(`Invalid workspace name "${name}". Names must be 3-63 characters of lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen. Pass a valid name with --workspace-name.`);
|
|
2863
|
+
}
|
|
2864
|
+
const BRANCH_RE = /^[A-Za-z0-9._/-]+$/;
|
|
2865
|
+
const TAG_PATTERN_RE = /^[A-Za-z0-9._/*?![\]-]+$/;
|
|
2866
|
+
function validateBranch(branch) {
|
|
2867
|
+
if (!BRANCH_RE.test(branch)) throw new Error(`Invalid branch name "${branch}". Only letters, numbers, ".", "_", "/", and "-" are supported here.`);
|
|
2868
|
+
}
|
|
2869
|
+
function validateTagPattern(pattern) {
|
|
2870
|
+
if (!TAG_PATTERN_RE.test(pattern)) throw new Error(`Invalid tag pattern "${pattern}". Only letters, numbers, ".", "_", "/", "-", and the glob characters "*?![]" are supported.`);
|
|
2871
|
+
}
|
|
2872
|
+
const ENVIRONMENT_RE = /^[A-Za-z0-9._/-]+$/;
|
|
2873
|
+
function validateEnvironment(environment) {
|
|
2874
|
+
if (!ENVIRONMENT_RE.test(environment)) throw new Error(`Invalid environment name "${environment}". Only letters, numbers, ".", "_", "/", and "-" are supported.`);
|
|
2875
|
+
}
|
|
2876
|
+
const DIR_RE = /^[A-Za-z0-9._/-]+$/;
|
|
2877
|
+
function validateDir(dir) {
|
|
2878
|
+
if (!DIR_RE.test(dir)) throw new Error(`Invalid --dir "${dir}". Only letters, numbers, ".", "_", "/", and "-" are supported.`);
|
|
2879
|
+
}
|
|
2880
|
+
function escapesRoot(rel) {
|
|
2881
|
+
return rel === ".." || rel.startsWith(`..${path.sep}`) || rel.startsWith("../") || path.isAbsolute(rel);
|
|
2882
|
+
}
|
|
2633
2883
|
/**
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2884
|
+
* Resolve the config file path for the given app directory.
|
|
2885
|
+
*
|
|
2886
|
+
* `--dir` must stay inside the repository: the value is embedded in workflow
|
|
2887
|
+
* `paths:` filters and the config under it gets mutated (id injection), so
|
|
2888
|
+
* absolute paths and `..` traversal are rejected.
|
|
2889
|
+
* @param outputDir - Repository root (cwd)
|
|
2890
|
+
* @param dir - App directory relative to the repo root
|
|
2891
|
+
* @returns Absolute path to tailor.config.ts
|
|
2637
2892
|
*/
|
|
2638
|
-
function
|
|
2639
|
-
const
|
|
2893
|
+
function resolveConfigPath(outputDir, dir) {
|
|
2894
|
+
const appDir = path.resolve(outputDir, dir);
|
|
2895
|
+
const rel = path.relative(outputDir, appDir);
|
|
2896
|
+
if (path.isAbsolute(dir) || escapesRoot(rel)) throw new Error(`--dir must be a relative path inside the repository (got "${dir}").`);
|
|
2897
|
+
if (fs$1.existsSync(appDir)) {
|
|
2898
|
+
const realAppDir = path.normalize(fs$1.realpathSync(appDir));
|
|
2899
|
+
const realOutputDir = path.normalize(fs$1.realpathSync(outputDir));
|
|
2900
|
+
if (escapesRoot(path.relative(realOutputDir, realAppDir))) throw new Error(`--dir must resolve to a directory inside the repository (got "${dir}", which links outside it).`);
|
|
2901
|
+
}
|
|
2902
|
+
const configPath = path.join(appDir, "tailor.config.ts");
|
|
2903
|
+
if (!fs$1.existsSync(configPath)) throw new Error(`tailor.config.ts not found at ${configPath}. Run this from your SDK project root, or pass the app directory with --dir.`);
|
|
2904
|
+
return configPath;
|
|
2905
|
+
}
|
|
2906
|
+
/**
|
|
2907
|
+
* Resolve all derived values and render the workflow content.
|
|
2908
|
+
* @param options - Setup options
|
|
2909
|
+
* @returns Resolved target metadata and rendered content
|
|
2910
|
+
*/
|
|
2911
|
+
async function resolve$1(options) {
|
|
2912
|
+
if (options.tag && !options.plan) throw new Error("--no-plan cannot be combined with --tag (tag targets always run plan before deploy). Drop --no-plan or use a branch target.");
|
|
2913
|
+
const dir = options.dir.replaceAll("\\", "/").replace(/\/{2,}/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2914
|
+
validateDir(dir);
|
|
2915
|
+
const workingDirectory = dir !== "." ? dir : void 0;
|
|
2916
|
+
const configPath = resolveConfigPath(options.outputDir, dir);
|
|
2917
|
+
const loadName = options.loadConfigName ?? defaultLoadConfigName;
|
|
2918
|
+
const workspaceName = options.workspaceName ?? await loadName(configPath);
|
|
2919
|
+
if (!workspaceName) throw new Error("Could not determine the workspace name. Pass --workspace-name, or set 'name' in tailor.config.ts.");
|
|
2920
|
+
validateWorkspaceName(workspaceName);
|
|
2921
|
+
const kind = options.tag ? "tag" : "branch";
|
|
2640
2922
|
const packageManager = detectPackageManager(options.outputDir);
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2923
|
+
const environment = options.environment ?? workspaceName;
|
|
2924
|
+
validateEnvironment(environment);
|
|
2925
|
+
if (kind === "tag") validateTagPattern(options.tagPattern);
|
|
2926
|
+
let branch = null;
|
|
2927
|
+
let render;
|
|
2928
|
+
if (kind === "branch") {
|
|
2929
|
+
branch = options.branch ?? detectDefaultBranch(options.outputDir, options.gitRunner);
|
|
2930
|
+
validateBranch(branch);
|
|
2931
|
+
render = renderBranchWorkflow({
|
|
2932
|
+
workspaceName,
|
|
2933
|
+
branch,
|
|
2649
2934
|
workingDirectory,
|
|
2935
|
+
environment,
|
|
2650
2936
|
packageManager,
|
|
2651
|
-
|
|
2652
|
-
})
|
|
2653
|
-
}
|
|
2937
|
+
plan: options.plan
|
|
2938
|
+
});
|
|
2939
|
+
} else {
|
|
2940
|
+
branch = options.branch ?? null;
|
|
2941
|
+
if (branch !== null) validateBranch(branch);
|
|
2942
|
+
render = renderTagWorkflow({
|
|
2943
|
+
workspaceName,
|
|
2944
|
+
tagPattern: options.tagPattern,
|
|
2945
|
+
branch: options.branch,
|
|
2946
|
+
workingDirectory,
|
|
2947
|
+
environment,
|
|
2948
|
+
packageManager
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
const file = `.github/workflows/tailor-${workspaceName}.yml`;
|
|
2952
|
+
const inputs = {
|
|
2953
|
+
branch,
|
|
2954
|
+
tagPattern: kind === "tag" ? options.tagPattern : null,
|
|
2955
|
+
environment,
|
|
2956
|
+
dir,
|
|
2957
|
+
packageManager,
|
|
2958
|
+
plan: kind === "branch" ? options.plan : true
|
|
2959
|
+
};
|
|
2960
|
+
return {
|
|
2961
|
+
kind,
|
|
2962
|
+
workspaceName,
|
|
2963
|
+
branch,
|
|
2964
|
+
environment,
|
|
2965
|
+
packageManager,
|
|
2966
|
+
render,
|
|
2967
|
+
inputs,
|
|
2968
|
+
file,
|
|
2969
|
+
configPath
|
|
2970
|
+
};
|
|
2654
2971
|
}
|
|
2655
2972
|
/**
|
|
2656
|
-
*
|
|
2657
|
-
* @param
|
|
2658
|
-
* @
|
|
2973
|
+
* Decide how to reconcile a target with the on-disk file and lock state.
|
|
2974
|
+
* @param obj - Decision inputs
|
|
2975
|
+
* @param obj.existing - The matching lock target, if any
|
|
2976
|
+
* @param obj.fileExists - Whether the workflow file is present on disk
|
|
2977
|
+
* @param obj.currentContent - On-disk content when present
|
|
2978
|
+
* @param obj.force - Whether --force was passed
|
|
2979
|
+
* @returns The reconciliation action
|
|
2659
2980
|
*/
|
|
2660
|
-
function
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
if (
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
fs$1.writeFileSync(file.path, file.content);
|
|
2670
|
-
written.push(file.path);
|
|
2981
|
+
function decideAction(obj) {
|
|
2982
|
+
const { existing, fileExists, currentContent, force } = obj;
|
|
2983
|
+
if (!existing) {
|
|
2984
|
+
if (!fileExists) return { action: "create" };
|
|
2985
|
+
if (force) return { action: "regenerate" };
|
|
2986
|
+
return {
|
|
2987
|
+
action: "conflict",
|
|
2988
|
+
reason: "An unmanaged workflow file already exists at this path. Delete it, or pass --force to bring it under SDK management (this overwrites it)."
|
|
2989
|
+
};
|
|
2671
2990
|
}
|
|
2991
|
+
if (!fileExists) return { action: "restore" };
|
|
2992
|
+
if (currentContent !== null && hashContent(currentContent) === existing.contentHash) return { action: "regenerate" };
|
|
2993
|
+
if (force) return { action: "regenerate" };
|
|
2672
2994
|
return {
|
|
2673
|
-
|
|
2674
|
-
|
|
2995
|
+
action: "conflict",
|
|
2996
|
+
reason: "This workflow file has been edited by hand since it was generated. Re-run with --force to discard those edits and regenerate, or revert your changes."
|
|
2675
2997
|
};
|
|
2676
2998
|
}
|
|
2677
2999
|
/**
|
|
2678
|
-
*
|
|
2679
|
-
* @param
|
|
3000
|
+
* Guard against two targets of different kinds colliding on the same file path.
|
|
3001
|
+
* @param obj - Conflict inputs
|
|
3002
|
+
* @param obj.lock - Existing lock file, or null
|
|
3003
|
+
* @param obj.kind - Target kind being generated
|
|
3004
|
+
* @param obj.workspaceName - Workspace name being generated
|
|
3005
|
+
* @param obj.file - Target file path
|
|
2680
3006
|
*/
|
|
2681
|
-
function
|
|
2682
|
-
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
3007
|
+
function assertNoKindCollision(obj) {
|
|
3008
|
+
const { lock, kind, workspaceName, file } = obj;
|
|
3009
|
+
const collision = lock?.targets.find((t) => t.file === file && !(t.kind === kind && t.workspaceName === workspaceName));
|
|
3010
|
+
if (collision) throw new Error(`A ${collision.kind} target already owns ${file}, which conflicts with this ${kind} target. Pass a different name with --workspace-name to generate a separate workflow.`);
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Print next-step guidance after generating workflow files.
|
|
3014
|
+
* @param obj - Output context
|
|
3015
|
+
* @param obj.environment - Resolved GitHub Environment name for this target
|
|
3016
|
+
* @param obj.idInjected - Whether an app id was injected into the config
|
|
3017
|
+
*/
|
|
3018
|
+
function printNextSteps(obj) {
|
|
3019
|
+
const { environment, idInjected } = obj;
|
|
2692
3020
|
logger.newline();
|
|
2693
|
-
logger.info("Next steps
|
|
2694
|
-
logger.
|
|
2695
|
-
logger.log(`
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3021
|
+
logger.info("Next steps:");
|
|
3022
|
+
logger.newline();
|
|
3023
|
+
logger.log(`1. Set the machine-user credentials as secrets on the "${environment}" environment:`);
|
|
3024
|
+
logger.log(` gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID --env ${environment}`);
|
|
3025
|
+
logger.log(` gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET --env ${environment}`);
|
|
3026
|
+
logger.newline();
|
|
3027
|
+
logger.log(`2. Provision the workspace and set its id as the TAILOR_PLATFORM_WORKSPACE_ID variable on the "${environment}" environment:`);
|
|
3028
|
+
logger.log(" tailor-sdk workspace create # if it does not exist yet; copy the id");
|
|
3029
|
+
logger.log(` gh variable set TAILOR_PLATFORM_WORKSPACE_ID --env ${environment}`);
|
|
3030
|
+
logger.newline();
|
|
3031
|
+
logger.log("3. Commit the generated files:");
|
|
3032
|
+
logger.log(" - .github/workflows/tailor-*.yml");
|
|
3033
|
+
logger.log(" - .github/tailor-sdk.lock");
|
|
3034
|
+
if (idInjected) logger.log(" - tailor.config.ts (app id was added)");
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Generate the GitHub Actions workflow for a deploy target and reconcile it
|
|
3038
|
+
* with the lock file.
|
|
3039
|
+
* @param options - Setup options
|
|
3040
|
+
*/
|
|
3041
|
+
async function setupGitHub(options) {
|
|
3042
|
+
logBetaWarning("setup github");
|
|
3043
|
+
const resolved = await resolve$1(options);
|
|
3044
|
+
const lock = readLock(options.outputDir);
|
|
3045
|
+
const absFile = path.join(options.outputDir, resolved.file);
|
|
3046
|
+
assertNoKindCollision({
|
|
3047
|
+
lock,
|
|
3048
|
+
kind: resolved.kind,
|
|
3049
|
+
workspaceName: resolved.workspaceName,
|
|
3050
|
+
file: resolved.file
|
|
3051
|
+
});
|
|
3052
|
+
const existing = findTarget(lock, resolved.kind, resolved.workspaceName);
|
|
3053
|
+
const fileExists = fs$1.existsSync(absFile);
|
|
3054
|
+
const decision = decideAction({
|
|
3055
|
+
existing,
|
|
3056
|
+
fileExists,
|
|
3057
|
+
currentContent: fileExists ? fs$1.readFileSync(absFile, "utf-8") : null,
|
|
3058
|
+
force: options.force
|
|
3059
|
+
});
|
|
3060
|
+
if (decision.action === "conflict") throw new Error(`${resolved.file}: ${decision.reason}`);
|
|
3061
|
+
const idResult = await ensureConfigId(resolved.configPath);
|
|
3062
|
+
if (idResult === null) logger.warn("Could not find a defineConfig() call to confirm an app id. The CI deploy will fail unless your config resolves to one with an 'id'.");
|
|
3063
|
+
const idInjected = idResult?.injected ?? false;
|
|
3064
|
+
fs$1.mkdirSync(path.dirname(absFile), { recursive: true });
|
|
3065
|
+
fs$1.writeFileSync(absFile, resolved.render.content, "utf-8");
|
|
3066
|
+
const newTarget = {
|
|
3067
|
+
kind: resolved.kind,
|
|
3068
|
+
workspaceName: resolved.workspaceName,
|
|
3069
|
+
file: resolved.file,
|
|
3070
|
+
templateVersion: 1,
|
|
3071
|
+
inputs: resolved.inputs,
|
|
3072
|
+
generatedIds: resolved.render.generatedIds,
|
|
3073
|
+
ejectedIds: existing?.ejectedIds ?? [],
|
|
3074
|
+
contentHash: hashContent(resolved.render.content)
|
|
3075
|
+
};
|
|
3076
|
+
const targets = [...lock?.targets ?? []];
|
|
3077
|
+
const index = targets.findIndex((t) => t.kind === newTarget.kind && t.workspaceName === newTarget.workspaceName);
|
|
3078
|
+
if (index === -1) targets.push(newTarget);
|
|
3079
|
+
else targets[index] = newTarget;
|
|
3080
|
+
writeLock(options.outputDir, {
|
|
3081
|
+
version: 1,
|
|
3082
|
+
targets
|
|
3083
|
+
});
|
|
3084
|
+
if (decision.action === "restore") logger.success(`Regenerated ${styles.path(resolved.file)} (was missing on disk)`);
|
|
3085
|
+
else if (decision.action === "regenerate") logger.success(`Regenerated ${styles.path(resolved.file)}`);
|
|
3086
|
+
else logger.success(`Generated ${styles.path(resolved.file)}`);
|
|
3087
|
+
printNextSteps({
|
|
3088
|
+
environment: resolved.environment,
|
|
3089
|
+
idInjected
|
|
3090
|
+
});
|
|
2701
3091
|
}
|
|
2702
3092
|
|
|
2703
3093
|
//#endregion
|
|
2704
3094
|
//#region src/cli/commands/setup/github/index.ts
|
|
2705
3095
|
const githubCommand = defineAppCommand({
|
|
2706
3096
|
name: "github",
|
|
2707
|
-
description: "Generate GitHub Actions workflow
|
|
3097
|
+
description: "Generate a GitHub Actions deploy workflow. (beta)",
|
|
2708
3098
|
args: z.object({
|
|
2709
|
-
"workspace-name": arg(z.string(), {
|
|
3099
|
+
"workspace-name": arg(z.string().min(1).optional(), {
|
|
2710
3100
|
alias: "n",
|
|
2711
|
-
description: "Workspace name"
|
|
3101
|
+
description: "Workspace name (defaults to the config 'name')"
|
|
2712
3102
|
}),
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
}),
|
|
2717
|
-
"
|
|
2718
|
-
|
|
2719
|
-
description: "Organization ID"
|
|
2720
|
-
}),
|
|
2721
|
-
"folder-id": arg(z.string(), {
|
|
2722
|
-
alias: "f",
|
|
2723
|
-
description: "Folder ID"
|
|
2724
|
-
}),
|
|
2725
|
-
dir: arg(z.string().default("."), {
|
|
3103
|
+
branch: arg(z.string().min(1).optional(), { description: "Branch target: deploy trigger branch (defaults to the detected default branch). Tag target: tag-reachability guard branch (no guard when omitted)" }),
|
|
3104
|
+
tag: arg(z.boolean().default(false), { description: "Generate a tag target (deploy on tag push)" }),
|
|
3105
|
+
"tag-pattern": arg(z.string().min(1).optional(), { description: "Tag glob to match (requires --tag; defaults to v*)" }),
|
|
3106
|
+
environment: arg(z.string().min(1).optional(), { description: "GitHub Environment for the plan/deploy jobs (defaults to the workspace name)" }),
|
|
3107
|
+
"no-plan": arg(z.boolean().default(false), { description: "Disable the plan job for a branch target (cannot be combined with --tag)" }),
|
|
3108
|
+
dir: arg(z.string().min(1).default("."), {
|
|
2726
3109
|
alias: "d",
|
|
2727
3110
|
description: "App directory (for monorepo setups)"
|
|
2728
3111
|
}),
|
|
2729
|
-
|
|
2730
|
-
alias: "p",
|
|
2731
|
-
description: "Include plan job for PR previews"
|
|
2732
|
-
})
|
|
3112
|
+
force: arg(z.boolean().default(false), { description: "Discard hand edits / take over unmanaged files and regenerate" })
|
|
2733
3113
|
}).strict(),
|
|
2734
|
-
run: (args) => {
|
|
2735
|
-
|
|
3114
|
+
run: async (args) => {
|
|
3115
|
+
if (args["tag-pattern"] !== void 0 && !args.tag) throw new Error("--tag-pattern requires --tag.");
|
|
3116
|
+
if (args["no-plan"] && args.tag) throw new Error("--no-plan cannot be combined with --tag.");
|
|
3117
|
+
await setupGitHub({
|
|
2736
3118
|
workspaceName: args["workspace-name"],
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
3119
|
+
branch: args.branch,
|
|
3120
|
+
tag: args.tag,
|
|
3121
|
+
tagPattern: args["tag-pattern"] ?? "v*",
|
|
3122
|
+
environment: args.environment,
|
|
3123
|
+
plan: !args["no-plan"],
|
|
2740
3124
|
dir: args.dir,
|
|
2741
|
-
|
|
2742
|
-
|
|
3125
|
+
force: args.force,
|
|
3126
|
+
outputDir: process.cwd()
|
|
2743
3127
|
});
|
|
2744
3128
|
}
|
|
2745
3129
|
});
|
|
@@ -3323,7 +3707,7 @@ const CLEAN_ROOM_NOTES = [
|
|
|
3323
3707
|
"It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals."
|
|
3324
3708
|
];
|
|
3325
3709
|
function buildRevision(schema) {
|
|
3326
|
-
return hashContent(JSON.stringify(schema)).slice(0, 16);
|
|
3710
|
+
return hashContent$1(JSON.stringify(schema)).slice(0, 16);
|
|
3327
3711
|
}
|
|
3328
3712
|
function toTypeSource(source) {
|
|
3329
3713
|
if (!source) return void 0;
|
|
@@ -4423,7 +4807,7 @@ async function fetchRemoteTypes(client, workspaceId, namespace) {
|
|
|
4423
4807
|
async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
4424
4808
|
const { config, plugins } = loaded;
|
|
4425
4809
|
const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
|
|
4426
|
-
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-
|
|
4810
|
+
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-av2raLs6.mjs");
|
|
4427
4811
|
const application = defineApplication({
|
|
4428
4812
|
config,
|
|
4429
4813
|
pluginManager
|