Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions .agents/skills/migrate-internal-package/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Confirm this workflow applies

Before changing either repository, record the source repository, its default
Expand Down Expand Up @@ -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)
Expand All @@ -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:

Expand All @@ -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 \
<pr-number> \
<source-split-tip> \
<dry-run-head-sha> \
--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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,28 +156,54 @@ gh pr view <pr-number> --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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## 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 \
<pr-number> \
<source-split-tip> \
--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 \
<pr-number> \
<source-split-tip> \
<dry-run-head-sha> \
--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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/<package>
pnpm exec oxfmt --check packages/<package>
pnpm format:check
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

If formatting changes files, rerun package lint and tests afterward. Do not mix
opportunistic cleanup or modernization into the format-only commit.

## Verification

Expand All @@ -94,6 +122,7 @@ Run the package through its Nx surface and exercise a real consumer:
```bash
pnpm nx run @tryghost/<package>:lint
pnpm nx run @tryghost/<package>:test
pnpm format:check
pnpm build
```

Expand Down
31 changes: 25 additions & 6 deletions .agents/skills/migrate-internal-package/scripts/merge-history-pr
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -22,15 +24,21 @@ fail() {
exit 1
}

[[ $# -eq 4 ]] || {
[[ $# -eq 4 || $# -eq 5 ]] || {
usage >&2
exit 2
}

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"
Expand All @@ -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"

Expand All @@ -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 ]] ||
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading