diff --git a/.changeset/setup-github-renovate-actions.md b/.changeset/setup-github-renovate-actions.md new file mode 100644 index 000000000..cf045e3d7 --- /dev/null +++ b/.changeset/setup-github-renovate-actions.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/sdk": patch +--- + +chore(setup github): bump bundled `tailor-platform/actions` ref to v1.1.0 and let Renovate keep it up to date going forward. diff --git a/packages/sdk/docs/cli/setup.md b/packages/sdk/docs/cli/setup.md index 772cdbff4..dd8d2b4e7 100644 --- a/packages/sdk/docs/cli/setup.md +++ b/packages/sdk/docs/cli/setup.md @@ -72,6 +72,7 @@ tailor-sdk setup github [options] | `--organization-id ` | `-o` | Organization ID | Yes | - | | `--folder-id ` | `-f` | Folder ID | Yes | - | | `--dir ` | `-d` | App directory (for monorepo setups) | No | `"."` | +| `--with-plan` | `-p` | Include plan job for PR previews | No | `false` | diff --git a/packages/sdk/src/cli/commands/setup/github/deploy.workflow.yml b/packages/sdk/src/cli/commands/setup/github/deploy.workflow.yml index 6a8291b84..810872379 100644 --- a/packages/sdk/src/cli/commands/setup/github/deploy.workflow.yml +++ b/packages/sdk/src/cli/commands/setup/github/deploy.workflow.yml @@ -1,24 +1,48 @@ -name: Deploy +name: Tailor Platform on: + pull_request: + branches: + - main push: branches: - main workflow_dispatch: concurrency: - group: deploy-__WORKSPACE_NAME__ - cancel-in-progress: false + group: tailor-__WORKSPACE_NAME__-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # __PLAN_JOB_START__ + plan: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + # __SETUP_STEPS__ + - uses: tailor-platform/actions/plan@e63ed98630a23fa21ee0636abf0f7fb75fcdce40 # v1.1.0 + with: + workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }} + # __WORKING_DIRECTORY__ + platform-client-id: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_ID }} + platform-client-secret: ${{ secrets.PLATFORM_MACHINE_USER_CLIENT_SECRET }} + github-token: ${{ secrets.GITHUB_TOKEN }} + # __PLAN_JOB_END__ deploy: + if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # __SETUP_STEPS__ - - uses: tailor-platform/actions/deploy@980aeba08963f4322b2b48ca7a920f4e14876842 # v1.0.0 + - uses: tailor-platform/actions/deploy@e63ed98630a23fa21ee0636abf0f7fb75fcdce40 # v1.1.0 with: workspace-name: __WORKSPACE_NAME__ workspace-region: __WORKSPACE_REGION__ diff --git a/packages/sdk/src/cli/commands/setup/github/github.test.ts b/packages/sdk/src/cli/commands/setup/github/github.test.ts index f6c034993..8ff21cd64 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.test.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.test.ts @@ -62,9 +62,7 @@ describe("renderDeploy", () => { it("references the composite action", () => { const content = renderDeploy(baseParams); - expect(content).toContain( - "uses: tailor-platform/actions/deploy@980aeba08963f4322b2b48ca7a920f4e14876842 # v1.0.0", - ); + expect(content).toMatch(/uses: tailor-platform\/actions\/deploy@[a-f0-9]+ # v\d+\.\d+\.\d+/); }); it("includes setup steps in correct order", () => { @@ -82,6 +80,7 @@ describe("renderDeploy", () => { expect(content).toMatch(/uses: actions\/checkout@[a-f0-9]+ # v\d+\.\d+\.\d+/); expect(content).toMatch(/uses: pnpm\/action-setup@[a-f0-9]+ # v\d+\.\d+\.\d+/); expect(content).toMatch(/uses: actions\/setup-node@[a-f0-9]+ # v\d+\.\d+\.\d+/); + expect(content).toMatch(/uses: tailor-platform\/actions\/deploy@[a-f0-9]+ # v\d+\.\d+\.\d+/); }); it("generates pnpm setup steps", () => { @@ -151,8 +150,42 @@ describe("renderDeploy", () => { it("parameterizes concurrency group with workspace name", () => { const content = renderDeploy(baseParams); - expect(content).toContain("group: deploy-my-app"); - expect(content).not.toContain("group: deploy\n"); + expect(content).toContain("group: tailor-my-app-"); + }); + + it("strips plan job by default", () => { + const content = renderDeploy(baseParams); + expect(content).not.toContain("tailor-platform/actions/plan@"); + expect(content).not.toContain("TAILOR_PLATFORM_WORKSPACE_ID"); + expect(content).not.toContain("__PLAN_JOB_START__"); + expect(content).not.toContain("__PLAN_JOB_END__"); + expect(content).toContain("tailor-platform/actions/deploy@"); + }); + + it("includes plan job when withPlan is true", () => { + const content = renderDeploy({ ...baseParams, withPlan: true }); + expect(content).toContain("tailor-platform/actions/plan@"); + expect(content).toContain("workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}"); + expect(content).toContain("github-token: ${{ secrets.GITHUB_TOKEN }}"); + expect(content).toContain("tailor-platform/actions/deploy@"); + expect(content).not.toContain("__PLAN_JOB_START__"); + expect(content).not.toContain("__PLAN_JOB_END__"); + }); + + it("includes setup steps in both plan and deploy jobs when withPlan is true", () => { + const content = renderDeploy({ ...baseParams, withPlan: true }); + const matches = content.match(/uses: pnpm\/action-setup@/g) ?? []; + expect(matches).toHaveLength(2); + }); + + it("propagates working-directory to plan job when withPlan is true", () => { + const content = renderDeploy({ + ...baseParams, + withPlan: true, + workingDirectory: "apps/foo", + }); + const matches = content.match(/working-directory: apps\/foo/g) ?? []; + expect(matches).toHaveLength(2); }); }); @@ -178,7 +211,7 @@ describe("buildFiles", () => { outputDir: testDir, }); expect(files).toHaveLength(1); - expect(files[0]!.path).toBe(path.join(testDir, ".github/workflows/deploy-my-app.yml")); + expect(files[0]!.path).toBe(path.join(testDir, ".github/workflows/tailor-my-app.yml")); }); it("detects package manager from project directory", () => { diff --git a/packages/sdk/src/cli/commands/setup/github/github.ts b/packages/sdk/src/cli/commands/setup/github/github.ts index b9f617886..20840202a 100644 --- a/packages/sdk/src/cli/commands/setup/github/github.ts +++ b/packages/sdk/src/cli/commands/setup/github/github.ts @@ -11,6 +11,7 @@ export type SetupGitHubOptions = { folderId: string; dir: string; outputDir: string; + withPlan?: boolean; }; type GeneratedFile = { @@ -30,16 +31,20 @@ type WriteResult = { */ export function buildFiles(options: SetupGitHubOptions): GeneratedFile[] { const githubDir = path.join(options.outputDir, ".github"); + const packageManager = detectPackageManager(options.outputDir); + const workingDirectory = options.dir !== "." ? options.dir : undefined; + return [ { - path: path.join(githubDir, `workflows/deploy-${options.workspaceName}.yml`), + path: path.join(githubDir, `workflows/tailor-${options.workspaceName}.yml`), content: renderDeploy({ workspaceName: options.workspaceName, workspaceRegion: options.workspaceRegion, organizationId: options.organizationId, folderId: options.folderId, - workingDirectory: options.dir !== "." ? options.dir : undefined, - packageManager: detectPackageManager(options.outputDir), + workingDirectory, + packageManager, + withPlan: options.withPlan, }), }, ]; @@ -91,4 +96,10 @@ export function setupGitHub(options: SetupGitHubOptions): void { logger.info("Next steps - set GitHub secrets:"); logger.log(` gh secret set PLATFORM_MACHINE_USER_CLIENT_ID`); logger.log(` gh secret set PLATFORM_MACHINE_USER_CLIENT_SECRET`); + + if (options.withPlan) { + logger.newline(); + logger.info("For plan job - set GitHub variable with your workspace ID:"); + logger.log(` gh variable set TAILOR_PLATFORM_WORKSPACE_ID`); + } } diff --git a/packages/sdk/src/cli/commands/setup/github/index.ts b/packages/sdk/src/cli/commands/setup/github/index.ts index 288d22f75..6d6ad4689 100644 --- a/packages/sdk/src/cli/commands/setup/github/index.ts +++ b/packages/sdk/src/cli/commands/setup/github/index.ts @@ -30,6 +30,10 @@ export const githubCommand = defineAppCommand({ alias: "d", description: "App directory (for monorepo setups)", }), + "with-plan": arg(z.boolean().default(false), { + alias: "p", + description: "Include plan job for PR previews", + }), }) .strict(), run: (args) => { @@ -40,6 +44,7 @@ export const githubCommand = defineAppCommand({ folderId: args["folder-id"], dir: args.dir, outputDir: process.cwd(), + withPlan: args["with-plan"], }); }, }); diff --git a/packages/sdk/src/cli/commands/setup/github/template-deploy.ts b/packages/sdk/src/cli/commands/setup/github/template-deploy.ts index 61ba58f9c..1ccbfc817 100644 --- a/packages/sdk/src/cli/commands/setup/github/template-deploy.ts +++ b/packages/sdk/src/cli/commands/setup/github/template-deploy.ts @@ -15,6 +15,7 @@ type DeployParams = { folderId: string; workingDirectory?: string; packageManager: PackageManager; + withPlan?: boolean; }; const setupSteps: Record = { @@ -48,12 +49,16 @@ export function detectPackageManager(dir: string): PackageManager { } /** - * Render the deploy caller workflow YAML. + * Render the deploy workflow YAML. * - * Generates a thin workflow that calls the composite deploy action + * Generates a workflow that calls the composite deploy action * from tailor-platform/actions. The environment setup steps (Node.js, * package manager, dependency install) are generated based on the * detected package manager. + * + * If withPlan is true, also includes a plan job that runs on pull requests. + * Otherwise, the plan job section delimited by __PLAN_JOB_START__ / + * __PLAN_JOB_END__ markers is stripped from the template. * @param params - Workspace and deployment configuration * @returns Workflow YAML content */ @@ -65,17 +70,23 @@ export function renderDeploy(params: DeployParams): string { folderId, workingDirectory, packageManager, + withPlan, } = params; const workingDirectoryLine = workingDirectory ? ` working-directory: ${workingDirectory}\n` : ""; - return deployTemplate + const stripPlanSection = (content: string): string => + withPlan + ? content.replace(/^ *# __PLAN_JOB_(?:START|END)__\n/gm, "") + : content.replace(/^ *# __PLAN_JOB_START__\n[\s\S]*?^ *# __PLAN_JOB_END__\n/m, ""); + + return stripPlanSection(deployTemplate) .replaceAll("__WORKSPACE_NAME__", () => workspaceName) .replaceAll("__WORKSPACE_REGION__", () => workspaceRegion) .replaceAll("__ORGANIZATION_ID__", () => organizationId) .replaceAll("__FOLDER_ID__", () => folderId) - .replace(/ *# __WORKING_DIRECTORY__\n/, () => workingDirectoryLine) - .replace(/^ *# __SETUP_STEPS__$/m, () => indentSnippet(setupSteps[packageManager], 6)); + .replace(/ *# __WORKING_DIRECTORY__\n/g, () => workingDirectoryLine) + .replace(/^ *# __SETUP_STEPS__$/gm, () => indentSnippet(setupSteps[packageManager], 6)); } diff --git a/renovate.json b/renovate.json index be2f2143c..4c705f47d 100644 --- a/renovate.json +++ b/renovate.json @@ -13,10 +13,11 @@ "customManagers": [ { "customType": "regex", - "managerFilePatterns": ["packages/sdk/src/cli/commands/setup/github/.+\\.ya?ml$"], + "managerFilePatterns": ["/^packages/sdk/src/cli/commands/setup/github/.+\\.ya?ml$/"], "matchStrings": [ - "uses:\\s+(?[\\w-]+/[\\w-]+)@(?[a-f0-9]+)\\s+#\\s+(?v[\\d.]+)" + "uses:\\s+(?[\\w-]+/[\\w-]+)(?(?:/[\\w-]+)?)@(?[a-f0-9]+)\\s+#\\s+(?v[\\d.]+)" ], + "autoReplaceStringTemplate": "uses: {{depName}}{{subpath}}@{{newDigest}} # {{newValue}}", "datasourceTemplate": "github-tags" } ],