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
40 changes: 2 additions & 38 deletions .agents/skills/commit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,12 @@ Use this skill whenever the user asks you to create a git commit for the current
- `git diff`
- `git log -5 --oneline`
2. Only stage files relevant to the requested change. Do not include unrelated untracked files, generated files, or likely-local artifacts.
3. Always follow Ghost's commit conventions (see below) for commit messages
3. Read and follow `.github/CONTRIBUTING.md#commit-messages`. It is the
source of truth for Ghost's commit conventions.
4. Run `git status --short` after committing and confirm the result.

## Important
- Do not push to remote unless the user explicitly asks
- Keep commits focused and avoid bundling unrelated changes
- If there are no relevant changes, do not create an empty commit
- If hooks fail, fix the issue and create a new commit. Never bypass hooks.

## Commit message format

We have a handful of simple standards for commit messages which help us to generate readable changelogs. Please follow this wherever possible and mention the associated issue number.

- **1st line:** Max 80 character summary
- Written in past tense e.g. “Fixed the thing” not “Fixes the thing”
- Start with one of: Fixed, Changed, Updated, Improved, Added, Removed, Reverted, Moved, Released, Bumped, Cleaned
- **2nd line:** [Always blank]
- **3rd line:** `ref <issue link>`, `fixes <issue link>`, `closes <issue link>` or blank
- **4th line:** Why this change was made - the code includes the what, the commit message should describe the context of why - why this, why now, why not something else?

If your change is **user-facing** please prepend the first line of your commit with **an emoji**.

Because emoji commits are the release notes, it's important that anything that gets an emoji is a user-facing change that's significant and relevant for end-users to see.

The first line of an emoji commit message should be from the perspective of the user. For example, 🐛 Fixed a race condition in the members service is technical and tells the user nothing, but 🐛 Fixed a bug causing active members to lose access to paid content tells the user reading the release notes “oh yeah, they fixed that bug I kept hitting.”

### Main emojis we are using:

- ✨ Feature
- 🎨 Improvement / change
- 🐛 Bug Fix
- 🌐 i18n (translation) submissions
- 💡 Anything else flagged to users or whoever is writing release notes

### Example

```
✨ Added config flag for disabling page analytics

ref https://linear.app/tryghost/issue/ENG-1234/

- analytics are brand new under development, therefore they need to be behind a flag
- not using the developerExperiments flag as that is already in wide use and we aren't ready to deploy this anywhere yet
- using the term `pageAnalytics` as this was discussed as best reflecting what this does
```
25 changes: 23 additions & 2 deletions .agents/skills/migrate-internal-package/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ Ghost worktree from the freshly fetched `origin/main`.

Run history-changing commands individually or in a fail-fast shell. A failed
`git worktree add` must not be followed by `git subtree add` in whichever
checkout happens to be current. Stop if the destination branch or path already
exists and inspect that state rather than reusing it implicitly.
checkout happens to be current. Before choosing a branch or path, list existing
worktrees and matching local and remote branches. Use a migration-specific slug,
for example `codex/import-<package>-from-<source>`, rather than a generic name.

If the destination branch or path already exists, stop and inspect its
cleanliness, base, divergence, source split and attached worktree. Do not mutate,
delete or silently reuse it. Present the evidence and ask the user whether to
resume, preserve and supersede, or remove it when more than one choice is
reasonable. A previous attempt can contain valid unmerged history even when its
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.
Expand Down Expand Up @@ -61,6 +69,11 @@ an independently supported API. Record the evidence and confidence behind the
ownership decision; stop and ask if current support expectations remain
unclear.

Run registry-only `npm view` commands from a neutral temporary directory. A
repository's `devEngines` policy can reject the host Node version before npm
contacts the registry, which is unrelated to the package metadata audit. Record
that failure separately if using a neutral directory does not resolve it.

## Produce these work products in order

1. A green Ghost import PR with reachable source history.
Expand Down Expand Up @@ -122,6 +135,14 @@ 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.

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;
- `Worked around`: the safe action taken, without hiding the original gap;
- `Skill change`: the concrete instruction, preflight or script improvement;
- `Tooling change`: anything that cannot be solved within this repository;
- `Confidence`: high, medium or low, with unresolved evidence called out.

## 2. Merge the import without rewriting history

This is the exceptional PR. It must use GitHub's **Create a merge commit**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@ Read this reference before creating the Ghost import PR.

## Create package-only source history

Work from an up-to-date, clean clone of the source repository. Use its actual
default branch and a temporary branch name that cannot be confused with a
product branch.
Work from an up-to-date, clean, full clone of the source repository. Shallow and
partial clones can complete `git subtree split` while still failing later when
Ghost fetches the split, because the local source cannot serve promised objects.
Reject them before splitting:

```bash
set -euo pipefail

test "$(git rev-parse --is-shallow-repository)" = "false"
test -z "$(git config --local --get extensions.partialClone || true)"
```

If either check fails, create a fresh full clone without `--depth`,
`--filter` or sparse/partial clone options. Use the source's actual default
branch and a temporary branch name that cannot be confused with a product
branch.

`git subtree split` may inspect thousands of commits and run for several
minutes. Use `--quiet` in agent or CI-style runners so its progress stream does
Expand Down Expand Up @@ -41,14 +54,26 @@ excluding unrelated source-repository paths.

## Attach the history to Ghost

Create or enter a dedicated Ghost worktree, then verify its branch, cleanliness,
base and empty destination before attaching history:
Before creating the worktree, inspect collisions rather than discovering them
halfway through the import:

```bash
git fetch --prune origin
git worktree list --porcelain
git branch --list 'codex/import-<package>*'
git branch --remotes --list 'origin/codex/import-<package>*'
```

If a match exists, record its worktree, cleanliness, base/divergence, imported
split and remote state. Do not delete or overwrite it without an explicit user
decision. Otherwise create or enter a dedicated Ghost worktree, then verify its
branch, cleanliness, base and empty destination before attaching history:

```bash
set -euo pipefail

test "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)"
test "$(git branch --show-current)" = "codex/import-<package>"
test "$(git branch --show-current)" = "codex/import-<package>-from-<source>"
test -z "$(git status --porcelain)"
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
test ! -e "packages/<ghost-directory>"
Expand Down Expand Up @@ -81,21 +106,34 @@ source_split_tip="<recorded-source-split-tip>"
subtree_commit=$(git rev-parse HEAD)
ghost_parent=$(git rev-parse HEAD^1)
imported_parent=$(git rev-parse HEAD^2)
source_path="path/to/representative-file"
destination_path="packages/<ghost-directory>/$source_path"

test "$ghost_parent" = "$(git rev-parse origin/main)"
test "$imported_parent" = "$source_split_tip"
git merge-base --is-ancestor "$source_split_tip" HEAD

source_history=$(git log --full-history --format=%H "$source_split_tip" -- "$source_path")
destination_history=$(git log --full-history --format=%H -- "$destination_path")
test -n "$source_history"
test -n "$destination_history"
test "$(git rev-parse "$source_split_tip:$source_path")" = \
"$(git rev-parse "$subtree_commit:$destination_path")"

git show --no-patch --format='%H%nparents: %P%n%B' "$subtree_commit"
git log --graph --oneline --decorate --all --max-count=40
git log --oneline -- packages/<ghost-directory>/path/to/representative-file
git log --oneline "$source_split_tip" -- path/to/representative-file
git log --full-history --oneline -- "$destination_path"
git log --full-history --oneline "$source_split_tip" -- "$source_path"
```

The subtree commit must have two parents: its first parent must equal the
recorded Ghost base and its second parent must equal the recorded split tip.
Checking the prefixed destination path and unprefixed split path separately is
more reliable around merge boundaries than relying only on `--follow`.
Checking the prefixed destination path with `--full-history` and the unprefixed
split path separately is more reliable around merge boundaries than relying on
ordinary path history or `--follow`, either of which may show only the subtree
merge. Requiring non-empty histories and equal blob IDs makes a missing or
mismatched representative file stop the import before integration edits obscure
the source state.

The Admin API schema migration from TryGhost/SDK is a known-good example:

Expand Down
6 changes: 6 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
profile: quiet
review_details: true
high_level_summary: false
collapse_walkthrough: false
changed_files_summary: false
sequence_diagrams: false
estimate_code_review_effort: false
poem: false
auto_review:
ignore_usernames:
- tryghost-renovate[bot]
- dependabot[bot]
pre_merge_checks:
docstrings:
mode: "off"
63 changes: 44 additions & 19 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,59 @@ Discuss new features and substantial product or architectural changes in the

## Commit Messages

We have a handful of simple standards for commit messages which help us to generate readable changelogs. Please follow this wherever possible and mention the associated issue number.
We have a handful of simple standards for commit messages which keep the main
branch readable and generate useful release notes. They matter most for pull
request titles and squash commits; follow them for intermediate commits where
practical.

- **1st line:** Max 80 character summary
- Written in past tense e.g. “Fixed the thing” not “Fixes the thing”
- Start with one of: Fixed, Changed, Updated, Improved, Added, Removed, Reverted, Moved, Released, Bumped, Cleaned
- **2nd line:** [Always blank]
- **3rd line:** `ref <issue link>`, `fixes <issue link>`, `closes <issue link>` or blank
- **4th line:** Why this change was made - the code includes the what, the commit message should describe the context of why - why this, why now, why not something else?
```text
<optional release-note emoji> <past-tense summary, at most 80 characters>

If your change is **user-facing** please prepend the first line of your commit with **an emoji key**. If the commit is for an alpha feature, no emoji is needed. We are following [gitmoji](https://gitmoji.carloscuesta.me/).
<optional issue relationship, or "no ref">

**Main emojis we are using:**
<why this change was made>
```

- ✨ Feature
- 🎨 Improvement / change
- 🐛 Bug Fix
- 🌐 i18n (translation) submissions [[See Translating Ghost docs for more detail](../docs/contributing/translating-ghost.md)]
- 💡 Anything else flagged to users or whoever is writing release notes
- Start the summary with `Fixed`, `Changed`, `Updated`, `Improved`, `Added`,
`Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, or `Cleaned`.
- Keep the second line blank.
- When an issue exists, use a supported relationship followed by its URL, such
as `ref <issue URL>`, `fixes <issue URL>`, or `closes <issue URL>`. Use
`no ref` when it is useful to state explicitly that there is no issue, or
leave this line blank.
- Explain the context in the body: why this change, why now, and why this
approach. The diff already describes what changed.

Good commit message examples: [new feature](https://github.com/TryGhost/Ghost/commit/61db6defde3b10a4022c86efac29cf15ae60983f), [bug fix](https://github.com/TryGhost/Ghost/commit/6ef835bb5879421ae9133541ebf8c4e560a4a90e) and [translation](https://github.com/TryGhost/Ghost/commit/83904c1611ae7ab3257b3b7d55f03e50cead62d7).
The local hook warns about most deviations without blocking the commit. It
does require the common invalid forms `refs ...` and `ref: ...` to be corrected
to a supported relationship such as `ref ...`.

**Bumping @tryghost dependencies**
### Release-note emojis

A leading release-note emoji opts the squash commit into generated release
notes. Add one only for a significant change that is relevant to users, and
write the summary from their perspective. Alpha or experimental work does not
need an emoji until it becomes user-facing.

When bumping `@tryghost/*` dependencies, the first line should follow the above format and say what has changed, not say what has been bumped.
- ✨ Feature
- 🎨 Improvement or change
- 🐛 Bug fix
- 💡 Other noteworthy user-facing change

Use 🌐 for [translation submissions](../docs/contributing/translating-ghost.md).
Translation commits are not selected for generated release notes by that emoji
alone.

There is no need to include what modules have changed in the commit message, as this is _very_ clear from the contents of the commit. The commit should focus on surfacing the underlying changes from the dependencies - what actually changed as a result of this dependency bump?
Good final commit examples include a [new feature](https://github.com/TryGhost/Ghost/commit/61db6defde3b10a4022c86efac29cf15ae60983f),
a [bug fix](https://github.com/TryGhost/Ghost/commit/6ef835bb5879421ae9133541ebf8c4e560a4a90e),
and a [translation](https://github.com/TryGhost/Ghost/commit/83904c1611ae7ab3257b3b7d55f03e50cead62d7).

**Bumping @tryghost dependencies**

[Good example](https://github.com/TryGhost/Ghost/commit/95751a0e5fb719bb5bca74cb97fb5f29b225094f)
When bumping `@tryghost/*` dependencies, describe the user-visible result rather
than which packages were bumped. The diff already shows the package changes;
the message should explain what changed because of them. See this
[good example](https://github.com/TryGhost/Ghost/commit/95751a0e5fb719bb5bca74cb97fb5f29b225094f).

## Changesets

Expand Down
23 changes: 12 additions & 11 deletions .github/hooks/commit-msg.bash
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,18 @@ if [ -z "$body" ]; then
echo -e "The body should explain: why this, why now, why not something else?"
fi

# Check for emoji in user-facing changes
if [[ "$subject" =~ ^[^[:space:]]*[[:space:]] ]]; then
first_word="${subject%% *}"
# Emoji are multi-byte (non-ASCII), so detect them by stripping every ASCII
# byte and seeing if anything is left. The previous check tested the first word
# against [[:punct:]], which never matches an emoji — so the warning fired on
# *every* correctly-prefixed commit (🐛, ✨, …) and passed on plain ASCII.
if [[ -z "$(printf '%s' "$first_word" | LC_ALL=C tr -d '\000-\177')" ]]; then
echo -e "${yellow}Warning: User-facing changes should start with an emoji${no_color}"
echo -e "Common emojis: ✨ (Feature), 🎨 (Improvement), 🐛 (Bug Fix), 🌐 (i18n), 💡 (User-facing)"
fi
# Give concise release-note guidance. Whether a change is user-facing requires
# human judgment, so these notices are informative rather than enforcement.
first_word="${subject%% *}"
contributor_emojis="✨ 🎨 🐛 🌐 💡"

if [[ "$first_word" =~ ^(✨|🎨|🐛|🌐|💡|🔒)$ ]]; then
echo -e "${yellow}Notice: Keep the emoji only for a significant user-facing PR title or squash commit.${no_color}"
elif [[ -n "$(printf '%s' "$first_word" | LC_ALL=C tr -d '\000-\177')" ]]; then
echo -e "${yellow}Warning: Unsupported leading emoji. Use one of: ${contributor_emojis}${no_color}"
echo -e "Emoji selection only matters for user-facing PR titles and squash commits."
else
echo -e "${yellow}Notice: If this is a significant user-facing PR title or squash commit, add one of: ${contributor_emojis}${no_color}"
fi

# Check for past tense verbs in subject
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ skill without duplicating it. Run `pnpm lint:agent-skills` to verify every
repository skill is linked correctly; CI runs the same check.

### Commit Messages
When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.
When the user asks you to create a commit or draft a commit message, load and
follow the `commit` skill from `.agents/skills/commit`. Read the canonical
[commit message guidelines](.github/CONTRIBUTING.md#commit-messages), and apply
the subject convention carefully to PR titles and proposed squash commits.
Local hook notices on intermediate commits are best-effort guidance.

### ESLint Config
Source of truth: two internal config packages — [`@internal/cfg-eslint`](configs/eslint/index.mjs) (shared rule atoms + the `nodeLibConfig` factory for Node libs) and [`@internal/cfg-eslint-react`](configs/eslint-react/index.mjs) (the `reactAppConfig` factory for every `apps/*` workspace). Both factories are synchronous and have full JSDoc with `@example`s; hover the call site in your editor. Consume them by name — declare the package as a `workspace:*` devDependency.
Expand Down
Loading
Loading