@tailor-platform/sdk 1.71.0 → 1.72.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.
@@ -2820,13 +2820,25 @@ function findTarget(lock, kind, workspaceName) {
2820
2820
  return lock?.targets.find((t) => t.kind === kind && t.workspaceName === workspaceName);
2821
2821
  }
2822
2822
 
2823
+ //#endregion
2824
+ //#region src/cli/commands/setup/action.yml
2825
+ 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 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 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 uses: tailor-platform/actions/deploy@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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";
2826
+
2823
2827
  //#endregion
2824
2828
  //#region src/cli/commands/setup/branch.workflow.yml
2825
- 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\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@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n persist-credentials: false\n - id: tailor-checkout-base\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\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-sdk.lock\");\n const baseTarget = lockTarget(\".tailor-erd-base/.github/tailor-sdk.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n persist-credentials: false\n - id: tailor-setup\n uses: tailor-platform/actions/setup@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-checkout-base\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\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@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9\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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\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@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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_sdk() {\n case \"__PACKAGE_MANAGER__\" in\n pnpm) pnpm exec tailor-sdk \"$@\" ;;\n npm) npx tailor-sdk \"$@\" ;;\n yarn) yarn tailor-sdk \"$@\" ;;\n bun) bunx tailor-sdk \"$@\" ;;\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_sdk_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_sdk_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_SDK_BIN=\"$tailor_sdk_bin\" TAILOR_SDK_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_sdk_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_SDK_BIN=\"$tailor_sdk_bin\" TAILOR_SDK_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-tailor-sdk.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_SDK_BIN;\n const commandCwd = process.env.TAILOR_SDK_CWD;\n if (!cliBin || !commandCwd) {\n throw new Error(\"TAILOR_SDK_BIN and TAILOR_SDK_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_sdk 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_sdk_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_sdk_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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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";
2829
+ 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_START__\n - id: tailor-seed-validate\n uses: tailor-platform/actions/seed-validate@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_END__\n - id: tailor-drift-check\n uses: tailor-platform/actions/drift-check@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n persist-credentials: false\n - id: tailor-checkout-base\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\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-sdk.lock\");\n const baseTarget = lockTarget(\".tailor-erd-base/.github/tailor-sdk.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n persist-credentials: false\n - id: tailor-setup\n uses: tailor-platform/actions/setup@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-checkout-base\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\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@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9\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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\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@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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_sdk() {\n case \"__PACKAGE_MANAGER__\" in\n pnpm) pnpm exec tailor-sdk \"$@\" ;;\n npm) npx tailor-sdk \"$@\" ;;\n yarn) yarn tailor-sdk \"$@\" ;;\n bun) bunx tailor-sdk \"$@\" ;;\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_sdk_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_sdk_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_SDK_BIN=\"$tailor_sdk_bin\" TAILOR_SDK_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_sdk_bin() {\n local command_cwd=\"$1\"\n shift\n\n run_head_cli_env() {\n env TAILOR_SDK_BIN=\"$tailor_sdk_bin\" TAILOR_SDK_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-tailor-sdk.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_SDK_BIN;\n const commandCwd = process.env.TAILOR_SDK_CWD;\n if (!cliBin || !commandCwd) {\n throw new Error(\"TAILOR_SDK_BIN and TAILOR_SDK_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_sdk 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_sdk_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_sdk_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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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";
2830
+
2831
+ //#endregion
2832
+ //#region src/cli/commands/setup/coordinate.workflow.yml
2833
+ 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: ./.github/actions/tailor-setup\n # __PER_APP_DEPLOY_STEPS__\n";
2834
+
2835
+ //#endregion
2836
+ //#region src/cli/commands/setup/preview.workflow.yml
2837
+ 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-sdk setup --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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n - id: tailor-preview-deploy\n uses: tailor-platform/actions/preview-deploy@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-preview-cleanup\n uses: tailor-platform/actions/preview-cleanup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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";
2826
2838
 
2827
2839
  //#endregion
2828
2840
  //#region src/cli/commands/setup/tag.workflow.yml
2829
- 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n - id: tailor-plan\n uses: tailor-platform/actions/plan@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@7817f37adf2891fbb2ebbe0a3b222a5fa2ed0c1f # v1.3.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";
2841
+ 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n uses: tailor-platform/actions/tag-guard@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-generate-check\n uses: tailor-platform/actions/generate-check@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_START__\n - id: tailor-seed-validate\n uses: tailor-platform/actions/seed-validate@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n # __WORKING_DIRECTORY__\n # __SEED_VALIDATE_END__\n - id: tailor-drift-check\n uses: tailor-platform/actions/drift-check@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n - id: tailor-setup\n uses: tailor-platform/actions/setup@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-install\n uses: tailor-platform/actions/install@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.0\n with:\n package-manager: __PACKAGE_MANAGER__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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@d203a37c87b7a69c6040611c33e9e50c1d564ed1 # v1.5.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";
2830
2842
 
2831
2843
  //#endregion
2832
2844
  //#region src/cli/commands/setup/templates.ts
@@ -2870,29 +2882,39 @@ function applyCommon(content, params) {
2870
2882
  }
2871
2883
  /**
2872
2884
  * Render the branch-target deploy workflow.
2873
- *
2874
- * When `plan` is false the plan job, the pull_request trigger, and the
2875
- * workflow_dispatch dry-run input are all removed (a plan-less workflow has no
2876
- * meaningful dry-run), and the deploy job runs unconditionally.
2877
2885
  * @param params - Workspace and rendering configuration
2878
2886
  * @returns Rendered YAML and the list of managed job/step ids
2879
2887
  */
2880
2888
  function renderBranchWorkflow(params) {
2881
- const { branch, plan } = params;
2882
- const erdPreview = plan ? params.erdPreview : null;
2889
+ const { branch } = params;
2890
+ const seedValidate = params.seedValidate ?? false;
2891
+ const migrationDriftCheck = params.migrationDriftCheck ?? false;
2892
+ const erdPreview = params.erdPreview;
2883
2893
  let out = branch_workflow_default;
2884
- out = block(out, "PLAN_JOB", plan);
2894
+ out = block(out, "PLAN_JOB", true);
2885
2895
  out = block(out, "ERD_PREVIEW_JOB", erdPreview !== null);
2886
2896
  out = block(out, "ERD_PREVIEW_COMMENT_JOB", erdPreview !== null);
2887
- out = block(out, "PULL_REQUEST", plan);
2888
- out = block(out, "DISPATCH_INPUTS", plan);
2889
- out = line(out, "DEPLOY_IF", plan ? `if: >-\n github.event_name == 'push' ||\n (github.event_name == 'workflow_dispatch' && !inputs['dry-run'])` : void 0);
2897
+ out = block(out, "PULL_REQUEST", true);
2898
+ out = block(out, "DISPATCH_INPUTS", true);
2899
+ out = block(out, "SEED_VALIDATE", seedValidate);
2900
+ out = block(out, "MIGRATION_DRIFT_CHECK", migrationDriftCheck);
2901
+ out = block(out, "SEED_DATA", false);
2902
+ out = line(out, "DEPLOY_IF", `if: >-\n github.event_name == 'push' ||\n (github.event_name == 'workflow_dispatch' && !inputs['dry-run'])`);
2890
2903
  out = line(out, "PATHS", params.workingDirectory ? `paths: ["${params.workingDirectory}/**"]` : void 0);
2891
2904
  out = applyCommon(out, params).replaceAll("__BRANCH__", () => branch);
2892
- const generatedIds = [];
2893
- if (plan) generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", "tailor-plan/tailor-setup", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-plan");
2905
+ const generatedIds = [
2906
+ "tailor-plan",
2907
+ "tailor-plan/tailor-checkout",
2908
+ "tailor-plan/tailor-setup",
2909
+ "tailor-plan/tailor-install",
2910
+ "tailor-plan/tailor-generate-check"
2911
+ ];
2912
+ if (seedValidate) generatedIds.push("tailor-plan/tailor-seed-validate");
2913
+ generatedIds.push("tailor-plan/tailor-drift-check");
2914
+ if (migrationDriftCheck) generatedIds.push("tailor-plan/tailor-migration-drift-check");
2915
+ generatedIds.push("tailor-plan/tailor-plan");
2894
2916
  if (erdPreview) generatedIds.push("tailor-erd-preview-matrix", "tailor-erd-preview-matrix/tailor-checkout", "tailor-erd-preview-matrix/tailor-checkout-base", "tailor-erd-preview-matrix/tailor-erd-preview-matrix", "tailor-erd-preview", "tailor-erd-preview/tailor-checkout", "tailor-erd-preview/tailor-setup", "tailor-erd-preview/tailor-checkout-base", "tailor-erd-preview/tailor-detect-base-package-manager", "tailor-erd-preview/tailor-setup-base-pnpm", "tailor-erd-preview/tailor-setup-base-node", "tailor-erd-preview/tailor-setup-base-yarn", "tailor-erd-preview/tailor-setup-base-bun", "tailor-erd-preview/tailor-install-base", "tailor-erd-preview/tailor-restore-head-setup", "tailor-erd-preview/tailor-build-erd-preview", "tailor-erd-preview/tailor-upload-erd-viewer", "tailor-erd-preview-comment", "tailor-erd-preview-comment/tailor-comment-erd-preview");
2895
- generatedIds.push("tailor-deploy", "tailor-deploy/tailor-checkout", "tailor-deploy/tailor-setup", "tailor-deploy/tailor-apply");
2917
+ generatedIds.push("tailor-deploy", "tailor-deploy/tailor-checkout", "tailor-deploy/tailor-setup", "tailor-deploy/tailor-install", "tailor-deploy/tailor-apply", "tailor-deploy/tailor-notify");
2896
2918
  return {
2897
2919
  content: out,
2898
2920
  generatedIds
@@ -2909,139 +2931,208 @@ function renderBranchWorkflow(params) {
2909
2931
  function renderTagWorkflow(params) {
2910
2932
  const { tagPattern, branch } = params;
2911
2933
  const hasGuard = branch !== void 0;
2934
+ const seedValidate = params.seedValidate ?? false;
2935
+ const migrationDriftCheck = params.migrationDriftCheck ?? false;
2912
2936
  let out = tag_workflow_default;
2913
2937
  out = block(out, "TAG_GUARD_JOB", hasGuard);
2938
+ out = block(out, "SEED_VALIDATE", seedValidate);
2939
+ out = block(out, "MIGRATION_DRIFT_CHECK", migrationDriftCheck);
2940
+ out = block(out, "SEED_DATA", false);
2914
2941
  out = line(out, "PLAN_NEEDS", hasGuard ? "needs: tailor-tag-guard" : void 0);
2915
2942
  out = line(out, "PLAN_IF", hasGuard ? `if: >-\n github.event_name == 'workflow_dispatch' ||\n needs.tailor-tag-guard.outputs.on-branch == 'true'` : void 0);
2916
2943
  out = applyCommon(out, params).replaceAll("__TAG_PATTERN__", () => tagPattern);
2917
2944
  if (hasGuard) out = out.replaceAll("__BRANCH__", () => branch);
2918
2945
  const generatedIds = [];
2919
2946
  if (hasGuard) generatedIds.push("tailor-tag-guard", "tailor-tag-guard/tailor-checkout", "tailor-tag-guard/tailor-tag-guard");
2920
- generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", "tailor-plan/tailor-setup", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-plan", "tailor-deploy", "tailor-deploy/tailor-checkout", "tailor-deploy/tailor-setup", "tailor-deploy/tailor-apply");
2947
+ generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", "tailor-plan/tailor-setup", "tailor-plan/tailor-install", "tailor-plan/tailor-generate-check");
2948
+ if (seedValidate) generatedIds.push("tailor-plan/tailor-seed-validate");
2949
+ generatedIds.push("tailor-plan/tailor-drift-check");
2950
+ if (migrationDriftCheck) generatedIds.push("tailor-plan/tailor-migration-drift-check");
2951
+ generatedIds.push("tailor-plan/tailor-plan", "tailor-deploy", "tailor-deploy/tailor-checkout", "tailor-deploy/tailor-setup", "tailor-deploy/tailor-install", "tailor-deploy/tailor-apply", "tailor-deploy/tailor-notify");
2921
2952
  return {
2922
2953
  content: out,
2923
2954
  generatedIds
2924
2955
  };
2925
2956
  }
2926
-
2927
- //#endregion
2928
- //#region src/cli/commands/setup/check.ts
2929
2957
  /**
2930
- * Compute drift findings for one target by comparing its recorded lock state
2931
- * against the current repository/config state.
2932
- * @param target - The lock target being audited
2933
- * @param state - Current repository/config state for this target
2934
- * @returns Drift findings (empty when the target is in sync)
2958
+ * Render the preview workflow (PR open/sync/close triggers).
2959
+ *
2960
+ * The deploy job runs on opened/synchronize/reopened events; the cleanup job
2961
+ * runs on the closed event. Both jobs run in the same workflow so PR number
2962
+ * context is always available.
2963
+ * @param params - Workspace and rendering configuration
2964
+ * @returns Rendered YAML and the list of managed job/step ids
2935
2965
  */
2936
- function findTargetDrift(target, state) {
2937
- const id = `${target.kind} ${target.workspaceName}`;
2938
- const findings = [];
2939
- if (!state.fileExists) findings.push({
2940
- target: id,
2941
- rule: "missing-file",
2942
- message: `${target.file} is missing or unreadable. Re-run setup to restore it.`
2943
- });
2944
- else if (state.currentHash !== null && state.currentHash !== target.contentHash) findings.push({
2945
- target: id,
2946
- rule: "hand-edit",
2947
- message: `${target.file} was edited by hand since it was generated. Re-run setup with --force to regenerate, or keep customizations in your own jobs/steps.`
2948
- });
2949
- if (target.templateVersion < state.templateVersion) findings.push({
2950
- target: id,
2951
- rule: "template-version",
2952
- message: `A newer workflow template is available (generated with v${String(target.templateVersion)}, current v${String(state.templateVersion)}). Re-run setup to update.`
2953
- });
2954
- if (!state.configExists) findings.push({
2955
- target: id,
2956
- rule: "config-dir",
2957
- message: `tailor.config.ts not found under "${target.inputs.dir}". The app directory may have moved; re-run setup with the correct --dir.`
2958
- });
2959
- if (target.kind === "branch" && target.inputs.branchAutoDetected !== false && state.defaultBranch !== null && target.inputs.branch !== null && target.inputs.branch !== state.defaultBranch) findings.push({
2960
- target: id,
2961
- rule: "default-branch",
2962
- message: `The workflow triggers on "${target.inputs.branch}" but the repository default branch is now "${state.defaultBranch}". If this is intentional, ignore this; otherwise re-run setup so the trigger matches the default branch.`
2963
- });
2964
- if (target.kind === "branch" && target.inputs.erdPreview && state.configExists) {
2965
- const recorded = [...target.inputs.erdNamespaces ?? []].toSorted((a, b) => a.localeCompare(b));
2966
- const current = state.erdNamespaces?.toSorted((a, b) => a.localeCompare(b)) ?? null;
2967
- if (current === null || recorded.length !== current.length || recorded.some((namespace, index) => namespace !== current[index])) findings.push({
2968
- target: id,
2969
- rule: "erd-namespaces",
2970
- message: "TailorDB namespaces for ERD preview changed. Re-run setup so the ERD preview matrix is regenerated."
2971
- });
2972
- }
2973
- return findings;
2974
- }
2975
- function detectDefaultBranchSafe(cwd, run) {
2976
- try {
2977
- return detectDefaultBranch(cwd, run);
2978
- } catch {
2979
- return null;
2980
- }
2981
- }
2982
- function escapesRoot$1(rel) {
2983
- return path.isAbsolute(rel) || rel === ".." || rel.startsWith(`..${path.sep}`) || rel.startsWith("../");
2966
+ function renderPreviewWorkflow(params) {
2967
+ const { branch } = params;
2968
+ const requirePreviewLabel = params.requirePreviewLabel ?? false;
2969
+ let out = preview_workflow_default;
2970
+ out = line(out, "PATHS", params.workingDirectory ? `paths: ["${params.workingDirectory}/**"]` : void 0);
2971
+ out = line(out, "PR_TYPES", requirePreviewLabel ? "types: [labeled, synchronize, reopened, closed]" : "types: [opened, synchronize, reopened, closed]");
2972
+ out = line(out, "DEPLOY_IF", requirePreviewLabel ? `if: >-\n contains(github.event.pull_request.labels.*.name, 'tailor:preview') &&\n github.event.action != 'closed' &&\n !github.event.pull_request.draft &&\n !github.event.pull_request.head.repo.fork` : `if: >-\n github.event.action != 'closed' &&\n !github.event.pull_request.draft &&\n !github.event.pull_request.head.repo.fork`);
2973
+ out = line(out, "CLEANUP_IF", `if: >-\n github.event.action == 'closed' &&\n !github.event.pull_request.head.repo.fork`);
2974
+ out = applyCommon(out, params).replaceAll("__BRANCH__", () => branch).replaceAll("__REGION__", () => params.region);
2975
+ return {
2976
+ content: out,
2977
+ generatedIds: [
2978
+ "tailor-preview-deploy",
2979
+ "tailor-preview-deploy/tailor-checkout",
2980
+ "tailor-preview-deploy/tailor-setup",
2981
+ "tailor-preview-deploy/tailor-install",
2982
+ "tailor-preview-deploy/tailor-generate-check",
2983
+ "tailor-preview-deploy/tailor-preview-deploy",
2984
+ "tailor-preview-deploy/tailor-preview-comment",
2985
+ "tailor-preview-cleanup",
2986
+ "tailor-preview-cleanup/tailor-checkout",
2987
+ "tailor-preview-cleanup/tailor-setup",
2988
+ "tailor-preview-cleanup/tailor-install",
2989
+ "tailor-preview-cleanup/tailor-preview-cleanup"
2990
+ ]
2991
+ };
2984
2992
  }
2985
- function resolveWithinRoot(outputDir, relPath) {
2986
- if (path.isAbsolute(relPath)) return null;
2987
- const abs = path.join(outputDir, relPath);
2988
- if (escapesRoot$1(path.relative(outputDir, abs))) return null;
2989
- try {
2990
- if (escapesRoot$1(path.relative(fs$1.realpathSync(outputDir), fs$1.realpathSync(abs)))) return null;
2991
- } catch {}
2992
- return abs;
2993
+ /**
2994
+ * Render the per-app composite action.
2995
+ *
2996
+ * The action contains only the deploy step and a failure notification.
2997
+ * Checkout, setup, and install are the caller's responsibility (coordinator workflow).
2998
+ * @param params - Workspace and rendering configuration
2999
+ * @returns Rendered YAML and the list of managed step ids
3000
+ */
3001
+ function renderActionWorkflow(params) {
3002
+ let out = action_default;
3003
+ out = line(out, "HEADER", HEADER);
3004
+ out = block(out, "STATIC_WEBSITE_BUILD", params.hasStaticWebsites ?? false);
3005
+ out = out.replaceAll("__WORKSPACE_NAME__", () => params.workspaceName);
3006
+ return {
3007
+ content: out,
3008
+ generatedIds: ["tailor-apply", "tailor-notify"]
3009
+ };
2993
3010
  }
2994
- function readHash(absFile) {
2995
- try {
2996
- return hashContent(fs$1.readFileSync(absFile, "utf-8"));
2997
- } catch {
2998
- return null;
3011
+ const ACTIONS_SHA = "d203a37c87b7a69c6040611c33e9e50c1d564ed1";
3012
+ /**
3013
+ * Render the coordinator workflow that orchestrates per-app composite actions.
3014
+ *
3015
+ * Generates per-app plan steps (generate-check, drift-check, plan) and per-app
3016
+ * deploy steps (calling each app's composite action at .github/actions/tailor-<name>).
3017
+ * Checkout, setup, and install are done once per job using the local tailor-setup action.
3018
+ * @param params - Coordinator and app configuration
3019
+ * @returns Rendered YAML and the list of managed job/step ids
3020
+ */
3021
+ function renderCoordinateWorkflow(params) {
3022
+ const { coordinatorName, kind, apps, environment, packageManager } = params;
3023
+ const isTag = kind === "tag";
3024
+ const branch = params.branch;
3025
+ const tagPattern = params.tagPattern ?? "v*";
3026
+ let out = coordinate_workflow_default;
3027
+ out = line(out, "HEADER", HEADER);
3028
+ out = block(out, "PULL_REQUEST", !isTag);
3029
+ out = block(out, "PUSH_BRANCHES", !isTag);
3030
+ out = block(out, "PUSH_TAGS", isTag);
3031
+ out = block(out, "TAG_GUARD_JOB", isTag && branch !== void 0);
3032
+ if (isTag && branch !== void 0) {
3033
+ out = line(out, "PLAN_NEEDS", "needs: tailor-tag-guard");
3034
+ out = line(out, "PLAN_IF", `if: >-\n github.event_name == 'workflow_dispatch' ||\n needs.tailor-tag-guard.outputs.on-branch == 'true'`);
3035
+ } else if (isTag) {
3036
+ out = line(out, "PLAN_NEEDS", void 0);
3037
+ out = line(out, "PLAN_IF", void 0);
3038
+ } else {
3039
+ out = line(out, "PLAN_NEEDS", void 0);
3040
+ out = line(out, "PLAN_IF", `if: >-\n github.event_name == 'pull_request' ||\n (github.event_name == 'workflow_dispatch' && inputs['dry-run'])`);
2999
3041
  }
3000
- }
3001
- async function defaultLoadErdNamespaces$1(configPath) {
3002
- const { config } = await loadConfig(configPath);
3003
- return extractOwnedNamespaces(config);
3042
+ if (isTag) {
3043
+ out = line(out, "DEPLOY_NEEDS", "needs: tailor-plan");
3044
+ out = line(out, "DEPLOY_IF", `if: \${{ !(github.event_name == 'workflow_dispatch' && inputs['dry-run']) }}`);
3045
+ out = line(out, "DEPLOY_ENVIRONMENT", `environment: ${environment}`);
3046
+ } else {
3047
+ out = line(out, "DEPLOY_NEEDS", void 0);
3048
+ out = line(out, "DEPLOY_IF", `if: >-\n github.event_name == 'push' ||\n (github.event_name == 'workflow_dispatch' && !inputs['dry-run'])`);
3049
+ out = line(out, "DEPLOY_ENVIRONMENT", `environment: ${environment}`);
3050
+ }
3051
+ const driftCheckStep = [
3052
+ `- id: tailor-drift-check`,
3053
+ ` uses: tailor-platform/actions/drift-check@${ACTIONS_SHA} # v1.5.0`,
3054
+ ` with:`,
3055
+ ` package-manager: ${packageManager}`
3056
+ ].join("\n");
3057
+ const perAppPlanSteps = apps.flatMap((app) => {
3058
+ const wd = app.dir !== "." ? app.dir : void 0;
3059
+ const wdLine = wd ? `\n working-directory: ${wd}` : "";
3060
+ return [
3061
+ `- id: tailor-generate-check-${app.name}`,
3062
+ ` uses: tailor-platform/actions/generate-check@${ACTIONS_SHA} # v1.5.0`,
3063
+ ` with:`,
3064
+ ` package-manager: ${packageManager}${wdLine}`,
3065
+ `- id: tailor-plan-${app.name}`,
3066
+ ` if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork`,
3067
+ ` uses: tailor-platform/actions/plan@${ACTIONS_SHA} # v1.5.0`,
3068
+ ` with:`,
3069
+ ` workspace-id: \${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}`,
3070
+ ` package-manager: ${packageManager}${wdLine}`,
3071
+ ` label: ${coordinatorName}/${app.name}`,
3072
+ ` platform-client-id: \${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}`,
3073
+ ` platform-client-secret: \${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}`,
3074
+ ` github-token: \${{ secrets.GITHUB_TOKEN }}`
3075
+ ];
3076
+ }).join("\n");
3077
+ out = line(out, "PER_APP_PLAN_STEPS", `${driftCheckStep}\n${perAppPlanSteps}`);
3078
+ const deploySteps = apps.flatMap((app) => [
3079
+ `- id: tailor-deploy-${app.name}`,
3080
+ ` uses: ./.github/actions/tailor-${app.name}`,
3081
+ ` with:`,
3082
+ ` workspace-id: \${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}`,
3083
+ ` name: ${app.name}`,
3084
+ ` working-directory: ${app.dir}`,
3085
+ ` package-manager: ${packageManager}`,
3086
+ ` platform-client-id: \${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}`,
3087
+ ` platform-client-secret: \${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}`,
3088
+ ` slack-token: \${{ secrets.TAILOR_SLACK_BOT_TOKEN }}`,
3089
+ ` slack-channel-id: \${{ vars.TAILOR_SLACK_CHANNEL_ID }}`,
3090
+ ` # editable: api-url:`
3091
+ ]).join("\n");
3092
+ out = line(out, "PER_APP_DEPLOY_STEPS", deploySteps);
3093
+ out = out.replaceAll("__WORKSPACE_NAME__", () => coordinatorName);
3094
+ out = out.replaceAll("__ENVIRONMENT__", () => environment);
3095
+ if (branch !== void 0) out = out.replaceAll("__BRANCH__", () => branch);
3096
+ if (isTag) out = out.replaceAll("__TAG_PATTERN__", () => tagPattern);
3097
+ const generatedIds = [];
3098
+ if (isTag && branch !== void 0) generatedIds.push("tailor-tag-guard", "tailor-tag-guard/tailor-checkout", "tailor-tag-guard/tailor-tag-guard");
3099
+ generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", "tailor-plan/tailor-setup", "tailor-plan/tailor-drift-check");
3100
+ for (const app of apps) generatedIds.push(`tailor-plan/tailor-generate-check-${app.name}`, `tailor-plan/tailor-plan-${app.name}`);
3101
+ generatedIds.push("tailor-deploy", "tailor-deploy/tailor-checkout", "tailor-deploy/tailor-setup");
3102
+ for (const app of apps) generatedIds.push(`tailor-deploy/tailor-deploy-${app.name}`);
3103
+ return {
3104
+ content: out,
3105
+ generatedIds
3106
+ };
3004
3107
  }
3005
3108
  /**
3006
- * Audit the generated workflows for drift against the current config/repo
3007
- * state. Read-only: never writes files, the lock, or the config.
3109
+ * Render the content of the user-owned .github/actions/tailor-setup/action.yml local action.
3008
3110
  *
3009
- * Throws when drift is found (so it composes like the other `:check`
3010
- * commands). The workflow drift-check step layers advisory behaviour on top
3011
- * (per-rule ignore / continue-on-error); the CLI itself reports via exit code.
3012
- * @param options - Check options
3111
+ * This file is generated once by `setup coordinate` and never overwritten.
3112
+ * It wraps the setup and install actions with the package manager baked in.
3113
+ * @param params - Package manager configuration
3114
+ * @returns File content string
3013
3115
  */
3014
- async function checkGitHub(options) {
3015
- logBetaWarning("setup");
3016
- const { outputDir } = options;
3017
- const lock = readLock(outputDir);
3018
- if (!lock || lock.targets.length === 0) throw new Error("No managed workflows found (.github/tailor-sdk.lock is missing or empty). Run `tailor-sdk setup` first.");
3019
- const exists = options.configExistsAt ?? ((p) => fs$1.existsSync(p));
3020
- const defaultBranch = detectDefaultBranchSafe(outputDir, options.gitRunner);
3021
- const loadErdNamespaces = options.loadErdNamespaces ?? defaultLoadErdNamespaces$1;
3022
- const findings = [];
3023
- for (const target of lock.targets) {
3024
- const absFile = resolveWithinRoot(outputDir, target.file);
3025
- const currentHash = absFile === null ? null : readHash(absFile);
3026
- const configAbs = resolveWithinRoot(outputDir, path.join(target.inputs.dir, "tailor.config.ts"));
3027
- const configExists = configAbs !== null && exists(configAbs);
3028
- const erdNamespaces = target.kind === "branch" && target.inputs.erdPreview && configAbs !== null && configExists ? await loadErdNamespaces(configAbs) : null;
3029
- findings.push(...findTargetDrift(target, {
3030
- fileExists: currentHash !== null,
3031
- currentHash,
3032
- configExists,
3033
- defaultBranch,
3034
- templateVersion: 5,
3035
- erdNamespaces
3036
- }));
3037
- }
3038
- const count = lock.targets.length;
3039
- if (findings.length === 0) {
3040
- logger.success(`No drift detected across ${String(count)} target(s).`);
3041
- return;
3042
- }
3043
- for (const finding of findings) logger.warn(`[${finding.target}] ${finding.message} (ignore key: ${finding.rule})`);
3044
- throw new Error(`Detected ${String(findings.length)} drift finding(s) across ${String(count)} target(s). Re-run \`tailor-sdk setup\` to regenerate, or address each finding above.`);
3116
+ function renderTailorSetupAction(params) {
3117
+ const { packageManager } = params;
3118
+ return [
3119
+ `# This file is user-owned. Generated once by \`tailor-sdk setup coordinate\` and never overwritten.`,
3120
+ `# Customize freely: add pre/post steps, change install flags, etc.`,
3121
+ `name: Tailor Setup`,
3122
+ `description: Install dependencies and run tailor-sdk setup for composite action callers.`,
3123
+ `runs:`,
3124
+ ` using: composite`,
3125
+ ` steps:`,
3126
+ ` - uses: tailor-platform/actions/setup@${ACTIONS_SHA} # v1.5.0`,
3127
+ ` with:`,
3128
+ ` package-manager: ${packageManager}`,
3129
+ ` # Add custom steps here (e.g. private registry authentication)`,
3130
+ ` - uses: tailor-platform/actions/install@${ACTIONS_SHA} # v1.5.0`,
3131
+ ` with:`,
3132
+ ` package-manager: ${packageManager}`,
3133
+ ` # editable: install-command:`,
3134
+ ``
3135
+ ].join("\n");
3045
3136
  }
3046
3137
 
3047
3138
  //#endregion
@@ -3050,13 +3141,25 @@ async function defaultLoadConfigName(configPath) {
3050
3141
  const { config } = await loadConfig(configPath);
3051
3142
  return config.name;
3052
3143
  }
3053
- async function defaultLoadErdNamespaces(configPath) {
3144
+ async function defaultLoadErdNamespaces$1(configPath) {
3054
3145
  const { config } = await loadConfig(configPath);
3055
3146
  return extractOwnedNamespaces(config);
3056
3147
  }
3148
+ async function defaultLoadHasMigrations$1(configPath) {
3149
+ const { config } = await loadConfig(configPath);
3150
+ return getNamespacesWithMigrations(config, path.dirname(configPath)).length > 0;
3151
+ }
3152
+ async function defaultLoadHasSeeds$1(configPath) {
3153
+ const { plugins } = await loadConfig(configPath);
3154
+ return plugins.some((p) => p.id === "@tailor-platform/seed");
3155
+ }
3156
+ async function defaultLoadHasStaticWebsites$1(configPath) {
3157
+ const { config } = await loadConfig(configPath);
3158
+ return (config.staticWebsites?.length ?? 0) > 0;
3159
+ }
3057
3160
  const WORKSPACE_NAME_RE = /^[a-z0-9-]+$/;
3058
3161
  function validateWorkspaceName(name) {
3059
- if (name.length < 3 || name.length > 63 || !WORKSPACE_NAME_RE.test(name) || name.startsWith("-") || name.endsWith("-")) throw new Error(`Invalid workspace name "${name}". Names must be 3-63 characters of lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen. Pass a valid name with --workspace-name.`);
3162
+ if (name.length < 3 || name.length > 63 || !WORKSPACE_NAME_RE.test(name) || name.startsWith("-") || name.endsWith("-")) throw new Error(`Invalid workspace name "${name}". Names must be 3-63 characters of lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen. Pass a valid name with --name.`);
3060
3163
  }
3061
3164
  const BRANCH_RE = /^[A-Za-z0-9._/-]+$/;
3062
3165
  const TAG_PATTERN_RE = /^[A-Za-z0-9._/*?![\]-]+$/;
@@ -3070,6 +3173,10 @@ const ENVIRONMENT_RE = /^[A-Za-z0-9._/-]+$/;
3070
3173
  function validateEnvironment(environment) {
3071
3174
  if (!ENVIRONMENT_RE.test(environment)) throw new Error(`Invalid environment name "${environment}". Only letters, numbers, ".", "_", "/", and "-" are supported.`);
3072
3175
  }
3176
+ const REGION_RE = /^[A-Za-z0-9._-]+$/;
3177
+ function validateRegion(region) {
3178
+ if (!REGION_RE.test(region)) throw new Error(`Invalid region "${region}". Only letters, numbers, ".", "_", and "-" are supported.`);
3179
+ }
3073
3180
  const DIR_RE = /^[A-Za-z0-9._/-]+$/;
3074
3181
  function validateDir(dir) {
3075
3182
  if (!DIR_RE.test(dir)) throw new Error(`Invalid --dir "${dir}". Only letters, numbers, ".", "_", "/", and "-" are supported.`);
@@ -3078,7 +3185,7 @@ const ERD_NAMESPACE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
3078
3185
  function validateErdNamespaces(namespaces) {
3079
3186
  for (const namespace of namespaces) if (!ERD_NAMESPACE_RE.test(namespace)) throw new Error(`TailorDB namespace "${namespace}" cannot be used in --erd-preview. Only letters, numbers, '.', '_', and '-' are supported, and the name must start with a letter or number.`);
3080
3187
  }
3081
- function escapesRoot(rel) {
3188
+ function escapesRoot$1(rel) {
3082
3189
  return rel === ".." || rel.startsWith(`..${path.sep}`) || rel.startsWith("../") || path.isAbsolute(rel);
3083
3190
  }
3084
3191
  /**
@@ -3094,11 +3201,11 @@ function escapesRoot(rel) {
3094
3201
  function resolveConfigPath(outputDir, dir) {
3095
3202
  const appDir = path.resolve(outputDir, dir);
3096
3203
  const rel = path.relative(outputDir, appDir);
3097
- if (path.isAbsolute(dir) || escapesRoot(rel)) throw new Error(`--dir must be a relative path inside the repository (got "${dir}").`);
3204
+ if (path.isAbsolute(dir) || escapesRoot$1(rel)) throw new Error(`--dir must be a relative path inside the repository (got "${dir}").`);
3098
3205
  if (fs$1.existsSync(appDir)) {
3099
3206
  const realAppDir = path.normalize(fs$1.realpathSync(appDir));
3100
3207
  const realOutputDir = path.normalize(fs$1.realpathSync(outputDir));
3101
- if (escapesRoot(path.relative(realOutputDir, realAppDir))) throw new Error(`--dir must resolve to a directory inside the repository (got "${dir}", which links outside it).`);
3208
+ if (escapesRoot$1(path.relative(realOutputDir, realAppDir))) throw new Error(`--dir must resolve to a directory inside the repository (got "${dir}", which links outside it).`);
3102
3209
  }
3103
3210
  const configPath = path.join(appDir, "tailor.config.ts");
3104
3211
  if (!fs$1.existsSync(configPath)) throw new Error(`tailor.config.ts not found at ${configPath}. Run this from your SDK project root, or pass the app directory with --dir.`);
@@ -3110,17 +3217,15 @@ function resolveConfigPath(outputDir, dir) {
3110
3217
  * @returns Resolved target metadata and rendered content
3111
3218
  */
3112
3219
  async function resolve$1(options) {
3113
- if (options.tag && !options.plan) throw new Error("--no-plan cannot be combined with --tag (tag targets always run plan before deploy). Drop --no-plan or use a branch target.");
3114
- if (options.erdPreview && (options.tag || !options.plan)) throw new Error("--erd-preview requires a branch target with plan enabled.");
3115
3220
  const dir = options.dir.replaceAll("\\", "/").replace(/\/{2,}/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
3116
3221
  validateDir(dir);
3117
3222
  const workingDirectory = dir !== "." ? dir : void 0;
3118
3223
  const configPath = resolveConfigPath(options.outputDir, dir);
3119
3224
  const loadName = options.loadConfigName ?? defaultLoadConfigName;
3120
3225
  const workspaceName = options.workspaceName ?? await loadName(configPath);
3121
- if (!workspaceName) throw new Error("Could not determine the workspace name. Pass --workspace-name, or set 'name' in tailor.config.ts.");
3226
+ if (!workspaceName) throw new Error("Could not determine the workspace name. Pass --name, or set 'name' in tailor.config.ts.");
3122
3227
  validateWorkspaceName(workspaceName);
3123
- const kind = options.tag ? "tag" : "branch";
3228
+ const { kind } = options;
3124
3229
  const packageManager = detectPackageManager(options.outputDir);
3125
3230
  const environment = options.environment ?? workspaceName;
3126
3231
  validateEnvironment(environment);
@@ -3129,47 +3234,85 @@ async function resolve$1(options) {
3129
3234
  let branchAutoDetected = false;
3130
3235
  let render;
3131
3236
  let erdNamespaces = [];
3132
- if (options.erdPreview) {
3133
- erdNamespaces = await (options.loadErdNamespaces ?? defaultLoadErdNamespaces)(configPath);
3134
- if (erdNamespaces.length === 0) throw new Error("No TailorDB namespaces found for --erd-preview. Define owned db namespaces in tailor.config.ts.");
3135
- validateErdNamespaces(erdNamespaces);
3136
- }
3237
+ let hasMigrations = false;
3238
+ let hasSeeds = false;
3239
+ let hasStaticWebsites = false;
3240
+ const loadHasMigrations = options.loadHasMigrations ?? defaultLoadHasMigrations$1;
3241
+ const loadHasSeeds = options.loadHasSeeds ?? defaultLoadHasSeeds$1;
3242
+ const loadHasStaticWebsites = options.loadHasStaticWebsites ?? defaultLoadHasStaticWebsites$1;
3137
3243
  if (kind === "branch") {
3244
+ if (options.erdPreview) {
3245
+ erdNamespaces = await (options.loadErdNamespaces ?? defaultLoadErdNamespaces$1)(configPath);
3246
+ if (erdNamespaces.length === 0) throw new Error("No TailorDB namespaces found for --erd-preview. Define owned db namespaces in tailor.config.ts.");
3247
+ validateErdNamespaces(erdNamespaces);
3248
+ }
3138
3249
  branchAutoDetected = options.branch === void 0;
3139
3250
  branch = options.branch ?? detectDefaultBranch(options.outputDir, options.gitRunner);
3140
3251
  validateBranch(branch);
3252
+ hasMigrations = await loadHasMigrations(configPath);
3253
+ hasSeeds = await loadHasSeeds(configPath);
3141
3254
  render = renderBranchWorkflow({
3142
3255
  workspaceName,
3143
3256
  branch,
3144
3257
  workingDirectory,
3145
3258
  environment,
3146
3259
  packageManager,
3147
- plan: options.plan,
3148
- erdPreview: options.erdPreview ? { namespaces: erdNamespaces } : null
3260
+ erdPreview: options.erdPreview ? { namespaces: erdNamespaces } : null,
3261
+ migrationDriftCheck: hasMigrations,
3262
+ seedValidate: hasSeeds
3149
3263
  });
3150
- } else {
3264
+ } else if (kind === "tag") {
3151
3265
  branch = options.branch ?? null;
3152
3266
  if (branch !== null) validateBranch(branch);
3267
+ hasMigrations = await loadHasMigrations(configPath);
3268
+ hasSeeds = await loadHasSeeds(configPath);
3153
3269
  render = renderTagWorkflow({
3154
3270
  workspaceName,
3155
3271
  tagPattern: options.tagPattern,
3156
3272
  branch: options.branch,
3157
3273
  workingDirectory,
3158
3274
  environment,
3159
- packageManager
3275
+ packageManager,
3276
+ migrationDriftCheck: hasMigrations,
3277
+ seedValidate: hasSeeds
3278
+ });
3279
+ } else if (kind === "preview") {
3280
+ branchAutoDetected = options.branch === void 0;
3281
+ branch = options.branch ?? detectDefaultBranch(options.outputDir, options.gitRunner);
3282
+ validateBranch(branch);
3283
+ validateRegion(options.region);
3284
+ render = renderPreviewWorkflow({
3285
+ workspaceName,
3286
+ branch,
3287
+ workingDirectory,
3288
+ environment,
3289
+ packageManager,
3290
+ region: options.region,
3291
+ requirePreviewLabel: options.requirePreviewLabel ?? false
3292
+ });
3293
+ } else {
3294
+ hasStaticWebsites = await loadHasStaticWebsites(configPath);
3295
+ render = renderActionWorkflow({
3296
+ workspaceName,
3297
+ workingDirectory,
3298
+ hasStaticWebsites
3160
3299
  });
3161
3300
  }
3162
- const file = `.github/workflows/tailor-${workspaceName}.yml`;
3301
+ const file = kind === "action" ? `.github/actions/tailor-${workspaceName}/action.yml` : `.github/workflows/tailor-${workspaceName}${kind === "tag" ? "-tag" : kind === "preview" ? "-preview" : ""}.yml`;
3163
3302
  const inputs = {
3164
- branch,
3165
- branchAutoDetected: kind === "branch" ? branchAutoDetected : void 0,
3303
+ branch: kind === "action" ? null : branch,
3304
+ branchAutoDetected: kind === "branch" || kind === "preview" ? branchAutoDetected : void 0,
3166
3305
  tagPattern: kind === "tag" ? options.tagPattern : null,
3167
3306
  environment,
3168
3307
  dir,
3169
3308
  packageManager,
3170
- plan: kind === "branch" ? options.plan : true,
3309
+ region: kind === "preview" ? options.region : void 0,
3310
+ requirePreviewLabel: kind === "preview" ? options.requirePreviewLabel ?? false : void 0,
3171
3311
  erdPreview: kind === "branch" ? options.erdPreview : false,
3172
- erdNamespaces: kind === "branch" && options.erdPreview ? erdNamespaces : void 0
3312
+ erdNamespaces: kind === "branch" && options.erdPreview ? erdNamespaces : void 0,
3313
+ migrationDriftCheck: kind === "branch" || kind === "tag" ? hasMigrations : void 0,
3314
+ seedValidate: kind === "branch" || kind === "tag" ? hasSeeds : void 0,
3315
+ hasStaticWebsites: kind === "action" ? hasStaticWebsites : void 0
3173
3316
  };
3174
3317
  return {
3175
3318
  kind,
@@ -3184,6 +3327,40 @@ async function resolve$1(options) {
3184
3327
  configPath
3185
3328
  };
3186
3329
  }
3330
+ const BUILD_SITE_RUN_RE = /([ \t]*- id: build-site\n[ \t]+shell: bash\n[ \t]+run: \|)([\s\S]*?)(\n[ \t]*- |\n*$)/;
3331
+ /**
3332
+ * Strip the mutable run: body of the build-site step before hashing.
3333
+ * When hasStaticWebsites is true the step is present; users are expected to
3334
+ * replace the placeholder command with their actual build command. We only
3335
+ * hash the structural parts (step id, shell declaration, run: key) so that
3336
+ * editing the build command does not look like unintended drift.
3337
+ * When hasStaticWebsites is false the step is absent, so the regex is a no-op.
3338
+ * @param content - Raw action workflow content
3339
+ * @returns Normalised content with the build-site run body replaced by a placeholder
3340
+ */
3341
+ function normalizeActionContent(content) {
3342
+ return content.replace(BUILD_SITE_RUN_RE, (_, header, _body, tail) => `${header}\n true${tail}`);
3343
+ }
3344
+ /**
3345
+ * Extract the run: body of the build-site step from action content.
3346
+ * Returns null when the step is absent (hasStaticWebsites is false).
3347
+ * @param content - Action workflow content
3348
+ * @returns The captured run body string (includes leading newline), or null
3349
+ */
3350
+ function extractBuildSiteRunBody(content) {
3351
+ const m = BUILD_SITE_RUN_RE.exec(content);
3352
+ return m ? m[2] ?? null : null;
3353
+ }
3354
+ /**
3355
+ * Replace the run: body of the build-site step in action content.
3356
+ * No-op when the step is absent.
3357
+ * @param content - Target action workflow content
3358
+ * @param body - Run body to inject (includes leading newline and indentation)
3359
+ * @returns Content with the run body replaced
3360
+ */
3361
+ function injectBuildSiteRunBody(content, body) {
3362
+ return content.replace(BUILD_SITE_RUN_RE, (_, header, _placeholder, tail) => `${header}${body}${tail}`);
3363
+ }
3187
3364
  /**
3188
3365
  * Decide how to reconcile a target with the on-disk file and lock state.
3189
3366
  * @param obj - Decision inputs
@@ -3191,10 +3368,11 @@ async function resolve$1(options) {
3191
3368
  * @param obj.fileExists - Whether the workflow file is present on disk
3192
3369
  * @param obj.currentContent - On-disk content when present
3193
3370
  * @param obj.force - Whether --force was passed
3371
+ * @param obj.normalize - Optional content normaliser applied before hashing
3194
3372
  * @returns The reconciliation action
3195
3373
  */
3196
3374
  function decideAction(obj) {
3197
- const { existing, fileExists, currentContent, force } = obj;
3375
+ const { existing, fileExists, currentContent, force, normalize } = obj;
3198
3376
  if (!existing) {
3199
3377
  if (!fileExists) return { action: "create" };
3200
3378
  if (force) return { action: "regenerate" };
@@ -3204,7 +3382,9 @@ function decideAction(obj) {
3204
3382
  };
3205
3383
  }
3206
3384
  if (!fileExists) return { action: "restore" };
3207
- if (currentContent !== null && hashContent(currentContent) === existing.contentHash) return { action: "regenerate" };
3385
+ if (currentContent !== null) {
3386
+ if (hashContent(normalize ? normalize(currentContent) : currentContent) === existing.contentHash) return { action: "regenerate" };
3387
+ }
3208
3388
  if (force) return { action: "regenerate" };
3209
3389
  return {
3210
3390
  action: "conflict",
@@ -3222,7 +3402,7 @@ function decideAction(obj) {
3222
3402
  function assertNoKindCollision(obj) {
3223
3403
  const { lock, kind, workspaceName, file } = obj;
3224
3404
  const collision = lock?.targets.find((t) => t.file === file && !(t.kind === kind && t.workspaceName === workspaceName));
3225
- if (collision) throw new Error(`A ${collision.kind} target already owns ${file}, which conflicts with this ${kind} target. Pass a different name with --workspace-name to generate a separate workflow.`);
3405
+ if (collision) throw new Error(`A ${collision.kind} target already owns ${file}, which conflicts with this ${kind} target. Pass a different name with --name to generate a separate workflow.`);
3226
3406
  }
3227
3407
  /**
3228
3408
  * Print next-step guidance after generating workflow files.
@@ -3249,11 +3429,10 @@ function printNextSteps(obj) {
3249
3429
  if (idInjected) logger.log(" - tailor.config.ts (app id was added)");
3250
3430
  }
3251
3431
  /**
3252
- * Generate the GitHub Actions workflow for a deploy target and reconcile it
3253
- * with the lock file.
3432
+ * Generate a deploy target workflow and reconcile it with the lock file.
3254
3433
  * @param options - Setup options
3255
3434
  */
3256
- async function setupGitHub(options) {
3435
+ async function setupTarget(options) {
3257
3436
  logBetaWarning("setup");
3258
3437
  const resolved = await resolve$1(options);
3259
3438
  const lock = readLock(options.outputDir);
@@ -3266,27 +3445,35 @@ async function setupGitHub(options) {
3266
3445
  });
3267
3446
  const existing = findTarget(lock, resolved.kind, resolved.workspaceName);
3268
3447
  const fileExists = fs$1.existsSync(absFile);
3448
+ const currentContent = fileExists ? fs$1.readFileSync(absFile, "utf-8") : null;
3449
+ const normalize = resolved.kind === "action" ? normalizeActionContent : void 0;
3269
3450
  const decision = decideAction({
3270
3451
  existing,
3271
3452
  fileExists,
3272
- currentContent: fileExists ? fs$1.readFileSync(absFile, "utf-8") : null,
3273
- force: options.force
3453
+ currentContent,
3454
+ force: options.force,
3455
+ normalize
3274
3456
  });
3275
3457
  if (decision.action === "conflict") throw new Error(`${resolved.file}: ${decision.reason}`);
3276
3458
  const idResult = await ensureConfigId(resolved.configPath);
3277
3459
  if (idResult === null) logger.warn("Could not find a defineConfig() call to confirm an app id. The CI deploy will fail unless your config resolves to one with an 'id'.");
3278
3460
  const idInjected = idResult?.injected ?? false;
3461
+ let contentToWrite = resolved.render.content;
3462
+ if (decision.action === "regenerate" && normalize && currentContent !== null) {
3463
+ const existingBody = extractBuildSiteRunBody(currentContent);
3464
+ if (existingBody !== null) contentToWrite = injectBuildSiteRunBody(contentToWrite, existingBody);
3465
+ }
3279
3466
  fs$1.mkdirSync(path.dirname(absFile), { recursive: true });
3280
- fs$1.writeFileSync(absFile, resolved.render.content, "utf-8");
3467
+ fs$1.writeFileSync(absFile, contentToWrite, "utf-8");
3281
3468
  const newTarget = {
3282
3469
  kind: resolved.kind,
3283
3470
  workspaceName: resolved.workspaceName,
3284
3471
  file: resolved.file,
3285
- templateVersion: 5,
3472
+ templateVersion: 6,
3286
3473
  inputs: resolved.inputs,
3287
3474
  generatedIds: resolved.render.generatedIds,
3288
3475
  ejectedIds: existing?.ejectedIds ?? [],
3289
- contentHash: hashContent(resolved.render.content)
3476
+ contentHash: hashContent(normalize ? normalize(resolved.render.content) : resolved.render.content)
3290
3477
  };
3291
3478
  const targets = [...lock?.targets ?? []];
3292
3479
  const index = targets.findIndex((t) => t.kind === newTarget.kind && t.workspaceName === newTarget.workspaceName);
@@ -3299,39 +3486,379 @@ async function setupGitHub(options) {
3299
3486
  if (decision.action === "restore") logger.success(`Regenerated ${styles.path(resolved.file)} (was missing on disk)`);
3300
3487
  else if (decision.action === "regenerate") logger.success(`Regenerated ${styles.path(resolved.file)}`);
3301
3488
  else logger.success(`Generated ${styles.path(resolved.file)}`);
3302
- printNextSteps({
3489
+ if (resolved.kind === "action") {
3490
+ logger.newline();
3491
+ logger.info("Next steps:");
3492
+ logger.newline();
3493
+ logger.log(`The composite action has been generated at ${styles.path(resolved.file)}.`);
3494
+ logger.log("Use `tailor-sdk setup coordinate` to generate a coordinator workflow that orchestrates this action.");
3495
+ } else printNextSteps({
3303
3496
  environment: resolved.environment,
3304
3497
  idInjected
3305
3498
  });
3306
3499
  }
3500
+ /**
3501
+ * Generate the coordinator workflow that orchestrates per-app composite actions.
3502
+ *
3503
+ * Unlike `setupTarget`, this function does not read a Tailor config. The coordinator
3504
+ * name is required via `--name`. App working directories are resolved from
3505
+ * the lock file entries created by `setup action`.
3506
+ * @param options - Coordinate setup options
3507
+ */
3508
+ async function setupCoordinate(options) {
3509
+ logBetaWarning("setup");
3510
+ const { coordinatorName, coordinateKind, actions, force, outputDir } = options;
3511
+ validateWorkspaceName(coordinatorName);
3512
+ if (actions.length === 0) throw new Error("At least one --action is required. Run `tailor-sdk setup action --dir <app-dir>` for each app first.");
3513
+ const environment = options.environment ?? coordinatorName;
3514
+ validateEnvironment(environment);
3515
+ let branch = null;
3516
+ let branchAutoDetected = false;
3517
+ if (coordinateKind === "branch") {
3518
+ branchAutoDetected = options.branch === void 0;
3519
+ branch = options.branch ?? detectDefaultBranch(outputDir, options.gitRunner);
3520
+ validateBranch(branch);
3521
+ } else if (options.branch !== void 0) {
3522
+ validateBranch(options.branch);
3523
+ branch = options.branch;
3524
+ }
3525
+ const tagPattern = options.tagPattern ?? "v*";
3526
+ if (coordinateKind === "tag") validateTagPattern(tagPattern);
3527
+ const packageManager = detectPackageManager(outputDir);
3528
+ const lock = readLock(outputDir);
3529
+ if (!lock) throw new Error(".github/tailor-sdk.lock not found. Run `tailor-sdk setup action --name <name>` for each app before running setup coordinate.");
3530
+ const seenNames = /* @__PURE__ */ new Set();
3531
+ const apps = actions.map((actionName) => {
3532
+ const name = actionName.startsWith("tailor-") ? actionName.slice(7) : actionName;
3533
+ if (seenNames.has(name)) throw new Error(`Duplicate --action "${name}". Each composite action can only appear once in a coordinator.`);
3534
+ seenNames.add(name);
3535
+ const entry = lock.targets.find((t) => t.kind === "action" && t.workspaceName === name);
3536
+ if (!entry) throw new Error(`Action target "${name}" not found in .github/tailor-sdk.lock. Run \`tailor-sdk setup action --name ${name}\` first.`);
3537
+ validateDir(entry.inputs.dir);
3538
+ return {
3539
+ name,
3540
+ dir: entry.inputs.dir
3541
+ };
3542
+ });
3543
+ const render = renderCoordinateWorkflow({
3544
+ coordinatorName,
3545
+ kind: coordinateKind,
3546
+ apps,
3547
+ branch: branch ?? void 0,
3548
+ tagPattern: coordinateKind === "tag" ? tagPattern : void 0,
3549
+ environment,
3550
+ packageManager
3551
+ });
3552
+ const file = `.github/workflows/tailor-coordinate-${coordinatorName}${coordinateKind === "tag" ? "-tag" : ""}.yml`;
3553
+ const absFile = path.join(outputDir, file);
3554
+ assertNoKindCollision({
3555
+ lock,
3556
+ kind: "coordinate",
3557
+ workspaceName: coordinatorName,
3558
+ file
3559
+ });
3560
+ const existing = findTarget(lock, "coordinate", coordinatorName);
3561
+ const fileExists = fs$1.existsSync(absFile);
3562
+ const decision = decideAction({
3563
+ existing,
3564
+ fileExists,
3565
+ currentContent: fileExists ? fs$1.readFileSync(absFile, "utf-8") : null,
3566
+ force
3567
+ });
3568
+ if (decision.action === "conflict") throw new Error(`${file}: ${decision.reason}`);
3569
+ fs$1.mkdirSync(path.dirname(absFile), { recursive: true });
3570
+ fs$1.writeFileSync(absFile, render.content, "utf-8");
3571
+ const tailorSetupFile = ".github/actions/tailor-setup/action.yml";
3572
+ const absTailorSetupFile = path.join(outputDir, tailorSetupFile);
3573
+ if (!fs$1.existsSync(absTailorSetupFile)) {
3574
+ fs$1.mkdirSync(path.dirname(absTailorSetupFile), { recursive: true });
3575
+ fs$1.writeFileSync(absTailorSetupFile, renderTailorSetupAction({ packageManager }), "utf-8");
3576
+ logger.success(`Generated ${styles.path(tailorSetupFile)}`);
3577
+ }
3578
+ const newTarget = {
3579
+ kind: "coordinate",
3580
+ workspaceName: coordinatorName,
3581
+ file,
3582
+ templateVersion: 6,
3583
+ inputs: {
3584
+ branch,
3585
+ branchAutoDetected: coordinateKind === "branch" ? branchAutoDetected : branch !== null ? false : void 0,
3586
+ tagPattern: coordinateKind === "tag" ? tagPattern : null,
3587
+ environment,
3588
+ dir: ".",
3589
+ packageManager,
3590
+ actionDirs: apps.map((a) => a.dir)
3591
+ },
3592
+ generatedIds: render.generatedIds,
3593
+ ejectedIds: existing?.ejectedIds ?? [],
3594
+ contentHash: hashContent(render.content)
3595
+ };
3596
+ const targets = [...lock.targets];
3597
+ const idx = targets.findIndex((t) => t.kind === newTarget.kind && t.workspaceName === newTarget.workspaceName);
3598
+ if (idx === -1) targets.push(newTarget);
3599
+ else targets[idx] = newTarget;
3600
+ writeLock(outputDir, {
3601
+ version: 1,
3602
+ targets
3603
+ });
3604
+ if (decision.action === "restore") logger.success(`Regenerated ${styles.path(file)} (was missing on disk)`);
3605
+ else if (decision.action === "regenerate") logger.success(`Regenerated ${styles.path(file)}`);
3606
+ else logger.success(`Generated ${styles.path(file)}`);
3607
+ logger.newline();
3608
+ logger.info("Next steps:");
3609
+ logger.newline();
3610
+ logger.log(`1. Set the machine-user credentials as secrets on the "${environment}" environment:`);
3611
+ logger.log(` gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID --env ${environment}`);
3612
+ logger.log(` gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET --env ${environment}`);
3613
+ logger.newline();
3614
+ logger.log(`2. Provision each workspace and set TAILOR_PLATFORM_WORKSPACE_ID on the "${environment}" environment:`);
3615
+ logger.log(" tailor-sdk workspace create # one per app; copy the id");
3616
+ logger.log(` gh variable set TAILOR_PLATFORM_WORKSPACE_ID --env ${environment}`);
3617
+ logger.newline();
3618
+ logger.log("3. Commit the generated files:");
3619
+ logger.log(` - ${file}`);
3620
+ logger.log(` - ${tailorSetupFile} (if newly created)`);
3621
+ logger.log(" - .github/tailor-sdk.lock");
3622
+ }
3623
+
3624
+ //#endregion
3625
+ //#region src/cli/commands/setup/check.ts
3626
+ /**
3627
+ * Compute drift findings for one target by comparing its recorded lock state
3628
+ * against the current repository/config state.
3629
+ * @param target - The lock target being audited
3630
+ * @param state - Current repository/config state for this target
3631
+ * @returns Drift findings (empty when the target is in sync)
3632
+ */
3633
+ function findTargetDrift(target, state) {
3634
+ const id = `${target.kind} ${target.workspaceName}`;
3635
+ const findings = [];
3636
+ if (!state.fileExists) findings.push({
3637
+ target: id,
3638
+ rule: "missing-file",
3639
+ message: `${target.file} is missing or unreadable. Re-run setup to restore it.`
3640
+ });
3641
+ else if (state.currentHash !== null && state.currentHash !== target.contentHash) findings.push({
3642
+ target: id,
3643
+ rule: "hand-edit",
3644
+ message: `${target.file} was edited by hand since it was generated. Re-run setup with --force to regenerate, or keep customizations in your own jobs/steps.`
3645
+ });
3646
+ if (target.templateVersion < state.templateVersion) findings.push({
3647
+ target: id,
3648
+ rule: "template-version",
3649
+ message: `A newer workflow template is available (generated with v${String(target.templateVersion)}, current v${String(state.templateVersion)}). Re-run setup to update.`
3650
+ });
3651
+ if (!state.configExists) findings.push({
3652
+ target: id,
3653
+ rule: "config-dir",
3654
+ message: `tailor.config.ts not found under "${target.inputs.dir}". The app directory may have moved; re-run setup with the correct --dir.`
3655
+ });
3656
+ if ((target.kind === "branch" || target.kind === "coordinate" || target.kind === "preview") && target.inputs.branchAutoDetected !== false && state.defaultBranch !== null && target.inputs.branch !== null && target.inputs.branch !== state.defaultBranch) findings.push({
3657
+ target: id,
3658
+ rule: "default-branch",
3659
+ message: `The workflow triggers on "${target.inputs.branch}" but the repository default branch is now "${state.defaultBranch}". If this is intentional, ignore this; otherwise re-run setup so the trigger matches the default branch.`
3660
+ });
3661
+ if (target.kind === "branch" && target.inputs.erdPreview && state.configExists) {
3662
+ const recorded = [...target.inputs.erdNamespaces ?? []].toSorted((a, b) => a.localeCompare(b));
3663
+ const current = state.erdNamespaces?.toSorted((a, b) => a.localeCompare(b)) ?? null;
3664
+ if (current === null || recorded.length !== current.length || recorded.some((namespace, index) => namespace !== current[index])) findings.push({
3665
+ target: id,
3666
+ rule: "erd-namespaces",
3667
+ message: "TailorDB namespaces for ERD preview changed. Re-run setup so the ERD preview matrix is regenerated."
3668
+ });
3669
+ }
3670
+ if ((target.kind === "branch" || target.kind === "tag") && target.inputs.migrationDriftCheck === false && state.hasMigrations === true) findings.push({
3671
+ target: id,
3672
+ rule: "migration-drift",
3673
+ message: "TailorDB namespaces with migrations were added to the config. Re-run setup so the tailor-migration-drift-check step is included in the plan job."
3674
+ });
3675
+ if ((target.kind === "branch" || target.kind === "tag") && target.inputs.seedValidate === false && state.hasSeeds === true) findings.push({
3676
+ target: id,
3677
+ rule: "seed-validate",
3678
+ message: "Seed plugin detected in the current config. Re-run setup so the tailor-seed-validate step is included in the plan job."
3679
+ });
3680
+ if (target.kind === "action" && target.inputs.hasStaticWebsites === false && state.hasStaticWebsites === true) findings.push({
3681
+ target: id,
3682
+ rule: "static-websites",
3683
+ message: "Static websites were added to the config. Re-run setup so the build-site slot is included in the composite action."
3684
+ });
3685
+ return findings;
3686
+ }
3687
+ function detectDefaultBranchSafe(cwd, run) {
3688
+ try {
3689
+ return detectDefaultBranch(cwd, run);
3690
+ } catch {
3691
+ return null;
3692
+ }
3693
+ }
3694
+ function escapesRoot(rel) {
3695
+ return path.isAbsolute(rel) || rel === ".." || rel.startsWith(`..${path.sep}`) || rel.startsWith("../");
3696
+ }
3697
+ function resolveWithinRoot(outputDir, relPath) {
3698
+ if (path.isAbsolute(relPath)) return null;
3699
+ const abs = path.join(outputDir, relPath);
3700
+ if (escapesRoot(path.relative(outputDir, abs))) return null;
3701
+ try {
3702
+ if (escapesRoot(path.relative(fs$1.realpathSync(outputDir), fs$1.realpathSync(abs)))) return null;
3703
+ } catch {}
3704
+ return abs;
3705
+ }
3706
+ function readHash(absFile, normalize) {
3707
+ try {
3708
+ const content = fs$1.readFileSync(absFile, "utf-8");
3709
+ return hashContent(normalize ? normalize(content) : content);
3710
+ } catch {
3711
+ return null;
3712
+ }
3713
+ }
3714
+ async function defaultLoadErdNamespaces(configPath) {
3715
+ const { config } = await loadConfig(configPath);
3716
+ return extractOwnedNamespaces(config);
3717
+ }
3718
+ async function defaultLoadHasMigrations(configPath) {
3719
+ const { config } = await loadConfig(configPath);
3720
+ return getNamespacesWithMigrations(config, path.dirname(configPath)).length > 0;
3721
+ }
3722
+ async function defaultLoadHasSeeds(configPath) {
3723
+ const { plugins } = await loadConfig(configPath);
3724
+ return plugins.some((p) => p.id === "@tailor-platform/seed");
3725
+ }
3726
+ async function defaultLoadHasStaticWebsites(configPath) {
3727
+ const { config } = await loadConfig(configPath);
3728
+ return (config.staticWebsites?.length ?? 0) > 0;
3729
+ }
3730
+ /**
3731
+ * Audit the generated workflows for drift against the current config/repo
3732
+ * state. Read-only: never writes files, the lock, or the config.
3733
+ *
3734
+ * Throws when drift is found (so it composes like the other `:check`
3735
+ * commands). The workflow drift-check step layers advisory behaviour on top
3736
+ * (per-rule ignore / continue-on-error); the CLI itself reports via exit code.
3737
+ * @param options - Check options
3738
+ */
3739
+ async function checkGitHub(options) {
3740
+ logBetaWarning("setup");
3741
+ const { outputDir } = options;
3742
+ const lock = readLock(outputDir);
3743
+ if (!lock || lock.targets.length === 0) throw new Error("No managed workflows found (.github/tailor-sdk.lock is missing or empty). Run `tailor-sdk setup branch` (or another setup subcommand) first.");
3744
+ if (!options.ci) {
3745
+ if (lock.targets.some((t) => t.kind === "branch" || t.kind === "tag" || t.kind === "coordinate") && !process.env["TAILOR_PLATFORM_WORKSPACE_ID"]) throw new Error("TAILOR_PLATFORM_WORKSPACE_ID is not set. Provision the workspace and set the variable:\n tailor-sdk workspace create # if it does not exist yet; copy the id\n gh variable set TAILOR_PLATFORM_WORKSPACE_ID --env <environment>");
3746
+ }
3747
+ const slackToken = Boolean(process.env["TAILOR_SLACK_BOT_TOKEN"]);
3748
+ if (slackToken !== Boolean(process.env["TAILOR_SLACK_CHANNEL_ID"])) throw new Error(`Slack is partially configured: ${slackToken ? "TAILOR_SLACK_BOT_TOKEN is set" : "TAILOR_SLACK_CHANNEL_ID is set"} but not both. TAILOR_SLACK_BOT_TOKEN (secret) and TAILOR_SLACK_CHANNEL_ID (variable) must be set together, or neither should be set.`);
3749
+ const exists = options.configExistsAt ?? ((p) => fs$1.existsSync(p));
3750
+ const defaultBranch = detectDefaultBranchSafe(outputDir, options.gitRunner);
3751
+ const loadErdNamespaces = options.loadErdNamespaces ?? defaultLoadErdNamespaces;
3752
+ const loadHasMigrations = options.loadHasMigrations ?? defaultLoadHasMigrations;
3753
+ const loadHasSeeds = options.loadHasSeeds ?? defaultLoadHasSeeds;
3754
+ const loadHasStaticWebsites = options.loadHasStaticWebsites ?? defaultLoadHasStaticWebsites;
3755
+ const findings = [];
3756
+ for (const target of lock.targets) {
3757
+ const absFile = resolveWithinRoot(outputDir, target.file);
3758
+ const currentHash = absFile === null ? null : readHash(absFile, target.kind === "action" ? normalizeActionContent : void 0);
3759
+ const configAbs = target.kind === "coordinate" ? null : resolveWithinRoot(outputDir, path.join(target.inputs.dir, "tailor.config.ts"));
3760
+ const configExists = target.kind === "coordinate" || configAbs !== null && exists(configAbs);
3761
+ const erdNamespaces = target.kind === "branch" && target.inputs.erdPreview && configAbs !== null && configExists ? await loadErdNamespaces(configAbs) : null;
3762
+ const hasMigrations = (target.kind === "branch" || target.kind === "tag") && target.inputs.migrationDriftCheck !== void 0 && configAbs !== null && configExists ? await loadHasMigrations(configAbs) : void 0;
3763
+ const hasSeeds = (target.kind === "branch" || target.kind === "tag") && target.inputs.seedValidate !== void 0 && configAbs !== null && configExists ? await loadHasSeeds(configAbs) : void 0;
3764
+ const hasStaticWebsites = target.kind === "action" && target.inputs.hasStaticWebsites !== void 0 && configAbs !== null && configExists ? await loadHasStaticWebsites(configAbs) : void 0;
3765
+ findings.push(...findTargetDrift(target, {
3766
+ fileExists: currentHash !== null,
3767
+ currentHash,
3768
+ configExists,
3769
+ defaultBranch,
3770
+ templateVersion: 6,
3771
+ erdNamespaces,
3772
+ hasMigrations,
3773
+ hasSeeds,
3774
+ hasStaticWebsites
3775
+ }));
3776
+ }
3777
+ const count = lock.targets.length;
3778
+ if (findings.length === 0) {
3779
+ logger.success(`No drift detected across ${String(count)} target(s).`);
3780
+ return;
3781
+ }
3782
+ for (const finding of findings) logger.warn(`[${finding.target}] ${finding.message} (ignore key: ${finding.rule})`);
3783
+ throw new Error(`Detected ${String(findings.length)} drift finding(s) across ${String(count)} target(s). Re-run \`tailor-sdk setup\` to regenerate, or address each finding above.`);
3784
+ }
3307
3785
 
3308
3786
  //#endregion
3309
3787
  //#region src/cli/commands/setup/index.ts
3310
3788
  const checkCommand = defineAppCommand({
3311
3789
  name: "check",
3312
3790
  description: "Audit generated workflows for drift against the current config/repo (read-only).",
3313
- args: z.object({}).strict(),
3314
- run: async () => {
3315
- await checkGitHub({ outputDir: process.cwd() });
3791
+ args: z.object({ ci: arg(z.boolean().default(false), { description: "Run in CI mode: skip checks that are handled by the runtime (e.g. TAILOR_PLATFORM_WORKSPACE_ID)." }) }).strict(),
3792
+ run: async (args) => {
3793
+ await checkGitHub({
3794
+ outputDir: process.cwd(),
3795
+ ci: args.ci
3796
+ });
3316
3797
  }
3317
3798
  });
3318
- const setupCommand = defineAppCommand({
3319
- name: "setup",
3320
- description: "Generate a CI deploy workflow for your project. (beta)",
3799
+ const coordinateCommand = defineAppCommand({
3800
+ name: "coordinate",
3801
+ description: "Generate a coordinator workflow that orchestrates multiple --action-generated composite actions.",
3321
3802
  args: z.object({
3322
- provider: arg(z.enum(["github"], { message: "Only the 'github' provider is supported." }).default("github"), {
3323
- alias: "p",
3324
- description: "CI provider to generate for (only 'github' is supported)"
3803
+ name: arg(z.string().min(1), {
3804
+ alias: "n",
3805
+ description: "Coordinator name (used in the generated workflow file name and job names)"
3325
3806
  }),
3326
- "workspace-name": arg(z.string().min(1).optional(), {
3807
+ action: arg(z.array(z.string().min(1)).min(1), { description: "Composite action to include (can be specified multiple times). tailor- prefix optional." }),
3808
+ branch: arg(z.string().min(1).optional(), { description: "Branch target: deploy trigger branch (defaults to the detected default branch)" }),
3809
+ tag: arg(z.boolean().default(false), { description: "Generate a tag target coordinator" }),
3810
+ environment: arg(z.string().min(1).optional(), { description: "GitHub Environment for the plan/deploy jobs" }),
3811
+ force: arg(z.boolean().default(false), { description: "Discard hand edits and regenerate" })
3812
+ }).strict(),
3813
+ run: async (args) => {
3814
+ const coordinateKind = args.tag ? "tag" : "branch";
3815
+ await setupCoordinate({
3816
+ coordinatorName: args.name,
3817
+ coordinateKind,
3818
+ actions: args.action,
3819
+ branch: args.branch,
3820
+ environment: args.environment,
3821
+ force: args.force,
3822
+ outputDir: process.cwd()
3823
+ });
3824
+ }
3825
+ });
3826
+ const actionCommand = defineAppCommand({
3827
+ name: "action",
3828
+ description: "Generate a per-app composite action for use with setup coordinate (monorepo multi-app deploys).",
3829
+ args: z.object({
3830
+ name: arg(z.string().min(1).optional(), {
3327
3831
  alias: "n",
3328
- description: "Workspace name (defaults to the config 'name')"
3832
+ description: "Name (defaults to the config 'name')"
3833
+ }),
3834
+ dir: arg(z.string().min(1).default("."), {
3835
+ alias: "d",
3836
+ description: "App directory"
3329
3837
  }),
3330
- branch: arg(z.string().min(1).optional(), { description: "Branch target: deploy trigger branch (defaults to the detected default branch). Tag target: tag-reachability guard branch (no guard when omitted)" }),
3331
- tag: arg(z.boolean().default(false), { description: "Generate a tag target (deploy on tag push)" }),
3332
- "tag-pattern": arg(z.string().min(1).optional(), { description: "Tag glob to match (requires --tag; defaults to v*)" }),
3838
+ environment: arg(z.string().min(1).optional(), { description: "GitHub Environment (defaults to the workspace name)" }),
3839
+ force: arg(z.boolean().default(false), { description: "Discard hand edits and regenerate" })
3840
+ }).strict(),
3841
+ run: async (args) => {
3842
+ await setupTarget({
3843
+ kind: "action",
3844
+ workspaceName: args.name,
3845
+ dir: args.dir,
3846
+ environment: args.environment,
3847
+ force: args.force,
3848
+ outputDir: process.cwd()
3849
+ });
3850
+ }
3851
+ });
3852
+ const branchCommand = defineAppCommand({
3853
+ name: "branch",
3854
+ description: "Generate a branch-target deploy workflow (push to branch triggers deploy).",
3855
+ args: z.object({
3856
+ name: arg(z.string().min(1).optional(), {
3857
+ alias: "n",
3858
+ description: "Name (defaults to the config 'name')"
3859
+ }),
3860
+ branch: arg(z.string().min(1).optional(), { description: "Deploy trigger branch (defaults to the detected default branch)" }),
3333
3861
  environment: arg(z.string().min(1).optional(), { description: "GitHub Environment for the plan/deploy jobs (defaults to the workspace name)" }),
3334
- "no-plan": arg(z.boolean().default(false), { description: "Disable the plan job for a branch target (cannot be combined with --tag)" }),
3335
3862
  "erd-preview": arg(z.boolean().default(false), { description: "Add PR ERD viewer artifacts with current/diff previews for TailorDB namespaces" }),
3336
3863
  dir: arg(z.string().min(1).default("."), {
3337
3864
  alias: "d",
@@ -3339,17 +3866,12 @@ const setupCommand = defineAppCommand({
3339
3866
  }),
3340
3867
  force: arg(z.boolean().default(false), { description: "Discard hand edits / take over unmanaged files and regenerate" })
3341
3868
  }).strict(),
3342
- subCommands: { check: checkCommand },
3343
3869
  run: async (args) => {
3344
- if (args["tag-pattern"] !== void 0 && !args.tag) throw new Error("--tag-pattern requires --tag.");
3345
- if (args["no-plan"] && args.tag) throw new Error("--no-plan cannot be combined with --tag.");
3346
- await setupGitHub({
3347
- workspaceName: args["workspace-name"],
3870
+ await setupTarget({
3871
+ kind: "branch",
3872
+ workspaceName: args.name,
3348
3873
  branch: args.branch,
3349
- tag: args.tag,
3350
- tagPattern: args["tag-pattern"] ?? "v*",
3351
3874
  environment: args.environment,
3352
- plan: !args["no-plan"],
3353
3875
  erdPreview: args["erd-preview"],
3354
3876
  dir: args.dir,
3355
3877
  force: args.force,
@@ -3357,6 +3879,80 @@ const setupCommand = defineAppCommand({
3357
3879
  });
3358
3880
  }
3359
3881
  });
3882
+ const tagCommand = defineAppCommand({
3883
+ name: "tag",
3884
+ description: "Generate a tag-target deploy workflow (tag push triggers deploy).",
3885
+ args: z.object({
3886
+ name: arg(z.string().min(1).optional(), {
3887
+ alias: "n",
3888
+ description: "Name (defaults to the config 'name')"
3889
+ }),
3890
+ "tag-pattern": arg(z.string().min(1).default("v*"), { description: "Tag glob to match (defaults to v*)" }),
3891
+ branch: arg(z.string().min(1).optional(), { description: "Tag-reachability guard branch (no guard when omitted)" }),
3892
+ environment: arg(z.string().min(1).optional(), { description: "GitHub Environment for the plan/deploy jobs (defaults to the workspace name)" }),
3893
+ dir: arg(z.string().min(1).default("."), {
3894
+ alias: "d",
3895
+ description: "App directory (for monorepo setups)"
3896
+ }),
3897
+ force: arg(z.boolean().default(false), { description: "Discard hand edits / take over unmanaged files and regenerate" })
3898
+ }).strict(),
3899
+ run: async (args) => {
3900
+ await setupTarget({
3901
+ kind: "tag",
3902
+ workspaceName: args.name,
3903
+ tagPattern: args["tag-pattern"],
3904
+ branch: args.branch,
3905
+ environment: args.environment,
3906
+ dir: args.dir,
3907
+ force: args.force,
3908
+ outputDir: process.cwd()
3909
+ });
3910
+ }
3911
+ });
3912
+ const previewCommand = defineAppCommand({
3913
+ name: "preview",
3914
+ description: "Generate a preview workflow (PR open/sync triggers deploy to a per-PR workspace).",
3915
+ args: z.object({
3916
+ name: arg(z.string().min(1).optional(), {
3917
+ alias: "n",
3918
+ description: "Name (defaults to the config 'name')"
3919
+ }),
3920
+ branch: arg(z.string().min(1).optional(), { description: "Branch to filter PRs by (defaults to the detected default branch)" }),
3921
+ region: arg(z.string().min(1), { description: "Workspace region for preview workspace creation (e.g. us-west). Required." }),
3922
+ "require-preview-label": arg(z.boolean().default(false), { description: "Deploy preview only for PRs labeled `tailor:preview` instead of all PRs." }),
3923
+ environment: arg(z.string().min(1).optional(), { description: "GitHub Environment for the preview jobs (defaults to the workspace name)" }),
3924
+ dir: arg(z.string().min(1).default("."), {
3925
+ alias: "d",
3926
+ description: "App directory (for monorepo setups)"
3927
+ }),
3928
+ force: arg(z.boolean().default(false), { description: "Discard hand edits / take over unmanaged files and regenerate" })
3929
+ }).strict(),
3930
+ run: async (args) => {
3931
+ await setupTarget({
3932
+ kind: "preview",
3933
+ workspaceName: args.name,
3934
+ branch: args.branch,
3935
+ region: args.region,
3936
+ requirePreviewLabel: args["require-preview-label"],
3937
+ environment: args.environment,
3938
+ dir: args.dir,
3939
+ force: args.force,
3940
+ outputDir: process.cwd()
3941
+ });
3942
+ }
3943
+ });
3944
+ const setupCommand = defineCommand({
3945
+ name: "setup",
3946
+ description: "Generate CI deploy workflows for your project. (beta)",
3947
+ subCommands: {
3948
+ branch: branchCommand,
3949
+ tag: tagCommand,
3950
+ preview: previewCommand,
3951
+ action: actionCommand,
3952
+ coordinate: coordinateCommand,
3953
+ check: checkCommand
3954
+ }
3955
+ });
3360
3956
 
3361
3957
  //#endregion
3362
3958
  //#region src/cli/shared/skills-installer.ts