diff --git a/.agents/skills/migrate-internal-package/SKILL.md b/.agents/skills/migrate-internal-package/SKILL.md index ca161ef3953..b80cb756407 100644 --- a/.agents/skills/migrate-internal-package/SKILL.md +++ b/.agents/skills/migrate-internal-package/SKILL.md @@ -9,6 +9,17 @@ 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 +checkpoints. + ## Authority boundaries Explain every cross-repository or administrative action before it happens. @@ -17,6 +28,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 @@ -41,6 +55,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 bootstrap`, not `pnpm setup`. +`pnpm setup` is a pnpm CLI command that configures pnpm's global home and may +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 Before changing either repository, record the source repository, its default @@ -120,7 +141,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 +159,17 @@ 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. + +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: @@ -151,24 +187,37 @@ 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 \ TryGhost/Ghost \ \ \ + \ --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. - -Afterward, independently fetch `main` and confirm the source split tip is an -ancestor before starting source-repository cleanup. +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. + +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 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. + +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 0e836b3e362..56e8d4c15d4 100644 --- a/.agents/skills/migrate-internal-package/references/history-and-merge.md +++ b/.agents/skills/migrate-internal-package/references/history-and-merge.md @@ -156,28 +156,54 @@ 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 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 \ TryGhost/Ghost \ \ \ + \ --confirm ``` -Use `--dry-run` instead of `--confirm` for read-only preflight. The script: +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 bc29be43e92..78ee109adeb 100644 --- a/.agents/skills/migrate-internal-package/references/legacy-integration.md +++ b/.agents/skills/migrate-internal-package/references/legacy-integration.md @@ -85,7 +85,35 @@ 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 packages/ +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 +122,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 ``` diff --git a/.agents/skills/migrate-internal-package/scripts/merge-history-pr b/.agents/skills/migrate-internal-package/scripts/merge-history-pr index 077394c97d4..6a2f72dc31c 100755 --- a/.agents/skills/migrate-internal-package/scripts/merge-history-pr +++ b/.agents/skills/migrate-internal-package/scripts/merge-history-pr @@ -6,14 +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 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; require the head validated by the + dry run, temporarily enable merge commits, merge the PR, restore the + setting, and verify the imported history. EOF } @@ -22,7 +24,7 @@ fail() { exit 1 } -[[ $# -eq 4 ]] || { +[[ $# -eq 4 || $# -eq 5 ]] || { usage >&2 exit 2 } @@ -30,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" @@ -39,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" @@ -52,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 ]] || @@ -81,6 +96,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/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..37959b06470 --- /dev/null +++ b/docs/contributing/internal-package-migrations.md @@ -0,0 +1,128 @@ +# 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. + +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 +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. + +## Run the migration + +### 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: + +- 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 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. + +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. + +### 2. Ask a repository administrator to merge it + +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 +.agents/skills/migrate-internal-package/scripts/merge-history-pr \ + TryGhost/Ghost \ + \ + \ + \ + --confirm +``` + +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 +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. + +### 3. Let the skill complete the migration + +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. +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 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 + 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).