From 7e14f5dcfb7adce3f36aaa817576ecf3baf74975 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 12:08:53 +0100 Subject: [PATCH 1/8] Updated internal package migration checks --- .../skills/migrate-internal-package/SKILL.md | 22 ++++++++++++-- .../references/legacy-integration.md | 30 ++++++++++++++++++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index ca161ef3953..ae40c31d9b5 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -41,6 +41,13 @@ remote branch is gone. Before importing, record and compare the destination `HEAD` and `origin/main`; they must match. Recheck the first parent immediately after the subtree commit. +Initialize a fresh Ghost worktree with `pnpm run setup`, not `pnpm setup`. +`pnpm setup` is a pnpm CLI command that configures pnpm's global home and may +edit shell startup files; the explicit `run` is required to invoke Ghost's +package script. Before continuing, confirm the command installed the workspace, +initialized the submodules and configured the repository-local blame ignore +file as described by Ghost's root `setup` script. + ## Confirm this workflow applies Before changing either repository, record the source repository, its default @@ -120,7 +127,13 @@ its destination state: Document temporary named-catalog entries for the modernization follow-up. Never inline dependency versions; Ghost's strict catalog policy still applies. Do not -import additional packages implicitly. +import additional packages implicitly. After `pnpm install`, inspect the +lockfile's resolved override and package snapshot entries for every translated +dependency. Repository-wide pnpm overrides take precedence over catalog +references without changing the dependency declaration in `package.json`; if a +global override changes an imported package's required version, add the +narrowest package-scoped override that preserves the source release and record +why it is needed. If the package is legacy JavaScript or CommonJS, read [`references/legacy-integration.md`](references/legacy-integration.md) @@ -132,8 +145,11 @@ history crosses into the source repository before opening the PR. Before opening the PR, also verify the source split is reachable from the branch tip, the consumer resolves the workspace package through its production import path, package lint and tests pass through Nx, relevant consumer tests pass, the -full build passes, and the Ghost archive contains the internal package. Record -the exact commit IDs and commands in the handoff. +repository formatting check passes, the full build passes, and the Ghost +archive contains the internal package. Record the exact commit IDs and commands +in the handoff. Keep mechanical formatting in a focused integration commit so +the subtree commit remains an exact history import and reviewers can distinguish +format-only changes from behavioral adaptation. For a pilot or first use, include a structured gap report in the handoff: diff --git a/.agents/skills/migrate-internal-package/references/legacy-integration.md b/.agents/skills/migrate-internal-package/references/legacy-integration.md index bc29be43e92..9e129ab3d73 100644 --- a/.agents/skills/migrate-internal-package/references/legacy-integration.md +++ b/.agents/skills/migrate-internal-package/references/legacy-integration.md @@ -85,7 +85,34 @@ not also Ghost workspaces. For each dependency: 4. Stop if no published or destination-workspace dependency can satisfy it. Record temporary named-catalog entries in the PR and reassess them during -modernization. +modernization. After installation, verify the versions actually resolved in +`pnpm-lock.yaml`; `overrides` take precedence over both default and named +catalogs. If a repository-wide override changes the imported package's source +version, preserve it with a package-scoped override such as: + +```yaml +overrides: + '@tryghost/imported-package>@tryghost/dependency': 'catalog:migration-catalog' +``` + +Keep the existing global override for other consumers and explain the scoped +exception next to it. Checking only `package.json` is insufficient because its +catalog reference remains unchanged when pnpm applies an override. + +## Formatting + +An exact subtree import may not satisfy Ghost's current formatter. Do not alter +the subtree commit: run the repository formatter against the imported package +afterward and commit purely mechanical output separately from behavioral +integration changes. Verify the focused path and the repository gate: + +```bash +pnpm exec oxfmt --check packages/ +pnpm format:check +``` + +If formatting changes files, rerun package lint and tests afterward. Do not mix +opportunistic cleanup or modernization into the format-only commit. ## Verification @@ -94,6 +121,7 @@ Run the package through its Nx surface and exercise a real consumer: ```bash pnpm nx run @tryghost/:lint pnpm nx run @tryghost/:test +pnpm format:check pnpm build ``` From 0677fcb9ec71ac41c0e57bafebe6c8ff71df001a Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 12:10:50 +0100 Subject: [PATCH 2/8] Added safeguards for history import PRs Prevent normal squash or rebase merges from discarding imported package ancestry by requiring a warning title, a prominent PR notice, and copyable guarded merge instructions in every handoff. --- .agents/skills/migrate-internal-package/SKILL.md | 14 ++++++++++++++ .../references/history-and-merge.md | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index ae40c31d9b5..2cdf8e59b28 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -151,6 +151,12 @@ in the handoff. Keep mechanical formatting in a focused integration commit so the subtree commit remains an exact history import and reviewers can distinguish format-only changes from behavioral adaptation. +Open the history-import PR with a title beginning `[Don't merge]`. Put a +prominent warning at the top of its body that squash and rebase merges destroy +the imported ancestry and that the PR must only be merged with the guarded +history script below. Do not remove the prefix or warning merely because CI is +green: they remain until the authorized user performs the exceptional merge. + For a pilot or first use, include a structured gap report in the handoff: - `Observed`: the exact failure or ambiguity and the command/state that exposed it; @@ -183,6 +189,14 @@ attempting a workaround. The script performs preflight checks, records and temporarily changes the setting, uses the correct merge form, restores the setting, and verifies the resulting history. +Every merge-checkpoint handoff must repeat the warning and render a directly +copyable command containing the actual PR number and recorded source split tip; +do not leave placeholders for the user to infer. Also provide the read-only +`--dry-run` form first. Explicitly say not to use GitHub's normal squash/rebase +buttons and not to remove `[Don't merge]` manually. The skill must never merge +the PR unless the user separately authorizes that administrative action after +seeing these instructions. + Afterward, independently fetch `main` and confirm the source split tip is an ancestor before starting source-repository cleanup. diff --git a/.agents/skills/migrate-internal-package/references/history-and-merge.md b/.agents/skills/migrate-internal-package/references/history-and-merge.md index 0e836b3e362..c6b6fc7d38b 100644 --- a/.agents/skills/migrate-internal-package/references/history-and-merge.md +++ b/.agents/skills/migrate-internal-package/references/history-and-merge.md @@ -156,6 +156,12 @@ gh pr view --repo TryGhost/Ghost \ Resolve real failures and base conflicts without flattening the subtree merge. Verify the graph again after any branch update. +The PR title must begin `[Don't merge]`, and the first paragraph of its body +must state that the normal squash and rebase controls must not be used. Include +the guarded merge command with the actual PR number and source split tip in the +body. Keep those protections in place through review and green CI so a reviewer +cannot accidentally apply Ghost's normal merge policy. + ## Use the guarded merge operation The merge must retain both the subtree topology and the import branch as the @@ -172,6 +178,12 @@ An authorized admin should run: --confirm ``` +The agent's handoff must substitute the real PR number and source split tip and +show `--dry-run` first, followed by `--confirm`; placeholders are not an +acceptable final handoff. The user should run the command from the Ghost +repository root. Do not use GitHub's normal squash/rebase buttons or manually +remove the `[Don't merge]` prefix before running it. + Use `--dry-run` instead of `--confirm` for read-only preflight. The script: 1. verifies authentication, PR state, checks and imported-history reachability; From d8f8881a7ae03ce6c2e7f20425af7699eff79da1 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 12:47:04 +0100 Subject: [PATCH 3/8] Updated migration setup command to bootstrap no ref\n\nKeep the migration workflow aligned with the repository bootstrap rename so it cannot invoke pnpm's shell-configuration command by accident. --- .agents/skills/migrate-internal-package/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index 2cdf8e59b28..1825e3c41f0 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -41,12 +41,12 @@ remote branch is gone. Before importing, record and compare the destination `HEAD` and `origin/main`; they must match. Recheck the first parent immediately after the subtree commit. -Initialize a fresh Ghost worktree with `pnpm run setup`, not `pnpm setup`. +Initialize a fresh Ghost worktree with `pnpm bootstrap`, not `pnpm setup`. `pnpm setup` is a pnpm CLI command that configures pnpm's global home and may -edit shell startup files; the explicit `run` is required to invoke Ghost's -package script. Before continuing, confirm the command installed the workspace, -initialized the submodules and configured the repository-local blame ignore -file as described by Ghost's root `setup` script. +edit shell startup files; it does not invoke Ghost's repository bootstrap. +Before continuing, confirm the command installed the workspace, initialized the +submodules and configured the repository-local blame ignore file as described +by Ghost's root `bootstrap` script. ## Confirm this workflow applies From 2d19456883ef919844ac6de0f2312fb910fbf636 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 13:35:47 +0100 Subject: [PATCH 4/8] Added human guide for package migrations no ref\n\nGive migration coordinators a clear two-step handoff for preparing and safely merging history-import PRs, including guarded commands, verification checkpoints, and post-merge sequencing. --- .../skills/migrate-internal-package/README.md | 115 ++++++++++++++++++ .../skills/migrate-internal-package/SKILL.md | 4 + 2 files changed, 119 insertions(+) create mode 100644 .agents/skills/migrate-internal-package/README.md diff --git a/.agents/skills/migrate-internal-package/README.md b/.agents/skills/migrate-internal-package/README.md new file mode 100644 index 00000000000..e0ac41e5570 --- /dev/null +++ b/.agents/skills/migrate-internal-package/README.md @@ -0,0 +1,115 @@ +# Migrating a package into Ghost + +This guide is for the human coordinating a package migration with the +`migrate-internal-package` skill. The skill moves a package from another +TryGhost repository into Ghost as an internal workspace package while retaining +the package's original Git history. + +History preservation makes the import PR different from an ordinary Ghost PR. +Do not use GitHub's squash or rebase merge controls for it. + +## Before starting + +Confirm that: + +- Ghost should become the package's owner; +- the package no longer needs independent releases for supported external use; +- both repositories are available locally as full clones; +- the agent may create branches and PRs, but understands that merging and + repository-setting changes require separate authorization. + +Start the workflow from the Ghost repository with a concrete source URL: + +```text +Use $migrate-internal-package to move +https://github.com/TryGhost//tree/main/packages/ +into Ghost as an internal-only package. +``` + +## The two-step import + +### 1. Let the agent prepare the import PR + +The agent audits ownership and consumers, extracts the package-only history, +imports it with an unsquashed Git subtree merge, integrates the package into the +Ghost workspace, and runs the required checks. + +The resulting PR must: + +- have a title beginning `[Don't merge]`; +- warn against using GitHub's normal merge controls; +- record the full source split SHA and subtree commit SHA; +- include ready-to-run `--dry-run` and `--confirm` commands with no placeholders; +- be green before reaching the merge checkpoint. + +At this point the agent stops. `[Don't merge]` means “do not merge normally,” +not “this work should never merge.” Keep the prefix until the guarded command +performs the exceptional merge. + +### 2. Perform the guarded merge + +An authorized Ghost administrator runs the commands from the Ghost repository +root. First run the read-only preflight copied from the PR: + +```bash +.agents/skills/migrate-internal-package/scripts/merge-history-pr \ + TryGhost/Ghost \ + \ + \ + --dry-run +``` + +Check that it reports the expected repository, PR head and source split SHA. It +also confirms that CI is green, the PR is mergeable, and the source history is +reachable from the PR branch. The dry run does not change settings or merge the +PR. + +If the output is correct, explicitly authorize the merge and run: + +```bash +.agents/skills/migrate-internal-package/scripts/merge-history-pr \ + TryGhost/Ghost \ + \ + \ + --confirm +``` + +The script records Ghost's merge-commit setting, temporarily enables merge +commits if required, merges with the PR head pinned, restores the original +setting, and verifies that the resulting commit has two parents and still +contains the imported ancestry. + +Do not substitute a manual `gh pr merge`, GitHub squash merge, rebase merge, or +merge queue. If the script reports a protection or permission failure, stop and +resolve that specific blocker rather than bypassing it. + +## After the guarded merge + +Ask the agent to continue the migration only after it fetches Ghost `main` and +confirms that the recorded source split SHA is an ancestor. The remaining work +is deliberately separate: + +1. Remove the package and its publishing configuration from the source + repository in a normal PR. +2. Decide whether the public npm versions should be deprecated for new direct + use. Existing versions remain published for old Ghost releases. +3. Remove migration-only catalog or Renovate configuration from Ghost. +4. Modernize the internal package in a focused PR when necessary. + +Source cleanup must not start before the history-preserving Ghost merge is +verified. This ordering prevents a period where neither repository owns the +package. + +## Recovery and escalation + +- If CI changes the PR head, rerun `--dry-run`; the script pins the merge to the + current reviewed head. +- If the source split SHA is missing or ambiguous, do not infer it. Ask the + agent to verify the subtree topology and update the PR instructions. +- If the repository merge setting cannot be restored, treat the script's + critical error as an administrative incident and restore the original value + before doing anything else. +- If external consumers still require new releases, stop. The internal-only + migration workflow is not the correct ownership model. + +For the agent's complete operating procedure, see [`SKILL.md`](SKILL.md). diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index 1825e3c41f0..b1849f34df3 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -9,6 +9,10 @@ Move a package from another TryGhost repository into Ghost without losing its history or creating a period where neither repository owns it. Keep migration mechanics separate from Ghost's lifetime package standards. +When handing this workflow to a human operator, link +[`README.md`](README.md), which explains the two-step import and guarded merge +in human-facing terms. + ## Authority boundaries Explain every cross-repository or administrative action before it happens. From 1cec5a67136e378542e79d088f102b0e22eaf53c Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 13:46:17 +0100 Subject: [PATCH 5/8] Moved package migration guide into contributor docs no ref\n\nMake the workflow discoverable to engineers starting from the codebase documentation and frame it around moving an SDK or framework package, the skill's expected output, and the exceptional merge process. --- .../skills/migrate-internal-package/README.md | 115 ---------------- .../skills/migrate-internal-package/SKILL.md | 7 +- docs/README.md | 1 + .../internal-package-migrations.md | 123 ++++++++++++++++++ 4 files changed, 128 insertions(+), 118 deletions(-) delete mode 100644 .agents/skills/migrate-internal-package/README.md create mode 100644 docs/contributing/internal-package-migrations.md diff --git a/.agents/skills/migrate-internal-package/README.md b/.agents/skills/migrate-internal-package/README.md deleted file mode 100644 index e0ac41e5570..00000000000 --- a/.agents/skills/migrate-internal-package/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# Migrating a package into Ghost - -This guide is for the human coordinating a package migration with the -`migrate-internal-package` skill. The skill moves a package from another -TryGhost repository into Ghost as an internal workspace package while retaining -the package's original Git history. - -History preservation makes the import PR different from an ordinary Ghost PR. -Do not use GitHub's squash or rebase merge controls for it. - -## Before starting - -Confirm that: - -- Ghost should become the package's owner; -- the package no longer needs independent releases for supported external use; -- both repositories are available locally as full clones; -- the agent may create branches and PRs, but understands that merging and - repository-setting changes require separate authorization. - -Start the workflow from the Ghost repository with a concrete source URL: - -```text -Use $migrate-internal-package to move -https://github.com/TryGhost//tree/main/packages/ -into Ghost as an internal-only package. -``` - -## The two-step import - -### 1. Let the agent prepare the import PR - -The agent audits ownership and consumers, extracts the package-only history, -imports it with an unsquashed Git subtree merge, integrates the package into the -Ghost workspace, and runs the required checks. - -The resulting PR must: - -- have a title beginning `[Don't merge]`; -- warn against using GitHub's normal merge controls; -- record the full source split SHA and subtree commit SHA; -- include ready-to-run `--dry-run` and `--confirm` commands with no placeholders; -- be green before reaching the merge checkpoint. - -At this point the agent stops. `[Don't merge]` means “do not merge normally,” -not “this work should never merge.” Keep the prefix until the guarded command -performs the exceptional merge. - -### 2. Perform the guarded merge - -An authorized Ghost administrator runs the commands from the Ghost repository -root. First run the read-only preflight copied from the PR: - -```bash -.agents/skills/migrate-internal-package/scripts/merge-history-pr \ - TryGhost/Ghost \ - \ - \ - --dry-run -``` - -Check that it reports the expected repository, PR head and source split SHA. It -also confirms that CI is green, the PR is mergeable, and the source history is -reachable from the PR branch. The dry run does not change settings or merge the -PR. - -If the output is correct, explicitly authorize the merge and run: - -```bash -.agents/skills/migrate-internal-package/scripts/merge-history-pr \ - TryGhost/Ghost \ - \ - \ - --confirm -``` - -The script records Ghost's merge-commit setting, temporarily enables merge -commits if required, merges with the PR head pinned, restores the original -setting, and verifies that the resulting commit has two parents and still -contains the imported ancestry. - -Do not substitute a manual `gh pr merge`, GitHub squash merge, rebase merge, or -merge queue. If the script reports a protection or permission failure, stop and -resolve that specific blocker rather than bypassing it. - -## After the guarded merge - -Ask the agent to continue the migration only after it fetches Ghost `main` and -confirms that the recorded source split SHA is an ancestor. The remaining work -is deliberately separate: - -1. Remove the package and its publishing configuration from the source - repository in a normal PR. -2. Decide whether the public npm versions should be deprecated for new direct - use. Existing versions remain published for old Ghost releases. -3. Remove migration-only catalog or Renovate configuration from Ghost. -4. Modernize the internal package in a focused PR when necessary. - -Source cleanup must not start before the history-preserving Ghost merge is -verified. This ordering prevents a period where neither repository owns the -package. - -## Recovery and escalation - -- If CI changes the PR head, rerun `--dry-run`; the script pins the merge to the - current reviewed head. -- If the source split SHA is missing or ambiguous, do not infer it. Ask the - agent to verify the subtree topology and update the PR instructions. -- If the repository merge setting cannot be restored, treat the script's - critical error as an administrative incident and restore the original value - before doing anything else. -- If external consumers still require new releases, stop. The internal-only - migration workflow is not the correct ownership model. - -For the agent's complete operating procedure, see [`SKILL.md`](SKILL.md). diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index b1849f34df3..bc667621a7d 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -9,9 +9,10 @@ Move a package from another TryGhost repository into Ghost without losing its history or creating a period where neither repository owns it. Keep migration mechanics separate from Ghost's lifetime package standards. -When handing this workflow to a human operator, link -[`README.md`](README.md), which explains the two-step import and guarded merge -in human-facing terms. +The contributor workflow is documented in +[`docs/contributing/internal-package-migrations.md`](../../../docs/contributing/internal-package-migrations.md). +Keep that guide aligned when this skill changes its outputs or human +checkpoints. ## Authority boundaries diff --git a/docs/README.md b/docs/README.md index 426683c2cf7..baeba218d0e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,6 +97,7 @@ Practice and contributor guides explain how to make and verify changes: - [Error handling](practices/error-handling.md) - [Feature flags](practices/feature-flags.md) - [Internationalization](practices/internationalization.md) +- [Internal package migrations](contributing/internal-package-migrations.md) - [Performance testing](contributing/performance-testing.md) - [Stripe testing](contributing/testing-stripe.md) - [Testing development URLs and devices](contributing/testing-development-urls.md) diff --git a/docs/contributing/internal-package-migrations.md b/docs/contributing/internal-package-migrations.md new file mode 100644 index 00000000000..33a893ddcdd --- /dev/null +++ b/docs/contributing/internal-package-migrations.md @@ -0,0 +1,123 @@ +# Move an internal package into Ghost + +Packages in TryGhost repositories such as SDK and framework can be moved into +Ghost when Ghost is their real owner and they no longer need independent +releases. Use the `migrate-internal-package` skill to preserve the package's Git +history, integrate it into the Ghost workspace and coordinate the cleanup work +across repositories. + +Start the skill from the Ghost repository with the source package URL: + +```text +Use $migrate-internal-package to move +https://github.com/TryGhost//tree/main/packages/ +into Ghost as an internal-only package. +``` + +The skill first checks the package's consumers, npm status, dependencies and +release configuration. It stops if the package still needs to be independently +versioned or supported for external use, because that requires a different +publishing model. + +## What the skill produces + +The migration is split into focused pull requests so the history import remains +reviewable: + +1. A Ghost import PR containing the original package history and the minimum + workspace integration. +2. After the import is merged and verified, a source-repository PR removing the + old package and its publishing configuration. +3. When applicable, follow-up PRs for migration-only configuration cleanup and + package modernization. + +The import PR keeps behavior changes and modernization out of the move. It +switches Ghost consumers to `workspace:*`, marks the package private, maps its +dependencies into the Ghost workspace and verifies the real production package +path and release archive. + +## Import and merge the package + +### 1. Review the prepared import PR + +The skill extracts the package-only history and imports it with an unsquashed +Git subtree merge. The resulting Ghost PR must: + +- have a title beginning `[Don't merge]`; +- warn against using GitHub's normal merge controls; +- record the full source split SHA and subtree commit SHA; +- include ready-to-run `--dry-run` and `--confirm` commands with no placeholders; +- have green CI before reaching the merge checkpoint. + +`[Don't merge]` means that the PR must not use Ghost's normal squash or rebase +merge. Keep the prefix in place until the guarded command performs the +history-preserving merge. + +### 2. Run the guarded merge + +The PR contains commands populated with its actual PR number and source split +SHA. Run the read-only preflight from the Ghost repository root first: + +```bash +.agents/skills/migrate-internal-package/scripts/merge-history-pr \ + TryGhost/Ghost \ + \ + \ + --dry-run +``` + +Check that it reports the expected repository, PR head and source split SHA. It +also confirms that CI is green, the PR is mergeable, and the source history is +reachable from the PR branch. The dry run does not change settings or merge the +PR. + +If the output is correct, authorize the merge and run the command provided in +the PR: + +```bash +.agents/skills/migrate-internal-package/scripts/merge-history-pr \ + TryGhost/Ghost \ + \ + \ + --confirm +``` + +The script records Ghost's merge-commit setting, temporarily enables merge +commits if required, merges with the reviewed PR head pinned, restores the +original setting, and verifies that the resulting commit has two parents and +still contains the imported ancestry. + +Do not substitute a manual `gh pr merge`, GitHub squash merge, rebase merge, or +merge queue. If the script reports a protection or permission failure, resolve +that specific blocker rather than bypassing it. + +## Complete the migration + +After the guarded merge, ask the skill to continue. It fetches Ghost `main` and +confirms that the recorded source split SHA is an ancestor before removing +anything from the source repository. It then prepares the remaining work: + +1. Remove the package and its publishing configuration from the source + repository in a normal PR. +2. Decide whether the public npm versions should be deprecated for new direct + use. Existing versions remain published for old Ghost releases. +3. Remove migration-only catalog or Renovate configuration from Ghost. +4. Modernize the internal package in a focused PR when necessary. + +Source cleanup must not start before the history-preserving Ghost merge is +verified. This ordering prevents a period where neither repository owns the +package. + +## If the merge cannot proceed + +- If CI changes the PR head, rerun `--dry-run`; the script pins the merge to the + current reviewed head. +- If the source split SHA is missing or ambiguous, ask the skill to verify the + subtree topology and update the PR instructions. Do not infer it. +- If the repository merge setting cannot be restored, restore its original + value before continuing. +- If the consumer audit finds supported external use, stop the internal-only + migration and keep an appropriate independent publishing path. + +The detailed agent procedure is in the +[`migrate-internal-package` skill](../../.agents/skills/migrate-internal-package/SKILL.md). From ee596a1aef768e69318568a86dfd56ac372290d0 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 17:26:06 +0100 Subject: [PATCH 6/8] Clarified human checkpoint for history imports no ref\n\nAutomate package preparation and read-only validation while reserving the temporary repository setting change and exceptional merge for an explicitly identified human administrator. --- .../skills/migrate-internal-package/SKILL.md | 36 +++++++------ .../references/history-and-merge.md | 24 ++++++--- .../scripts/merge-history-pr | 9 +++- .../internal-package-migrations.md | 50 +++++++++---------- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index bc667621a7d..585c0f26ab0 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -22,6 +22,9 @@ Explain every cross-repository or administrative action before it happens. - If the user says "tell me how, I do it", provide the command and wait. - Require explicit authorization before changing repository settings, merging a PR, deprecating npm versions, or force-pushing. +- Never run the history import's `--confirm` operation. That command is the + deliberate human administrator checkpoint because it temporarily changes a + Ghost repository setting and merges the exceptional PR. - Never ask for, display, or store credentials or OTPs. ## Work in isolated checkouts @@ -178,8 +181,10 @@ method: - squash merge discards the imported ancestry; - rebase merge cannot preserve the subtree merge topology. -Make CI green, then stop at the merge checkpoint. If the merge-commit option is -disabled, an authorized org admin should run: +Make CI green and run the guarded script with `--dry-run` as part of the +automated preparation. Resolve preflight failures that are within the import's +scope. Once it passes, stop and ask a human Ghost repository administrator to +run this command from the Ghost repository root: ```bash .agents/skills/migrate-internal-package/scripts/merge-history-pr \ @@ -189,21 +194,22 @@ disabled, an authorized org admin should run: --confirm ``` -An agent without admin authority should provide that exact handoff rather than -attempting a workaround. The script performs preflight checks, records and -temporarily changes the setting, uses the correct merge form, restores the -setting, and verifies the resulting history. +The script repeats the preflight, records and temporarily changes the merge +setting, uses the correct merge form, restores the setting, and verifies the +resulting history. The skill must not run this command on the administrator's +behalf, even if its current GitHub session appears to have sufficient access. Every merge-checkpoint handoff must repeat the warning and render a directly -copyable command containing the actual PR number and recorded source split tip; -do not leave placeholders for the user to infer. Also provide the read-only -`--dry-run` form first. Explicitly say not to use GitHub's normal squash/rebase -buttons and not to remove `[Don't merge]` manually. The skill must never merge -the PR unless the user separately authorizes that administrative action after -seeing these instructions. - -Afterward, independently fetch `main` and confirm the source split tip is an -ancestor before starting source-repository cleanup. +copyable `--confirm` command containing the actual PR number and recorded source +split tip; do not leave placeholders for the administrator to infer. Include +the successful dry-run evidence and explain that the command temporarily +enables merge commits, merges the pinned PR head, restores the original setting +and verifies the ancestry. Explicitly say not to use GitHub's normal +squash/rebase buttons and not to remove `[Don't merge]` manually. + +Wait for the administrator to report that the command completed. Afterward, +independently fetch `main` and confirm the source split tip is an ancestor, then +continue the automated source-repository cleanup and modernization workflow. ## 3. Remove the package from the source repository diff --git a/.agents/skills/migrate-internal-package/references/history-and-merge.md b/.agents/skills/migrate-internal-package/references/history-and-merge.md index c6b6fc7d38b..91b878e0296 100644 --- a/.agents/skills/migrate-internal-package/references/history-and-merge.md +++ b/.agents/skills/migrate-internal-package/references/history-and-merge.md @@ -168,7 +168,17 @@ The merge must retain both the subtree topology and the import branch as the second parent of GitHub's merge commit. Do not manually reproduce the repository-setting sequence from prose. -An authorized admin should run: +The agent runs the read-only preflight while preparing the import: + +```bash +.agents/skills/migrate-internal-package/scripts/merge-history-pr \ + TryGhost/Ghost \ + \ + \ + --dry-run +``` + +Once it passes, a human Ghost repository administrator—not the agent—runs: ```bash .agents/skills/migrate-internal-package/scripts/merge-history-pr \ @@ -178,13 +188,13 @@ An authorized admin should run: --confirm ``` -The agent's handoff must substitute the real PR number and source split tip and -show `--dry-run` first, followed by `--confirm`; placeholders are not an -acceptable final handoff. The user should run the command from the Ghost -repository root. Do not use GitHub's normal squash/rebase buttons or manually -remove the `[Don't merge]` prefix before running it. +The handoff must substitute the real PR number and source split tip; placeholders +are not acceptable. Include the successful dry-run result so the administrator +can verify the expected PR head and ancestry before running the command. Do not +use GitHub's normal squash/rebase buttons or manually remove the `[Don't merge]` +prefix. -Use `--dry-run` instead of `--confirm` for read-only preflight. The script: +The script: 1. verifies authentication, PR state, checks and imported-history reachability; 2. records the current `allow_merge_commit` setting; diff --git a/.agents/skills/migrate-internal-package/scripts/merge-history-pr b/.agents/skills/migrate-internal-package/scripts/merge-history-pr index 077394c97d4..e7b6ab71bd7 100755 --- a/.agents/skills/migrate-internal-package/scripts/merge-history-pr +++ b/.agents/skills/migrate-internal-package/scripts/merge-history-pr @@ -12,8 +12,9 @@ Safely merge a Ghost history-import PR with a merge commit. The script records the repository's merge-commit setting, enables it only when needed, restores it on every exit path, and verifies that SOURCE_SPLIT_TIP remains reachable. ---dry-run Run read-only preflight checks without changing or merging anything. ---confirm Confirm explicit authorization to change settings and merge the PR. +--dry-run Agent preflight; no settings are changed and no merge is performed. +--confirm Human administrator checkpoint; temporarily enable merge commits, + merge the PR, restore the setting, and verify the imported history. EOF } @@ -81,6 +82,10 @@ if [[ $mode == --dry-run ]]; then exit 0 fi +is_admin=$(gh api "repos/$repo" --jq '.permissions.admin') +[[ $is_admin == true ]] || + fail "--confirm requires a human operator with repository administration permission" + setting_changed=false restore_setting() { diff --git a/docs/contributing/internal-package-migrations.md b/docs/contributing/internal-package-migrations.md index 33a893ddcdd..84068f50b1e 100644 --- a/docs/contributing/internal-package-migrations.md +++ b/docs/contributing/internal-package-migrations.md @@ -36,9 +36,9 @@ switches Ghost consumers to `workspace:*`, marks the package private, maps its dependencies into the Ghost workspace and verifies the real production package path and release archive. -## Import and merge the package +## Run the migration -### 1. Review the prepared import PR +### 1. Let the skill prepare the import The skill extracts the package-only history and imports it with an unsquashed Git subtree merge. The resulting Ghost PR must: @@ -46,33 +46,23 @@ Git subtree merge. The resulting Ghost PR must: - have a title beginning `[Don't merge]`; - warn against using GitHub's normal merge controls; - record the full source split SHA and subtree commit SHA; -- include ready-to-run `--dry-run` and `--confirm` commands with no placeholders; +- include a ready-to-run `--confirm` command with no placeholders; - have green CI before reaching the merge checkpoint. `[Don't merge]` means that the PR must not use Ghost's normal squash or rebase merge. Keep the prefix in place until the guarded command performs the history-preserving merge. -### 2. Run the guarded merge +The skill also runs the guarded merge script in read-only mode. This verifies +the PR state, CI, reviewed head, repository setting and imported ancestry +without changing GitHub. It resolves preparation failures where possible and +includes the successful preflight evidence in its handoff. -The PR contains commands populated with its actual PR number and source split -SHA. Run the read-only preflight from the Ghost repository root first: +### 2. Ask a repository administrator to merge it -```bash -.agents/skills/migrate-internal-package/scripts/merge-history-pr \ - TryGhost/Ghost \ - \ - \ - --dry-run -``` - -Check that it reports the expected repository, PR head and source split SHA. It -also confirms that CI is green, the PR is mergeable, and the source history is -reachable from the PR branch. The dry run does not change settings or merge the -PR. - -If the output is correct, authorize the merge and run the command provided in -the PR: +The skill stops at the only manual checkpoint and asks a Ghost repository +administrator to run the command provided in the PR from the Ghost repository +root: ```bash .agents/skills/migrate-internal-package/scripts/merge-history-pr \ @@ -87,15 +77,20 @@ commits if required, merges with the reviewed PR head pinned, restores the original setting, and verifies that the resulting commit has two parents and still contains the imported ancestry. +This operation requires repository administration permission because Ghost +normally has merge commits disabled. It is intentionally performed by a human +administrator rather than by the skill. + Do not substitute a manual `gh pr merge`, GitHub squash merge, rebase merge, or merge queue. If the script reports a protection or permission failure, resolve that specific blocker rather than bypassing it. -## Complete the migration +### 3. Let the skill complete the migration -After the guarded merge, ask the skill to continue. It fetches Ghost `main` and -confirms that the recorded source split SHA is an ancestor before removing -anything from the source repository. It then prepares the remaining work: +After the administrator reports that the command completed, ask the skill to +continue. It fetches Ghost `main` and confirms that the recorded source split +SHA is an ancestor before removing anything from the source repository. It then +automates or prepares the remaining work: 1. Remove the package and its publishing configuration from the source repository in a normal PR. @@ -110,8 +105,9 @@ package. ## If the merge cannot proceed -- If CI changes the PR head, rerun `--dry-run`; the script pins the merge to the - current reviewed head. +- If CI or another commit changes the PR head, ask the skill to rerun its + preflight and produce a new handoff; the script pins the merge to the current + reviewed head. - If the source split SHA is missing or ambiguous, ask the skill to verify the subtree topology and update the PR instructions. Do not infer it. - If the repository merge setting cannot be restored, restore its original From 6729712a7bd0a8bccdae9b34f91b892459b06c69 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 4 Sep 2026 17:27:06 +0100 Subject: [PATCH 7/8] Clarified contributor access to migration workflow no ref\n\nKeep the package migration automation available to every contributor while isolating repository administration permission to the short history-preserving merge handoff. --- .../skills/migrate-internal-package/SKILL.md | 8 +++++++- .../contributing/internal-package-migrations.md | 17 +++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index 585c0f26ab0..0e339175b45 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -9,6 +9,12 @@ Move a package from another TryGhost repository into Ghost without losing its history or creating a period where neither repository owns it. Keep migration mechanics separate from Ghost's lifetime package standards. +Any contributor can run this skill. Do not require repository administration +permission for the audit, import, PR preparation, CI, read-only preflight, +post-merge verification or follow-up work. Administration is required only at +the exceptional merge checkpoint, which is handed to one of the small set of +Ghost repository administrators. + The contributor workflow is documented in [`docs/contributing/internal-package-migrations.md`](../../../docs/contributing/internal-package-migrations.md). Keep that guide aligned when this skill changes its outputs or human @@ -194,7 +200,7 @@ run this command from the Ghost repository root: --confirm ``` -The script repeats the preflight, records and temporarily changes the merge +The human administrator's script repeats the preflight, records and temporarily changes the merge setting, uses the correct merge form, restores the setting, and verifies the resulting history. The skill must not run this command on the administrator's behalf, even if its current GitHub session appears to have sufficient access. diff --git a/docs/contributing/internal-package-migrations.md b/docs/contributing/internal-package-migrations.md index 84068f50b1e..d437394961c 100644 --- a/docs/contributing/internal-package-migrations.md +++ b/docs/contributing/internal-package-migrations.md @@ -6,6 +6,10 @@ releases. Use the `migrate-internal-package` skill to preserve the package's Git history, integrate it into the Ghost workspace and coordinate the cleanup work across repositories. +Any contributor can run the skill. Repository administration permission is not +needed for preparing the import or completing the follow-up work; it is needed +only for the short merge checkpoint described below. + Start the skill from the Ghost repository with the source package URL: ```text @@ -60,8 +64,9 @@ includes the successful preflight evidence in its handoff. ### 2. Ask a repository administrator to merge it -The skill stops at the only manual checkpoint and asks a Ghost repository -administrator to run the command provided in the PR from the Ghost repository +The skill stops at the only manual checkpoint and gives the contributor a +complete handoff for one of the Ghost repository administrators. The +administrator runs the command provided in the PR from the Ghost repository root: ```bash @@ -87,10 +92,10 @@ that specific blocker rather than bypassing it. ### 3. Let the skill complete the migration -After the administrator reports that the command completed, ask the skill to -continue. It fetches Ghost `main` and confirms that the recorded source split -SHA is an ancestor before removing anything from the source repository. It then -automates or prepares the remaining work: +After the administrator reports that the command completed, the contributor +asks the skill to continue. It fetches Ghost `main` and confirms that the +recorded source split SHA is an ancestor before removing anything from the +source repository. It then automates or prepares the remaining work: 1. Remove the package and its publishing configuration from the source repository in a normal PR. From e4201d50655a9d5e370aa1ef2b6d3ce7eb245677 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 7 Sep 2026 14:44:32 +0100 Subject: [PATCH 8/8] Fixed package migration review safeguards no ref The administrator merge checkpoint must operate on the exact PR head the contributor reviewed, and imported legacy files need the repository formatter applied before its formatting gates are checked. --- .../skills/migrate-internal-package/SKILL.md | 6 +++-- .../references/history-and-merge.md | 16 +++++++----- .../references/legacy-integration.md | 1 + .../scripts/merge-history-pr | 26 ++++++++++++++----- .../internal-package-migrations.md | 12 ++++++--- 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index 0e339175b45..b80cb756407 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -197,6 +197,7 @@ run this command from the Ghost repository root: TryGhost/Ghost \ \ \ + \ --confirm ``` @@ -207,8 +208,9 @@ behalf, even if its current GitHub session appears to have sufficient access. Every merge-checkpoint handoff must repeat the warning and render a directly copyable `--confirm` command containing the actual PR number and recorded source -split tip; do not leave placeholders for the administrator to infer. Include -the successful dry-run evidence and explain that the command temporarily +split tip plus the exact head SHA reported by the dry run; do not leave +placeholders for the administrator to infer. Include the successful dry-run +evidence and explain that the command rejects a changed head, temporarily enables merge commits, merges the pinned PR head, restores the original setting and verifies the ancestry. Explicitly say not to use GitHub's normal squash/rebase buttons and not to remove `[Don't merge]` manually. diff --git a/.agents/skills/migrate-internal-package/references/history-and-merge.md b/.agents/skills/migrate-internal-package/references/history-and-merge.md index 91b878e0296..56e8d4c15d4 100644 --- a/.agents/skills/migrate-internal-package/references/history-and-merge.md +++ b/.agents/skills/migrate-internal-package/references/history-and-merge.md @@ -185,21 +185,25 @@ Once it passes, a human Ghost repository administrator—not the agent—runs: TryGhost/Ghost \ \ \ + \ --confirm ``` -The handoff must substitute the real PR number and source split tip; placeholders -are not acceptable. Include the successful dry-run result so the administrator -can verify the expected PR head and ancestry before running the command. Do not -use GitHub's normal squash/rebase buttons or manually remove the `[Don't merge]` -prefix. +The handoff must substitute the real PR number, source split tip and head SHA +reported by the successful dry run; placeholders are not acceptable. The +script rejects `--confirm` if the PR head has changed, so any intervening push +returns control to the agent for a fresh review and preflight. Include the +dry-run result so the administrator can verify the expected PR head and ancestry +before running the command. Do not use GitHub's normal squash/rebase buttons or +manually remove the `[Don't merge]` prefix. The script: 1. verifies authentication, PR state, checks and imported-history reachability; 2. records the current `allow_merge_commit` setting; 3. enables merge commits only when necessary; -4. merges with `--merge --match-head-commit`; +4. rejects any head other than the SHA validated by the dry run and merges with + `--merge --match-head-commit`; 5. restores the setting through an exit trap; 6. verifies the merged commit has two parents; 7. verifies the source split tip remains an ancestor of the merged result. diff --git a/.agents/skills/migrate-internal-package/references/legacy-integration.md b/.agents/skills/migrate-internal-package/references/legacy-integration.md index 9e129ab3d73..78ee109adeb 100644 --- a/.agents/skills/migrate-internal-package/references/legacy-integration.md +++ b/.agents/skills/migrate-internal-package/references/legacy-integration.md @@ -107,6 +107,7 @@ afterward and commit purely mechanical output separately from behavioral integration changes. Verify the focused path and the repository gate: ```bash +pnpm exec oxfmt packages/ pnpm exec oxfmt --check packages/ pnpm format:check ``` diff --git a/.agents/skills/migrate-internal-package/scripts/merge-history-pr b/.agents/skills/migrate-internal-package/scripts/merge-history-pr index e7b6ab71bd7..6a2f72dc31c 100755 --- a/.agents/skills/migrate-internal-package/scripts/merge-history-pr +++ b/.agents/skills/migrate-internal-package/scripts/merge-history-pr @@ -6,15 +6,16 @@ usage() { cat <<'EOF' Usage: merge-history-pr OWNER/REPO PR_NUMBER SOURCE_SPLIT_TIP --dry-run - merge-history-pr OWNER/REPO PR_NUMBER SOURCE_SPLIT_TIP --confirm + merge-history-pr OWNER/REPO PR_NUMBER SOURCE_SPLIT_TIP EXPECTED_HEAD --confirm Safely merge a Ghost history-import PR with a merge commit. The script records the repository's merge-commit setting, enables it only when needed, restores it on every exit path, and verifies that SOURCE_SPLIT_TIP remains reachable. --dry-run Agent preflight; no settings are changed and no merge is performed. ---confirm Human administrator checkpoint; temporarily enable merge commits, - merge the PR, restore the setting, and verify the imported history. +--confirm Human administrator checkpoint; require the head validated by the + dry run, temporarily enable merge commits, merge the PR, restore the + setting, and verify the imported history. EOF } @@ -23,7 +24,7 @@ fail() { exit 1 } -[[ $# -eq 4 ]] || { +[[ $# -eq 4 || $# -eq 5 ]] || { usage >&2 exit 2 } @@ -31,7 +32,13 @@ fail() { repo=$1 pr_number=$2 split_tip=$3 -mode=$4 +if [[ $# -eq 4 ]]; then + expected_head='' + mode=$4 +else + expected_head=$4 + mode=$5 +fi [[ $repo =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || fail "repository must be OWNER/REPO" @@ -40,7 +47,10 @@ mode=$4 [[ $split_tip =~ ^[0-9a-fA-F]{40}$ ]] || fail "source split tip must be a full 40-character Git SHA" [[ $mode == --dry-run || $mode == --confirm ]] || - fail "fourth argument must be --dry-run or --confirm" + fail "final argument must be --dry-run or --confirm" +[[ $mode == --dry-run && -z $expected_head ]] || + [[ $mode == --confirm && $expected_head =~ ^[0-9a-fA-F]{40}$ ]] || + fail "--confirm requires the full PR head SHA reported by --dry-run" command -v gh >/dev/null || fail "gh is required" @@ -53,6 +63,10 @@ mergeable=$(gh pr view "$pr_number" --repo "$repo" --json mergeable --jq '.merge merge_state=$(gh pr view "$pr_number" --repo "$repo" --json mergeStateStatus --jq '.mergeStateStatus') head_sha=$(gh pr view "$pr_number" --repo "$repo" --json headRefOid --jq '.headRefOid') +if [[ $mode == --confirm && $head_sha != "$expected_head" ]]; then + fail "PR head changed after dry-run (expected: $expected_head, current: $head_sha)" +fi + [[ $state == OPEN ]] || fail "PR is not open (state: $state)" [[ $is_draft == false ]] || fail "PR is still a draft" [[ $mergeable == MERGEABLE ]] || diff --git a/docs/contributing/internal-package-migrations.md b/docs/contributing/internal-package-migrations.md index d437394961c..37959b06470 100644 --- a/docs/contributing/internal-package-migrations.md +++ b/docs/contributing/internal-package-migrations.md @@ -74,13 +74,17 @@ root: TryGhost/Ghost \ \ \ + \ --confirm ``` -The script records Ghost's merge-commit setting, temporarily enables merge -commits if required, merges with the reviewed PR head pinned, restores the -original setting, and verifies that the resulting commit has two parents and -still contains the imported ancestry. +The script first checks that the PR still has the exact head SHA validated by +the skill's dry run. It then records Ghost's merge-commit setting, temporarily +enables merge commits if required, merges with that reviewed head pinned, +restores the original setting, and verifies that the resulting commit has two +parents and still contains the imported ancestry. If the head changed, return +to the skill for another review and preflight rather than updating the SHA +manually. This operation requires repository administration permission because Ghost normally has merge commits disabled. It is intentionally performed by a human