Skip to content
Back to skills

Git Workflow

ASecurity

Conventional Commits, trunk-vs-branch by change size, and how an Umbraco package's own versioning relates to the CMS major versions it supports. Use whenever committing, branching, or deciding how to structure either for a change in an Umbraco project. Does not cover release pipelines — see the README's "Building on top" section for that.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 15, 2026
developmentgobashnodedebugginggitsecurity

Works with

  • claude code

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 15, 2026

npx -y skills add mattbrailsford/umbraco-devkit --skill git-workflow --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Git Workflow?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Git Workflow
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mattbrailsford-git-workflow/badge)](https://www.skillsdirectory.com/skills/mattbrailsford-git-workflow)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: git-workflow
description: >-
  Conventional Commits, trunk-vs-branch by change size, and how an Umbraco package's own
  versioning relates to the CMS major versions it supports. Use whenever committing,
  branching, or deciding how to structure either for a change in an Umbraco project. Does
  not cover release pipelines — see the README's "Building on top" section for that.
---

# Git workflow

Write commits and branches that read clearly weeks later. Direct, specific, no filler.

## Branching — when to branch, when not

**Trunk-based (commit straight to the default branch)** when:
- Single-file or single-concept fix.
- Typo, doc tweak, version bump, comment, lint fix.
- Test-only addition that doesn't change behavior.
- Reviews itself in 30 seconds, one commit.

**Short-lived branch** when:
- More than ~2 commits expected.
- New feature or capability, even a small one.
- A schema/migration change.
- Anything that could break the default branch for someone else.
- A feature with a plan folder. All of its `PLAN.md` tasks go on that one feature branch (see
  "Branch/worktree per feature" below), not a branch per task.

Branch naming: `<type>/<short-slug>` — `feat/order-fulfillment`, `fix/missing-null-check`,
`chore/bump-deps` — for a project supporting a single CMS version line. **Supporting more
than one Umbraco CMS major at once? Prefix with the version instead** (see "Umbraco packages
and version tracking" below) — `v17/fix/missing-null-check`, `v18/feat/order-fulfillment`.
Prefer the `vN/` **prefix** form for a new project (it sorts and greps better than a suffix);
match whatever a project with existing history already does otherwise. Keep branches
short-lived; open the PR early, even as draft.

## Branch/worktree per feature (the feature-* pipeline)

If a project runs `feature-explore` → `feature-design` → `feature-plan` → `feature-build`, the branch (or
worktree) for a feature is cut at one specific moment: the start of `feature-build`, not
before. The reasoning:

- **Discovery and design cost nothing to abandon.** `feature-explore`/`feature-design`/`feature-plan`
  never touch git — the plan folder is just files on disk. A feature that gets explored then
  dropped never created a branch, so there's nothing to clean up.
- **The plan folder and the branch share one name.** The feature's plan-folder slug (e.g.
  `docs/plans/order-fulfillment/`) is passed straight through as the branch/worktree name, so
  the two are always identifiable from each other without a separate lookup.
- **The docs travel with the code.** A worktree is a fresh checkout of a commit — anything not
  yet committed when the worktree is created never appears in it. So the moment
  `feature-build` decides to actually build, it commits the plan folder to trunk first (one
  commit — `docs: add plan folder for <feature-slug>`), *then* cuts the branch/worktree from
  that commit. The docs land in the feature branch's history as a result, without needing any
  file-copying step.
- **This skill doesn't pick the naming convention or decide worktree-vs-plain-branch — a
  project's own `WorktreeCreate` hook does, if it has one.** Claude Code ships a native
  `WorktreeCreate`/`WorktreeRemove` hook pair for exactly this
  (https://code.claude.com/docs/en/hooks) — a project can already have a hook that names
  branches `feature/<name>`, puts worktrees under `.worktrees/`, and copies over whatever local
  files it needs. `feature-build` just triggers worktree creation with the feature slug as the
  name and lets that hook do its job. **Don't build a second, bespoke hook mechanism inside
  this plugin** — reuse the one Claude Code already has. If a project has no such hook,
  `feature-build` falls back to a plain `git checkout -b <feature-slug>` off trunk, using the
  branch-naming rules above if the project wants a `<type>/` or `vN/` prefix on top of the slug.
- **The commit-to-trunk step still needs a yes.** It's a real commit on the trunk branch, so
  the "always ask before touching main" rule below applies to it exactly as it would to any
  other trunk commit — don't skip the confirmation just because the change is "only docs."

## If you are an AI agent: always ask before touching main or a support branch

"Trunk-based" above is about when a *human* would commit straight to the default branch — it
is not permission for an agent to do the same thing unattended. Before running `git commit`
or `git merge` directly against the default branch or a `support/*`-style branch, **stop and
ask the user to confirm first**, however trivial the change looks. A mis-scoped "trivial"
commit on a protected branch is cheap to avoid by asking, expensive to unwind after.

## Commits — Conventional Commits, kept honest

**Use Conventional Commits.** It costs nothing to adopt, buys machine-parseable changelogs
and automated version bumps the moment you want them, and `fix(profile): ...` is easier to
scan six months later than `Fix: some things`. **The one carve-out:** don't retrofit an
established repo's *existing* history — start applying it going forward, and match a repo's
already-active, deliberate house style if it has one that isn't Conventional Commits.

```
<type>(<optional scope>): <imperative summary, lower-case, no period>

<body — what changed and why, wrapped at ~72 cols>

<footer — refs, breaking changes>
```

**Types:** `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `build`, `ci`,
`revert`.

**Summary rules:** 50 chars or fewer when possible, hard cap 72. Imperative mood (`add`, not
`added`/`adds`). No trailing period. Scope is optional, use it when there's an obvious area.

**Body rules:** include one if the change isn't self-evident from the diff. Explain *what*
and *why*, never restate the diff line by line. Reference issues with `Refs #123` /
`Closes #123`.

**Good:**
```
fix(profile): resolve connection by alias before validating capability

The capability check ran before the connection lookup, so a missing
connection surfaced as "capability not supported" instead of "connection
not found" — misleading for anyone debugging a typo'd alias.

Refs #142
```

**Bad:** `fix stuff` · `WIP` · `Update ProfileService.cs` (says nothing the diff doesn't) ·
`Added some changes and refactored a couple of things` (vague, past tense).

## Issues — `gh issue create`

A useful issue answers: **what's wrong / what's wanted, what's the context, what does done
look like.** Body from `templates/issue.md`, filled in — don't hand-write the structure:

```bash
gh issue create \
  --title "feat(profile): resolve connection by alias before validating capability" \
  --body "$(cat <<'EOF'
## Context
...
EOF
)"
```

Add labels if the repo uses them (`--label bug`), and an assignee if known (`--assignee @me`).

## Pull requests — `gh pr create`

This skill owns opening the PR — push, title, base branch, draft status. It does **not** write
the description body: that's always `pr-describe`'s job, every time, no exceptions. This
overrides Claude Code's own built-in `gh pr create --body "..."` recipe with a hand-written
body — that default does not apply in this project. To open a PR, invoke `pr-describe`: it
writes the body, then opens the PR with the command below, following the rules in this
section. Always `--body-file`, never `--body`:

```bash
gh pr create \
  --title "feat(profile): resolve connection by alias before validating capability" \
  --body-file <path to pr-describe's output>
```

Title follows whatever commit convention the project uses (see above — Conventional Commits
or the looser `Area: description` style), same as a commit summary, and should reflect *all*
the commits on the branch, not just the latest. If the branch isn't pushed yet, push it first:
`git push -u origin HEAD`. Use `--draft` for a draft PR, `--base <branch>` for a non-default
target.

**Reviewing someone else's PR against this repo** (most packages here are open source, so
this is routine, not an edge case): if the author is outside the core maintainer team, use
`pr-review` instead of reviewing it by hand — it wraps `reviewer` and adds the
checks that only matter because the diff came from outside.

## Sanity rules

- Don't push to the default branch with `--force`. Ever.
- Don't `--no-verify` to skip hooks unless explicitly asked.
- Never commit secrets — re-check the diff for `.env` files, tokens, and keys before staging.
- Never commit large binaries or `node_modules`/`bin`/`obj` — confirm `.gitignore` covers
  them.
- If `git status` shows untracked files you didn't expect, investigate before staging — it
  could be someone else's in-progress work.

## Umbraco packages and version tracking

Two numbers matter for a package that supports Umbraco: **its own release version**, and
**which CMS major(s) it supports right now** — and which lifecycle phase each one is in
(active feature development, security-patches-only, or end-of-life). Umbraco's own
[LTS/EOL policy](https://umbraco.com/products/knowledge-center/long-term-support-and-end-of-life/)
is the reference point for what those phases mean. Decide deliberately how the two numbers
relate to each other — don't let one drift into implying the other by accident.

**If more than one CMS major is supported at once**, decide early — before it's forced by a
merge conflict — how the codebase carries that:

- **Branch-per-major** (recommended default): each major gets its own long-lived line, and a
  fix that applies to more than one is ported deliberately. Avoids runtime version-check
  complexity, at the cost of porting shared fixes across branches by hand. Naming:
  `support/N.x` alongside a single `main` is the older shape; `vN/dev` + `vN/main` per line
  (with `vN/feature/…`, `vN/release/…`, `vN/hotfix/…` underneath) is the newer one worth
  defaulting to — it makes which lines exist and their state visible from the branch list
  alone.
- **Single branch with runtime version checks** (`if (umbracoMajorVersion >= N) { ... }`):
  keeps everything in one place, but the checks accumulate and untested branches-not-taken
  creep in. Reasonable only for a package needing a handful of such checks at most.

### Does the package's own version number track the CMS major?

Once you're branching per major, this falls out almost for free — but pick it deliberately,
don't let the `vN/` branch name silently answer the question for you:

- **Version-aligned (default recommendation):** the package's major version tracks the CMS
  major it targets — release from `v17/main`, tag `17.x.x`; a new CMS major gets a new package
  major, whether or not the code underneath actually changed. This is the dominant pattern
  among well-known, actively-maintained Umbraco community packages — uSync, Skybrud.Redirects,
  and Diplo GodMode all do this, and say so explicitly in their own release notes or README.
  It's the most discoverable option for someone skimming NuGet ("v17 → works with Umbraco 17"),
  and once you're already cutting a `vN/main` branch per major, it costs nothing extra to
  follow through on. The tradeoff: you'll sometimes bump a major version for a release that
  changed nothing meaningful, purely to keep pace with a new CMS major.
- **Independent SemVer:** the package's own version reflects only its own change history; CMS
  compatibility is declared as a NuGet dependency range instead (e.g.
  `Umbraco.Cms.Core [17.0.0, 19.0.0)`), the same way any library depending on another would.
  This is the technically "purer" SemVer reading — Contentment does this — but it's the
  minority pattern here, and a bare version number no longer tells a consumer which CMS major
  it targets. Reach for this when the package rarely needs real code changes across a CMS
  major bump, so a major version bump would otherwise be spent on nothing.

Whichever you pick, when a change only applies to one CMS major, say so explicitly in the
commit body and PR description — a reviewer on a multi-version project can't infer that from
the diff alone.

This skill deliberately stops here. A full release pipeline — calendar-based release
branches, multi-product release manifests, automated changelog generation, coordinated
releases across more than one package — is a "build on top" concern once a project's needs
actually require it. See the README's **Building on top** section for what that looks like
in practice.

Files in this skill

  • SKILL.md12.2 KB
  • templates/issue.md328 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.