Repository: JoshuaKGoldberg/create-typescript-app (ts)

Issue:
🚀 Feature: Use a standalone CI file for CTA transitions
### Feature Request Checklist

- [x] I have pulled the latest `main` branch of the repository.
- [x] I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue.

### Overview

Back in #1965 -> #2000 I added a `blockCTATransitions` that creates a CI step to run `pnpx create-typescript-app` and check in any updates. It adds a job to `.github/actions/ci.yml`.

I'd like to instead have a standalone `.github/actions/cta-transition.yml` file. This isn't really part of normal "CI". And it's quite different from the other parts of `ci.yml`.

### Additional Info

This is ... more safe? less unsafe? now that pnpm doesn't run lifecycle scripts by default (https://github.com/pnpm/pnpm/pull/8897). Good!

🎁 

Pull request:
feat: standalone CTA Transitions workflow
<!-- 👋 Hi, thanks for sending a PR to create-typescript-app! 🎁.
Please fill out all fields below and make sure each item is true and [x] checked.
Otherwise we may not be able to review your PR. -->

## PR Checklist

- [x] Addresses an existing open issue: fixes #2009
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

Moves the transition mode logic into two files:

1. `.github/actions/transition/action.yml`: A dedicated composite action for running in transition mode and checking in & commenting any changes
2. `.github/workflows/cta.yml`: Checks if the action should be run, and runs it if so

This means the job won't be marked as _Skipped_ if the logic is skipped. It'll still be _Success_ and thus not block PRs.

🎁 

Patch:
diff --git a/src/blocks/blockCTATransitions.ts b/src/blocks/blockCTATransitions.ts
index 8c2fa7a..8b83b68 100644
--- a/src/blocks/blockCTATransitions.ts
+++ b/src/blocks/blockCTATransitions.ts
@@ -1,8 +1,10 @@
 import { base } from "../base.js";
 import { packageData } from "../data/packageData.js";
 import { resolveUses } from "./actions/resolveUses.js";
-import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
 import { blockPackageJson } from "./blockPackageJson.js";
+import { blockRepositoryBranchRuleset } from "./blockRepositoryBranchRuleset.js";
+import { createSoloWorkflowFile } from "./files/createSoloWorkflowFile.js";
+import { formatYamlAction } from "./files/formatYamlAction.js";
 
 export const blockCTATransitions = base.createBlock({
 	about: {
@@ -11,66 +13,105 @@ export const blockCTATransitions = base.createBlock({
 	produce({ options }) {
 		return {
 			addons: [
-				blockGitHubActionsCI({
-					jobs: [
-						{
-							checkoutWith: {
-								"fetch-depth": "0",
-								ref: "${{github.event.pull_request.head.ref}}",
-								repository:
-									"${{github.event.pull_request.head.repo.full_name}}",
-								token: '"${{ secrets.ACCESS_TOKEN }}"',
+				blockPackageJson({
+					properties: {
+						devDependencies: {
+							"create-typescript-app": packageData.version,
+						},
+					},
+				}),
+				blockRepositoryBranchRuleset({
+					requiredStatusChecks: ["Transition"],
+				}),
+			],
+			files: {
+				".github": {
+					actions: {
+						transition: {
+							"action.yml": formatYamlAction({
+								description: "Runs create-typescript-app in transition mode",
+								name: "Transition",
+								runs: {
+									steps: [
+										{ uses: "./.github/actions/prepare" },
+										{
+											run: "pnpx create-typescript-app",
+											shell: "bash",
+										},
+										{
+											id: "auto-commit-action",
+											uses: "stefanzweifel/git-auto-commit-action@v5",
+											with: {
+												commit_author:
+													"The Friendly Bingo Bot <bot@create.bingo>",
+												commit_message:
+													"Check in changes from re-running npx create-typescript-app",
+												commit_user_email: "bot@create.bingo",
+												commit_user_name: "The Friendly Bingo Bot",
+											},
+										},
+										{
+											if: "steps.auto-commit-action.outputs.changes_detected == 'true'",
+											uses: "mshick/add-pr-comment@v2",
+											with: {
+												issue: "${{ github.event.pull_request.number }}",
+												message: [
+													"🤖 Beep boop! I ran `pnpx create-typescript-app` and it updated some files.",
+													"I went ahead and checked those changes into this PR for you. Please review the latest commit to see if you want to merge it.",
+													"Cheers!",
+													" — _The Friendly Bingo Bot_ 💝",
+													"",
+													"> ℹ️ These automatic commits keep your repository up-to-date with new versions of [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). If you want to opt out, delete your `.github/workflows/cta-transitions.yml` file.",
+												].join("\n"),
+											},
+										},
+									],
+									using: "composite",
+								},
+							}),
+						},
+					},
+					workflows: {
+						"cta.yml": createSoloWorkflowFile({
+							jobName: "Transition",
+							name: "CTA",
+							on: {
+								pull_request: {
+									branches: ["main"],
+								},
+							},
+							permissions: {
+								"pull-requests": "write",
 							},
-							if: "${{ startsWith(github.head_ref, 'renovate/') && contains(github.event.pull_request.title, 'create-typescript-app') }}",
-							name: "CTA Transitions",
 							steps: [
-								{ run: "pnpx create-typescript-app" },
 								{
 									uses: resolveUses(
-										"stefanzweifel/git-auto-commit-action",
-										"v5",
+										"actions/checkout",
+										"v4",
 										options.workflowsVersions,
 									),
 									with: {
-										commit_author: "The Friendly Bingo Bot <bot@create.bingo>",
-										commit_message:
-											"Check in changes from re-running npx create-typescript-app",
-										commit_user_email: "bot@create.bingo",
-										commit_user_name: "The Friendly Bingo Bot",
+										"fetch-depth": 0,
+										ref: "${{github.event.pull_request.head.ref}}",
+										repository:
+											"${{github.event.pull_request.head.repo.full_name}}",
+										token: "${{ secrets.ACCESS_TOKEN }}",
 									},
 								},
 								{
-									uses: resolveUses(
-										"mshick/add-pr-comment",
-										"v2",
-										options.workflowsVersions,
-									),
-									with: {
-										issue: "${{ github.event.pull_request.number }}",
-										message: [
-											"|",
-											"🤖 Beep boop! I ran `npx create-typescript-app` and found same changes.",
-											"Please review the latest commit to see if you want to merge it.",
-											"Cheers! 💝",
-											"",
-											"> This change was automatically made in CI to keep your repository up-to-date with the templating in [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).",
-											"> If you want to opt out of these automatic updates, delete the `.github/workflows/cta-transitions.yml` file on your `main` branch.",
-										].join("\n"),
-										"repo-token": "${{ secrets.ACCESS_TOKEN }}",
-									},
+									id: "check",
+									if: `(github.actor == '${options.owner}' || github.actor == 'renovate[bot]') && startsWith(github.head_ref, 'renovate/') && contains(github.event.pull_request.title, 'create-typescript-app')`,
+									uses: "./.github/actions/transition",
+								},
+								{
+									if: "steps.check.outcome == 'skipped'",
+									run: "echo 'Skipping transition mode because the PR does not appear to be an automated or owner-created update to create-typescript-app.'",
 								},
 							],
-						},
-					],
-				}),
-				blockPackageJson({
-					properties: {
-						devDependencies: {
-							"create-typescript-app": packageData.version,
-						},
+						}),
 					},
-				}),
-			],
+				},
+			},
 		};
 	},
 });
diff --git a/src/blocks/blockGitHubActionsCI.ts b/src/blocks/blockGitHubActionsCI.ts
index c7bb3b7..6aa5985 100644
--- a/src/blocks/blockGitHubActionsCI.ts
+++ b/src/blocks/blockGitHubActionsCI.ts
@@ -1,4 +1,3 @@
-import jsYaml from "js-yaml";
 import { z } from "zod";
 
 import { base } from "../base.js";
@@ -7,7 +6,7 @@ import { blockRemoveFiles } from "./blockRemoveFiles.js";
 import { blockRepositoryBranchRuleset } from "./blockRepositoryBranchRuleset.js";
 import { createMultiWorkflowFile } from "./files/createMultiWorkflowFile.js";
 import { createSoloWorkflowFile } from "./files/createSoloWorkflowFile.js";
-import { removeUsesQuotes } from "./files/removeUsesQuotes.js";
+import { formatYamlAction } from "./files/formatYamlAction.js";
 
 export const zActionStep = z.intersection(
 	z.object({
@@ -48,38 +47,34 @@ export const blockGitHubActionsCI = base.createBlock({
 				".github": {
 					actions: {
 						prepare: {
-							"action.yml": removeUsesQuotes(
-								jsYaml
-									.dump({
-										description: "Prepares the repo for a typical CI job",
-										name: "Prepare",
-										runs: {
-											steps: [
-												{
-													uses: resolveUses(
-														"pnpm/action-setup",
-														"v4",
-														options.workflowsVersions,
-													),
-												},
-												{
-													uses: resolveUses(
-														"actions/setup-node",
-														"v4",
-														options.workflowsVersions,
-													),
-													with: { cache: "pnpm", "node-version": "20" },
-												},
-												{
-													run: "pnpm install --frozen-lockfile",
-													shell: "bash",
-												},
-											],
-											using: "composite",
+							"action.yml": formatYamlAction({
+								description: "Prepares the repo for a typical CI job",
+								name: "Prepare",
+								runs: {
+									steps: [
+										{
+											uses: resolveUses(
+												"pnpm/action-setup",
+												"v4",
+												options.workflowsVersions,
+											),
 										},
-									})
-									.replaceAll(/\n(\S)/g, "\n\n$1"),
-							),
+										{
+											uses: resolveUses(
+												"actions/setup-node",
+												"v4",
+												options.workflowsVersions,
+											),
+											with: { cache: "pnpm", "node-version": "20" },
+										},
+										{
+											run: "pnpm install --frozen-lockfile",
+											shell: "bash",
+										},
+									],
+									using: "composite",
+								},
+							}),
 						},
 					},
 					workflows: {
diff --git a/src/blocks/files/createSoloWorkflowFile.ts b/src/blocks/files/createSoloWorkflowFile.ts
index 78fbce7..11233c3 100644
--- a/src/blocks/files/createSoloWorkflowFile.ts
+++ b/src/blocks/files/createSoloWorkflowFile.ts
@@ -34,6 +34,7 @@ interface WorkflowFileOn {
 interface WorkflowFileOptions {
 	concurrency?: WorkflowFileConcurrency;
 	if?: string;
+	jobName?: string;
 	name: string;
 	on?: WorkflowFileOn;
 	permissions?: WorkflowFilePermissions;
@@ -49,6 +50,7 @@ interface WorkflowFilePermissions {
 
 interface WorkflowFileStep {
 	env?: Record<string, string>;
+	id?: string;
 	if?: string;
 	name?: string;
 	run?: string;
@@ -58,6 +60,7 @@ interface WorkflowFileStep {
 
 export function createSoloWorkflowFile({
 	concurrency,
+	jobName,
 	name,
 	on,
 	permissions,
@@ -66,8 +69,9 @@ export function createSoloWorkflowFile({
 	return formatWorkflowYaml({
 		concurrency,
 		jobs: {
-			[createJobName(name)]: {
+			[createJobName(jobName ?? name)]: {
 				...(options.if && { if: options.if }),
+				...(jobName && { name: jobName }),
 				"runs-on": "ubuntu-latest",
 				steps: options.steps,
 			},
diff --git a/src/blocks/files/formatYamlAction.ts b/src/blocks/files/formatYamlAction.ts
new file mode 100644
index 0000000..d8251d9
--- /dev/null
+++ b/src/blocks/files/formatYamlAction.ts
@@ -0,0 +1,7 @@
+import jsYaml from "js-yaml";
+
+import { removeUsesQuotes } from "./removeUsesQuotes.js";
+
+export function formatYamlAction(value: object) {
+	return removeUsesQuotes(jsYaml.dump(value).replaceAll(/\n(\S)/g, "\n\n$1"));
+}


Test patch:
diff --git a/src/blocks/blockCTATransitions.test.ts b/src/blocks/blockCTATransitions.test.ts
index d6386d3..7067cd7 100644
--- a/src/blocks/blockCTATransitions.test.ts
+++ b/src/blocks/blockCTATransitions.test.ts
@@ -3,8 +3,8 @@ import { describe, expect, test } from "vitest";
 
 import { packageData } from "../data/packageData.js";
 import { blockCTATransitions } from "./blockCTATransitions.js";
-import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
 import { blockPackageJson } from "./blockPackageJson.js";
+import { blockRepositoryBranchRuleset } from "./blockRepositoryBranchRuleset.js";
 import { optionsBase } from "./options.fakes.js";
 
 describe("blockCTATransitions", () => {
@@ -13,67 +13,93 @@ describe("blockCTATransitions", () => {
 			options: optionsBase,
 		});
 
-		expect(creation).toEqual({
-			addons: [
-				{
-					addons: {
-						jobs: [
-							{
-								checkoutWith: {
-									"fetch-depth": "0",
-									ref: "${{github.event.pull_request.head.ref}}",
-									repository:
-										"${{github.event.pull_request.head.repo.full_name}}",
-									token: '"${{ secrets.ACCESS_TOKEN }}"',
-								},
-								if: "${{ startsWith(github.head_ref, 'renovate/') && contains(github.event.pull_request.title, 'create-typescript-app') }}",
-								name: "CTA Transitions",
-								steps: [
-									{
-										run: "pnpx create-typescript-app",
-									},
-									{
-										uses: "stefanzweifel/git-auto-commit-action@v5",
-										with: {
-											commit_author:
-												"The Friendly Bingo Bot <bot@create.bingo>",
-											commit_message:
-												"Check in changes from re-running npx create-typescript-app",
-											commit_user_email: "bot@create.bingo",
-											commit_user_name: "The Friendly Bingo Bot",
-										},
-									},
-									{
-										uses: "mshick/add-pr-comment@v2",
-										with: {
-											issue: "${{ github.event.pull_request.number }}",
-											message: `|
-🤖 Beep boop! I ran \`npx create-typescript-app\` and found same changes.
-Please review the latest commit to see if you want to merge it.
-Cheers! 💝
-
-> This change was automatically made in CI to keep your repository up-to-date with the templating in [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).
-> If you want to opt out of these automatic updates, delete the \`.github/workflows/cta-transitions.yml\` file on your \`main\` branch.`,
-											"repo-token": "${{ secrets.ACCESS_TOKEN }}",
-										},
-									},
-								],
-							},
-						],
-					},
-					block: blockGitHubActionsCI,
-				},
-				{
-					addons: {
-						properties: {
-							devDependencies: {
-								"create-typescript-app": packageData.version,
-							},
-						},
+		expect(creation.addons).toEqual([
+			blockPackageJson({
+				properties: {
+					devDependencies: {
+						"create-typescript-app": packageData.version,
 					},
-					block: blockPackageJson,
 				},
-			],
-		});
+			}),
+			blockRepositoryBranchRuleset({
+				requiredStatusChecks: ["Transition"],
+			}),
+		]);
+		expect(creation.files).toMatchInlineSnapshot(`
+			{
+			  ".github": {
+			    "actions": {
+			      "transition": {
+			        "action.yml": "description: Runs create-typescript-app in transition mode
+
+			name: Transition
+
+			runs:
+			  steps:
+			    - uses: ./.github/actions/prepare
+			    - run: pnpx create-typescript-app
+			      shell: bash
+			    - id: auto-commit-action
+			      uses: stefanzweifel/git-auto-commit-action@v5
+			      with:
+			        commit_author: The Friendly Bingo Bot <bot@create.bingo>
+			        commit_message: Check in changes from re-running npx create-typescript-app
+			        commit_user_email: bot@create.bingo
+			        commit_user_name: The Friendly Bingo Bot
+			    - if: steps.auto-commit-action.outputs.changes_detected == 'true'
+			      uses: mshick/add-pr-comment@v2
+			      with:
+			        issue: \${{ github.event.pull_request.number }}
+			        message: >-
+			          🤖 Beep boop! I ran \`pnpx create-typescript-app\` and it updated some
+			          files.
+
+			          I went ahead and checked those changes into this PR for you. Please
+			          review the latest commit to see if you want to merge it.
+
+			          Cheers!
+			           — _The Friendly Bingo Bot_ 💝
+
+			          > ℹ️ These automatic commits keep your repository up-to-date with new
+			          versions of
+			          [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).
+			          If you want to opt out, delete your
+			          \`.github/workflows/cta-transitions.yml\` file.
+			  using: composite
+			",
+			      },
+			    },
+			    "workflows": {
+			      "cta.yml": "jobs:
+			  transition:
+			    name: Transition
+			    runs-on: ubuntu-latest
+			    steps:
+			      - uses: actions/checkout@v4
+			        with:
+			          fetch-depth: 0
+			          ref: \${{github.event.pull_request.head.ref}}
+			          repository: \${{github.event.pull_request.head.repo.full_name}}
+			          token: \${{ secrets.ACCESS_TOKEN }}
+			      - id: check
+			        if: (github.actor == 'test-owner' || github.actor == 'renovate[bot]') && startsWith(github.head_ref, 'renovate/') && contains(github.event.pull_request.title, 'create-typescript-app')
+			        uses: ./.github/actions/transition
+			      - if: steps.check.outcome == 'skipped'
+			        run: echo 'Skipping transition mode because the PR does not appear to be an automated or owner-created update to create-typescript-app.'
+
+			name: CTA
+
+			on:
+			  pull_request:
+			    branches:
+			      - main
+
+			permissions:
+			  pull-requests: write
+			",
+			    },
+			  },
+			}
+		`);
 	});
 });