---
title: "GitHub Workflow"
slug: "github"
category: "tooling"
tags: ["tooling", "github", "git", "ci"]
status: "stable"
last_updated: 2026-05-14
summary: "Branching, PR hygiene, commit format, Actions, and protected-branch defaults for small teams shipping to main."
related: ["[[ops/github-pages]]", "[[tooling/claude-code-workflow]]", "[[tooling/quartz]]"]
---

## Overview

GitHub is the source of truth and the deploy trigger for most projects on this site. The rules below cover branching, pull requests, commits, Actions, and the protected-branch settings that keep `main` green. They assume a small team or a solo author shipping to `main` via short-lived branches.

## `main` is the deploy branch

`main` is what production runs. Treat it as immutable from local machines.

- Never push directly to `main`. Land work via pull request.
- Protect `main` (Settings - Branches - Branch protection): require a pull request, require status checks to pass, require branches to be up to date, and disallow force pushes.
- Tag releases off `main` when the project uses versioned releases. Otherwise the latest commit on `main` is the release.

## Use short-lived feature branches

One branch per task. Delete after merge.

- `feat/<name>` for human-authored features. `fix/<name>` for bug fixes.
- `claude/<name>` for branches that originate in a Claude Code session. The prefix makes provenance obvious during review.
- Keep branches under a week old. Long-lived branches drift and produce painful merges. If a branch outgrows a week, split it.
- Rebase onto `main` before opening the PR; do not merge `main` back into the feature branch.

## Open PRs as drafts until they are ready

A draft PR signals "in progress." A ready PR signals "review me."

- Open as draft on the first push. Promote to ready when CI is green and the description is complete.
- Title is one line, sentence case, with the Conventional Commits scope: `feat(tooling): add Quartz CNAME plugin`.
- Body has two sections at minimum: a summary of what changed and a test plan. The test plan is a checklist the reviewer can run.
- Link related issues with `Closes #123` so merging the PR closes the issue.

## Conventional Commits on every commit

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).

```
feat(scope): short imperative summary
fix(scope): short imperative summary
chore(scope): short imperative summary
docs(scope): short imperative summary
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`. Scope is the area of the codebase: `tooling`, `frontend`, `ops`, or a package name. Keep the summary under 72 characters; put detail in the body.

## Squash merge by default

Choose one merge strategy and stick with it.

- Default to squash merge. `main` history reads as one commit per PR, which keeps `git log` legible and makes reverts trivial.
- Use merge commits only when the feature branch's history is meaningful (a long-running effort with intentional commits worth preserving).
- Disable rebase merge to avoid two merge styles in one repo.

Set the default in Settings - General - Pull Requests, and disable the other strategies.

## One workflow per concern

Actions workflows are easier to maintain when each file owns one job.

- `deploy.yml`: build and deploy on push to `main`.
- `ci.yml`: lint and test on every pull request.
- `release.yml`: cut a release on tag push.

Keep workflow YAML under 100 lines. If a job grows past that, extract a composite action or a reusable workflow. See [[file-organization/large-codebase]] for the CODEOWNERS rules that pair with multi-workflow setups.

## Lock down `GITHUB_TOKEN` permissions

The default `GITHUB_TOKEN` has broader permissions than most jobs need. Scope down at the workflow level.

```yaml
permissions:
  contents: read
  pages: write
  id-token: write
```

Set the minimum scope per workflow. For deploy workflows that publish to GitHub Pages, `pages: write` and `id-token: write` are required. For lint and test workflows, `contents: read` is usually enough. Store secrets that exceed `GITHUB_TOKEN` scope in Settings - Secrets - Actions and reference them as `${{ secrets.NAME }}`.

## Related

- [[ops/github-pages]]
- [[tooling/claude-code-workflow]]
- [[tooling/quartz]]
