@tailor-platform/sdk 2.1.0 → 2.3.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 +199 -0
- package/{dist/cli/index.mjs → bin/tailor.mjs} +1 -1
- package/dist/application-BZfk4HKm.mjs +3 -0
- package/dist/{application-D77KJFKD.mjs → application-F-nl107y.mjs} +121 -27
- package/dist/application-F-nl107y.mjs.map +1 -0
- package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -1
- package/dist/cli/commands/deploy/types.d.mts +2 -1
- package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +52 -22
- package/dist/cli/commands/tailordb/migrate/file-state.d.mts +1 -0
- package/dist/cli/commands/tailordb/migrate/generate.d.mts +6 -0
- package/dist/cli/commands/tailordb/migrate/rename-detection.d.mts +22 -0
- package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +2 -2
- package/dist/cli/commands/tailordb/migrate/snapshot-types.d.mts +9 -2
- package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +30 -9
- package/dist/cli/lib.d.mts +4 -4
- package/dist/cli/lib.mjs +3 -482
- package/dist/cli/lib.mjs.map +1 -1
- package/dist/cli/main.mjs +1151 -208
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/ts-hook.mjs +52 -7
- package/dist/completion/zsh-worker.zsh +167 -4
- package/dist/configure/index.d.mts +3 -3
- package/dist/configure/index.mjs +90 -23
- package/dist/configure/index.mjs.map +1 -1
- package/dist/configure/services/index.d.mts +3 -2
- package/dist/configure/services/tailordb/index.d.mts +2 -1
- package/dist/configure/services/tailordb/schema.d.mts +46 -46
- package/dist/configure/services/tailordb/types.d.mts +30 -4
- package/dist/configure/services/workflow/index.d.mts +2 -2
- package/dist/configure/services/workflow/wait-point.d.mts +72 -13
- package/dist/{crashreport-B5LqCTfF.mjs → crashreport-CIIfLnoO.mjs} +2 -2
- package/dist/{crashreport-B5LqCTfF.mjs.map → crashreport-CIIfLnoO.mjs.map} +1 -1
- package/dist/{crashreport-eI7ty7ok.mjs → crashreport-CINZ5dVN.mjs} +1 -1
- package/dist/errors-BVb6vYGy.mjs +89 -0
- package/dist/errors-BVb6vYGy.mjs.map +1 -0
- package/dist/kysely/index.d.mts +53 -5
- package/dist/kysely/index.mjs +7 -2
- package/dist/kysely/index.mjs.map +1 -1
- package/dist/{logger-CXQq9YIp.mjs → logger-DSdExfor.mjs} +83 -42
- package/dist/logger-DSdExfor.mjs.map +1 -0
- package/dist/plugin/builtin/seed/index.mjs +1 -1
- package/dist/{register-ts-hook-BU18uU44.mjs → register-ts-hook-ClI226n2.mjs} +3541 -703
- package/dist/register-ts-hook-ClI226n2.mjs.map +1 -0
- package/dist/schema--xYWRGfe.mjs.map +1 -1
- package/dist/seed/index.d.mts +52 -3
- package/dist/seed/index.mjs +200 -44
- package/dist/seed/index.mjs.map +1 -1
- package/dist/{seed-B-yckgQY.mjs → seed-xqM9WIHP.mjs} +8 -8
- package/dist/seed-xqM9WIHP.mjs.map +1 -0
- package/dist/{service-k1xy_A5G.mjs → service-C_WpbKHu.mjs} +39 -5
- package/dist/service-C_WpbKHu.mjs.map +1 -0
- package/dist/service-D1RCdzIL.mjs +3 -0
- package/dist/{service-B5WynBHQ.mjs → service-hZskxZmg.mjs} +3 -3
- package/dist/{service-B5WynBHQ.mjs.map → service-hZskxZmg.mjs.map} +1 -1
- package/dist/{service_pb-D5--My_M.mjs → service_pb-BEWj1pOV.mjs} +2 -2
- package/dist/{service_pb-D5--My_M.mjs.map → service_pb-BEWj1pOV.mjs.map} +1 -1
- package/dist/{service_pb-i8CmnQP4.mjs → service_pb-BjlhvK9E.mjs} +1 -1
- package/dist/utils/test/index.d.mts +6 -3
- package/dist/utils/test/index.mjs +25 -13
- package/dist/utils/test/index.mjs.map +1 -1
- package/dist/vitest/index.d.mts +2 -1
- package/dist/vitest/index.mjs +167 -3
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/mocks/workflow.d.mts +13 -1
- package/dist/vitest/pglite-kysely.d.mts +43 -0
- package/dist/wait-point-invoker-__oE88_P.mjs +148 -0
- package/dist/wait-point-invoker-__oE88_P.mjs.map +1 -0
- package/dist/wait-point-registry-TL99zotw.mjs +47 -0
- package/dist/wait-point-registry-TL99zotw.mjs.map +1 -0
- package/dist/{workspace_resource_pb-DXnO2AT-.mjs → workspace_resource_pb-HNIL_Vfj.mjs} +10 -2
- package/dist/{workspace_resource_pb-DXnO2AT-.mjs.map → workspace_resource_pb-HNIL_Vfj.mjs.map} +1 -1
- package/docs/cli/setup.md +14 -1
- package/docs/cli/tailordb.md +96 -28
- package/docs/cli-reference.md +15 -12
- package/docs/configuration.md +1 -1
- package/docs/github-actions.md +73 -28
- package/docs/migration/v2.md +55 -1
- package/docs/plugin/custom.md +48 -48
- package/docs/plugin/index.md +24 -24
- package/docs/services/auth.md +10 -10
- package/docs/services/executor.md +1 -1
- package/docs/services/resolver.md +2 -2
- package/docs/services/tailordb-migration.md +485 -84
- package/docs/services/tailordb.md +28 -28
- package/docs/services/workflow.md +52 -2
- package/docs/testing.md +14 -0
- package/package.json +12 -7
- package/dist/application-Af1zIqSI.mjs +0 -3
- package/dist/application-D77KJFKD.mjs.map +0 -1
- package/dist/errors-D7583Zz7.mjs +0 -58
- package/dist/errors-D7583Zz7.mjs.map +0 -1
- package/dist/logger-CXQq9YIp.mjs.map +0 -1
- package/dist/register-ts-hook-BU18uU44.mjs.map +0 -1
- package/dist/seed-B-yckgQY.mjs.map +0 -1
- package/dist/service-M7OYziQP.mjs +0 -3
- package/dist/service-k1xy_A5G.mjs.map +0 -1
- package/dist/test-env-key-D7UkZp99.mjs +0 -75
- package/dist/test-env-key-D7UkZp99.mjs.map +0 -1
package/dist/cli/main.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { r as parseInputFields } from "../schema--xYWRGfe.mjs";
|
|
3
|
-
import { At as PATScope, Ct as AuthInvokerSchema, E as CustomDomainStatus, W as FunctionExecution_Type } from "../workspace_resource_pb-
|
|
3
|
+
import { At as PATScope, Ct as AuthInvokerSchema, E as CustomDomainStatus, Ft as CloneOperationStatus, W as FunctionExecution_Type } from "../workspace_resource_pb-HNIL_Vfj.mjs";
|
|
4
4
|
import { t as assertDefined } from "../assert-DBxo8jPo.mjs";
|
|
5
|
-
import { n as logger, r as styles } from "../logger-
|
|
6
|
-
import { $
|
|
7
|
-
import {
|
|
8
|
-
import { i as ExecutorSchema, r as stripExecutorTriggerArgs } from "../service-
|
|
5
|
+
import { n as logger, r as styles } from "../logger-DSdExfor.mjs";
|
|
6
|
+
import { $ as treeCommand, $n as sanitizeMigrationLabel, An as MIGRATION_FILE_NAMES, Ar as readPlatformConfig, B as MIGRATION_REVIEW_REQUIRED_MARKER, Bn as getLatestMigrationNumber, Br as deploymentArgs, Bt as getCommand$6, C as healthCommand, Cn as captureFileState, Cr as loadConfigPath, Ct as listCommand$9, Dr as loadStoredUserTokens, E as resumeCommand, Er as loadPlatformClientConfig, F as chunkSeedData, Fn as assertValidMigrationFiles, Fr as writePlatformConfig, Ft as jobsCommand, G as showCommand, Gn as loadDiff, Gr as resolveMachineUserInputSource, Gt as functionExecutionStatusToString, H as deployCommand$1, Hn as getMigrationFilePath, Hr as multiConfigArg, Ht as executionsCommand, I as bundleSeedScript, In as compareLocalTypesWithSnapshot, Ir as defineAppCommand, Jn as reconstructSnapshotFromMigrations, K as extractOwnedNamespaces, Kn as loadSnapshot, Kr as toPageDirection, Kt as getCommand$1, Ln as compareSnapshots, Lr as commonArgs, M as loadSeedContext, Mr as resolveConfigUser, N as loadApplicationNamespaces, Nn as UnsupportedMigrationFileVersionError, Nr as resolveUserTokenKey, O as listCommand$14, Or as loadWorkspaceId, Ot as webhookCommand, Pn as assertMigrationNumberExists, Pr as saveUserTokens, Qn as handleOptionalToRequiredError, Qt as listCommand$17, R as generateCommand$1, Rn as createSnapshotFromLocalTypes, Rr as configArg, Rt as startCommand, Sn as protoGqlPermission, Sr as loadAccessToken, St as generate, Tr as loadMachineUserName, Tt as getCommand$2, Un as getMigrationFiles, Ur as pagedLogArgs, V as generateSchemaFile, Vr as isVerbose, Wr as paginationArgs, X as updateCommand$3, Xn as MIGRATION_LABEL_KEY, Xt as deployMigrationTestBaseline, Y as removeCommand$1, Yn as MIGRATION_HISTORY_LABEL_KEY, Yt as deployFromCLI, Zn as createMigrationHistoryId, Zt as deployMigrationTestTarget, _n as verifyRemoteSchema, _r as loadConfig, a as queryCommand, an as ensureConfigId, ar as resourceTrn, bn as compareSnapshotWithRemote, br as hasAnyUserTokenEntry, bt as listCommand$10, c as removeCommand, cn as executeScript, cr as sdkVersionLabelKey, ct as getCommand$4, dn as bundleMigrationScript, en as createCommand$4, er as formatDiffSummary, et as listCommand$13, f as inviteCommand, fn as checkMigrationDiffs, fr as formatMigrationNumber, ft as createCommand$3, g as getCommand$7, gn as toTailorDBDeployInput, gr as assertWritable, gt as getCommand$3, hn as logRemoteDriftGuidance, hr as apiCall, in as workspaceNameSchema, ir as selectTargetNamespace, it as updateCommand$2, j as truncateCommand, jn as MIGRATION_NUMBER_PATTERN, jr as removeLegacyUserAlias, jt as listCommand$8, kr as platformConfigFromProfile, kt as triggerCommand, lr as writeMetadataLabels, m as restoreCommand, mn as formatRemoteVerificationResults, mr as prompt, mt as listCommand$11, nr as hasChanges, nt as getCommand$5, o as updateCommand$4, on as getMigrationMachineUser, or as sdkAppIdLabelKey, ot as listCommand$12, pn as fetchRemoteSchemaSnapshot, pr as parseMigrationNumberArg, q as logBetaWarning, qn as normalizeSchemaSnapshot, qr as workspaceArgs, r as serializeError, rn as formatKeyValueTable, rr as getNamespacesWithMigrations, sn as updateMigrationLabel, sr as sdkNameLabelKey, t as registerTsHook, tn as createValidatedWorkspaceWithClient, tr as formatMigrationDiff, u as listCommand$16, un as formatMigrationScriptCommand, ur as PluginManager, ut as deleteCommand$4, v as deleteCommand$5, vn as fetchRemoteMigrationNumber, vr as deleteUserTokens, w as waitCommand, wn as captureMigrationFileState, wr as loadConsoleBaseUrl, x as listCommand$15, xn as generateAllTypeManifestsFromSnapshot, xr as hasUserTokenEntry, yn as fetchRemoteMigrationState, yr as fetchLatestToken, yt as tokenCommand$1, z as scriptCommand, zn as formatSchemaDrifts, zr as confirmationArgs } from "../register-ts-hook-ClI226n2.mjs";
|
|
7
|
+
import { B as fetchPaged, C as resolveTSConfigWithFallback, D as resolveBundleLogLevel, E as createLogLevelTreeshakeOptions, F as closeConnectionPool, G as getOrNull, H as fetchUserInfo, I as defaultPlatformBaseUrl, J as initOperatorClient, K as getPlatformBaseUrl, L as fetchAll, M as getDistDir, O as createBundleLog, R as fetchAllTolerant, S as createTsconfigPathsPlugin, T as composeFunctionTreeshakeOptions, V as fetchPlatformMachineUserToken, W as getOAuth2ClientId, Y as isDefaultPlatform, a as resolveInlineSourcemap, c as WorkflowJobSchema, f as INVOKER_EXPR, g as assertUniqueLocalTailorDBTypeNames, h as buildResolverPermissionAndInputCheckExpr, l as resolveResolverDefaultPermissionForFile, q as initOAuth2Client, u as ResolverSchema, w as platformBundleDefinePlugin, x as createGeneratedEntryResolverPlugin } from "../application-F-nl107y.mjs";
|
|
8
|
+
import { h as loadFilesWithIgnores, i as ExecutorSchema, r as stripExecutorTriggerArgs } from "../service-C_WpbKHu.mjs";
|
|
9
9
|
import { t as multiline } from "../multiline-sfHpTZZK.mjs";
|
|
10
10
|
import { t as readPackageJson } from "../package-json-8b0O9TlX.mjs";
|
|
11
11
|
import { t as userAgent } from "../user-agent-Bgsszb5I.mjs";
|
|
12
|
-
import {
|
|
13
|
-
import { t as OperatorService } from "../service_pb-
|
|
14
|
-
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-
|
|
12
|
+
import { a as typeOnlyImportHint, i as isCLIError, r as formatNextAction, t as createCLIError } from "../errors-BVb6vYGy.mjs";
|
|
13
|
+
import { t as OperatorService } from "../service_pb-BEWj1pOV.mjs";
|
|
14
|
+
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-CIIfLnoO.mjs";
|
|
15
15
|
import * as path from "pathe";
|
|
16
16
|
import { dirname, resolve } from "pathe";
|
|
17
17
|
import { resolvePackageJSON } from "pkg-types";
|
|
@@ -27,7 +27,7 @@ import { pathToFileURL } from "node:url";
|
|
|
27
27
|
import { generateCodeVerifier } from "@badgateway/oauth2-client";
|
|
28
28
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
29
29
|
import * as crypto from "node:crypto";
|
|
30
|
-
import { createHash } from "node:crypto";
|
|
30
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
31
31
|
import * as http from "node:http";
|
|
32
32
|
import open from "open";
|
|
33
33
|
import * as rolldown from "rolldown";
|
|
@@ -1498,7 +1498,7 @@ function composeExecutionErrorString(error) {
|
|
|
1498
1498
|
* @returns Formatted fallback string for display
|
|
1499
1499
|
*/
|
|
1500
1500
|
function formatExecutionErrorFallback(error) {
|
|
1501
|
-
const [headerLine, ...frameLines] = composeExecutionErrorString(error).split("\n");
|
|
1501
|
+
const [headerLine = "", ...frameLines] = composeExecutionErrorString(error).split("\n");
|
|
1502
1502
|
return [` ${styles.error(headerLine)}`, ...frameLines.map((line) => ` ${styles.dim(line)}`)].join("\n");
|
|
1503
1503
|
}
|
|
1504
1504
|
/**
|
|
@@ -3818,6 +3818,14 @@ function detectDefaultBranch(cwd, run = defaultGitRunner) {
|
|
|
3818
3818
|
const LOCK_VERSION = 1;
|
|
3819
3819
|
/** Lock file path, relative to the repository root. */
|
|
3820
3820
|
const LOCK_FILENAME = ".github/tailor.lock";
|
|
3821
|
+
function assertSafeLockPath(outputDir) {
|
|
3822
|
+
for (const relativePath of [".github", LOCK_FILENAME]) try {
|
|
3823
|
+
if (fs.lstatSync(path.join(outputDir, relativePath)).isSymbolicLink()) throw new Error(`Refusing to use ${LOCK_FILENAME}: "${relativePath}" is a symbolic link.`);
|
|
3824
|
+
} catch (error) {
|
|
3825
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
3826
|
+
throw error;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3821
3829
|
/**
|
|
3822
3830
|
* Compute the lock content hash for a rendered workflow file.
|
|
3823
3831
|
* @param content - File content to hash
|
|
@@ -3843,6 +3851,7 @@ function lockPath(outputDir) {
|
|
|
3843
3851
|
* @returns Parsed lock file, or null when absent
|
|
3844
3852
|
*/
|
|
3845
3853
|
function readLock(outputDir) {
|
|
3854
|
+
assertSafeLockPath(outputDir);
|
|
3846
3855
|
const file = lockPath(outputDir);
|
|
3847
3856
|
if (!fs.existsSync(file)) return null;
|
|
3848
3857
|
let parsed;
|
|
@@ -3862,6 +3871,7 @@ function readLock(outputDir) {
|
|
|
3862
3871
|
* @param lock - Lock file contents to serialize
|
|
3863
3872
|
*/
|
|
3864
3873
|
function writeLock(outputDir, lock) {
|
|
3874
|
+
assertSafeLockPath(outputDir);
|
|
3865
3875
|
const file = lockPath(outputDir);
|
|
3866
3876
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
3867
3877
|
fs.writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
@@ -3880,23 +3890,23 @@ function findTarget(lock, kind, workspaceName) {
|
|
|
3880
3890
|
|
|
3881
3891
|
//#endregion
|
|
3882
3892
|
//#region src/cli/commands/setup/action.yml
|
|
3883
|
-
var action_default = "# __HEADER__\nname: __WORKSPACE_NAME__\ndescription: >-\n Deploy __WORKSPACE_NAME__ to Tailor Platform.\n Checkout and dependency installation are expected to be handled by the caller.\n\ninputs:\n workspace-id:\n description: Tailor Platform workspace id\n required: true\n name:\n description: Workspace name (used in Slack notifications)\n required: true\n working-directory:\n description: App directory relative to the repository root\n required: false\n default: \".\"\n config:\n description: SDK config path, or comma-separated config paths for multi-config deploys\n required: false\n default: \"tailor.config.ts\"\n deploy:\n description: Whether to run the Tailor deploy step after build steps\n required: false\n default: \"true\"\n build-site:\n description: Whether to run the static website build slot when present\n required: false\n default: \"true\"\n platform-client-id:\n description: Machine-user client id\n required: true\n platform-client-secret:\n description: Machine-user client secret\n required: true\n slack-token:\n description: Slack bot token\n required: false\n slack-channel-id:\n description: Slack channel id\n required: false\n user-mapping:\n description: JSON string mapping GitHub usernames to Slack user IDs\n required: false\n package-manager:\n description: Package manager used for dependency installation (pnpm, npm, yarn, bun)\n required: false\n default: pnpm\n\noutputs:\n app-url:\n description: Deployed application URL\n value: ${{ steps.tailor-apply.outputs.app-url }}\n\nruns:\n using: composite\n steps:\n # __STATIC_WEBSITE_BUILD_START__\n - id: build-site\n if: inputs.build-site == 'true'\n shell: bash\n run: |\n # Replace with your static website build command.\n # Example: pnpm --filter <package-name> run build:static\n # Note: this run: command is user-editable. Custom build commands are preserved across\n # reruns and are not treated as drift by setup check. Other edits to this file are flagged\n # as drift and will be overwritten if you re-run setup action with --force.\n true\n # __STATIC_WEBSITE_BUILD_END__\n - id: tailor-apply\n if: inputs.deploy == 'true'\n uses: tailor-platform/actions/deploy@
|
|
3893
|
+
var action_default = "# __HEADER__\nname: __WORKSPACE_NAME__\ndescription: >-\n Deploy __WORKSPACE_NAME__ to Tailor Platform.\n Checkout and dependency installation are expected to be handled by the caller.\n\ninputs:\n workspace-id:\n description: Tailor Platform workspace id\n required: true\n name:\n description: Workspace name (used in Slack notifications)\n required: true\n working-directory:\n description: App directory relative to the repository root\n required: false\n default: \".\"\n config:\n description: SDK config path, or comma-separated config paths for multi-config deploys\n required: false\n default: \"tailor.config.ts\"\n deploy:\n description: Whether to run the Tailor deploy step after build steps\n required: false\n default: \"true\"\n build-site:\n description: Whether to run the static website build slot when present\n required: false\n default: \"true\"\n platform-client-id:\n description: Machine-user client id\n required: true\n platform-client-secret:\n description: Machine-user client secret\n required: true\n slack-token:\n description: Slack bot token\n required: false\n slack-channel-id:\n description: Slack channel id\n required: false\n user-mapping:\n description: JSON string mapping GitHub usernames to Slack user IDs\n required: false\n package-manager:\n description: Package manager used for dependency installation (pnpm, npm, yarn, bun)\n required: false\n default: pnpm\n\noutputs:\n app-url:\n description: Deployed application URL\n value: ${{ steps.tailor-apply.outputs.app-url }}\n\nruns:\n using: composite\n steps:\n # __STATIC_WEBSITE_BUILD_START__\n - id: build-site\n if: inputs.build-site == 'true'\n shell: bash\n run: |\n # Replace with your static website build command.\n # Example: pnpm --filter <package-name> run build:static\n # Note: this run: command is user-editable. Custom build commands are preserved across\n # reruns and are not treated as drift by setup check. Other edits to this file are flagged\n # as drift and will be overwritten if you re-run setup action with --force.\n true\n # __STATIC_WEBSITE_BUILD_END__\n - id: tailor-apply\n if: inputs.deploy == 'true'\n uses: tailor-platform/actions/deploy@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n env:\n TAILOR_PLATFORM_SDK_CONFIG_PATH: ${{ inputs.config }}\n with:\n workspace-id: ${{ inputs.workspace-id }}\n working-directory: ${{ inputs.working-directory }}\n package-manager: ${{ inputs.package-manager }}\n platform-client-id: ${{ inputs.platform-client-id }}\n platform-client-secret: ${{ inputs.platform-client-secret }}\n - id: tailor-notify\n if: failure() && inputs.slack-token != '' && inputs.slack-channel-id != ''\n uses: tailor-platform/actions/notify@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n provider: slack\n status: failure\n workspace-name: ${{ inputs.name }}\n slack-token: ${{ inputs.slack-token }}\n slack-channel-id: ${{ inputs.slack-channel-id }}\n user-mapping: ${{ inputs.user-mapping }}\n";
|
|
3884
3894
|
|
|
3885
3895
|
//#endregion
|
|
3886
3896
|
//#region src/cli/commands/setup/branch.workflow.yml
|
|
3887
|
-
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_START__\n - id: tailor-seed-validate\n run: __TAILOR_CLI__ seed validate\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_END__\n - id: tailor-drift-check\n uses: tailor-platform/actions/drift-check@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __MIGRATION_DRIFT_CHECK_START__\n - id: tailor-migration-drift-check\n uses: tailor-platform/actions/migration-drift-check@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __MIGRATION_DRIFT_CHECK_END__\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@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n package-manager: __PACKAGE_MANAGER__\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 # __ERD_PREVIEW_JOB_START__\n tailor-erd-preview-matrix:\n if: github.event_name == 'pull_request'\n runs-on: ubuntu-latest\n timeout-minutes: 5\n permissions:\n contents: read\n outputs:\n namespaces: ${{ steps.tailor-erd-preview-matrix.outputs.namespaces }}\n base-app-dir: ${{ steps.tailor-erd-preview-matrix.outputs['base-app-dir'] }}\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n persist-credentials: false\n - id: tailor-checkout-base\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n repository: ${{ github.event.pull_request.base.repo.full_name }}\n ref: ${{ github.event.pull_request.base.sha }}\n path: .tailor-erd-base\n persist-credentials: false\n - id: tailor-erd-preview-matrix\n shell: bash\n env:\n APP_DIR: \"__APP_DIR__\"\n WORKSPACE_NAME: \"__WORKSPACE_NAME__\"\n run: |\n set -euo pipefail\n node <<'NODE'\n const fs = require(\"node:fs\");\n\n const appDir = process.env.APP_DIR || \".\";\n const workspaceName = process.env.WORKSPACE_NAME;\n const output = process.env.GITHUB_OUTPUT;\n const dirPattern = /^[A-Za-z0-9._/-]+$/;\n const namespacePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\n function lockTarget(file) {\n try {\n const lock = JSON.parse(fs.readFileSync(file, \"utf8\"));\n return lock.targets?.find(\n (candidate) =>\n candidate.kind === \"branch\" &&\n candidate.workspaceName === workspaceName,\n );\n } catch (error) {\n if (error && error.code === \"ENOENT\") return undefined;\n throw error;\n }\n }\n\n function namespacesFromTarget(target, label) {\n const namespaces =\n target?.inputs?.erdPreview === true && Array.isArray(target.inputs?.erdNamespaces)\n ? target.inputs.erdNamespaces\n : [];\n for (const namespace of namespaces) {\n if (typeof namespace !== \"string\" || !namespacePattern.test(namespace)) {\n throw new Error(`Invalid ERD namespace in ${label}: ${String(namespace)}`);\n }\n }\n return namespaces;\n }\n\n function normalizeDir(value, label) {\n const raw = typeof value === \"string\" && value.length > 0 ? value : appDir;\n const normalized =\n raw.replaceAll(\"\\\\\", \"/\").replace(/\\/{2,}/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/$/, \"\") ||\n \".\";\n if (\n !dirPattern.test(normalized) ||\n normalized.startsWith(\"/\") ||\n normalized.split(\"/\").includes(\"..\")\n ) {\n throw new Error(`Invalid ERD app dir in ${label}: ${raw}`);\n }\n return normalized;\n }\n\n const headTarget = lockTarget(\".github/tailor.lock\");\n const baseTarget = lockTarget(\".tailor-erd-base/.github/tailor.lock\");\n const baseAppDir = normalizeDir(baseTarget?.inputs?.dir, \"base setup lock\");\n const namespaces = [\n ...new Set([\n ...namespacesFromTarget(headTarget, \"head setup lock\"),\n ...namespacesFromTarget(baseTarget, \"base setup lock\"),\n ]),\n ].sort((a, b) => a.localeCompare(b));\n\n if (namespaces.length === 0) {\n console.error(\"No ERD preview namespaces found in the head or base setup lock.\");\n process.exit(1);\n }\n\n fs.appendFileSync(output, `namespaces=${JSON.stringify(namespaces)}\\n`);\n fs.appendFileSync(output, `base-app-dir=${baseAppDir}\\n`);\n NODE\n\n tailor-erd-preview:\n needs: tailor-erd-preview-matrix\n if: github.event_name == 'pull_request'\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n strategy:\n fail-fast: false\n matrix:\n namespace: ${{ fromJSON(needs.tailor-erd-preview-matrix.outputs.namespaces) }}\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n persist-credentials: false\n - id: tailor-setup\n uses: tailor-platform/actions/setup@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-checkout-base\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n repository: ${{ github.event.pull_request.base.repo.full_name }}\n ref: ${{ github.event.pull_request.base.sha }}\n path: .tailor-erd-base\n persist-credentials: false\n - id: tailor-detect-base-package-manager\n shell: bash\n run: |\n set -euo pipefail\n cd .tailor-erd-base\n base_package_manager=\"$(\n if [ -f pnpm-lock.yaml ]; then\n echo pnpm\n elif [ -f yarn.lock ]; then\n echo yarn\n elif [ -f bun.lockb ] || [ -f bun.lock ]; then\n echo bun\n elif [ -f package-lock.json ]; then\n echo npm\n else\n echo \"__PACKAGE_MANAGER__\"\n fi\n )\"\n echo \"package-manager=$base_package_manager\" >> \"$GITHUB_OUTPUT\"\n if [ \"$base_package_manager\" = \"pnpm\" ]; then\n base_pnpm_version=\"$(node <<'NODE'\n const fs = require(\"node:fs\");\n\n const fallback = \"10\";\n\n function readManifest(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return {};\n }\n }\n\n function pnpmVersion(manifest) {\n const packageManager = manifest.packageManager;\n if (typeof packageManager === \"string\" && packageManager.startsWith(\"pnpm@\")) {\n return packageManager.slice(\"pnpm@\".length).split(\"+\")[0];\n }\n\n const devPackageManager = manifest.devEngines?.packageManager;\n if (devPackageManager?.name === \"pnpm\" && devPackageManager.version) {\n return devPackageManager.version;\n }\n\n return undefined;\n }\n\n process.stdout.write(\n pnpmVersion(readManifest(\"package.json\")) ?? fallback,\n );\n NODE\n )\"\n echo \"pnpm-version=$base_pnpm_version\" >> \"$GITHUB_OUTPUT\"\n fi\n - id: tailor-setup-base-pnpm\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] == 'pnpm'\n uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10\n with:\n version: ${{ steps.tailor-detect-base-package-manager.outputs['pnpm-version'] }}\n package_json_file: .tailor-erd-base/package.json\n run_install: false\n - id: tailor-setup-base-node\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] != 'bun'\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7\n with:\n node-version-file: .tailor-erd-base/package.json\n - id: tailor-setup-base-yarn\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] == 'yarn'\n shell: bash\n working-directory: .tailor-erd-base\n run: |\n set -euo pipefail\n corepack enable\n - id: tailor-setup-base-bun\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] == 'bun'\n uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0\n - id: tailor-install-base\n shell: bash\n working-directory: .tailor-erd-base\n env:\n BASE_PACKAGE_MANAGER: ${{ steps.tailor-detect-base-package-manager.outputs['package-manager'] }}\n run: |\n set -euo pipefail\n case \"$BASE_PACKAGE_MANAGER\" in\n pnpm) pnpm install --frozen-lockfile ;;\n npm) npm ci ;;\n yarn)\n if grep -q '^__metadata:' yarn.lock; then\n yarn install --immutable\n else\n yarn install --frozen-lockfile\n fi\n ;;\n bun) bun install --frozen-lockfile ;;\n *)\n echo \"::error::Unsupported base package-manager '$BASE_PACKAGE_MANAGER' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n - id: tailor-restore-head-setup\n uses: tailor-platform/actions/setup@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-build-erd-preview\n shell: bash\n env:\n APP_DIR: \"__APP_DIR__\"\n BASE_APP_DIR: ${{ needs.tailor-erd-preview-matrix.outputs['base-app-dir'] }}\n BASE_PACKAGE_MANAGER: ${{ steps.tailor-detect-base-package-manager.outputs['package-manager'] }}\n NAMESPACE: ${{ matrix.namespace }}\n run: |\n set -euo pipefail\n\n run_tailor_cli() {\n case \"__PACKAGE_MANAGER__\" in\n pnpm) pnpm exec tailor \"$@\" ;;\n npm) npx @tailor-platform/sdk \"$@\" ;;\n yarn) yarn tailor \"$@\" ;;\n bun) bun run tailor \"$@\" ;;\n *)\n echo \"::error::Unsupported package-manager '__PACKAGE_MANAGER__' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n }\n\n run_head_node() {\n case \"__PACKAGE_MANAGER__\" in\n pnpm) pnpm exec node \"$@\" ;;\n npm) node \"$@\" ;;\n yarn) yarn node \"$@\" ;;\n bun) node \"$@\" ;;\n *)\n echo \"::error::Unsupported package-manager '__PACKAGE_MANAGER__' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n }\n\n tailor_cli_bin=\"$(\n run_head_node - <<'NODE'\n const path = require(\"node:path\");\n const { createRequire } = require(\"node:module\");\n const appDir = process.env.APP_DIR || \".\";\n const githubWorkspace = process.env.GITHUB_WORKSPACE;\n if (!githubWorkspace) {\n throw new Error(\"GITHUB_WORKSPACE is required.\");\n }\n\n let lastError;\n for (const manifestPath of [\n path.join(githubWorkspace, appDir, \"package.json\"),\n path.join(githubWorkspace, \"package.json\"),\n ]) {\n try {\n const requireFromManifest = createRequire(manifestPath);\n process.stdout.write(\n path.join(\n path.dirname(requireFromManifest.resolve(\"@tailor-platform/sdk/cli\")),\n \"index.mjs\",\n ),\n );\n process.exit(0);\n } catch (error) {\n lastError = error;\n }\n }\n throw lastError || new Error(\"Failed to resolve @tailor-platform/sdk/cli.\");\n NODE\n )\"\n\n run_head_tailor_cli_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_CLI_BIN=\"$tailor_cli_bin\" TAILOR_CLI_CWD=\"$command_cwd\" \"$@\"\n }\n\n (\n cd \"$GITHUB_WORKSPACE\" || exit 1\n case \"__PACKAGE_MANAGER__\" in\n pnpm) run_head_cli_env pnpm exec node \"$head_cli_runner\" \"$@\" ;;\n npm) run_head_cli_env node \"$head_cli_runner\" \"$@\" ;;\n yarn) run_head_cli_env yarn node \"$head_cli_runner\" \"$@\" ;;\n bun) run_head_cli_env bun \"$head_cli_runner\" \"$@\" ;;\n *)\n echo \"::error::Unsupported package-manager '__PACKAGE_MANAGER__' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n )\n }\n\n run_base_tailor_cli_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_CLI_BIN=\"$tailor_cli_bin\" TAILOR_CLI_CWD=\"$command_cwd\" \"$@\"\n }\n\n (\n cd \"$command_cwd\" || exit 1\n case \"$BASE_PACKAGE_MANAGER\" in\n pnpm) run_head_cli_env pnpm exec node \"$head_cli_runner\" \"$@\" ;;\n npm) run_head_cli_env node \"$head_cli_runner\" \"$@\" ;;\n yarn) run_head_cli_env yarn node \"$head_cli_runner\" \"$@\" ;;\n bun) run_head_cli_env bun \"$head_cli_runner\" \"$@\" ;;\n *)\n echo \"::error::Unsupported base package-manager '$BASE_PACKAGE_MANAGER' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n )\n }\n\n head_output=\"$RUNNER_TEMP/tailor-erd/head\"\n base_output=\"$RUNNER_TEMP/tailor-erd/base\"\n preview_output=\"$RUNNER_TEMP/tailor-erd/preview\"\n dts_output=\"$RUNNER_TEMP/tailor-erd/dts\"\n head_cli_runner=\"$RUNNER_TEMP/tailor-erd/run-head-cli.mjs\"\n head_log=\"$RUNNER_TEMP/tailor-erd/head-$NAMESPACE.log\"\n base_log=\"$RUNNER_TEMP/tailor-erd/base-$NAMESPACE.log\"\n rm -rf \"$head_output\" \"$base_output\" \"$preview_output\" \"$dts_output\"\n mkdir -p \"$head_output\" \"$base_output\" \"$preview_output\" \"$dts_output\"\n cat > \"$head_cli_runner\" <<'NODE'\n import { pathToFileURL } from \"node:url\";\n\n const cliBin = process.env.TAILOR_CLI_BIN;\n const commandCwd = process.env.TAILOR_CLI_CWD;\n if (!cliBin || !commandCwd) {\n throw new Error(\"TAILOR_CLI_BIN and TAILOR_CLI_CWD are required.\");\n }\n\n process.chdir(commandCwd);\n process.argv = [process.argv[0], cliBin, ...process.argv.slice(2)];\n await import(pathToFileURL(cliBin).href);\n NODE\n\n head_config=\"$GITHUB_WORKSPACE/$APP_DIR/tailor.config.ts\"\n base_config=\"$GITHUB_WORKSPACE/.tailor-erd-base/$BASE_APP_DIR/tailor.config.ts\"\n head_html=\"$head_output/$NAMESPACE/dist/index.html\"\n base_html=\"$base_output/$NAMESPACE/dist/index.html\"\n preview_html=\"$preview_output/$NAMESPACE-viewer.html\"\n\n head_missing=\"false\"\n if [ ! -f \"$head_config\" ]; then\n echo \"Head ERD config not found; rendering base objects as removed.\"\n head_missing=\"true\"\n elif ! (\n cd \"$GITHUB_WORKSPACE/$APP_DIR\" || exit 1\n TAILOR_PLATFORM_SDK_DTS_PATH=\"$dts_output/head-$NAMESPACE.d.ts\" \\\n run_tailor_cli tailordb erd export --config \"$head_config\" --namespace \"$NAMESPACE\" --output \"$head_output\"\n ) >\"$head_log\" 2>&1; then\n if grep -q 'not found in local config.db' \"$head_log\"; then\n cat \"$head_log\"\n echo \"Head ERD namespace '$NAMESPACE' not found; rendering base objects as removed.\"\n head_missing=\"true\"\n else\n cat \"$head_log\"\n exit 1\n fi\n elif [ -s \"$head_log\" ]; then\n cat \"$head_log\"\n fi\n\n base_missing=\"false\"\n if [ ! -f \"$base_config\" ]; then\n echo \"Base ERD config not found; rendering current objects as added.\"\n base_missing=\"true\"\n elif ! (\n TAILOR_PLATFORM_SDK_DTS_PATH=\"$dts_output/base-$NAMESPACE.d.ts\" \\\n run_base_tailor_cli_bin \"$GITHUB_WORKSPACE/.tailor-erd-base/$BASE_APP_DIR\" tailordb erd export --config \"$base_config\" --namespace \"$NAMESPACE\" --output \"$base_output\"\n ) >\"$base_log\" 2>&1; then\n if grep -q 'not found in local config.db' \"$base_log\"; then\n cat \"$base_log\"\n echo \"Base ERD namespace '$NAMESPACE' not found; rendering current objects as added.\"\n base_missing=\"true\"\n else\n cat \"$base_log\"\n exit 1\n fi\n elif [ -s \"$base_log\" ]; then\n cat \"$base_log\"\n fi\n\n if [ \"$head_missing\" = \"true\" ] && [ \"$base_missing\" = \"true\" ]; then\n echo \"::error::ERD namespace '$NAMESPACE' was not found in head or base.\"\n exit 1\n fi\n if [ \"$head_missing\" = \"false\" ] && [ ! -f \"$head_html\" ]; then\n echo \"::error::Head ERD viewer was not generated at $head_html\"\n exit 1\n fi\n if [ \"$base_missing\" = \"false\" ] && [ ! -f \"$base_html\" ]; then\n echo \"::error::Base ERD viewer was not generated at $base_html\"\n exit 1\n fi\n\n diff_args=(--namespace \"$NAMESPACE\" --output \"$preview_html\")\n if [ \"$head_missing\" = \"false\" ]; then\n diff_args+=(--head-html \"$head_html\")\n fi\n if [ \"$base_missing\" = \"false\" ]; then\n diff_args+=(--base-html \"$base_html\")\n fi\n run_head_tailor_cli_bin \"$GITHUB_WORKSPACE\" tailordb erd diff \"${diff_args[@]}\"\n - id: tailor-upload-erd-viewer\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n with:\n name: ${{ matrix.namespace }}-viewer.html\n path: ${{ runner.temp }}/tailor-erd/preview/${{ matrix.namespace }}-viewer.html\n if-no-files-found: error\n archive: false\n retention-days: 7\n\n # __ERD_PREVIEW_JOB_END__\n # __ERD_PREVIEW_COMMENT_JOB_START__\n tailor-erd-preview-comment:\n needs: tailor-erd-preview\n if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository\n runs-on: ubuntu-latest\n timeout-minutes: 5\n permissions:\n actions: read\n pull-requests: write\n steps:\n - id: tailor-comment-erd-preview\n env:\n GH_TOKEN: ${{ github.token }}\n REPO: ${{ github.repository }}\n RUN_ID: ${{ github.run_id }}\n SERVER: ${{ github.server_url }}\n PR_NUMBER: ${{ github.event.pull_request.number }}\n BASE_REF: ${{ github.event.pull_request.base.ref }}\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n MARKER: \"<!-- tailor-erd-preview: __WORKSPACE_NAME__ -->\"\n run: |\n set -euo pipefail\n\n current_head=$(gh api \"repos/$REPO/pulls/$PR_NUMBER\" --jq '.head.sha')\n if [ \"$current_head\" != \"$HEAD_SHA\" ]; then\n echo \"Skipping stale ERD preview comment for $HEAD_SHA; current head is $current_head.\"\n exit 0\n fi\n\n body=\"$MARKER\"$'\\n'\"### TailorDB ERD preview (__WORKSPACE_NAME__)\"$'\\n\\n'\n body+=\"Self-contained ERD viewer HTML artifacts for this pull request. Each viewer can switch between the current schema and a diff against base branch \\`$BASE_REF\\`.\"$'\\n'\n\n while IFS=$'\\t' read -r name id; do\n [ -z \"$name\" ] && continue\n if [[ \"$name\" != *-viewer.html ]]; then\n continue\n fi\n ns=${name%-viewer.html}\n label=\"$ns viewer\"\n body+=$'\\n'\"- **$label**: [$name]($SERVER/$REPO/actions/runs/$RUN_ID/artifacts/$id)\"\n done < <(gh api \"repos/$REPO/actions/runs/$RUN_ID/artifacts\" --paginate \\\n --jq '.artifacts[] | select(.name | endswith(\"-viewer.html\")) | [.name, (.id|tostring)] | @tsv' | sort)\n\n existing=$(gh api \"repos/$REPO/issues/$PR_NUMBER/comments\" --paginate \\\n --jq 'map(select(.body | contains(env.MARKER))) | .[0].id // empty')\n if [ -n \"$existing\" ]; then\n gh api --method PATCH \"repos/$REPO/issues/comments/$existing\" -f body=\"$body\" >/dev/null\n else\n gh api --method POST \"repos/$REPO/issues/$PR_NUMBER/comments\" -f body=\"$body\" >/dev/null\n fi\n\n # __ERD_PREVIEW_COMMENT_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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n package-manager: __PACKAGE_MANAGER__\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 # __SEED_DATA_START__\n - id: seed-data\n if: false\n shell: bash\n # __WORKING_DIRECTORY__\n run: |\n # Uncomment and implement to seed data on deploy.\n # Run after apply so the schema is ready.\n # Example: node scripts/seed.mjs\n true\n # __SEED_DATA_END__\n - id: slack-prereq\n if: always()\n shell: bash\n env:\n SLACK_TOKEN: ${{ secrets.TAILOR_SLACK_BOT_TOKEN }}\n SLACK_CHANNEL_ID: ${{ vars.TAILOR_SLACK_CHANNEL_ID }}\n run: |\n if [[ -n \"$SLACK_TOKEN\" && -n \"$SLACK_CHANNEL_ID\" ]]; then\n echo \"ready=true\" >> \"$GITHUB_OUTPUT\"\n fi\n - id: tailor-notify\n if: always() && steps.slack-prereq.outputs.ready == 'true'\n uses: tailor-platform/actions/notify@1f2d0cc26b84b9fdae8a3a0427526df31e6c966b # v2.0.0\n with:\n provider: slack\n status: ${{ job.status }}\n workspace-name: __WORKSPACE_NAME__\n slack-token: ${{ secrets.TAILOR_SLACK_BOT_TOKEN }}\n slack-channel-id: ${{ vars.TAILOR_SLACK_CHANNEL_ID }}\n # user-mapping: ${{ vars.TAILOR_SLACK_USER_MAPPING }}\n";
|
|
3897
|
+
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_START__\n - id: tailor-seed-validate\n run: __TAILOR_CLI__ seed validate\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_END__\n - id: tailor-drift-check\n uses: tailor-platform/actions/drift-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n fail-on-drift: ${{ vars.TAILOR_PLATFORM_FAIL_ON_DRIFT == 'true' }}\n # __MIGRATION_DRIFT_CHECK_START__\n - id: tailor-migration-drift-check\n uses: tailor-platform/actions/migration-drift-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __MIGRATION_DRIFT_CHECK_END__\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@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n package-manager: __PACKAGE_MANAGER__\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 # __ERD_PREVIEW_JOB_START__\n tailor-erd-preview-matrix:\n if: github.event_name == 'pull_request'\n runs-on: ubuntu-latest\n timeout-minutes: 5\n permissions:\n contents: read\n outputs:\n namespaces: ${{ steps.tailor-erd-preview-matrix.outputs.namespaces }}\n base-app-dir: ${{ steps.tailor-erd-preview-matrix.outputs['base-app-dir'] }}\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n persist-credentials: false\n - id: tailor-checkout-base\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n repository: ${{ github.event.pull_request.base.repo.full_name }}\n ref: ${{ github.event.pull_request.base.sha }}\n path: .tailor-erd-base\n persist-credentials: false\n - id: tailor-erd-preview-matrix\n shell: bash\n env:\n APP_DIR: \"__APP_DIR__\"\n WORKSPACE_NAME: \"__WORKSPACE_NAME__\"\n run: |\n set -euo pipefail\n node <<'NODE'\n const fs = require(\"node:fs\");\n\n const appDir = process.env.APP_DIR || \".\";\n const workspaceName = process.env.WORKSPACE_NAME;\n const output = process.env.GITHUB_OUTPUT;\n const dirPattern = /^[A-Za-z0-9._/-]+$/;\n const namespacePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\n function lockTarget(file) {\n try {\n const lock = JSON.parse(fs.readFileSync(file, \"utf8\"));\n return lock.targets?.find(\n (candidate) =>\n candidate.kind === \"branch\" &&\n candidate.workspaceName === workspaceName,\n );\n } catch (error) {\n if (error && error.code === \"ENOENT\") return undefined;\n throw error;\n }\n }\n\n function namespacesFromTarget(target, label) {\n const namespaces =\n target?.inputs?.erdPreview === true && Array.isArray(target.inputs?.erdNamespaces)\n ? target.inputs.erdNamespaces\n : [];\n for (const namespace of namespaces) {\n if (typeof namespace !== \"string\" || !namespacePattern.test(namespace)) {\n throw new Error(`Invalid ERD namespace in ${label}: ${String(namespace)}`);\n }\n }\n return namespaces;\n }\n\n function normalizeDir(value, label) {\n const raw = typeof value === \"string\" && value.length > 0 ? value : appDir;\n const normalized =\n raw.replaceAll(\"\\\\\", \"/\").replace(/\\/{2,}/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/$/, \"\") ||\n \".\";\n if (\n !dirPattern.test(normalized) ||\n normalized.startsWith(\"/\") ||\n normalized.split(\"/\").includes(\"..\")\n ) {\n throw new Error(`Invalid ERD app dir in ${label}: ${raw}`);\n }\n return normalized;\n }\n\n const headTarget = lockTarget(\".github/tailor.lock\");\n const baseTarget = lockTarget(\".tailor-erd-base/.github/tailor.lock\");\n const baseAppDir = normalizeDir(baseTarget?.inputs?.dir, \"base setup lock\");\n const namespaces = [\n ...new Set([\n ...namespacesFromTarget(headTarget, \"head setup lock\"),\n ...namespacesFromTarget(baseTarget, \"base setup lock\"),\n ]),\n ].sort((a, b) => a.localeCompare(b));\n\n if (namespaces.length === 0) {\n console.error(\"No ERD preview namespaces found in the head or base setup lock.\");\n process.exit(1);\n }\n\n fs.appendFileSync(output, `namespaces=${JSON.stringify(namespaces)}\\n`);\n fs.appendFileSync(output, `base-app-dir=${baseAppDir}\\n`);\n NODE\n\n tailor-erd-preview:\n needs: tailor-erd-preview-matrix\n if: github.event_name == 'pull_request'\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n strategy:\n fail-fast: false\n matrix:\n namespace: ${{ fromJSON(needs.tailor-erd-preview-matrix.outputs.namespaces) }}\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n persist-credentials: false\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-checkout-base\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n repository: ${{ github.event.pull_request.base.repo.full_name }}\n ref: ${{ github.event.pull_request.base.sha }}\n path: .tailor-erd-base\n persist-credentials: false\n - id: tailor-detect-base-package-manager\n shell: bash\n run: |\n set -euo pipefail\n cd .tailor-erd-base\n base_package_manager=\"$(\n if [ -f pnpm-lock.yaml ]; then\n echo pnpm\n elif [ -f yarn.lock ]; then\n echo yarn\n elif [ -f bun.lockb ] || [ -f bun.lock ]; then\n echo bun\n elif [ -f package-lock.json ]; then\n echo npm\n else\n echo \"__PACKAGE_MANAGER__\"\n fi\n )\"\n echo \"package-manager=$base_package_manager\" >> \"$GITHUB_OUTPUT\"\n if [ \"$base_package_manager\" = \"pnpm\" ]; then\n base_pnpm_version=\"$(node <<'NODE'\n const fs = require(\"node:fs\");\n\n const fallback = \"10\";\n\n function readManifest(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return {};\n }\n }\n\n function pnpmVersion(manifest) {\n const packageManager = manifest.packageManager;\n if (typeof packageManager === \"string\" && packageManager.startsWith(\"pnpm@\")) {\n return packageManager.slice(\"pnpm@\".length).split(\"+\")[0];\n }\n\n const devPackageManager = manifest.devEngines?.packageManager;\n if (devPackageManager?.name === \"pnpm\" && devPackageManager.version) {\n return devPackageManager.version;\n }\n\n return undefined;\n }\n\n process.stdout.write(\n pnpmVersion(readManifest(\"package.json\")) ?? fallback,\n );\n NODE\n )\"\n echo \"pnpm-version=$base_pnpm_version\" >> \"$GITHUB_OUTPUT\"\n fi\n - id: tailor-setup-base-pnpm\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] == 'pnpm'\n uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10\n with:\n version: ${{ steps.tailor-detect-base-package-manager.outputs['pnpm-version'] }}\n package_json_file: .tailor-erd-base/package.json\n run_install: false\n - id: tailor-setup-base-node\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] != 'bun'\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7\n with:\n node-version-file: .tailor-erd-base/package.json\n - id: tailor-setup-base-yarn\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] == 'yarn'\n shell: bash\n working-directory: .tailor-erd-base\n run: |\n set -euo pipefail\n corepack enable\n - id: tailor-setup-base-bun\n if: steps.tailor-detect-base-package-manager.outputs['package-manager'] == 'bun'\n uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0\n - id: tailor-install-base\n shell: bash\n working-directory: .tailor-erd-base\n env:\n BASE_PACKAGE_MANAGER: ${{ steps.tailor-detect-base-package-manager.outputs['package-manager'] }}\n run: |\n set -euo pipefail\n case \"$BASE_PACKAGE_MANAGER\" in\n pnpm) pnpm install --frozen-lockfile ;;\n npm) npm ci ;;\n yarn)\n if grep -q '^__metadata:' yarn.lock; then\n yarn install --immutable\n else\n yarn install --frozen-lockfile\n fi\n ;;\n bun) bun install --frozen-lockfile ;;\n *)\n echo \"::error::Unsupported base package-manager '$BASE_PACKAGE_MANAGER' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n - id: tailor-restore-head-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-build-erd-preview\n shell: bash\n env:\n APP_DIR: \"__APP_DIR__\"\n BASE_APP_DIR: ${{ needs.tailor-erd-preview-matrix.outputs['base-app-dir'] }}\n BASE_PACKAGE_MANAGER: ${{ steps.tailor-detect-base-package-manager.outputs['package-manager'] }}\n NAMESPACE: ${{ matrix.namespace }}\n run: |\n set -euo pipefail\n\n run_tailor_cli() {\n case \"__PACKAGE_MANAGER__\" in\n pnpm) pnpm exec tailor \"$@\" ;;\n npm) npx @tailor-platform/sdk \"$@\" ;;\n yarn) yarn tailor \"$@\" ;;\n bun) bun run tailor \"$@\" ;;\n *)\n echo \"::error::Unsupported package-manager '__PACKAGE_MANAGER__' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n }\n\n run_head_node() {\n case \"__PACKAGE_MANAGER__\" in\n pnpm) pnpm exec node \"$@\" ;;\n npm) node \"$@\" ;;\n yarn) yarn node \"$@\" ;;\n bun) node \"$@\" ;;\n *)\n echo \"::error::Unsupported package-manager '__PACKAGE_MANAGER__' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n }\n\n tailor_cli_bin=\"$(\n run_head_node - <<'NODE'\n const path = require(\"node:path\");\n const { createRequire } = require(\"node:module\");\n const appDir = process.env.APP_DIR || \".\";\n const githubWorkspace = process.env.GITHUB_WORKSPACE;\n if (!githubWorkspace) {\n throw new Error(\"GITHUB_WORKSPACE is required.\");\n }\n\n let lastError;\n for (const manifestPath of [\n path.join(githubWorkspace, appDir, \"package.json\"),\n path.join(githubWorkspace, \"package.json\"),\n ]) {\n try {\n const requireFromManifest = createRequire(manifestPath);\n process.stdout.write(\n path.join(\n path.dirname(requireFromManifest.resolve(\"@tailor-platform/sdk/cli\")),\n \"index.mjs\",\n ),\n );\n process.exit(0);\n } catch (error) {\n lastError = error;\n }\n }\n throw lastError || new Error(\"Failed to resolve @tailor-platform/sdk/cli.\");\n NODE\n )\"\n\n run_head_tailor_cli_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_CLI_BIN=\"$tailor_cli_bin\" TAILOR_CLI_CWD=\"$command_cwd\" \"$@\"\n }\n\n (\n cd \"$GITHUB_WORKSPACE\" || exit 1\n case \"__PACKAGE_MANAGER__\" in\n pnpm) run_head_cli_env pnpm exec node \"$head_cli_runner\" \"$@\" ;;\n npm) run_head_cli_env node \"$head_cli_runner\" \"$@\" ;;\n yarn) run_head_cli_env yarn node \"$head_cli_runner\" \"$@\" ;;\n bun) run_head_cli_env bun \"$head_cli_runner\" \"$@\" ;;\n *)\n echo \"::error::Unsupported package-manager '__PACKAGE_MANAGER__' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n )\n }\n\n run_base_tailor_cli_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_CLI_BIN=\"$tailor_cli_bin\" TAILOR_CLI_CWD=\"$command_cwd\" \"$@\"\n }\n\n (\n cd \"$command_cwd\" || exit 1\n case \"$BASE_PACKAGE_MANAGER\" in\n pnpm) run_head_cli_env pnpm exec node \"$head_cli_runner\" \"$@\" ;;\n npm) run_head_cli_env node \"$head_cli_runner\" \"$@\" ;;\n yarn) run_head_cli_env yarn node \"$head_cli_runner\" \"$@\" ;;\n bun) run_head_cli_env bun \"$head_cli_runner\" \"$@\" ;;\n *)\n echo \"::error::Unsupported base package-manager '$BASE_PACKAGE_MANAGER' (expected pnpm, npm, yarn, or bun)\"\n exit 1\n ;;\n esac\n )\n }\n\n head_output=\"$RUNNER_TEMP/tailor-erd/head\"\n base_output=\"$RUNNER_TEMP/tailor-erd/base\"\n preview_output=\"$RUNNER_TEMP/tailor-erd/preview\"\n dts_output=\"$RUNNER_TEMP/tailor-erd/dts\"\n head_cli_runner=\"$RUNNER_TEMP/tailor-erd/run-head-cli.mjs\"\n head_log=\"$RUNNER_TEMP/tailor-erd/head-$NAMESPACE.log\"\n base_log=\"$RUNNER_TEMP/tailor-erd/base-$NAMESPACE.log\"\n rm -rf \"$head_output\" \"$base_output\" \"$preview_output\" \"$dts_output\"\n mkdir -p \"$head_output\" \"$base_output\" \"$preview_output\" \"$dts_output\"\n cat > \"$head_cli_runner\" <<'NODE'\n import { pathToFileURL } from \"node:url\";\n\n const cliBin = process.env.TAILOR_CLI_BIN;\n const commandCwd = process.env.TAILOR_CLI_CWD;\n if (!cliBin || !commandCwd) {\n throw new Error(\"TAILOR_CLI_BIN and TAILOR_CLI_CWD are required.\");\n }\n\n process.chdir(commandCwd);\n process.argv = [process.argv[0], cliBin, ...process.argv.slice(2)];\n await import(pathToFileURL(cliBin).href);\n NODE\n\n head_config=\"$GITHUB_WORKSPACE/$APP_DIR/tailor.config.ts\"\n base_config=\"$GITHUB_WORKSPACE/.tailor-erd-base/$BASE_APP_DIR/tailor.config.ts\"\n head_html=\"$head_output/$NAMESPACE/dist/index.html\"\n base_html=\"$base_output/$NAMESPACE/dist/index.html\"\n preview_html=\"$preview_output/$NAMESPACE-viewer.html\"\n\n head_missing=\"false\"\n if [ ! -f \"$head_config\" ]; then\n echo \"Head ERD config not found; rendering base objects as removed.\"\n head_missing=\"true\"\n elif ! (\n cd \"$GITHUB_WORKSPACE/$APP_DIR\" || exit 1\n TAILOR_PLATFORM_SDK_DTS_PATH=\"$dts_output/head-$NAMESPACE.d.ts\" \\\n run_tailor_cli tailordb erd export --config \"$head_config\" --namespace \"$NAMESPACE\" --output \"$head_output\"\n ) >\"$head_log\" 2>&1; then\n if grep -q 'not found in local config.db' \"$head_log\"; then\n cat \"$head_log\"\n echo \"Head ERD namespace '$NAMESPACE' not found; rendering base objects as removed.\"\n head_missing=\"true\"\n else\n cat \"$head_log\"\n exit 1\n fi\n elif [ -s \"$head_log\" ]; then\n cat \"$head_log\"\n fi\n\n base_missing=\"false\"\n if [ ! -f \"$base_config\" ]; then\n echo \"Base ERD config not found; rendering current objects as added.\"\n base_missing=\"true\"\n elif ! (\n TAILOR_PLATFORM_SDK_DTS_PATH=\"$dts_output/base-$NAMESPACE.d.ts\" \\\n run_base_tailor_cli_bin \"$GITHUB_WORKSPACE/.tailor-erd-base/$BASE_APP_DIR\" tailordb erd export --config \"$base_config\" --namespace \"$NAMESPACE\" --output \"$base_output\"\n ) >\"$base_log\" 2>&1; then\n if grep -q 'not found in local config.db' \"$base_log\"; then\n cat \"$base_log\"\n echo \"Base ERD namespace '$NAMESPACE' not found; rendering current objects as added.\"\n base_missing=\"true\"\n else\n cat \"$base_log\"\n exit 1\n fi\n elif [ -s \"$base_log\" ]; then\n cat \"$base_log\"\n fi\n\n if [ \"$head_missing\" = \"true\" ] && [ \"$base_missing\" = \"true\" ]; then\n echo \"::error::ERD namespace '$NAMESPACE' was not found in head or base.\"\n exit 1\n fi\n if [ \"$head_missing\" = \"false\" ] && [ ! -f \"$head_html\" ]; then\n echo \"::error::Head ERD viewer was not generated at $head_html\"\n exit 1\n fi\n if [ \"$base_missing\" = \"false\" ] && [ ! -f \"$base_html\" ]; then\n echo \"::error::Base ERD viewer was not generated at $base_html\"\n exit 1\n fi\n\n diff_args=(--namespace \"$NAMESPACE\" --output \"$preview_html\")\n if [ \"$head_missing\" = \"false\" ]; then\n diff_args+=(--head-html \"$head_html\")\n fi\n if [ \"$base_missing\" = \"false\" ]; then\n diff_args+=(--base-html \"$base_html\")\n fi\n run_head_tailor_cli_bin \"$GITHUB_WORKSPACE\" tailordb erd diff \"${diff_args[@]}\"\n - id: tailor-upload-erd-viewer\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n with:\n name: ${{ matrix.namespace }}-viewer.html\n path: ${{ runner.temp }}/tailor-erd/preview/${{ matrix.namespace }}-viewer.html\n if-no-files-found: error\n archive: false\n retention-days: 7\n\n # __ERD_PREVIEW_JOB_END__\n # __ERD_PREVIEW_COMMENT_JOB_START__\n tailor-erd-preview-comment:\n needs: tailor-erd-preview\n if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository\n runs-on: ubuntu-latest\n timeout-minutes: 5\n permissions:\n actions: read\n pull-requests: write\n steps:\n - id: tailor-comment-erd-preview\n env:\n GH_TOKEN: ${{ github.token }}\n REPO: ${{ github.repository }}\n RUN_ID: ${{ github.run_id }}\n SERVER: ${{ github.server_url }}\n PR_NUMBER: ${{ github.event.pull_request.number }}\n BASE_REF: ${{ github.event.pull_request.base.ref }}\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n MARKER: \"<!-- tailor-erd-preview: __WORKSPACE_NAME__ -->\"\n run: |\n set -euo pipefail\n\n current_head=$(gh api \"repos/$REPO/pulls/$PR_NUMBER\" --jq '.head.sha')\n if [ \"$current_head\" != \"$HEAD_SHA\" ]; then\n echo \"Skipping stale ERD preview comment for $HEAD_SHA; current head is $current_head.\"\n exit 0\n fi\n\n body=\"$MARKER\"$'\\n'\"### TailorDB ERD preview (__WORKSPACE_NAME__)\"$'\\n\\n'\n body+=\"Self-contained ERD viewer HTML artifacts for this pull request. Each viewer can switch between the current schema and a diff against base branch \\`$BASE_REF\\`.\"$'\\n'\n\n while IFS=$'\\t' read -r name id; do\n [ -z \"$name\" ] && continue\n if [[ \"$name\" != *-viewer.html ]]; then\n continue\n fi\n ns=${name%-viewer.html}\n label=\"$ns viewer\"\n body+=$'\\n'\"- **$label**: [$name]($SERVER/$REPO/actions/runs/$RUN_ID/artifacts/$id)\"\n done < <(gh api \"repos/$REPO/actions/runs/$RUN_ID/artifacts\" --paginate \\\n --jq '.artifacts[] | select(.name | endswith(\"-viewer.html\")) | [.name, (.id|tostring)] | @tsv' | sort)\n\n existing=$(gh api \"repos/$REPO/issues/$PR_NUMBER/comments\" --paginate \\\n --jq 'map(select(.body | contains(env.MARKER))) | .[0].id // empty')\n if [ -n \"$existing\" ]; then\n gh api --method PATCH \"repos/$REPO/issues/comments/$existing\" -f body=\"$body\" >/dev/null\n else\n gh api --method POST \"repos/$REPO/issues/$PR_NUMBER/comments\" -f body=\"$body\" >/dev/null\n fi\n\n # __ERD_PREVIEW_COMMENT_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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n package-manager: __PACKAGE_MANAGER__\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 # __SEED_DATA_START__\n - id: seed-data\n if: false\n shell: bash\n # __WORKING_DIRECTORY__\n run: |\n # Uncomment and implement to seed data on deploy.\n # Run after apply so the schema is ready.\n # Example: node scripts/seed.mjs\n true\n # __SEED_DATA_END__\n - id: slack-prereq\n if: always()\n shell: bash\n env:\n SLACK_TOKEN: ${{ secrets.TAILOR_SLACK_BOT_TOKEN }}\n SLACK_CHANNEL_ID: ${{ vars.TAILOR_SLACK_CHANNEL_ID }}\n run: |\n if [[ -n \"$SLACK_TOKEN\" && -n \"$SLACK_CHANNEL_ID\" ]]; then\n echo \"ready=true\" >> \"$GITHUB_OUTPUT\"\n fi\n - id: tailor-notify\n if: always() && steps.slack-prereq.outputs.ready == 'true'\n uses: tailor-platform/actions/notify@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n provider: slack\n status: ${{ job.status }}\n workspace-name: __WORKSPACE_NAME__\n slack-token: ${{ secrets.TAILOR_SLACK_BOT_TOKEN }}\n slack-channel-id: ${{ vars.TAILOR_SLACK_CHANNEL_ID }}\n # user-mapping: ${{ vars.TAILOR_SLACK_USER_MAPPING }}\n";
|
|
3888
3898
|
|
|
3889
3899
|
//#endregion
|
|
3890
3900
|
//#region src/cli/commands/setup/coordinate.workflow.yml
|
|
3891
|
-
var coordinate_workflow_default = "# __HEADER__\nname: Tailor Coordinator (__WORKSPACE_NAME__)\n\non:\n # __PULL_REQUEST_START__\n pull_request:\n branches: [\"__BRANCH__\"]\n # __PULL_REQUEST_END__\n push:\n # __PUSH_BRANCHES_START__\n branches: [\"__BRANCH__\"]\n # __PUSH_BRANCHES_END__\n # __PUSH_TAGS_START__\n tags: [__TAG_PATTERN__]\n # __PUSH_TAGS_END__\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: 5\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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@
|
|
3901
|
+
var coordinate_workflow_default = "# __HEADER__\nname: Tailor Coordinator (__WORKSPACE_NAME__)\n\non:\n # __PULL_REQUEST_START__\n pull_request:\n branches: [\"__BRANCH__\"]\n # __PULL_REQUEST_END__\n push:\n # __PUSH_BRANCHES_START__\n branches: [\"__BRANCH__\"]\n # __PUSH_BRANCHES_END__\n # __PUSH_TAGS_START__\n tags: [__TAG_PATTERN__]\n # __PUSH_TAGS_END__\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: 5\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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n target-branch: __BRANCH__\n # __TAG_GUARD_JOB_END__\n\n tailor-plan:\n # __PLAN_NEEDS__\n # __PLAN_IF__\n runs-on: ubuntu-latest\n timeout-minutes: 60\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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: ./.github/actions/tailor-setup\n # __PER_APP_PLAN_STEPS__\n\n tailor-deploy:\n # __DEPLOY_NEEDS__\n # __DEPLOY_IF__\n # __DEPLOY_ENVIRONMENT__\n runs-on: ubuntu-latest\n timeout-minutes: 60\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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: ./.github/actions/tailor-setup\n # __PER_APP_DEPLOY_STEPS__\n";
|
|
3892
3902
|
|
|
3893
3903
|
//#endregion
|
|
3894
3904
|
//#region src/cli/commands/setup/preview.workflow.yml
|
|
3895
|
-
var preview_workflow_default = "# __HEADER__\nname: Tailor Preview (__WORKSPACE_NAME__)\n\non:\n pull_request:\n branches: [\"__BRANCH__\"]\n # __PR_TYPES__\n # To deploy only for PRs that carry the `tailor:preview` label instead of all PRs,\n # re-run: tailor setup preview --require-preview-label\n # __PATHS__\n\npermissions:\n contents: read\n pull-requests: write\n\njobs:\n tailor-preview-deploy:\n # __DEPLOY_IF__\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-preview-__WORKSPACE_NAME__-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@
|
|
3905
|
+
var preview_workflow_default = "# __HEADER__\nname: Tailor Preview (__WORKSPACE_NAME__)\n\non:\n pull_request:\n branches: [\"__BRANCH__\"]\n # __PR_TYPES__\n # To deploy only for PRs that carry the `tailor:preview` label instead of all PRs,\n # re-run: tailor setup preview --require-preview-label\n # __PATHS__\n\npermissions:\n contents: read\n pull-requests: write\n\njobs:\n tailor-preview-deploy:\n # __DEPLOY_IF__\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-preview-__WORKSPACE_NAME__-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n - id: tailor-drift-check\n uses: tailor-platform/actions/drift-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n fail-on-drift: ${{ vars.TAILOR_PLATFORM_FAIL_ON_DRIFT == 'true' }}\n - id: tailor-preview-deploy\n uses: tailor-platform/actions/preview-deploy@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-name-prefix: __WORKSPACE_NAME__\n region: \"__REGION__\"\n organization-id: ${{ vars.TAILOR_PLATFORM_ORGANIZATION_ID }}\n folder-id: ${{ vars.TAILOR_PLATFORM_FOLDER_ID }}\n package-manager: __PACKAGE_MANAGER__\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 - id: tailor-preview-comment\n if: always()\n uses: tailor-platform/actions/preview-comment@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-id: ${{ steps.tailor-preview-deploy.outputs.workspace-id }}\n workspace-name: ${{ steps.tailor-preview-deploy.outputs.workspace-name }}\n status: ${{ job.status == 'success' && 'success' || 'failure' }}\n app-url: ${{ steps.tailor-preview-deploy.outputs.app-url }}\n github-token: ${{ secrets.GITHUB_TOKEN }}\n\n tailor-preview-cleanup:\n # __CLEANUP_IF__\n runs-on: ubuntu-latest\n timeout-minutes: 15\n environment: __ENVIRONMENT__\n permissions:\n contents: read\n pull-requests: write\n steps:\n - id: tailor-checkout\n uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-preview-cleanup\n uses: tailor-platform/actions/preview-cleanup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-name-prefix: __WORKSPACE_NAME__\n package-manager: __PACKAGE_MANAGER__\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";
|
|
3896
3906
|
|
|
3897
3907
|
//#endregion
|
|
3898
3908
|
//#region src/cli/commands/setup/tag.workflow.yml
|
|
3899
|
-
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@
|
|
3909
|
+
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n target-branch: \"__BRANCH__\"\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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_START__\n - id: tailor-seed-validate\n run: __TAILOR_CLI__ seed validate\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_END__\n - id: tailor-drift-check\n uses: tailor-platform/actions/drift-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n fail-on-drift: ${{ vars.TAILOR_PLATFORM_FAIL_ON_DRIFT == 'true' }}\n # __MIGRATION_DRIFT_CHECK_START__\n - id: tailor-migration-drift-check\n uses: tailor-platform/actions/migration-drift-check@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __MIGRATION_DRIFT_CHECK_END__\n - id: tailor-plan\n uses: tailor-platform/actions/plan@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n package-manager: __PACKAGE_MANAGER__\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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n - id: tailor-setup\n uses: tailor-platform/actions/setup@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n package-manager: __PACKAGE_MANAGER__\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 # __SEED_DATA_START__\n - id: seed-data\n if: false\n shell: bash\n # __WORKING_DIRECTORY__\n run: |\n # Uncomment and implement to seed data on deploy.\n # Run after apply so the schema is ready.\n # Example: node scripts/seed.mjs\n true\n # __SEED_DATA_END__\n - id: slack-prereq\n if: always()\n shell: bash\n env:\n SLACK_TOKEN: ${{ secrets.TAILOR_SLACK_BOT_TOKEN }}\n SLACK_CHANNEL_ID: ${{ vars.TAILOR_SLACK_CHANNEL_ID }}\n run: |\n if [[ -n \"$SLACK_TOKEN\" && -n \"$SLACK_CHANNEL_ID\" ]]; then\n echo \"ready=true\" >> \"$GITHUB_OUTPUT\"\n fi\n - id: tailor-notify\n if: always() && steps.slack-prereq.outputs.ready == 'true'\n uses: tailor-platform/actions/notify@85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c # v2.2.0\n with:\n provider: slack\n status: ${{ job.status }}\n workspace-name: __WORKSPACE_NAME__\n slack-token: ${{ secrets.TAILOR_SLACK_BOT_TOKEN }}\n slack-channel-id: ${{ vars.TAILOR_SLACK_CHANNEL_ID }}\n # user-mapping: ${{ vars.TAILOR_SLACK_USER_MAPPING }}\n";
|
|
3900
3910
|
|
|
3901
3911
|
//#endregion
|
|
3902
3912
|
//#region src/cli/commands/setup/templates.ts
|
|
@@ -4044,6 +4054,7 @@ function renderPreviewWorkflow(params) {
|
|
|
4044
4054
|
"tailor-preview-deploy/tailor-setup",
|
|
4045
4055
|
"tailor-preview-deploy/tailor-install",
|
|
4046
4056
|
"tailor-preview-deploy/tailor-generate-check",
|
|
4057
|
+
"tailor-preview-deploy/tailor-drift-check",
|
|
4047
4058
|
"tailor-preview-deploy/tailor-preview-deploy",
|
|
4048
4059
|
"tailor-preview-deploy/tailor-preview-comment",
|
|
4049
4060
|
"tailor-preview-cleanup",
|
|
@@ -4072,8 +4083,8 @@ function renderActionWorkflow(params) {
|
|
|
4072
4083
|
generatedIds: ["tailor-apply", "tailor-notify"]
|
|
4073
4084
|
};
|
|
4074
4085
|
}
|
|
4075
|
-
const ACTIONS_SHA = "
|
|
4076
|
-
const ACTIONS_VERSION = "v2.
|
|
4086
|
+
const ACTIONS_SHA = "85f0ec85d3bcbdec915daa315b6ddf46ce9b6d2c";
|
|
4087
|
+
const ACTIONS_VERSION = "v2.2.0";
|
|
4077
4088
|
/**
|
|
4078
4089
|
* Render the coordinator workflow that orchestrates per-app composite actions.
|
|
4079
4090
|
*
|
|
@@ -4117,7 +4128,8 @@ function renderCoordinateWorkflow(params) {
|
|
|
4117
4128
|
`- id: tailor-drift-check`,
|
|
4118
4129
|
` uses: tailor-platform/actions/drift-check@${ACTIONS_SHA} # ${ACTIONS_VERSION}`,
|
|
4119
4130
|
` with:`,
|
|
4120
|
-
` package-manager: ${packageManager}
|
|
4131
|
+
` package-manager: ${packageManager}`,
|
|
4132
|
+
` fail-on-drift: \${{ vars.TAILOR_PLATFORM_FAIL_ON_DRIFT == 'true' }}`
|
|
4121
4133
|
].join("\n");
|
|
4122
4134
|
const configPath = (app) => app.dir === "." ? "tailor.config.ts" : `${app.dir}/tailor.config.ts`;
|
|
4123
4135
|
const groupConfigPath = (group) => group.apps.map(configPath).join(",");
|
|
@@ -4621,7 +4633,7 @@ async function setupTarget(options) {
|
|
|
4621
4633
|
kind: resolved.kind,
|
|
4622
4634
|
workspaceName: resolved.workspaceName,
|
|
4623
4635
|
file: resolved.file,
|
|
4624
|
-
templateVersion:
|
|
4636
|
+
templateVersion: 11,
|
|
4625
4637
|
inputs: resolved.inputs,
|
|
4626
4638
|
generatedIds: resolved.render.generatedIds,
|
|
4627
4639
|
ejectedIds: existing?.ejectedIds ?? [],
|
|
@@ -4689,7 +4701,7 @@ async function setupCoordinate(options) {
|
|
|
4689
4701
|
seenNames.add(name);
|
|
4690
4702
|
const entry = actionTargets.get(name);
|
|
4691
4703
|
if (!entry) throw new Error(`Action target "${name}" not found in .github/tailor.lock. Run \`tailor setup action --name ${name}\` first.`);
|
|
4692
|
-
if (names.length > 1 && entry.templateVersion <
|
|
4704
|
+
if (names.length > 1 && entry.templateVersion < 11) throw new Error(`Action target "${name}" was generated with an older setup template. Run \`tailor setup action --name ${name} --force\` before grouping it in setup coordinate.`);
|
|
4693
4705
|
validateDir(entry.inputs.dir);
|
|
4694
4706
|
return {
|
|
4695
4707
|
name,
|
|
@@ -4741,7 +4753,7 @@ async function setupCoordinate(options) {
|
|
|
4741
4753
|
kind: "coordinate",
|
|
4742
4754
|
workspaceName: coordinatorName,
|
|
4743
4755
|
file,
|
|
4744
|
-
templateVersion:
|
|
4756
|
+
templateVersion: 11,
|
|
4745
4757
|
inputs: {
|
|
4746
4758
|
branch,
|
|
4747
4759
|
branchAutoDetected: coordinateKind === "branch" ? branchAutoDetected : branch !== null ? false : void 0,
|
|
@@ -4785,6 +4797,7 @@ async function setupCoordinate(options) {
|
|
|
4785
4797
|
|
|
4786
4798
|
//#endregion
|
|
4787
4799
|
//#region src/cli/commands/setup/check.ts
|
|
4800
|
+
const DRIFT_COUNT_MARKER = "TAILOR_SETUP_CHECK_DRIFT_COUNT";
|
|
4788
4801
|
/**
|
|
4789
4802
|
* Compute drift findings for one target by comparing its recorded lock state
|
|
4790
4803
|
* against the current repository/config state.
|
|
@@ -4929,7 +4942,7 @@ async function checkGitHub(options) {
|
|
|
4929
4942
|
currentHash,
|
|
4930
4943
|
configExists,
|
|
4931
4944
|
defaultBranch,
|
|
4932
|
-
templateVersion:
|
|
4945
|
+
templateVersion: 11,
|
|
4933
4946
|
erdNamespaces,
|
|
4934
4947
|
hasMigrations,
|
|
4935
4948
|
hasSeeds,
|
|
@@ -4942,6 +4955,7 @@ async function checkGitHub(options) {
|
|
|
4942
4955
|
return;
|
|
4943
4956
|
}
|
|
4944
4957
|
for (const finding of findings) logger.warn(`[${finding.target}] ${finding.message} (ignore key: ${finding.rule})`);
|
|
4958
|
+
if (options.ci) logger.log(`${DRIFT_COUNT_MARKER}=${String(findings.length)}`);
|
|
4945
4959
|
throw new Error(`Detected ${String(findings.length)} drift finding(s) across ${String(count)} target(s). Re-run \`tailor setup\` to regenerate, or address each finding above.`);
|
|
4946
4960
|
}
|
|
4947
4961
|
|
|
@@ -5014,6 +5028,134 @@ async function setupDelete(options) {
|
|
|
5014
5028
|
}
|
|
5015
5029
|
}
|
|
5016
5030
|
|
|
5031
|
+
//#endregion
|
|
5032
|
+
//#region src/cli/commands/setup/renovate.ts
|
|
5033
|
+
const RENOVATE_CONFIG_FILE = "renovate.json";
|
|
5034
|
+
const RENOVATE_PRESET = "github>tailor-inc/renovate-config";
|
|
5035
|
+
const RENOVATE_CONFIG_FILES = [
|
|
5036
|
+
RENOVATE_CONFIG_FILE,
|
|
5037
|
+
"renovate.jsonc",
|
|
5038
|
+
"renovate.json5",
|
|
5039
|
+
".github/renovate.json",
|
|
5040
|
+
".github/renovate.jsonc",
|
|
5041
|
+
".github/renovate.json5",
|
|
5042
|
+
".gitlab/renovate.json",
|
|
5043
|
+
".gitlab/renovate.jsonc",
|
|
5044
|
+
".gitlab/renovate.json5",
|
|
5045
|
+
".renovaterc",
|
|
5046
|
+
".renovaterc.json",
|
|
5047
|
+
".renovaterc.jsonc",
|
|
5048
|
+
".renovaterc.json5"
|
|
5049
|
+
];
|
|
5050
|
+
function getPathEntry(filePath) {
|
|
5051
|
+
try {
|
|
5052
|
+
return fs.lstatSync(filePath);
|
|
5053
|
+
} catch (error) {
|
|
5054
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
5055
|
+
throw error;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
function pathEntryExists(filePath) {
|
|
5059
|
+
return getPathEntry(filePath) !== null;
|
|
5060
|
+
}
|
|
5061
|
+
function extendsPreset(config) {
|
|
5062
|
+
if (!("extends" in config)) return false;
|
|
5063
|
+
const value = config.extends;
|
|
5064
|
+
return Array.isArray(value) && value.includes("github>tailor-inc/renovate-config");
|
|
5065
|
+
}
|
|
5066
|
+
function readJsonConfig(filePath) {
|
|
5067
|
+
const entry = getPathEntry(filePath);
|
|
5068
|
+
if (entry === null || !entry.isFile()) return null;
|
|
5069
|
+
let parsed;
|
|
5070
|
+
try {
|
|
5071
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5072
|
+
} catch {
|
|
5073
|
+
return null;
|
|
5074
|
+
}
|
|
5075
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
5076
|
+
}
|
|
5077
|
+
function classifyConfig(location, filePath, config) {
|
|
5078
|
+
if (config === null) return {
|
|
5079
|
+
kind: "unreadable",
|
|
5080
|
+
location
|
|
5081
|
+
};
|
|
5082
|
+
if (extendsPreset(config)) return {
|
|
5083
|
+
kind: "extends-preset",
|
|
5084
|
+
location
|
|
5085
|
+
};
|
|
5086
|
+
return {
|
|
5087
|
+
kind: "appendable",
|
|
5088
|
+
location,
|
|
5089
|
+
filePath,
|
|
5090
|
+
config
|
|
5091
|
+
};
|
|
5092
|
+
}
|
|
5093
|
+
function findExistingConfig(outputDir) {
|
|
5094
|
+
for (const file of RENOVATE_CONFIG_FILES) {
|
|
5095
|
+
const filePath = path.join(outputDir, file);
|
|
5096
|
+
if (!pathEntryExists(filePath)) continue;
|
|
5097
|
+
return classifyConfig(file, filePath, readJsonConfig(filePath));
|
|
5098
|
+
}
|
|
5099
|
+
const packageJsonPath = path.join(outputDir, "package.json");
|
|
5100
|
+
if (!fs.existsSync(packageJsonPath)) return null;
|
|
5101
|
+
let packageJson;
|
|
5102
|
+
try {
|
|
5103
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
5104
|
+
} catch (cause) {
|
|
5105
|
+
throw new Error("package.json is not valid JSON; could not inspect its Renovate config.", { cause });
|
|
5106
|
+
}
|
|
5107
|
+
if (typeof packageJson === "object" && packageJson !== null && "renovate" in packageJson) {
|
|
5108
|
+
const renovate = packageJson.renovate;
|
|
5109
|
+
return classifyConfig("package.json#renovate", packageJsonPath, typeof renovate === "object" && renovate !== null && !Array.isArray(renovate) ? renovate : null);
|
|
5110
|
+
}
|
|
5111
|
+
return null;
|
|
5112
|
+
}
|
|
5113
|
+
function detectIndent(source) {
|
|
5114
|
+
return /^(?<indent>[ ]+)"/m.exec(source)?.groups?.indent?.length ?? 2;
|
|
5115
|
+
}
|
|
5116
|
+
function appendPreset(existing) {
|
|
5117
|
+
const source = fs.readFileSync(existing.filePath, "utf-8");
|
|
5118
|
+
const current = "extends" in existing.config ? existing.config.extends : void 0;
|
|
5119
|
+
if (current !== void 0 && !Array.isArray(current)) throw new Error(`Renovate config at "${existing.location}" has a non-array "extends". No files were changed. Add "${RENOVATE_PRESET}" to it manually.`);
|
|
5120
|
+
const config = {
|
|
5121
|
+
...existing.config,
|
|
5122
|
+
extends: [...current ?? [], RENOVATE_PRESET]
|
|
5123
|
+
};
|
|
5124
|
+
const root = existing.location === "package.json#renovate" ? {
|
|
5125
|
+
...JSON.parse(source),
|
|
5126
|
+
renovate: config
|
|
5127
|
+
} : config;
|
|
5128
|
+
const trailingNewline = source.endsWith("\n") ? "\n" : "";
|
|
5129
|
+
fs.writeFileSync(existing.filePath, `${JSON.stringify(root, null, detectIndent(source))}${trailingNewline}`, "utf-8");
|
|
5130
|
+
}
|
|
5131
|
+
function renderRenovateConfig() {
|
|
5132
|
+
return `${JSON.stringify({
|
|
5133
|
+
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
|
5134
|
+
extends: [RENOVATE_PRESET]
|
|
5135
|
+
}, null, 2)}\n`;
|
|
5136
|
+
}
|
|
5137
|
+
/**
|
|
5138
|
+
* Generate a repository-level Renovate config that extends Tailor's shared preset.
|
|
5139
|
+
* @param options - Renovate setup options
|
|
5140
|
+
*/
|
|
5141
|
+
async function setupRenovate(options) {
|
|
5142
|
+
logBetaWarning("setup");
|
|
5143
|
+
const existingConfig = findExistingConfig(options.outputDir);
|
|
5144
|
+
if (existingConfig !== null) {
|
|
5145
|
+
if (existingConfig.kind === "extends-preset") {
|
|
5146
|
+
logger.info(`Renovate is already set up at ${styles.path(existingConfig.location)}.`);
|
|
5147
|
+
return;
|
|
5148
|
+
}
|
|
5149
|
+
if (existingConfig.kind === "unreadable") throw new Error(`Found a Renovate config at "${existingConfig.location}" but could not parse it as JSON. Comment-containing JSON5/JSONC configs are not supported yet. No files were changed. Check its extends array for "${RENOVATE_PRESET}" manually.`);
|
|
5150
|
+
appendPreset(existingConfig);
|
|
5151
|
+
logger.success(`Added the Tailor preset to ${styles.path(existingConfig.location)}`);
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5154
|
+
const outputPath = path.join(options.outputDir, RENOVATE_CONFIG_FILE);
|
|
5155
|
+
fs.writeFileSync(outputPath, renderRenovateConfig(), "utf-8");
|
|
5156
|
+
logger.success(`Generated ${styles.path(RENOVATE_CONFIG_FILE)}`);
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5017
5159
|
//#endregion
|
|
5018
5160
|
//#region src/cli/commands/setup/index.ts
|
|
5019
5161
|
const checkCommand = defineAppCommand({
|
|
@@ -5172,6 +5314,14 @@ const previewCommand = defineAppCommand({
|
|
|
5172
5314
|
});
|
|
5173
5315
|
}
|
|
5174
5316
|
});
|
|
5317
|
+
const renovateCommand = defineAppCommand({
|
|
5318
|
+
name: "renovate",
|
|
5319
|
+
description: "Generate a Renovate config for Tailor dependency and workflow updates.",
|
|
5320
|
+
args: z.strictObject({}),
|
|
5321
|
+
run: async () => {
|
|
5322
|
+
await setupRenovate({ outputDir: process.cwd() });
|
|
5323
|
+
}
|
|
5324
|
+
});
|
|
5175
5325
|
const deleteCommand$1 = defineAppCommand({
|
|
5176
5326
|
name: "delete",
|
|
5177
5327
|
description: "Delete managed workflow/action file(s) and their .github/tailor.lock entries.",
|
|
@@ -5192,13 +5342,14 @@ const deleteCommand$1 = defineAppCommand({
|
|
|
5192
5342
|
});
|
|
5193
5343
|
const setupCommand = defineCommand({
|
|
5194
5344
|
name: "setup",
|
|
5195
|
-
description: "
|
|
5345
|
+
description: "Set up repository automation for your project. (beta)",
|
|
5196
5346
|
subCommands: {
|
|
5197
5347
|
branch: branchCommand,
|
|
5198
5348
|
tag: tagCommand,
|
|
5199
5349
|
preview: previewCommand,
|
|
5200
5350
|
action: actionCommand,
|
|
5201
5351
|
coordinate: coordinateCommand,
|
|
5352
|
+
renovate: renovateCommand,
|
|
5202
5353
|
check: checkCommand,
|
|
5203
5354
|
delete: deleteCommand$1
|
|
5204
5355
|
}
|
|
@@ -5425,164 +5576,214 @@ const staticwebsiteCommand = defineCommand({
|
|
|
5425
5576
|
});
|
|
5426
5577
|
|
|
5427
5578
|
//#endregion
|
|
5428
|
-
//#region src/cli/commands/tailordb/migrate/
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5579
|
+
//#region src/cli/commands/tailordb/migrate/rebaseline.ts
|
|
5580
|
+
const MIGRATION_ARTIFACT_NAMES = new Set(Object.values(MIGRATION_FILE_NAMES));
|
|
5581
|
+
async function activateBaseline(migrationsDir, snapshot, namespace, rebaseline) {
|
|
5582
|
+
const resolvedMigrationsDir = await fsPromises.realpath(migrationsDir);
|
|
5583
|
+
const parentDir = path.dirname(resolvedMigrationsDir);
|
|
5584
|
+
const baseName = path.basename(resolvedMigrationsDir);
|
|
5585
|
+
const stagingDir = await fsPromises.mkdtemp(path.join(parentDir, `.${baseName}-rebaseline-`));
|
|
5586
|
+
const backupDir = `${stagingDir}-previous`;
|
|
5587
|
+
let movedLiveDirectory = false;
|
|
5588
|
+
let activatedStagingDirectory = false;
|
|
5589
|
+
try {
|
|
5590
|
+
await fsPromises.cp(resolvedMigrationsDir, stagingDir, { recursive: true });
|
|
5591
|
+
for (const entry of await fsPromises.readdir(stagingDir, { withFileTypes: true })) {
|
|
5592
|
+
if (!entry.isDirectory() || !MIGRATION_NUMBER_PATTERN.test(entry.name)) continue;
|
|
5593
|
+
const entryPath = path.join(stagingDir, entry.name);
|
|
5594
|
+
if (!(await fsPromises.readdir(entryPath)).some((childName) => MIGRATION_ARTIFACT_NAMES.has(childName))) continue;
|
|
5595
|
+
await fsPromises.rm(entryPath, {
|
|
5596
|
+
recursive: true,
|
|
5597
|
+
force: true
|
|
5598
|
+
});
|
|
5599
|
+
}
|
|
5600
|
+
loadSnapshot((await generateSchemaFile({
|
|
5601
|
+
...snapshot,
|
|
5602
|
+
namespace,
|
|
5603
|
+
version: 5,
|
|
5604
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5605
|
+
rebaseline
|
|
5606
|
+
}, stagingDir, 0)).filePath);
|
|
5607
|
+
await fsPromises.rename(resolvedMigrationsDir, backupDir);
|
|
5608
|
+
movedLiveDirectory = true;
|
|
5609
|
+
try {
|
|
5610
|
+
await fsPromises.rename(stagingDir, resolvedMigrationsDir);
|
|
5611
|
+
activatedStagingDirectory = true;
|
|
5612
|
+
} catch (error) {
|
|
5613
|
+
await fsPromises.rename(backupDir, resolvedMigrationsDir);
|
|
5614
|
+
movedLiveDirectory = false;
|
|
5615
|
+
throw error;
|
|
5616
|
+
}
|
|
5617
|
+
try {
|
|
5618
|
+
await fsPromises.rm(backupDir, {
|
|
5619
|
+
recursive: true,
|
|
5620
|
+
force: true
|
|
5621
|
+
});
|
|
5622
|
+
} catch (error) {
|
|
5623
|
+
logger.warn(`The previous migration directory remains at ${backupDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5624
|
+
}
|
|
5625
|
+
movedLiveDirectory = false;
|
|
5626
|
+
} finally {
|
|
5627
|
+
if (fs.existsSync(stagingDir)) await fsPromises.rm(stagingDir, {
|
|
5628
|
+
recursive: true,
|
|
5629
|
+
force: true
|
|
5630
|
+
});
|
|
5631
|
+
if (!activatedStagingDirectory && movedLiveDirectory && fs.existsSync(backupDir)) await fsPromises.rename(backupDir, resolvedMigrationsDir);
|
|
5632
|
+
}
|
|
5472
5633
|
}
|
|
5473
|
-
|
|
5474
|
-
* Add a migrate.ts template to an existing migration directory.
|
|
5475
|
-
* @param {ScriptOptions} options - Command options
|
|
5476
|
-
*/
|
|
5477
|
-
async function script(options) {
|
|
5634
|
+
async function rebaseline(options) {
|
|
5478
5635
|
logBetaWarning("tailordb migration");
|
|
5479
|
-
const
|
|
5480
|
-
|
|
5481
|
-
const
|
|
5482
|
-
const
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5636
|
+
const { config, plugins } = await loadConfig(options.configPath);
|
|
5637
|
+
const configDir = path.dirname(config.path);
|
|
5638
|
+
const target = selectTargetNamespace(getNamespacesWithMigrations(config, configDir), options.namespace);
|
|
5639
|
+
const generateCommand = formatNextAction({
|
|
5640
|
+
command: "tailor",
|
|
5641
|
+
args: [
|
|
5642
|
+
"tailordb",
|
|
5643
|
+
"migration",
|
|
5644
|
+
"generate",
|
|
5645
|
+
"--config",
|
|
5646
|
+
config.path
|
|
5647
|
+
]
|
|
5648
|
+
});
|
|
5649
|
+
assertValidMigrationFiles(target.migrationsDir, target.namespace);
|
|
5650
|
+
const latestSnapshot = reconstructSnapshotFromMigrations(target.migrationsDir);
|
|
5651
|
+
if (!latestSnapshot) throw new Error(`No migration history found for namespace "${target.namespace}". Run ${generateCommand} first.`);
|
|
5652
|
+
const latestMigration = getLatestMigrationNumber(target.migrationsDir);
|
|
5653
|
+
const currentHistoryId = reconstructSnapshotFromMigrations(target.migrationsDir, 0)?.rebaseline?.historyId ?? null;
|
|
5654
|
+
const initialFileState = captureMigrationFileState([target])[target.namespace];
|
|
5655
|
+
const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
|
|
5656
|
+
const { defineApplication } = await import("../application-BZfk4HKm.mjs");
|
|
5657
|
+
const application = defineApplication({
|
|
5658
|
+
config,
|
|
5659
|
+
pluginManager
|
|
5660
|
+
});
|
|
5661
|
+
for (const service of application.tailorDBServices) {
|
|
5662
|
+
await service.loadTypes();
|
|
5663
|
+
await service.processNamespacePlugins();
|
|
5498
5664
|
}
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5665
|
+
const targetService = application.tailorDBServices.find((service) => service.namespace === target.namespace);
|
|
5666
|
+
if (!targetService) throw new Error(`No TailorDB service found for namespace "${target.namespace}"`);
|
|
5667
|
+
const assertLocalTypesReady = () => {
|
|
5668
|
+
const currentSnapshot = createSnapshotFromLocalTypes(targetService.types, target.namespace);
|
|
5669
|
+
const localDiff = compareLocalTypesWithSnapshot(latestSnapshot, currentSnapshot.tables, target.namespace);
|
|
5670
|
+
if (hasChanges(localDiff)) {
|
|
5671
|
+
logger.error(`Migration history does not reproduce the current local schema for namespace ${styles.bold(target.namespace)}:`);
|
|
5672
|
+
logger.log(formatMigrationDiff(localDiff));
|
|
5673
|
+
throw new Error(`Refusing to re-baseline: the migration history must reproduce the current local schema. Run ${generateCommand} first.`);
|
|
5674
|
+
}
|
|
5675
|
+
};
|
|
5676
|
+
const localSourceFiles = () => [config.path, ...loadFilesWithIgnores(targetService.config, configDir)];
|
|
5677
|
+
assertLocalTypesReady();
|
|
5678
|
+
const initialLocalFileState = captureFileState(localSourceFiles());
|
|
5679
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
5680
|
+
const workspaceId = await loadWorkspaceId({
|
|
5681
|
+
workspaceId: options.workspaceId,
|
|
5682
|
+
profile: options.profile
|
|
5683
|
+
});
|
|
5684
|
+
const remoteContextArgs = [
|
|
5685
|
+
"--config",
|
|
5686
|
+
config.path,
|
|
5687
|
+
"--workspace-id",
|
|
5688
|
+
workspaceId,
|
|
5689
|
+
...options.profile ? ["--profile", options.profile] : []
|
|
5690
|
+
];
|
|
5691
|
+
const setBaselineCommand = formatNextAction({
|
|
5692
|
+
command: "tailor",
|
|
5693
|
+
args: [
|
|
5694
|
+
"tailordb",
|
|
5695
|
+
"migration",
|
|
5696
|
+
"set",
|
|
5697
|
+
"0",
|
|
5698
|
+
"--namespace",
|
|
5699
|
+
target.namespace,
|
|
5700
|
+
...remoteContextArgs
|
|
5701
|
+
]
|
|
5702
|
+
});
|
|
5703
|
+
const deployCommand = formatNextAction({
|
|
5704
|
+
command: "tailor",
|
|
5705
|
+
args: ["deploy", ...remoteContextArgs]
|
|
5706
|
+
});
|
|
5707
|
+
const assertConnectedWorkspaceReady = async (expectedHistoryId, phase) => {
|
|
5708
|
+
const remoteState = await fetchRemoteMigrationState(client, resourceTrn(workspaceId, "tailordb", target.namespace));
|
|
5709
|
+
const remoteMigration = remoteState.number;
|
|
5710
|
+
if (remoteMigration !== latestMigration) {
|
|
5711
|
+
const actual = remoteMigration === null ? "<unset>" : formatMigrationNumber(remoteMigration);
|
|
5712
|
+
throw new Error(`The connected workspace must be at the latest migration ${formatMigrationNumber(latestMigration)} before re-baselining; current checkpoint is ${actual}.`);
|
|
5713
|
+
}
|
|
5714
|
+
if (remoteState.historyIdInvalid) throw new Error("Refusing to re-baseline: the connected workspace has an invalid migration history marker.");
|
|
5715
|
+
if (remoteState.historyId !== expectedHistoryId) throw new Error(phase === "confirmation" ? "The connected workspace migration history changed while waiting for confirmation. Run the command again." : "Refusing to re-baseline: the connected workspace does not match the local migration history.");
|
|
5716
|
+
const remoteResults = await verifyRemoteSchema(client, workspaceId, [target], config, [toTailorDBDeployInput(targetService)]);
|
|
5717
|
+
const remoteResult = remoteResults[0];
|
|
5718
|
+
if (!remoteResult || remoteResult.remoteMigrationNumber !== latestMigration || remoteResult.hasDrift || remoteResult.checkpointMissingLocal) {
|
|
5719
|
+
if (remoteResult?.hasDrift) {
|
|
5720
|
+
logger.error("Remote schema drift detected:");
|
|
5721
|
+
logger.log(formatRemoteVerificationResults(remoteResults));
|
|
5722
|
+
}
|
|
5723
|
+
throw new Error("Refusing to re-baseline: the connected workspace remote schema must match the latest migration.");
|
|
5724
|
+
}
|
|
5725
|
+
};
|
|
5726
|
+
await assertConnectedWorkspaceReady(currentHistoryId, "initial");
|
|
5514
5727
|
logger.newline();
|
|
5515
|
-
logger.
|
|
5516
|
-
logger.log(
|
|
5517
|
-
|
|
5518
|
-
|
|
5728
|
+
logger.warn(`This will replace the migration history for ${styles.bold(target.namespace)}.`);
|
|
5729
|
+
logger.log(` Latest migration: ${formatMigrationNumber(latestMigration)}`);
|
|
5730
|
+
logger.log(" New history: 0000/schema.json only");
|
|
5731
|
+
logger.log(" migrate.ts and db.ts files will disappear from the working tree.");
|
|
5732
|
+
logger.log(" Committed migration files will remain in Git history. Preserve any uncommitted files before continuing.");
|
|
5733
|
+
logger.log(" Every other environment must already be at the latest migration.");
|
|
5734
|
+
logger.log(" The connected workspace checkpoint will be reset to 0000.");
|
|
5519
5735
|
logger.newline();
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
if (requested) {
|
|
5529
|
-
if (!namespacesWithMigrations.some((ns) => ns.namespace === requested)) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
|
|
5530
|
-
return requested;
|
|
5736
|
+
if (!options.yes) {
|
|
5737
|
+
if (!await prompt.confirm({
|
|
5738
|
+
message: "Re-baseline this migration history and reset the connected workspace checkpoint?",
|
|
5739
|
+
default: false
|
|
5740
|
+
})) {
|
|
5741
|
+
logger.info("Operation cancelled.");
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5531
5744
|
}
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5745
|
+
assertValidMigrationFiles(target.migrationsDir, target.namespace);
|
|
5746
|
+
if (captureMigrationFileState([target])[target.namespace] !== initialFileState) throw new Error("Migration files changed while waiting for confirmation. Run the command again.");
|
|
5747
|
+
if (captureFileState(localSourceFiles()) !== initialLocalFileState) throw new Error("Local TailorDB type or config files changed. Run the command again.");
|
|
5748
|
+
assertLocalTypesReady();
|
|
5749
|
+
await assertConnectedWorkspaceReady(currentHistoryId, "confirmation");
|
|
5750
|
+
const rebaselineMarker = {
|
|
5751
|
+
historyId: createMigrationHistoryId(),
|
|
5752
|
+
replacedHistoryId: currentHistoryId,
|
|
5753
|
+
replacedLatestMigration: latestMigration
|
|
5754
|
+
};
|
|
5755
|
+
await activateBaseline(target.migrationsDir, latestSnapshot, target.namespace, rebaselineMarker);
|
|
5756
|
+
try {
|
|
5757
|
+
await updateMigrationLabel(client, workspaceId, target.namespace, 0, rebaselineMarker.historyId);
|
|
5758
|
+
} catch (error) {
|
|
5759
|
+
throw new Error(`The local migration history was re-baselined, but the connected workspace checkpoint could not be updated. Run ${setBaselineCommand}, or run ${deployCommand} with schema checks enabled after resolving the connection error.`, { cause: error });
|
|
5535
5760
|
}
|
|
5536
|
-
|
|
5761
|
+
logger.success(`Re-baselined ${styles.bold(target.namespace)} and reset the connected workspace checkpoint to 0000.`);
|
|
5537
5762
|
}
|
|
5538
|
-
const
|
|
5539
|
-
name: "
|
|
5540
|
-
description: "
|
|
5763
|
+
const rebaselineCommand = defineAppCommand({
|
|
5764
|
+
name: "rebaseline",
|
|
5765
|
+
description: "Collapse the full migration history into a new 0000 baseline.",
|
|
5766
|
+
notes: `Re-baselining removes migrations after 0000 from the working tree, records a new migration history ID, and resets the connected workspace checkpoint without changing its schema or data. Every environment must already have applied the latest migration before you run this command.`,
|
|
5541
5767
|
args: z.strictObject({
|
|
5542
|
-
...
|
|
5543
|
-
|
|
5544
|
-
positional: true,
|
|
5545
|
-
description: "Migration number to add a script to (e.g., 0001 or 1)"
|
|
5546
|
-
}),
|
|
5768
|
+
...deploymentArgs,
|
|
5769
|
+
...confirmationArgs,
|
|
5547
5770
|
namespace: arg(z.string().optional(), {
|
|
5548
5771
|
alias: "n",
|
|
5549
5772
|
description: "Target TailorDB namespace (required if multiple namespaces exist)"
|
|
5550
|
-
})
|
|
5551
|
-
"no-script": arg(z.boolean().optional(), { description: "Record that this migration intentionally runs without a migration script (requires --reason)" }),
|
|
5552
|
-
reason: arg(z.string().optional(), { description: "Reason why no migration script is needed (used with --no-script)" })
|
|
5773
|
+
})
|
|
5553
5774
|
}),
|
|
5554
5775
|
run: async (args) => {
|
|
5555
|
-
await
|
|
5776
|
+
await assertWritable({ profile: args.profile });
|
|
5777
|
+
await rebaseline({
|
|
5556
5778
|
configPath: args.config,
|
|
5557
|
-
number: args.number,
|
|
5558
5779
|
namespace: args.namespace,
|
|
5559
|
-
|
|
5560
|
-
|
|
5780
|
+
yes: args.yes,
|
|
5781
|
+
workspaceId: args["workspace-id"],
|
|
5782
|
+
profile: args.profile
|
|
5561
5783
|
});
|
|
5562
5784
|
}
|
|
5563
5785
|
});
|
|
5564
5786
|
|
|
5565
|
-
//#endregion
|
|
5566
|
-
//#region src/cli/commands/tailordb/migrate/remote-state.ts
|
|
5567
|
-
/**
|
|
5568
|
-
* Fetch the namespace's current migration number from its metadata labels.
|
|
5569
|
-
*
|
|
5570
|
-
* Only a namespace that has not been deployed yet reads as "no current
|
|
5571
|
-
* migration". Every other lookup failure must propagate: treating it as
|
|
5572
|
-
* unset would misreport a transient error as "no checkpoint", showing
|
|
5573
|
-
* every migration as pending to callers.
|
|
5574
|
-
* @param client - Operator client
|
|
5575
|
-
* @param trn - Namespace TRN
|
|
5576
|
-
* @returns Parsed current migration number, or null when unset or unparseable
|
|
5577
|
-
*/
|
|
5578
|
-
async function fetchRemoteMigrationNumber(client, trn) {
|
|
5579
|
-
const label = (await getOrNull(async () => {
|
|
5580
|
-
const { metadata } = await client.getMetadata({ trn });
|
|
5581
|
-
return metadata;
|
|
5582
|
-
}))?.labels[MIGRATION_LABEL_KEY];
|
|
5583
|
-
return label ? parseMigrationLabelNumber(label) : null;
|
|
5584
|
-
}
|
|
5585
|
-
|
|
5586
5787
|
//#endregion
|
|
5587
5788
|
//#region src/cli/commands/tailordb/migrate/set.ts
|
|
5588
5789
|
/**
|
|
@@ -5597,18 +5798,24 @@ async function set(options) {
|
|
|
5597
5798
|
const targetNamespace = target.namespace;
|
|
5598
5799
|
assertValidMigrationFiles(target.migrationsDir, targetNamespace);
|
|
5599
5800
|
assertMigrationNumberExists(target.migrationsDir, migrationNumber);
|
|
5801
|
+
const historyId = reconstructSnapshotFromMigrations(target.migrationsDir, 0)?.rebaseline?.historyId;
|
|
5600
5802
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
5601
5803
|
const trn = resourceTrn(await loadWorkspaceId({
|
|
5602
5804
|
workspaceId: options.workspaceId,
|
|
5603
5805
|
profile: options.profile
|
|
5604
5806
|
}), "tailordb", targetNamespace);
|
|
5605
|
-
const
|
|
5807
|
+
const currentState = await fetchRemoteMigrationState(client, trn);
|
|
5808
|
+
const current = currentState.number;
|
|
5606
5809
|
const currentMigration = current ?? 0;
|
|
5810
|
+
const currentHistoryId = currentState.historyIdInvalid ? "<invalid>" : currentState.historyId ?? "<unset>";
|
|
5811
|
+
const newHistoryId = historyId ?? "<unset>";
|
|
5607
5812
|
logger.newline();
|
|
5608
|
-
logger.warn("This operation will change
|
|
5813
|
+
logger.warn("This operation will change TailorDB migration state metadata.");
|
|
5609
5814
|
logger.log(`Namespace: ${styles.bold(targetNamespace)}`);
|
|
5610
5815
|
logger.log(`Current migration: ${current === null ? "<unset>" : styles.bold(formatMigrationNumber(current))}`);
|
|
5611
5816
|
logger.log(`New migration: ${styles.bold(formatMigrationNumber(migrationNumber))}`);
|
|
5817
|
+
logger.log(`Current migration history ID: ${styles.bold(currentHistoryId)}`);
|
|
5818
|
+
logger.log(`New migration history ID: ${styles.bold(newHistoryId)}`);
|
|
5612
5819
|
logger.newline();
|
|
5613
5820
|
if (migrationNumber < currentMigration) {
|
|
5614
5821
|
logger.warn(`Setting migration number backwards (${formatMigrationNumber(currentMigration)} → ${formatMigrationNumber(migrationNumber)}) will cause previous migrations to be re-executed on next apply.`);
|
|
@@ -5619,7 +5826,7 @@ async function set(options) {
|
|
|
5619
5826
|
}
|
|
5620
5827
|
if (!options.yes) {
|
|
5621
5828
|
if (!await prompt.confirm({
|
|
5622
|
-
message: "Continue with migration checkpoint update?",
|
|
5829
|
+
message: "Continue with migration checkpoint and history ID update?",
|
|
5623
5830
|
default: false
|
|
5624
5831
|
})) {
|
|
5625
5832
|
logger.info("Operation cancelled.");
|
|
@@ -5629,7 +5836,11 @@ async function set(options) {
|
|
|
5629
5836
|
}
|
|
5630
5837
|
await writeMetadataLabels(client, {
|
|
5631
5838
|
trn,
|
|
5632
|
-
labels: {
|
|
5839
|
+
labels: {
|
|
5840
|
+
[MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber),
|
|
5841
|
+
...historyId ? { [MIGRATION_HISTORY_LABEL_KEY]: historyId } : {}
|
|
5842
|
+
},
|
|
5843
|
+
remove: historyId ? void 0 : [MIGRATION_HISTORY_LABEL_KEY]
|
|
5633
5844
|
});
|
|
5634
5845
|
logger.success(`Migration checkpoint set to ${styles.bold(formatMigrationNumber(migrationNumber))} for namespace ${styles.bold(targetNamespace)}`);
|
|
5635
5846
|
}
|
|
@@ -5675,17 +5886,67 @@ async function collectMigrationStatuses(options) {
|
|
|
5675
5886
|
if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
|
|
5676
5887
|
const targetNamespaces = options.namespace ? namespacesWithMigrations.filter((ns) => ns.namespace === options.namespace) : namespacesWithMigrations;
|
|
5677
5888
|
if (targetNamespaces.length === 0) throw new Error(`Namespace "${options.namespace}" not found or does not have migrations configured`);
|
|
5889
|
+
const localStates = /* @__PURE__ */ new Map();
|
|
5890
|
+
const localFailures = /* @__PURE__ */ new Map();
|
|
5891
|
+
for (const { namespace, migrationsDir } of targetNamespaces) try {
|
|
5892
|
+
const migrationFiles = getMigrationFiles(migrationsDir);
|
|
5893
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
5894
|
+
let historyId = null;
|
|
5895
|
+
for (const file of migrationFiles) {
|
|
5896
|
+
if (file.type === "schema") {
|
|
5897
|
+
const snapshot = loadSnapshot(file.path);
|
|
5898
|
+
if (file.number === 0) historyId = snapshot.rebaseline?.historyId ?? null;
|
|
5899
|
+
continue;
|
|
5900
|
+
}
|
|
5901
|
+
try {
|
|
5902
|
+
const diff = loadDiff(file.path);
|
|
5903
|
+
if (diff.description) descriptions.set(file.number, diff.description);
|
|
5904
|
+
} catch (error) {
|
|
5905
|
+
if (error instanceof UnsupportedMigrationFileVersionError) throw error;
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
localStates.set(namespace, {
|
|
5909
|
+
migrationFiles,
|
|
5910
|
+
descriptions,
|
|
5911
|
+
historyId
|
|
5912
|
+
});
|
|
5913
|
+
} catch (error) {
|
|
5914
|
+
localFailures.set(namespace, error instanceof Error ? error.message : String(error));
|
|
5915
|
+
}
|
|
5916
|
+
if (localStates.size === 0) return targetNamespaces.map(({ namespace }) => ({
|
|
5917
|
+
status: "error",
|
|
5918
|
+
namespace,
|
|
5919
|
+
error: localFailures.get(namespace) ?? "Failed to read local migration history."
|
|
5920
|
+
}));
|
|
5678
5921
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
5679
5922
|
const workspaceId = await loadWorkspaceId({
|
|
5680
5923
|
workspaceId: options.workspaceId,
|
|
5681
5924
|
profile: options.profile
|
|
5682
5925
|
});
|
|
5683
5926
|
const rows = [];
|
|
5684
|
-
for (const { namespace
|
|
5927
|
+
for (const { namespace } of targetNamespaces) {
|
|
5928
|
+
if (localFailures.has(namespace)) {
|
|
5929
|
+
rows.push({
|
|
5930
|
+
status: "error",
|
|
5931
|
+
namespace,
|
|
5932
|
+
error: localFailures.get(namespace) ?? "Failed to read local migration history."
|
|
5933
|
+
});
|
|
5934
|
+
continue;
|
|
5935
|
+
}
|
|
5936
|
+
const localState = localStates.get(namespace);
|
|
5937
|
+
if (!localState) {
|
|
5938
|
+
rows.push({
|
|
5939
|
+
status: "error",
|
|
5940
|
+
namespace,
|
|
5941
|
+
error: "Failed to read local migration history."
|
|
5942
|
+
});
|
|
5943
|
+
continue;
|
|
5944
|
+
}
|
|
5945
|
+
const { migrationFiles, descriptions, historyId: localHistoryId } = localState;
|
|
5685
5946
|
const trn = resourceTrn(workspaceId, "tailordb", namespace);
|
|
5686
|
-
let
|
|
5947
|
+
let remoteState;
|
|
5687
5948
|
try {
|
|
5688
|
-
|
|
5949
|
+
remoteState = await fetchRemoteMigrationState(client, trn);
|
|
5689
5950
|
} catch (error) {
|
|
5690
5951
|
rows.push({
|
|
5691
5952
|
status: "error",
|
|
@@ -5694,19 +5955,28 @@ async function collectMigrationStatuses(options) {
|
|
|
5694
5955
|
});
|
|
5695
5956
|
continue;
|
|
5696
5957
|
}
|
|
5697
|
-
const currentMigration =
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
}
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5958
|
+
const currentMigration = remoteState.number ?? 0;
|
|
5959
|
+
if (remoteState.historyIdInvalid) {
|
|
5960
|
+
rows.push({
|
|
5961
|
+
status: "error",
|
|
5962
|
+
namespace,
|
|
5963
|
+
error: "Remote migration history ID is invalid."
|
|
5964
|
+
});
|
|
5965
|
+
continue;
|
|
5966
|
+
}
|
|
5967
|
+
if ((remoteState.number !== null || remoteState.historyId !== null) && remoteState.historyId !== localHistoryId) {
|
|
5968
|
+
rows.push({
|
|
5969
|
+
status: "error",
|
|
5970
|
+
namespace,
|
|
5971
|
+
error: `Remote migration history ID ${remoteState.historyId ?? "<unset>"} does not match local migration history ID ${localHistoryId ?? "<unset>"}.`
|
|
5972
|
+
});
|
|
5973
|
+
continue;
|
|
5974
|
+
}
|
|
5975
|
+
const pendingMigrations = migrationFiles.map((f) => f.number).filter((n, i, arr) => arr.indexOf(n) === i).toSorted((a, b) => a - b).filter((n) => n > currentMigration).map((num) => ({
|
|
5976
|
+
number: num,
|
|
5977
|
+
label: formatMigrationNumber(num),
|
|
5978
|
+
...descriptions.has(num) ? { description: descriptions.get(num) } : {}
|
|
5979
|
+
}));
|
|
5710
5980
|
rows.push({
|
|
5711
5981
|
status: "ok",
|
|
5712
5982
|
namespace,
|
|
@@ -5722,7 +5992,7 @@ function printMigrationStatuses(rows) {
|
|
|
5722
5992
|
logger.newline();
|
|
5723
5993
|
logger.info(`Namespace: ${styles.bold(row.namespace)}`);
|
|
5724
5994
|
if (isStatusFailure(row)) {
|
|
5725
|
-
logger.error(`
|
|
5995
|
+
logger.error(` Migration status error: ${row.error}`);
|
|
5726
5996
|
continue;
|
|
5727
5997
|
}
|
|
5728
5998
|
logger.log(` Current migration: ${styles.bold(row.currentMigrationLabel)}`);
|
|
@@ -5746,13 +6016,13 @@ async function status(options) {
|
|
|
5746
6016
|
const failures = rows.filter(isStatusFailure);
|
|
5747
6017
|
if (failures.length > 0) {
|
|
5748
6018
|
const namespaces = failures.map((f) => f.namespace).join(", ");
|
|
5749
|
-
throw new Error(`
|
|
6019
|
+
throw new Error(`Migration status check failed for ${failures.length} namespace${failures.length === 1 ? "" : "s"}: ${namespaces}`);
|
|
5750
6020
|
}
|
|
5751
6021
|
}
|
|
5752
6022
|
const statusCommand = defineAppCommand({
|
|
5753
6023
|
name: "status",
|
|
5754
6024
|
description: "Show the current migration status for TailorDB namespaces, including applied and pending migrations.",
|
|
5755
|
-
notes: "
|
|
6025
|
+
notes: "Every local migration file is checked for a compatible format version, and deployed migration history IDs must match the local baseline. Compatibility errors, history mismatches, and metadata lookup failures are reported per namespace and make the command exit non-zero; only a not-yet-deployed namespace is treated as having no applied migrations.",
|
|
5756
6026
|
args: z.strictObject({
|
|
5757
6027
|
...deploymentArgs,
|
|
5758
6028
|
namespace: arg(z.string().optional(), {
|
|
@@ -5821,7 +6091,7 @@ async function fetchRemoteTypes(client, workspaceId, namespace) {
|
|
|
5821
6091
|
async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
5822
6092
|
const { config, plugins } = loaded;
|
|
5823
6093
|
const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
|
|
5824
|
-
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-
|
|
6094
|
+
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-BZfk4HKm.mjs");
|
|
5825
6095
|
const application = defineApplication({
|
|
5826
6096
|
config,
|
|
5827
6097
|
pluginManager
|
|
@@ -5833,7 +6103,7 @@ async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
|
5833
6103
|
await service.processNamespacePlugins();
|
|
5834
6104
|
}
|
|
5835
6105
|
const pluginExecutorFiles = generatePluginFilesIfNeeded(pluginManager, application.tailorDBServices, config.path);
|
|
5836
|
-
const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-
|
|
6106
|
+
const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-D1RCdzIL.mjs")).createExecutorService({
|
|
5837
6107
|
config: { files: [] },
|
|
5838
6108
|
baseDir: path.dirname(config.path)
|
|
5839
6109
|
}) : void 0);
|
|
@@ -5847,7 +6117,7 @@ async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
|
5847
6117
|
};
|
|
5848
6118
|
const latestSnapshot = reconstructSnapshotFromMigrations(target.migrationsDir);
|
|
5849
6119
|
if (!latestSnapshot) return manifestOptions;
|
|
5850
|
-
const diff = compareLocalTypesWithSnapshot(latestSnapshot, createSnapshotFromLocalTypes(tailordbService.types, target.namespace).
|
|
6120
|
+
const diff = compareLocalTypesWithSnapshot(latestSnapshot, createSnapshotFromLocalTypes(tailordbService.types, target.namespace).tables, target.namespace);
|
|
5851
6121
|
if (!hasChanges(diff)) return manifestOptions;
|
|
5852
6122
|
logger.error(`Migration history does not reproduce the current local schema for namespace ${styles.bold(target.namespace)}:`);
|
|
5853
6123
|
logger.log(formatMigrationDiff(diff));
|
|
@@ -5895,7 +6165,7 @@ async function sync(options) {
|
|
|
5895
6165
|
const { creates, updates, deletes } = compareSnapshotWithRemote(snapshot, new Set(remoteTypes.map((t) => t.name)));
|
|
5896
6166
|
const remoteGqlPermissions = await fetchRemoteGqlPermissions(client, workspaceId, target.namespace);
|
|
5897
6167
|
const remoteGqlPermissionTypes = new Set(remoteGqlPermissions.map((p) => p.typeName));
|
|
5898
|
-
const desiredGqlPermissions = Object.entries(snapshot.
|
|
6168
|
+
const desiredGqlPermissions = Object.entries(snapshot.tables).flatMap(([typeName, snapshotType]) => snapshotType.permissions?.gql ? [{
|
|
5899
6169
|
typeName,
|
|
5900
6170
|
permission: protoGqlPermission(snapshotType.permissions.gql)
|
|
5901
6171
|
}] : []);
|
|
@@ -5978,7 +6248,11 @@ async function sync(options) {
|
|
|
5978
6248
|
})));
|
|
5979
6249
|
await writeMetadataLabels(client, {
|
|
5980
6250
|
trn,
|
|
5981
|
-
labels: {
|
|
6251
|
+
labels: {
|
|
6252
|
+
[MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(targetVersion),
|
|
6253
|
+
...snapshot.rebaseline?.historyId ? { [MIGRATION_HISTORY_LABEL_KEY]: snapshot.rebaseline.historyId } : {}
|
|
6254
|
+
},
|
|
6255
|
+
remove: snapshot.rebaseline?.historyId ? void 0 : [MIGRATION_HISTORY_LABEL_KEY]
|
|
5982
6256
|
});
|
|
5983
6257
|
logger.success(`Synced namespace ${styles.bold(target.namespace)} to migration ${styles.bold(formatMigrationNumber(targetVersion))}.`);
|
|
5984
6258
|
if (targetVersion < latest) {
|
|
@@ -6014,23 +6288,647 @@ const syncCommand = defineAppCommand({
|
|
|
6014
6288
|
}
|
|
6015
6289
|
});
|
|
6016
6290
|
|
|
6291
|
+
//#endregion
|
|
6292
|
+
//#region src/cli/commands/tailordb/migrate/test-runtime.ts
|
|
6293
|
+
const CLONE_POLL_INTERVAL = 1e3;
|
|
6294
|
+
const CLONE_TIMEOUT = 300 * 1e3;
|
|
6295
|
+
function delay(milliseconds) {
|
|
6296
|
+
return milliseconds > 0 ? new Promise((resolve) => setTimeout(resolve, milliseconds)) : Promise.resolve();
|
|
6297
|
+
}
|
|
6298
|
+
function assertSeedDataDirectory(dataDir) {
|
|
6299
|
+
let stats;
|
|
6300
|
+
try {
|
|
6301
|
+
stats = fs.statSync(dataDir);
|
|
6302
|
+
} catch (error) {
|
|
6303
|
+
if (error.code === "ENOENT") throw new Error(`Seed data directory not found: ${dataDir}. Run 'tailor generate' before testing migrations.`, { cause: error });
|
|
6304
|
+
throw error;
|
|
6305
|
+
}
|
|
6306
|
+
if (!stats.isDirectory()) throw new Error(`Seed data path is not a directory: ${dataDir}`);
|
|
6307
|
+
}
|
|
6308
|
+
function assertAssertionScript(assertionPath) {
|
|
6309
|
+
const resolvedPath = path.resolve(assertionPath);
|
|
6310
|
+
let stats;
|
|
6311
|
+
try {
|
|
6312
|
+
stats = fs.statSync(resolvedPath);
|
|
6313
|
+
} catch (error) {
|
|
6314
|
+
if (error.code === "ENOENT") throw new Error(`Migration assertion script not found: ${resolvedPath}`, { cause: error });
|
|
6315
|
+
throw error;
|
|
6316
|
+
}
|
|
6317
|
+
if (!stats.isFile()) throw new Error(`Migration assertion path is not a file: ${resolvedPath}`);
|
|
6318
|
+
}
|
|
6319
|
+
/**
|
|
6320
|
+
* Require a designated clone target to share the source workspace's region.
|
|
6321
|
+
* @param sourceRegion - Source workspace region
|
|
6322
|
+
* @param targetRegion - Target workspace region
|
|
6323
|
+
*/
|
|
6324
|
+
function assertCloneTargetRegion(sourceRegion, targetRegion) {
|
|
6325
|
+
if (sourceRegion !== targetRegion) throw new Error(`Clone mode requires source and target workspaces in the same region (source: ${sourceRegion}, target: ${targetRegion}).`);
|
|
6326
|
+
}
|
|
6327
|
+
/**
|
|
6328
|
+
* Require the migration test target workspace to differ from the source workspace.
|
|
6329
|
+
* @param sourceWorkspaceId - Source workspace ID
|
|
6330
|
+
* @param targetWorkspaceId - Designated target workspace ID, if any
|
|
6331
|
+
*/
|
|
6332
|
+
function assertTargetWorkspaceDiffers(sourceWorkspaceId, targetWorkspaceId) {
|
|
6333
|
+
if (targetWorkspaceId?.toLowerCase() === sourceWorkspaceId.toLowerCase()) throw new Error("The migration test target workspace must differ from the source workspace.");
|
|
6334
|
+
}
|
|
6335
|
+
/**
|
|
6336
|
+
* Resolve the namespace an assertion script runs against.
|
|
6337
|
+
* @param pendingNamespaces - Namespaces with pending migrations
|
|
6338
|
+
* @param assertionNamespace - Explicit namespace from `--assert-namespace`, if any
|
|
6339
|
+
* @returns Resolved assertion namespace
|
|
6340
|
+
*/
|
|
6341
|
+
function resolveAssertionNamespace(pendingNamespaces, assertionNamespace) {
|
|
6342
|
+
const namespace = assertionNamespace ?? (pendingNamespaces.length === 1 ? pendingNamespaces[0] : void 0);
|
|
6343
|
+
if (!namespace) throw new Error("--assert-namespace is required when pending migrations span multiple namespaces.");
|
|
6344
|
+
if (!pendingNamespaces.includes(namespace)) throw new Error(`Assertion namespace "${namespace}" has no pending migrations.`);
|
|
6345
|
+
return namespace;
|
|
6346
|
+
}
|
|
6347
|
+
/**
|
|
6348
|
+
* Build the schema overrides used before migration-test data is loaded.
|
|
6349
|
+
* @param options - Source workspace, local inputs, and migration baselines
|
|
6350
|
+
* @returns Schema snapshots keyed by namespace
|
|
6351
|
+
*/
|
|
6352
|
+
async function createMigrationTestBaselineSnapshots(options) {
|
|
6353
|
+
const snapshots = new Map([...options.baselines].map(([namespace, baseline]) => [namespace, baseline.snapshot]));
|
|
6354
|
+
if (options.dataMode !== "clone") return snapshots;
|
|
6355
|
+
const unmigratedInputs = options.inputs.filter((input) => !snapshots.has(input.namespace));
|
|
6356
|
+
const remoteSnapshots = await Promise.all(unmigratedInputs.map((input) => fetchRemoteSchemaSnapshot(options.client, options.workspaceId, input.namespace)));
|
|
6357
|
+
unmigratedInputs.forEach((input, index) => {
|
|
6358
|
+
snapshots.set(input.namespace, assertDefined(remoteSnapshots[index], `remote snapshot missing for "${input.namespace}"`));
|
|
6359
|
+
});
|
|
6360
|
+
return snapshots;
|
|
6361
|
+
}
|
|
6362
|
+
/**
|
|
6363
|
+
* Wait for an application-data clone operation to reach a terminal state.
|
|
6364
|
+
* @param client - Operator client
|
|
6365
|
+
* @param options - Clone operation identifiers and polling limits
|
|
6366
|
+
*/
|
|
6367
|
+
async function waitForCloneApplicationData(client, options) {
|
|
6368
|
+
const pollInterval = options.pollInterval ?? CLONE_POLL_INTERVAL;
|
|
6369
|
+
const timeout = options.timeout ?? CLONE_TIMEOUT;
|
|
6370
|
+
const deadline = Date.now() + timeout;
|
|
6371
|
+
while (Date.now() <= deadline) {
|
|
6372
|
+
const operation = await client.getCloneApplicationDataOperation({
|
|
6373
|
+
sourceWorkspaceId: options.sourceWorkspaceId,
|
|
6374
|
+
targetWorkspaceId: options.targetWorkspaceId,
|
|
6375
|
+
operationId: options.operationId
|
|
6376
|
+
});
|
|
6377
|
+
if (operation.status === CloneOperationStatus.COMPLETED) return;
|
|
6378
|
+
if (operation.status === CloneOperationStatus.FAILED) throw new Error(`Application data clone failed: ${operation.errorMessage || "unknown platform error"}`);
|
|
6379
|
+
if (operation.status !== CloneOperationStatus.PENDING && operation.status !== CloneOperationStatus.PROCESSING) throw new Error(`Application data clone returned unexpected status ${operation.status}.`);
|
|
6380
|
+
await delay(pollInterval);
|
|
6381
|
+
}
|
|
6382
|
+
throw new Error(`Application data clone timed out after ${Math.round(timeout / 1e3)} seconds.`);
|
|
6383
|
+
}
|
|
6384
|
+
/**
|
|
6385
|
+
* Load generated JSONL seed rows for the types in a baseline snapshot.
|
|
6386
|
+
* @param dataDir - Directory containing `<Type>.jsonl` files
|
|
6387
|
+
* @param typeNames - Baseline type names to load
|
|
6388
|
+
* @param snapshot - Baseline schema used to remove fields introduced by pending migrations
|
|
6389
|
+
* @returns Seed rows keyed by type name
|
|
6390
|
+
*/
|
|
6391
|
+
function loadSnapshotSeedData(dataDir, typeNames, snapshot) {
|
|
6392
|
+
const data = {};
|
|
6393
|
+
for (const typeName of typeNames) {
|
|
6394
|
+
const snapshotType = snapshot?.tables[typeName];
|
|
6395
|
+
const jsonlPath = path.join(dataDir, `${typeName}.jsonl`);
|
|
6396
|
+
let content;
|
|
6397
|
+
try {
|
|
6398
|
+
content = fs.readFileSync(jsonlPath, "utf8").trim();
|
|
6399
|
+
} catch (error) {
|
|
6400
|
+
if (error.code === "ENOENT") {
|
|
6401
|
+
data[typeName] = [];
|
|
6402
|
+
continue;
|
|
6403
|
+
}
|
|
6404
|
+
throw error;
|
|
6405
|
+
}
|
|
6406
|
+
data[typeName] = content ? content.split("\n").map((line, index) => {
|
|
6407
|
+
let value;
|
|
6408
|
+
try {
|
|
6409
|
+
value = JSON.parse(line);
|
|
6410
|
+
} catch (error) {
|
|
6411
|
+
throw new Error(`Invalid JSON in ${jsonlPath} at line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
6412
|
+
}
|
|
6413
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid seed row in ${jsonlPath} at line ${index + 1}: expected a JSON object`);
|
|
6414
|
+
const row = value;
|
|
6415
|
+
return snapshotType ? projectSeedObject(row, snapshotType.fields, /* @__PURE__ */ new Set(["id"])) : row;
|
|
6416
|
+
}) : [];
|
|
6417
|
+
}
|
|
6418
|
+
return data;
|
|
6419
|
+
}
|
|
6420
|
+
function projectSeedObject(value, fields, implicitFields = /* @__PURE__ */ new Set()) {
|
|
6421
|
+
return Object.fromEntries(Object.entries(value).flatMap(([fieldName, fieldValue]) => {
|
|
6422
|
+
const field = fields[fieldName];
|
|
6423
|
+
if (!field) return implicitFields.has(fieldName) ? [[fieldName, fieldValue]] : [];
|
|
6424
|
+
return [[fieldName, projectSeedValue(fieldValue, field)]];
|
|
6425
|
+
}));
|
|
6426
|
+
}
|
|
6427
|
+
function projectSeedValue(value, field) {
|
|
6428
|
+
const fields = field.fields;
|
|
6429
|
+
if (!fields) return value;
|
|
6430
|
+
if (field.array) {
|
|
6431
|
+
if (!Array.isArray(value)) return value;
|
|
6432
|
+
return value.map((entry) => isJsonObject(entry) ? projectSeedObject(entry, fields) : entry);
|
|
6433
|
+
}
|
|
6434
|
+
return isJsonObject(value) ? projectSeedObject(value, fields) : value;
|
|
6435
|
+
}
|
|
6436
|
+
function isJsonObject(value) {
|
|
6437
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6438
|
+
}
|
|
6439
|
+
/**
|
|
6440
|
+
* Derive insertion order and self-referencing types from a baseline snapshot.
|
|
6441
|
+
* @param snapshot - Baseline schema snapshot
|
|
6442
|
+
* @returns Dependency-ordered type names and self-referencing types
|
|
6443
|
+
*/
|
|
6444
|
+
function sortSeedTypesForSnapshot(snapshot) {
|
|
6445
|
+
const typeNames = Object.keys(snapshot.tables);
|
|
6446
|
+
const available = new Set(typeNames);
|
|
6447
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
6448
|
+
const selfRefTypes = [];
|
|
6449
|
+
for (const [typeName, type] of Object.entries(snapshot.tables)) {
|
|
6450
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
6451
|
+
for (const field of Object.values(type.fields)) {
|
|
6452
|
+
const target = field.foreignKeyType;
|
|
6453
|
+
if (!target) continue;
|
|
6454
|
+
if (target === typeName) selfRefTypes.push(typeName);
|
|
6455
|
+
else if (available.has(target)) referenced.add(target);
|
|
6456
|
+
}
|
|
6457
|
+
dependencies.set(typeName, [...referenced]);
|
|
6458
|
+
}
|
|
6459
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6460
|
+
const order = [];
|
|
6461
|
+
const visit = (typeName) => {
|
|
6462
|
+
if (visited.has(typeName)) return;
|
|
6463
|
+
visited.add(typeName);
|
|
6464
|
+
for (const dependency of dependencies.get(typeName) ?? []) visit(dependency);
|
|
6465
|
+
order.push(typeName);
|
|
6466
|
+
};
|
|
6467
|
+
for (const typeName of typeNames) visit(typeName);
|
|
6468
|
+
return {
|
|
6469
|
+
order,
|
|
6470
|
+
selfRefTypes
|
|
6471
|
+
};
|
|
6472
|
+
}
|
|
6473
|
+
function temporaryWorkspaceName() {
|
|
6474
|
+
return `migration-test-${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14)}-${randomBytes(3).toString("hex")}`;
|
|
6475
|
+
}
|
|
6476
|
+
function stateOrThrow(state) {
|
|
6477
|
+
return assertDefined(state, "migration test runtime was used before preparation");
|
|
6478
|
+
}
|
|
6479
|
+
/**
|
|
6480
|
+
* Require the source workspace to still match the prepared baselines.
|
|
6481
|
+
*
|
|
6482
|
+
* The source is verified during preparation, but the baseline deploy runs in
|
|
6483
|
+
* between; a source deploy in that window would make the clone diverge from
|
|
6484
|
+
* the already-deployed baseline schemas.
|
|
6485
|
+
* @param state - Prepared migration test runtime state
|
|
6486
|
+
* @param prepared - Baselines captured during preparation
|
|
6487
|
+
* @param sourceWorkspaceId - Source workspace ID
|
|
6488
|
+
*/
|
|
6489
|
+
async function assertSourceBaselineFresh(state, prepared, sourceWorkspaceId) {
|
|
6490
|
+
const loaded = state.loaded;
|
|
6491
|
+
const namespaces = getNamespacesWithMigrations(loaded.config, path.dirname(loaded.config.path));
|
|
6492
|
+
const inputs = loaded.application.tailorDBServices.map(toTailorDBDeployInput);
|
|
6493
|
+
const invalidRemote = (await verifyRemoteSchema(state.client, sourceWorkspaceId, namespaces, loaded.config, inputs)).find((check) => check.hasDrift || check.checkpointMissingLocal || check.checkpointRepair);
|
|
6494
|
+
if (invalidRemote) throw new Error(`Source namespace "${invalidRemote.namespace}" changed after migration test preparation. Run the migration test again.`);
|
|
6495
|
+
for (const namespace of namespaces) {
|
|
6496
|
+
const baseline = prepared.baselines.get(namespace.namespace);
|
|
6497
|
+
if (!baseline) continue;
|
|
6498
|
+
const migrationNumber = await fetchRemoteMigrationNumber(state.client, resourceTrn(sourceWorkspaceId, "tailordb", namespace.namespace));
|
|
6499
|
+
if (migrationNumber !== baseline.migrationNumber) throw new Error(`Source namespace "${namespace.namespace}" moved from migration ${baseline.migrationNumber} to ${migrationNumber ?? "none"} after migration test preparation. Run the migration test again.`);
|
|
6500
|
+
}
|
|
6501
|
+
const unmigratedInputs = inputs.filter((input) => !prepared.baselines.has(input.namespace));
|
|
6502
|
+
for (const input of unmigratedInputs) {
|
|
6503
|
+
const snapshot = prepared.baselineSnapshots.get(input.namespace);
|
|
6504
|
+
if (!snapshot) continue;
|
|
6505
|
+
if (compareSnapshots(snapshot, await fetchRemoteSchemaSnapshot(state.client, sourceWorkspaceId, input.namespace)).changes.length > 0) throw new Error(`Source namespace "${input.namespace}" schema changed after migration test preparation. Run the migration test again.`);
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
/**
|
|
6509
|
+
* Delete the target workspace's user profile config when one exists.
|
|
6510
|
+
*
|
|
6511
|
+
* A retained target may carry a user profile config referencing types the
|
|
6512
|
+
* baseline deploy replaces; deleting it up front lets the baseline's planAuth
|
|
6513
|
+
* see no existing config instead of reordering delete phases.
|
|
6514
|
+
* @param state - Prepared migration test runtime state
|
|
6515
|
+
* @param targetWorkspaceId - Target workspace ID
|
|
6516
|
+
*/
|
|
6517
|
+
async function deleteExistingUserProfileConfig(state, targetWorkspaceId) {
|
|
6518
|
+
const auth = state.loaded.application.authService;
|
|
6519
|
+
if (!auth) return;
|
|
6520
|
+
const namespaceName = auth.config.name;
|
|
6521
|
+
if (!await getOrNull(() => state.client.getUserProfileConfig({
|
|
6522
|
+
workspaceId: targetWorkspaceId,
|
|
6523
|
+
namespaceName
|
|
6524
|
+
}))) return;
|
|
6525
|
+
await state.client.deleteUserProfileConfig({
|
|
6526
|
+
workspaceId: targetWorkspaceId,
|
|
6527
|
+
namespaceName
|
|
6528
|
+
});
|
|
6529
|
+
}
|
|
6530
|
+
function migrationConfig(loaded, namespace) {
|
|
6531
|
+
return (loaded.config.db?.[namespace])?.migration;
|
|
6532
|
+
}
|
|
6533
|
+
function authExecutionContext(state, namespace, explicitMachineUser) {
|
|
6534
|
+
const auth = state.loaded.application.authService;
|
|
6535
|
+
if (!auth) throw new Error("Auth configuration is required to execute migration test scripts.");
|
|
6536
|
+
const machineUserName = explicitMachineUser ?? getMigrationMachineUser(migrationConfig(state.loaded, namespace), auth.config.machineUsers ? Object.keys(auth.config.machineUsers) : void 0);
|
|
6537
|
+
if (!machineUserName) throw new Error(`No machine user is available for namespace "${namespace}". Pass --machine-user or configure one in auth or db.${namespace}.migration.`);
|
|
6538
|
+
return {
|
|
6539
|
+
authNamespace: auth.config.name,
|
|
6540
|
+
machineUserName
|
|
6541
|
+
};
|
|
6542
|
+
}
|
|
6543
|
+
async function prepareMigrationTest(options) {
|
|
6544
|
+
const loaded = await loadApplicationNamespaces({ configPath: options.configPath });
|
|
6545
|
+
const configDir = path.dirname(loaded.config.path);
|
|
6546
|
+
const namespaces = getNamespacesWithMigrations(loaded.config, configDir);
|
|
6547
|
+
if (namespaces.length === 0) throw new Error("No TailorDB services with migrations configuration found.");
|
|
6548
|
+
for (const namespace of namespaces) assertValidMigrationFiles(namespace.migrationsDir, namespace.namespace);
|
|
6549
|
+
let seedContext;
|
|
6550
|
+
if (options.data === "seed") {
|
|
6551
|
+
seedContext = await loadSeedContext({ configPath: loaded.config.path });
|
|
6552
|
+
assertSeedDataDirectory(path.join(seedContext.distPath, "data"));
|
|
6553
|
+
}
|
|
6554
|
+
if (options.assertionPath) assertAssertionScript(options.assertionPath);
|
|
6555
|
+
const inputs = loaded.application.tailorDBServices.map(toTailorDBDeployInput);
|
|
6556
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }), await loadPlatformClientConfig({ profile: options.profile }));
|
|
6557
|
+
const sourceWorkspaceId = await loadWorkspaceId({
|
|
6558
|
+
workspaceId: options.workspaceId,
|
|
6559
|
+
profile: options.profile
|
|
6560
|
+
});
|
|
6561
|
+
assertTargetWorkspaceDiffers(sourceWorkspaceId, options.targetWorkspaceId);
|
|
6562
|
+
const [workspaceResponse, applicationResponse, targetWorkspaceResponse] = await Promise.all([
|
|
6563
|
+
client.getWorkspace({ workspaceId: sourceWorkspaceId }),
|
|
6564
|
+
client.getApplication({
|
|
6565
|
+
workspaceId: sourceWorkspaceId,
|
|
6566
|
+
applicationName: loaded.config.name
|
|
6567
|
+
}),
|
|
6568
|
+
options.data === "clone" && options.targetWorkspaceId ? client.getWorkspace({ workspaceId: options.targetWorkspaceId }) : Promise.resolve(void 0)
|
|
6569
|
+
]);
|
|
6570
|
+
const sourceWorkspace = assertDefined(workspaceResponse.workspace, `source workspace "${sourceWorkspaceId}" not found`);
|
|
6571
|
+
assertDefined(applicationResponse.application, `application "${loaded.config.name}" not found in source workspace`);
|
|
6572
|
+
const designatedTarget = targetWorkspaceResponse ? assertDefined(targetWorkspaceResponse.workspace, `target workspace "${options.targetWorkspaceId}" not found`) : void 0;
|
|
6573
|
+
const remoteChecks = await verifyRemoteSchema(client, sourceWorkspaceId, namespaces, loaded.config, inputs);
|
|
6574
|
+
const invalidRemote = remoteChecks.find((check) => check.hasDrift || check.checkpointMissingLocal);
|
|
6575
|
+
if (invalidRemote) throw new Error(`Source namespace "${invalidRemote.namespace}" does not match its migration checkpoint. Run 'tailor tailordb migration validate' first.`);
|
|
6576
|
+
const pendingRepair = remoteChecks.find((check) => check.checkpointRepair);
|
|
6577
|
+
if (pendingRepair) throw new Error(`Source namespace "${pendingRepair.namespace}" has a pending migration checkpoint repair. Run 'tailor deploy' against the source workspace first.`);
|
|
6578
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
6579
|
+
const targetSnapshots = /* @__PURE__ */ new Map();
|
|
6580
|
+
const pendingNamespaces = [];
|
|
6581
|
+
for (const namespace of namespaces) {
|
|
6582
|
+
const migrationNumber = await fetchRemoteMigrationNumber(client, resourceTrn(sourceWorkspaceId, "tailordb", namespace.namespace));
|
|
6583
|
+
if (migrationNumber === null) throw new Error(`Source namespace "${namespace.namespace}" has no sdk-migration checkpoint. Deploy or set its migration checkpoint before testing.`);
|
|
6584
|
+
const latest = getLatestMigrationNumber(namespace.migrationsDir);
|
|
6585
|
+
if (migrationNumber > latest) throw new Error(`Source namespace "${namespace.namespace}" is at migration ${migrationNumber}, but the local history ends at ${latest}.`);
|
|
6586
|
+
const snapshot = reconstructSnapshotFromMigrations(namespace.migrationsDir, migrationNumber);
|
|
6587
|
+
if (!snapshot) throw new Error(`No migration baseline snapshot found for namespace "${namespace.namespace}".`);
|
|
6588
|
+
baselines.set(namespace.namespace, {
|
|
6589
|
+
migrationNumber,
|
|
6590
|
+
snapshot,
|
|
6591
|
+
historyId: snapshot.rebaseline?.historyId ?? null
|
|
6592
|
+
});
|
|
6593
|
+
const targetSnapshot = reconstructSnapshotFromMigrations(namespace.migrationsDir, latest);
|
|
6594
|
+
if (!targetSnapshot) throw new Error(`No target migration snapshot found for namespace "${namespace.namespace}".`);
|
|
6595
|
+
targetSnapshots.set(namespace.namespace, targetSnapshot);
|
|
6596
|
+
if (migrationNumber < latest) pendingNamespaces.push(namespace.namespace);
|
|
6597
|
+
}
|
|
6598
|
+
if (pendingNamespaces.length === 0) throw new Error("No pending TailorDB migrations found in the source workspace.");
|
|
6599
|
+
const baselineSnapshots = await createMigrationTestBaselineSnapshots({
|
|
6600
|
+
client,
|
|
6601
|
+
workspaceId: sourceWorkspaceId,
|
|
6602
|
+
dataMode: options.data,
|
|
6603
|
+
inputs,
|
|
6604
|
+
baselines
|
|
6605
|
+
});
|
|
6606
|
+
const prepared = {
|
|
6607
|
+
sourceWorkspaceId,
|
|
6608
|
+
sourceApplicationName: loaded.config.name,
|
|
6609
|
+
temporaryWorkspace: {
|
|
6610
|
+
name: temporaryWorkspaceName(),
|
|
6611
|
+
region: sourceWorkspace.region,
|
|
6612
|
+
...sourceWorkspace.organizationId ? { organizationId: sourceWorkspace.organizationId } : {},
|
|
6613
|
+
...sourceWorkspace.folderId ? { folderId: sourceWorkspace.folderId } : {}
|
|
6614
|
+
},
|
|
6615
|
+
baselines,
|
|
6616
|
+
baselineSnapshots,
|
|
6617
|
+
targetSnapshots,
|
|
6618
|
+
pendingNamespaces,
|
|
6619
|
+
...designatedTarget ? { designatedTarget: {
|
|
6620
|
+
id: designatedTarget.id,
|
|
6621
|
+
region: designatedTarget.region
|
|
6622
|
+
} } : {}
|
|
6623
|
+
};
|
|
6624
|
+
return {
|
|
6625
|
+
state: {
|
|
6626
|
+
client,
|
|
6627
|
+
loaded,
|
|
6628
|
+
options,
|
|
6629
|
+
...seedContext ? { seedContext } : {}
|
|
6630
|
+
},
|
|
6631
|
+
prepared
|
|
6632
|
+
};
|
|
6633
|
+
}
|
|
6634
|
+
/**
|
|
6635
|
+
* Create the platform operations used by `tailordb migration test`.
|
|
6636
|
+
* @returns Migration test dependencies backed by the Operator API
|
|
6637
|
+
*/
|
|
6638
|
+
function createMigrationTestDependencies() {
|
|
6639
|
+
let runtimeState;
|
|
6640
|
+
return {
|
|
6641
|
+
prepare: async (options) => {
|
|
6642
|
+
const { state, prepared } = await prepareMigrationTest(options);
|
|
6643
|
+
runtimeState = state;
|
|
6644
|
+
return prepared;
|
|
6645
|
+
},
|
|
6646
|
+
createWorkspace: async (prepared) => {
|
|
6647
|
+
const workspace = await createValidatedWorkspaceWithClient(stateOrThrow(runtimeState).client, prepared.temporaryWorkspace);
|
|
6648
|
+
logger.info(`Created temporary workspace ${workspace.name} (${workspace.id}).`);
|
|
6649
|
+
return {
|
|
6650
|
+
id: workspace.id,
|
|
6651
|
+
name: workspace.name
|
|
6652
|
+
};
|
|
6653
|
+
},
|
|
6654
|
+
deployBaseline: async ({ prepared, targetWorkspaceId }) => {
|
|
6655
|
+
const state = stateOrThrow(runtimeState);
|
|
6656
|
+
await deleteExistingUserProfileConfig(state, targetWorkspaceId);
|
|
6657
|
+
await deployMigrationTestBaseline({
|
|
6658
|
+
configPath: state.loaded.config.path,
|
|
6659
|
+
profile: state.options.profile,
|
|
6660
|
+
workspaceId: targetWorkspaceId,
|
|
6661
|
+
yes: true
|
|
6662
|
+
}, prepared.baselines, prepared.baselineSnapshots);
|
|
6663
|
+
for (const [namespace, baseline] of prepared.baselines) await updateMigrationLabel(state.client, targetWorkspaceId, namespace, baseline.migrationNumber, baseline.historyId ?? void 0);
|
|
6664
|
+
},
|
|
6665
|
+
seedData: async ({ prepared, targetWorkspaceId }) => {
|
|
6666
|
+
const state = stateOrThrow(runtimeState);
|
|
6667
|
+
const seedContext = assertDefined(state.seedContext, "seed context missing after migration test preparation");
|
|
6668
|
+
const dataDir = path.join(seedContext.distPath, "data");
|
|
6669
|
+
assertSeedDataDirectory(dataDir);
|
|
6670
|
+
const seedSnapshots = new Map(state.loaded.application.tailorDBServices.map((service) => {
|
|
6671
|
+
const baseline = prepared.baselines.get(service.namespace);
|
|
6672
|
+
const input = toTailorDBDeployInput(service);
|
|
6673
|
+
return [service.namespace, baseline?.snapshot ?? normalizeSchemaSnapshot({
|
|
6674
|
+
version: 1,
|
|
6675
|
+
namespace: service.namespace,
|
|
6676
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6677
|
+
tables: input.types
|
|
6678
|
+
})];
|
|
6679
|
+
}));
|
|
6680
|
+
for (const [namespace, snapshot] of seedSnapshots) {
|
|
6681
|
+
const { order, selfRefTypes } = sortSeedTypesForSnapshot(snapshot);
|
|
6682
|
+
const data = loadSnapshotSeedData(dataDir, order, snapshot);
|
|
6683
|
+
const typesWithData = order.filter((typeName) => (data[typeName]?.length ?? 0) > 0);
|
|
6684
|
+
if (typesWithData.length === 0) continue;
|
|
6685
|
+
const bundled = await bundleSeedScript(namespace, typesWithData, path.dirname(state.loaded.config.path));
|
|
6686
|
+
const chunks = chunkSeedData({
|
|
6687
|
+
data,
|
|
6688
|
+
order,
|
|
6689
|
+
codeByteSize: new TextEncoder().encode(bundled.bundledCode).length
|
|
6690
|
+
});
|
|
6691
|
+
const auth = authExecutionContext(state, namespace, state.options.machineUser ?? seedContext.machineUserName);
|
|
6692
|
+
for (const chunk of chunks) {
|
|
6693
|
+
const execution = await executeScript({
|
|
6694
|
+
client: state.client,
|
|
6695
|
+
workspaceId: targetWorkspaceId,
|
|
6696
|
+
name: `migration-test-seed-${namespace}.ts`,
|
|
6697
|
+
code: bundled.bundledCode,
|
|
6698
|
+
arg: {
|
|
6699
|
+
data: chunk.data,
|
|
6700
|
+
order: chunk.order,
|
|
6701
|
+
selfRefTypes,
|
|
6702
|
+
upsert: false
|
|
6703
|
+
},
|
|
6704
|
+
invoker: {
|
|
6705
|
+
namespace: auth.authNamespace,
|
|
6706
|
+
machineUserName: auth.machineUserName
|
|
6707
|
+
}
|
|
6708
|
+
});
|
|
6709
|
+
if (!execution.success) throw new Error(execution.error ?? `Seed execution failed for namespace "${namespace}".`);
|
|
6710
|
+
if (JSON.parse(execution.result || "{}").success !== true) throw new Error(`Seed execution reported failure for namespace "${namespace}".`);
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
},
|
|
6714
|
+
cloneData: async ({ prepared, sourceWorkspaceId, targetWorkspaceId, applicationName }) => {
|
|
6715
|
+
const state = stateOrThrow(runtimeState);
|
|
6716
|
+
await assertSourceBaselineFresh(state, prepared, sourceWorkspaceId);
|
|
6717
|
+
logger.warn("Clone mode copies TailorDB records only; IdP users and file blobs are not copied.");
|
|
6718
|
+
let operationId;
|
|
6719
|
+
try {
|
|
6720
|
+
operationId = (await state.client.cloneApplicationData({
|
|
6721
|
+
sourceWorkspaceId,
|
|
6722
|
+
sourceApplicationName: applicationName,
|
|
6723
|
+
targetWorkspaceId,
|
|
6724
|
+
targetApplicationName: applicationName
|
|
6725
|
+
})).operationId;
|
|
6726
|
+
} catch (error) {
|
|
6727
|
+
if (error instanceof ConnectError && error.code === Code.Unimplemented) throw new Error("Application data clone is not enabled for this platform. Retry with '--data seed'.", { cause: error });
|
|
6728
|
+
throw error;
|
|
6729
|
+
}
|
|
6730
|
+
if (!operationId) throw new Error("Application data clone returned no operation ID.");
|
|
6731
|
+
await waitForCloneApplicationData(state.client, {
|
|
6732
|
+
sourceWorkspaceId,
|
|
6733
|
+
targetWorkspaceId,
|
|
6734
|
+
operationId
|
|
6735
|
+
});
|
|
6736
|
+
},
|
|
6737
|
+
deployMigrations: async ({ prepared, targetWorkspaceId }) => {
|
|
6738
|
+
const state = stateOrThrow(runtimeState);
|
|
6739
|
+
await deployMigrationTestTarget({
|
|
6740
|
+
configPath: state.loaded.config.path,
|
|
6741
|
+
profile: state.options.profile,
|
|
6742
|
+
workspaceId: targetWorkspaceId,
|
|
6743
|
+
yes: true
|
|
6744
|
+
}, prepared.targetSnapshots);
|
|
6745
|
+
},
|
|
6746
|
+
runAssertion: async ({ prepared, targetWorkspaceId, assertionPath, assertionNamespace, machineUser }) => {
|
|
6747
|
+
const state = stateOrThrow(runtimeState);
|
|
6748
|
+
const namespace = resolveAssertionNamespace(prepared.pendingNamespaces, assertionNamespace);
|
|
6749
|
+
const auth = authExecutionContext(state, namespace, machineUser);
|
|
6750
|
+
const bundled = await bundleMigrationScript(assertionPath, namespace, 0, state.loaded.config.env ?? {}, path.dirname(state.loaded.config.path));
|
|
6751
|
+
const execution = await executeScript({
|
|
6752
|
+
client: state.client,
|
|
6753
|
+
workspaceId: targetWorkspaceId,
|
|
6754
|
+
name: `migration-test-assert-${namespace}.ts`,
|
|
6755
|
+
code: bundled.bundledCode,
|
|
6756
|
+
invoker: {
|
|
6757
|
+
namespace: auth.authNamespace,
|
|
6758
|
+
machineUserName: auth.machineUserName
|
|
6759
|
+
}
|
|
6760
|
+
});
|
|
6761
|
+
if (!execution.success) throw new Error(execution.error ?? `Migration assertion failed for namespace "${namespace}".`);
|
|
6762
|
+
},
|
|
6763
|
+
deleteWorkspace: async (workspaceId) => {
|
|
6764
|
+
await stateOrThrow(runtimeState).client.deleteWorkspace({ workspaceId });
|
|
6765
|
+
}
|
|
6766
|
+
};
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
//#endregion
|
|
6770
|
+
//#region src/cli/commands/tailordb/migrate/test.ts
|
|
6771
|
+
/**
|
|
6772
|
+
* Run pending migrations against an isolated target workspace.
|
|
6773
|
+
* @param options - Migration test options
|
|
6774
|
+
* @param dependencies - Runtime operations used by the migration test
|
|
6775
|
+
* @returns Migration test result
|
|
6776
|
+
*/
|
|
6777
|
+
async function runMigrationTest(options, dependencies = createMigrationTestDependencies()) {
|
|
6778
|
+
if (options.targetWorkspaceId && !options.yes) throw new Error("--target-workspace-id requires --yes because the target may be overwritten.");
|
|
6779
|
+
if (options.keep && options.targetWorkspaceId) throw new Error("--keep applies only to automatically created workspaces; a designated target is always retained.");
|
|
6780
|
+
if (options.assertionNamespace && !options.assertionPath) throw new Error("--assert is required when --assert-namespace is provided.");
|
|
6781
|
+
const prepared = await dependencies.prepare(options);
|
|
6782
|
+
assertTargetWorkspaceDiffers(prepared.sourceWorkspaceId, options.targetWorkspaceId);
|
|
6783
|
+
if (options.assertionPath) resolveAssertionNamespace(prepared.pendingNamespaces, options.assertionNamespace);
|
|
6784
|
+
if (options.data === "clone" && options.targetWorkspaceId) {
|
|
6785
|
+
const target = assertDefined(prepared.designatedTarget, "designated clone target was not loaded during preparation");
|
|
6786
|
+
assertCloneTargetRegion(prepared.temporaryWorkspace.region, target.region);
|
|
6787
|
+
}
|
|
6788
|
+
const temporary = options.targetWorkspaceId === void 0;
|
|
6789
|
+
const workspace = temporary ? await dependencies.createWorkspace(prepared) : {
|
|
6790
|
+
id: assertDefined(options.targetWorkspaceId, "designated target workspace missing"),
|
|
6791
|
+
name: void 0
|
|
6792
|
+
};
|
|
6793
|
+
let primaryError;
|
|
6794
|
+
let failed = false;
|
|
6795
|
+
let deleted = false;
|
|
6796
|
+
try {
|
|
6797
|
+
const target = {
|
|
6798
|
+
prepared,
|
|
6799
|
+
targetWorkspaceId: workspace.id
|
|
6800
|
+
};
|
|
6801
|
+
await dependencies.deployBaseline(target);
|
|
6802
|
+
const dataTarget = {
|
|
6803
|
+
...target,
|
|
6804
|
+
sourceWorkspaceId: prepared.sourceWorkspaceId,
|
|
6805
|
+
applicationName: prepared.sourceApplicationName
|
|
6806
|
+
};
|
|
6807
|
+
if (options.data === "clone") await dependencies.cloneData(dataTarget);
|
|
6808
|
+
else await dependencies.seedData(dataTarget);
|
|
6809
|
+
await dependencies.deployMigrations(target);
|
|
6810
|
+
if (options.assertionPath) await dependencies.runAssertion({
|
|
6811
|
+
...target,
|
|
6812
|
+
assertionPath: options.assertionPath,
|
|
6813
|
+
assertionNamespace: options.assertionNamespace,
|
|
6814
|
+
machineUser: options.machineUser
|
|
6815
|
+
});
|
|
6816
|
+
} catch (error) {
|
|
6817
|
+
primaryError = error;
|
|
6818
|
+
failed = true;
|
|
6819
|
+
}
|
|
6820
|
+
if (temporary && options.keep) logger.info(`Keeping temporary workspace ${workspace.name ?? workspace.id}.`);
|
|
6821
|
+
else if (temporary) try {
|
|
6822
|
+
await dependencies.deleteWorkspace(workspace.id);
|
|
6823
|
+
deleted = true;
|
|
6824
|
+
} catch (cleanupError) {
|
|
6825
|
+
if (!failed) throw new Error(`Failed to delete temporary workspace ${workspace.id}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, { cause: cleanupError });
|
|
6826
|
+
logger.warn(`Failed to delete temporary workspace ${workspace.id}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
6827
|
+
}
|
|
6828
|
+
if (failed) throw primaryError;
|
|
6829
|
+
return {
|
|
6830
|
+
success: true,
|
|
6831
|
+
workspaceId: workspace.id,
|
|
6832
|
+
...workspace.name ? { workspaceName: workspace.name } : {},
|
|
6833
|
+
temporary,
|
|
6834
|
+
deleted,
|
|
6835
|
+
data: options.data,
|
|
6836
|
+
pendingNamespaces: prepared.pendingNamespaces
|
|
6837
|
+
};
|
|
6838
|
+
}
|
|
6839
|
+
async function migrationTest(options) {
|
|
6840
|
+
logBetaWarning("tailordb migration");
|
|
6841
|
+
const result = await runMigrationTest(options);
|
|
6842
|
+
if (options.json) {
|
|
6843
|
+
logger.out(result);
|
|
6844
|
+
return;
|
|
6845
|
+
}
|
|
6846
|
+
logger.success(`Pending migrations completed in workspace ${result.workspaceName ?? result.workspaceId}.`);
|
|
6847
|
+
}
|
|
6848
|
+
const testCommand = defineAppCommand({
|
|
6849
|
+
name: "test",
|
|
6850
|
+
description: "Test pending migrations with seed fixtures or cloned data in a temporary workspace.",
|
|
6851
|
+
notes: "The source workspace is read-only. Without --target-workspace-id, the command creates a workspace in the source workspace's region and deletes it after success or failure; pass --keep to retain it for inspection. A designated target is retained and requires --yes. Clone mode copies TailorDB records only; it does not copy IdP users or file blobs.",
|
|
6852
|
+
args: z.strictObject({
|
|
6853
|
+
...deploymentArgs,
|
|
6854
|
+
yes: arg(z.boolean().default(false), {
|
|
6855
|
+
alias: "y",
|
|
6856
|
+
description: "Acknowledge that a designated target workspace may be overwritten"
|
|
6857
|
+
}),
|
|
6858
|
+
data: arg(z.enum(["seed", "clone"]).default("seed"), { description: "Data source for the migration test (seed or clone)" }),
|
|
6859
|
+
"target-workspace-id": arg(z.uuid().optional(), { description: "Existing throwaway workspace to retain after the test (requires --yes)" }),
|
|
6860
|
+
keep: arg(z.boolean().default(false), { description: "Keep the automatically created workspace after the test" }),
|
|
6861
|
+
assert: arg(z.string().optional(), {
|
|
6862
|
+
description: "Path to a TypeScript assertion script to run after migrations",
|
|
6863
|
+
completion: {
|
|
6864
|
+
type: "file",
|
|
6865
|
+
extensions: ["ts"]
|
|
6866
|
+
}
|
|
6867
|
+
}),
|
|
6868
|
+
"assert-namespace": arg(z.string().optional(), { description: "TailorDB namespace exposed to the assertion script" }),
|
|
6869
|
+
"machine-user": arg(z.string().optional(), { description: "Machine user for seed and assertion script execution" })
|
|
6870
|
+
}),
|
|
6871
|
+
run: async (args) => {
|
|
6872
|
+
await assertWritable({ profile: args.profile });
|
|
6873
|
+
await migrationTest({
|
|
6874
|
+
configPath: args.config,
|
|
6875
|
+
workspaceId: args["workspace-id"],
|
|
6876
|
+
profile: args.profile,
|
|
6877
|
+
data: args.data,
|
|
6878
|
+
targetWorkspaceId: args["target-workspace-id"],
|
|
6879
|
+
keep: args.keep,
|
|
6880
|
+
assertionPath: args.assert,
|
|
6881
|
+
assertionNamespace: args["assert-namespace"],
|
|
6882
|
+
machineUser: args["machine-user"],
|
|
6883
|
+
yes: args.yes,
|
|
6884
|
+
json: logger.jsonMode
|
|
6885
|
+
});
|
|
6886
|
+
}
|
|
6887
|
+
});
|
|
6888
|
+
|
|
6017
6889
|
//#endregion
|
|
6018
6890
|
//#region src/cli/commands/tailordb/migrate/validate.ts
|
|
6019
6891
|
/**
|
|
6020
|
-
*
|
|
6021
|
-
* migration script
|
|
6892
|
+
* Walk the local migration history once to assert that every required
|
|
6893
|
+
* migration script exists, no migration carries both a --no-script
|
|
6894
|
+
* acknowledgment and a migrate.ts, and generated normalization logic has
|
|
6895
|
+
* been reviewed, and to collect migrations with data-loss warnings that
|
|
6896
|
+
* have neither a migrate.ts nor a recorded --no-script acknowledgment
|
|
6022
6897
|
* @param {string} migrationsDir - Migrations directory path
|
|
6023
6898
|
* @param {string} namespace - TailorDB namespace (for error messages)
|
|
6899
|
+
* @param {string} [configPath] - Config path the current run used (for remediation hints)
|
|
6900
|
+
* @returns {UnacknowledgedWarningMigration[]} Unacknowledged warning migrations in the local history
|
|
6024
6901
|
*/
|
|
6025
|
-
function
|
|
6902
|
+
function assertMigrationScriptsReady(migrationsDir, namespace, configPath) {
|
|
6026
6903
|
const missing = [];
|
|
6904
|
+
const conflicting = [];
|
|
6905
|
+
const unreviewed = [];
|
|
6906
|
+
const unacknowledgedWarnings = [];
|
|
6027
6907
|
for (const file of getMigrationFiles(migrationsDir)) {
|
|
6028
6908
|
if (file.type !== "diff") continue;
|
|
6029
6909
|
const diff = loadDiff(file.path);
|
|
6030
|
-
|
|
6031
|
-
|
|
6910
|
+
const migrateFilePath = getMigrationFilePath(migrationsDir, file.number, "migrate");
|
|
6911
|
+
const hasScript = fs.existsSync(migrateFilePath);
|
|
6912
|
+
if (diff.requiresMigrationScript && !diff.scriptSkipped && !hasScript) missing.push(file.number);
|
|
6913
|
+
if (diff.scriptSkipped && hasScript) conflicting.push(file.number);
|
|
6914
|
+
if (hasScript && fs.readFileSync(migrateFilePath, "utf8").includes("TODO(tailor-migration-review)")) unreviewed.push(file.number);
|
|
6915
|
+
if (diff.hasWarnings && !diff.scriptSkipped && !hasScript) unacknowledgedWarnings.push({
|
|
6916
|
+
migrationNumber: file.number,
|
|
6917
|
+
warnings: diff.warnings
|
|
6918
|
+
});
|
|
6032
6919
|
}
|
|
6033
6920
|
if (missing.length > 0) throw new Error(`Migration(s) ${missing.map(formatMigrationNumber).join(", ")} in namespace "${namespace}" require a migration script but have no migrate.ts. Add one with 'tailor tailordb migration script <number>', or record that no script is needed with 'tailor tailordb migration script <number> --no-script --reason "..."'.`);
|
|
6921
|
+
if (conflicting.length > 0) {
|
|
6922
|
+
const clearCommands = conflicting.map((migrationNumber) => ` ${formatMigrationScriptCommand({
|
|
6923
|
+
migrationNumber,
|
|
6924
|
+
namespace,
|
|
6925
|
+
configPath
|
|
6926
|
+
})}`).join("\n");
|
|
6927
|
+
throw new Error(`Migration(s) ${conflicting.map(formatMigrationNumber).join(", ")} in namespace "${namespace}" have both a --no-script skip acknowledgment and migrate.ts. Clear the stale acknowledgment(s):
|
|
6928
|
+
${clearCommands}\nOr delete migrate.ts to keep the skip.`);
|
|
6929
|
+
}
|
|
6930
|
+
if (unreviewed.length > 0) throw new Error(`Migration(s) ${unreviewed.map(formatMigrationNumber).join(", ")} in namespace "${namespace}" contain generated normalization logic that still requires review in migrate.ts. Review each ${MIGRATION_REVIEW_REQUIRED_MARKER} marker, then remove the marker and its associated \`never\` annotation.`);
|
|
6931
|
+
return unacknowledgedWarnings;
|
|
6034
6932
|
}
|
|
6035
6933
|
/**
|
|
6036
6934
|
* Build per-namespace reports from the checks that completed
|
|
@@ -6038,7 +6936,7 @@ function assertRequiredMigrationScripts(migrationsDir, namespace) {
|
|
|
6038
6936
|
* @returns {NamespaceValidationReport[]} Validation reports per namespace
|
|
6039
6937
|
*/
|
|
6040
6938
|
function buildValidationReports(options) {
|
|
6041
|
-
const { targetNamespaces, migrationFileErrors, localResults, remoteResults } = options;
|
|
6939
|
+
const { targetNamespaces, migrationFileErrors, localResults, remoteResults, unacknowledgedWarnings } = options;
|
|
6042
6940
|
return targetNamespaces.map((target) => {
|
|
6043
6941
|
const fileError = migrationFileErrors.get(target.namespace);
|
|
6044
6942
|
if (fileError !== void 0) return {
|
|
@@ -6068,14 +6966,25 @@ function buildValidationReports(options) {
|
|
|
6068
6966
|
hasDrift: remote.hasDrift,
|
|
6069
6967
|
drifts: remote.drifts,
|
|
6070
6968
|
...remote.skipped ? { skipped: remote.skipped } : {},
|
|
6071
|
-
...checkpointMissingLocal ? { checkpointMissingLocal: true } : {}
|
|
6969
|
+
...checkpointMissingLocal ? { checkpointMissingLocal: true } : {},
|
|
6970
|
+
...remote.checkpointRepair ? { checkpointRepair: remote.checkpointRepair } : {}
|
|
6072
6971
|
};
|
|
6972
|
+
const candidates = unacknowledgedWarnings?.get(target.namespace);
|
|
6973
|
+
let warningAcknowledgments;
|
|
6974
|
+
if (candidates !== void 0) {
|
|
6975
|
+
const missing = candidates.filter((candidate) => candidate.migrationNumber > remote.remoteMigrationNumber);
|
|
6976
|
+
warningAcknowledgments = {
|
|
6977
|
+
valid: missing.length === 0,
|
|
6978
|
+
missing
|
|
6979
|
+
};
|
|
6980
|
+
}
|
|
6073
6981
|
return {
|
|
6074
6982
|
namespace: target.namespace,
|
|
6075
|
-
valid: !localSchema.hasDiff && !remoteSchema.hasDrift && !checkpointMissingLocal,
|
|
6983
|
+
valid: !localSchema.hasDiff && !remoteSchema.hasDrift && !checkpointMissingLocal && warningAcknowledgments?.valid !== false,
|
|
6076
6984
|
migrationFiles: { valid: true },
|
|
6077
6985
|
localSchema,
|
|
6078
|
-
remoteSchema
|
|
6986
|
+
remoteSchema,
|
|
6987
|
+
...warningAcknowledgments ? { warningAcknowledgments } : {}
|
|
6079
6988
|
};
|
|
6080
6989
|
});
|
|
6081
6990
|
}
|
|
@@ -6091,7 +7000,7 @@ async function collectValidationReports(options) {
|
|
|
6091
7000
|
const targetNamespaces = options.namespace ? namespacesWithMigrations.filter((ns) => ns.namespace === options.namespace) : namespacesWithMigrations;
|
|
6092
7001
|
if (targetNamespaces.length === 0) throw new Error(`Namespace "${options.namespace}" not found or does not have migrations configured`);
|
|
6093
7002
|
const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
|
|
6094
|
-
const { defineApplication } = await import("../application-
|
|
7003
|
+
const { defineApplication } = await import("../application-BZfk4HKm.mjs");
|
|
6095
7004
|
const application = defineApplication({
|
|
6096
7005
|
config,
|
|
6097
7006
|
pluginManager
|
|
@@ -6106,10 +7015,12 @@ async function collectValidationReports(options) {
|
|
|
6106
7015
|
const typesByNamespace = new Map(tailorDBInputs.map((input) => [input.namespace, input.types]));
|
|
6107
7016
|
const migrationFileErrors = /* @__PURE__ */ new Map();
|
|
6108
7017
|
const checkableNamespaces = [];
|
|
7018
|
+
const unacknowledgedWarnings = options.strict ? /* @__PURE__ */ new Map() : void 0;
|
|
6109
7019
|
for (const target of targetNamespaces) try {
|
|
6110
7020
|
assertValidMigrationFiles(target.migrationsDir, target.namespace);
|
|
6111
7021
|
reconstructSnapshotFromMigrations(target.migrationsDir);
|
|
6112
|
-
|
|
7022
|
+
const namespaceWarnings = assertMigrationScriptsReady(target.migrationsDir, target.namespace, options.configPath);
|
|
7023
|
+
unacknowledgedWarnings?.set(target.namespace, namespaceWarnings);
|
|
6113
7024
|
checkableNamespaces.push(target);
|
|
6114
7025
|
} catch (error) {
|
|
6115
7026
|
migrationFileErrors.set(target.namespace, error instanceof Error ? error.message : String(error));
|
|
@@ -6117,7 +7028,8 @@ async function collectValidationReports(options) {
|
|
|
6117
7028
|
const localReportOptions = {
|
|
6118
7029
|
targetNamespaces,
|
|
6119
7030
|
migrationFileErrors,
|
|
6120
|
-
localResults: await checkMigrationDiffs(typesByNamespace, checkableNamespaces)
|
|
7031
|
+
localResults: await checkMigrationDiffs(typesByNamespace, checkableNamespaces),
|
|
7032
|
+
...unacknowledgedWarnings ? { unacknowledgedWarnings } : {}
|
|
6121
7033
|
};
|
|
6122
7034
|
if (checkableNamespaces.length === 0) return { reports: buildValidationReports(localReportOptions) };
|
|
6123
7035
|
let remoteResults;
|
|
@@ -6156,6 +7068,7 @@ function printValidationReports(reports) {
|
|
|
6156
7068
|
else logger.log(` Local schema: ${styles.success("OK")}`);
|
|
6157
7069
|
const remote = report.remoteSchema;
|
|
6158
7070
|
if (remote?.skipped === "check_failed") logger.log(` Remote schema: ${styles.error("not checked")}`);
|
|
7071
|
+
else if (remote?.checkpointRepair) logger.log(` Remote schema: ${styles.success("OK")} (next deploy will reset the checkpoint to 0000 from ${formatMigrationNumber(remote.checkpointRepair.from)})`);
|
|
6159
7072
|
else if (remote?.checkpointMissingLocal) logger.log(` Remote schema: ${styles.error(`remote migration ${formatMigrationNumber(remote.remoteMigrationNumber)} is not in the local migration history`)}`);
|
|
6160
7073
|
else if (remote?.hasDrift) {
|
|
6161
7074
|
logger.log(` Remote schema: ${styles.error("drift detected")} (remote migration: ${formatMigrationNumber(remote.remoteMigrationNumber)})`);
|
|
@@ -6164,14 +7077,36 @@ function printValidationReports(reports) {
|
|
|
6164
7077
|
else if (remote?.skipped === "no_migration_label") logger.log(` Remote schema: ${styles.dim("skipped (deployed namespace has no migration state)")}`);
|
|
6165
7078
|
else if (remote?.skipped === "no_snapshot") logger.log(` Remote schema: ${styles.dim(`skipped (no local snapshot for remote migration ${formatMigrationNumber(remote.remoteMigrationNumber)})`)}`);
|
|
6166
7079
|
else if (remote) logger.log(` Remote schema: ${styles.success("OK")} (remote migration: ${formatMigrationNumber(remote.remoteMigrationNumber)})`);
|
|
7080
|
+
const acknowledgments = report.warningAcknowledgments;
|
|
7081
|
+
if (acknowledgments) if (acknowledgments.valid) logger.log(` Warning acknowledgments: ${styles.success("OK")}`);
|
|
7082
|
+
else {
|
|
7083
|
+
logger.log(` Warning acknowledgments: ${styles.error("missing for pending migration(s) with possible data loss")}`);
|
|
7084
|
+
for (const migration of acknowledgments.missing) {
|
|
7085
|
+
const label = formatMigrationNumber(migration.migrationNumber);
|
|
7086
|
+
for (const warning of migration.warnings) {
|
|
7087
|
+
const field = warning.fieldName ? `.${warning.fieldName}` : "";
|
|
7088
|
+
logger.log(` ${label}: ${warning.tableName}${field}: ${warning.reason}`);
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
6167
7092
|
}
|
|
6168
7093
|
logger.newline();
|
|
6169
7094
|
}
|
|
6170
|
-
function printResolutionHints(reports) {
|
|
7095
|
+
function printResolutionHints(reports, configPath) {
|
|
6171
7096
|
if (reports.some((r) => r.localSchema?.hasDiff && !r.localSchema.diff)) logger.info("Run 'tailor tailordb migration generate' to create the initial snapshot.");
|
|
6172
7097
|
if (reports.some((r) => r.localSchema?.diff)) logger.info("Run 'tailor tailordb migration generate' to create migration files.");
|
|
6173
7098
|
if (reports.some((r) => r.remoteSchema?.checkpointMissingLocal)) logger.info("The remote migration checkpoint is ahead of the local history. Pull the latest migration files, or run 'tailor tailordb migration status' to compare.");
|
|
6174
7099
|
if (reports.some((r) => r.remoteSchema?.hasDrift)) logRemoteDriftGuidance();
|
|
7100
|
+
const missingAcknowledgments = reports.filter((r) => r.warningAcknowledgments && !r.warningAcknowledgments.valid);
|
|
7101
|
+
if (missingAcknowledgments.length > 0) {
|
|
7102
|
+
logger.info("For each migration listed, either add a data migration script with 'tailor tailordb migration script <number>' or record why none is needed:");
|
|
7103
|
+
for (const report of missingAcknowledgments) for (const migration of report.warningAcknowledgments?.missing ?? []) logger.info(` ${formatMigrationScriptCommand({
|
|
7104
|
+
migrationNumber: migration.migrationNumber,
|
|
7105
|
+
namespace: report.namespace,
|
|
7106
|
+
configPath,
|
|
7107
|
+
noScript: true
|
|
7108
|
+
})}`, { mode: "plain" });
|
|
7109
|
+
}
|
|
6175
7110
|
}
|
|
6176
7111
|
/**
|
|
6177
7112
|
* Validate migration files and schema state without deploying
|
|
@@ -6186,20 +7121,21 @@ async function validate(options) {
|
|
|
6186
7121
|
else {
|
|
6187
7122
|
printValidationReports(reports);
|
|
6188
7123
|
if (invalidCount === 0 && !("remoteError" in collected)) logger.success("All migration validation checks passed.");
|
|
6189
|
-
else printResolutionHints(reports);
|
|
7124
|
+
else printResolutionHints(reports, options.configPath);
|
|
6190
7125
|
}
|
|
6191
7126
|
if ("remoteError" in collected) throw collected.remoteError;
|
|
6192
7127
|
if (invalidCount > 0) throw new Error(`Migration validation failed for ${invalidCount} namespace(s)`);
|
|
6193
7128
|
}
|
|
6194
7129
|
const validateCommand = defineAppCommand({
|
|
6195
7130
|
name: "validate",
|
|
6196
|
-
description: "Validate the full migration history and
|
|
7131
|
+
description: "Validate the full migration history, unreviewed generated migration scripts, and schema drift (local types vs. migration snapshot, remote schema vs. migration checkpoint) without deploying. This includes the migration and schema-drift checks used by 'deploy' and exits with a non-zero code when issues are found.",
|
|
6197
7132
|
args: z.strictObject({
|
|
6198
7133
|
...deploymentArgs,
|
|
6199
7134
|
namespace: arg(z.string().optional(), {
|
|
6200
7135
|
alias: "n",
|
|
6201
7136
|
description: "Target TailorDB namespace (validates all namespaces if not specified)"
|
|
6202
|
-
})
|
|
7137
|
+
}),
|
|
7138
|
+
strict: arg(z.boolean().default(false), { description: "Also fail when a pending migration can drop data without an acknowledgment" })
|
|
6203
7139
|
}),
|
|
6204
7140
|
run: async (args) => {
|
|
6205
7141
|
await validate({
|
|
@@ -6207,7 +7143,8 @@ const validateCommand = defineAppCommand({
|
|
|
6207
7143
|
namespace: args.namespace,
|
|
6208
7144
|
workspaceId: args["workspace-id"],
|
|
6209
7145
|
profile: args.profile,
|
|
6210
|
-
json: logger.jsonMode
|
|
7146
|
+
json: logger.jsonMode,
|
|
7147
|
+
strict: args.strict
|
|
6211
7148
|
});
|
|
6212
7149
|
}
|
|
6213
7150
|
});
|
|
@@ -6219,10 +7156,12 @@ const validateCommand = defineAppCommand({
|
|
|
6219
7156
|
*
|
|
6220
7157
|
* Subcommands:
|
|
6221
7158
|
* - generate: Generate migration files from schema differences
|
|
7159
|
+
* - rebaseline: Collapse the full migration history into a new baseline
|
|
6222
7160
|
* - script: Add a migrate.ts template to an existing migration
|
|
6223
7161
|
* - set: Set migration checkpoint to a specific number
|
|
6224
7162
|
* - status: Show migration status for TailorDB namespaces
|
|
6225
7163
|
* - sync: Sync remote TailorDB schema to a specific migration snapshot
|
|
7164
|
+
* - test: Test pending migrations in an isolated workspace
|
|
6226
7165
|
* - validate: Validate migration files and detect schema drift without deploying
|
|
6227
7166
|
*/
|
|
6228
7167
|
const migrationCommand = defineCommand({
|
|
@@ -6230,10 +7169,12 @@ const migrationCommand = defineCommand({
|
|
|
6230
7169
|
description: "Manage TailorDB schema migrations.",
|
|
6231
7170
|
subCommands: {
|
|
6232
7171
|
generate: generateCommand$1,
|
|
7172
|
+
rebaseline: rebaselineCommand,
|
|
6233
7173
|
script: scriptCommand,
|
|
6234
7174
|
set: setCommand,
|
|
6235
7175
|
status: statusCommand,
|
|
6236
7176
|
sync: syncCommand,
|
|
7177
|
+
test: testCommand,
|
|
6237
7178
|
validate: validateCommand
|
|
6238
7179
|
}
|
|
6239
7180
|
});
|
|
@@ -6269,7 +7210,7 @@ const upgradeCommand = defineAppCommand({
|
|
|
6269
7210
|
run: async (args) => {
|
|
6270
7211
|
const { initTelemetry } = await import("../telemetry-CkbkeJxl.mjs");
|
|
6271
7212
|
await initTelemetry();
|
|
6272
|
-
const { upgrade } = await import("../service-
|
|
7213
|
+
const { upgrade } = await import("../service-hZskxZmg.mjs");
|
|
6273
7214
|
await upgrade({
|
|
6274
7215
|
from: args.from,
|
|
6275
7216
|
dryRun: args["dry-run"],
|
|
@@ -6773,10 +7714,12 @@ runMain(mainCommand, {
|
|
|
6773
7714
|
if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
|
|
6774
7715
|
} else if (error instanceof Error) {
|
|
6775
7716
|
logger.error(error.message);
|
|
7717
|
+
const hint = typeOnlyImportHint(error);
|
|
7718
|
+
if (hint) logger.log(` ${styles.info("Suggestion:")} ${hint}`);
|
|
6776
7719
|
if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
|
|
6777
7720
|
} else logger.error(`Unknown error: ${error}`);
|
|
6778
7721
|
if (!isCLIError(error) && (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError)) {
|
|
6779
|
-
const { reportCrash } = await import("../crashreport-
|
|
7722
|
+
const { reportCrash } = await import("../crashreport-CINZ5dVN.mjs");
|
|
6780
7723
|
await reportCrash(error, "handledError");
|
|
6781
7724
|
}
|
|
6782
7725
|
}
|