Workflows

Git and Commits

Branch names, Conventional Commits, PR titles, squash-merging, and the pre-push hook — the moving parts that keep our history readable and our Linear board in sync.

A change in this repo flows the same way every time: branch → commits → PR → review → squash-merge. The branch name, the commits on it, and the PR title all share one shape — <type>(<scope>): <description> — so the final merge commit on master reads cleanly without anyone having to rewrite history.

We follow this discipline because the merge commit isn't just for you. It feeds the Linear issue, the squash on master, the GitHub PR list, and the git log --oneline history that someone will grep through six months from now. If everyone names branches and commits the same way, releases summarise themselves and bisecting a regression is fast.

This chapter walks through the workflow, the naming format, the scopes you can pick from, and the pre-push hook that catches missing translations before they hit CI.

The shape — one example end to end

A typical change looks like this:

# Branch off master
git checkout -b feat/admincp-controllers-message-show

# Commit as you go (Conventional Commits)
git commit -m "feat(admincp-controllers): add show page for MessageController"
git commit -m "test(admincp-controllers): cover MessageController@show"

# Push (pre-push hook runs translations:check --changed)
git push -u origin feat/admincp-controllers-message-show

# Open a PR with title matching the same format,
# and body referencing the Linear issue (e.g. "Closes WIN-123")

The branch name, each commit, and the PR title all use <type>(<scope>): <description>. When the PR is squash-merged, the title becomes the single Conventional Commit on master and the linked Linear issue auto-closes.

The five-step workflow

Every change goes through these steps:

  1. Create a branch for the feature or issue.
  2. Open a pull request. The body MUST contain the associated Linear identifier (e.g. Closes WIN-123).
  3. Request a review when the PR is ready — Linear moves to "In Review" automatically.
  4. Iterate on review feedback if needed.
  5. Once approved, squash-merge. The squash collapses the branch into a single Conventional Commit on master, and the linked Linear issue auto-closes.
Force-pushing to your feature branch during review is fine — the final squash erases the messy history anyway. Never force-push or rebase master.

Branch naming

Format: <type>/<scope>-<short-description>. Lowercase, kebab-cased. Think of the branch name as a preview of the merge commit.

✅ Good:

feat/admincp-web-routes-login-pages
fix/businesscp-resources-user-data
refactor/domain-services-provider-contract
fix/domain-actions-correct-order-total

❌ Bad:

my-fix
Haytham/login
WIN-123
update_stuff

Branches like my-fix or update_stuff give nobody useful information — neither you nor the reviewer can tell what's on it. Use the type and scope so the branch list itself is searchable.

Conventional Commits

Format: <type>(<scope>): <description>. Lowercase imperative mood in the description ("add", "fix", "remove" — not "added" / "adds" / "fixes").

Types

  • feat — new feature
  • fix — bug fix
  • refactor — code change that is neither a feature nor a bug fix
  • perf — refactor that improves performance
  • test — adding or updating tests
  • style — code style only (formatting; not CSS)
  • ci — continuous integration pipeline changes
  • build — build pipeline changes
  • docs — README or documentation-only changes
  • chore — anything else (deps, maintenance)

Examples

feat(domain-actions): add CreateOrder action with transaction support
fix(admincp-controllers): correct validation in UserController
refactor(domain-services): migrate ProviderService to use GetProvider action
perf(domain-entities): add eager loading for order relationships
docs(readme): update installation instructions

Scopes — domain or presentation panel

The scope identifies what part of the codebase changed. Pick the most specific scope that fits — usually a domain layer concept or a control panel.

Domain layer

  • domain-actions — Actions
  • domain-contracts — Contracts
  • domain-services — Services
  • domain-dtos — DataTransferObjects
  • domain-enums — Enums
  • domain-exceptions — Exceptions
  • domain-entities — Models / Entities
  • domain-scopes — Query Scopes
  • domain-abilities — Abilities
  • domain-model-traits — Model Traits
  • domain-providers — Service Providers
  • domain-observers — Observers
  • domain-traits — Domain Traits

Presentation layer

Replace name with the control panel (admincp, businesscp, customercp, ownercp, providercp):

  • {name}cp-controllers
  • {name}cp-requests
  • {name}cp-providers
  • {name}cp-resources
  • {name}cp-api-routes
  • {name}cp-web-routes

Common components

  • migrations — database migrations
  • middleware — HTTP middleware
  • routes — route definitions
  • config — configuration files
  • events — event classes and listeners
  • jobs — queued jobs
  • notifications — notifications
  • assets — CSS / JS / images
  • tests — test files
  • exceptions — exception handlers
  • helpers — helper functions
  • lang — translations

PR titles

Same format as commits: <type>(<scope>): <description>. Keep titles concise; put detail in the body.

✅ Good:

feat(security-contracts): implement login functionality
fix(accounting-actions): correct user data retrieval

❌ Bad:

Login work
WIP - fixes
Updated some files

The PR body MUST reference the Linear issue — typically as Closes WIN-123 or Refs WIN-123. Without it, Linear has no idea this PR exists and the issue won't auto-close on merge.

PR body skeleton

Copy this every time:

## Summary
- Short bullet list of what changed and why.

## Test plan
- [ ] Manual steps the reviewer can follow.
- [ ] Any automated tests added.

Closes WIN-123

Writing commit messages

The subject (<type>(<scope>): <description>) must stand alone — it is what appears in git log --oneline, the squash commit on master, and the GitHub PR list. Aim for under 72 characters.

If the change needs context, add a blank line and then a body explaining the why. Do not restate what the diff already shows.

❌ Bad — restates the diff:

refactor(domain-services): rename foo to bar

Renamed foo to bar in OrderService.

✅ Good — explains the why:

refactor(domain-services): rename foo to bar

`foo` collided with the new `Foo` enum introduced in WIN-119. Renaming
unblocks the enum migration without changing public API behavior.

The first version tells the reader nothing they couldn't get from git show. The second answers the question every future reader asks: "why was this changed?"

Examples by scope

feat(domain-actions): add CreateOrder action with transaction support
feat(admincp-controllers): add show page for AddDataRequest
fix(domain-entities): correct soft-delete cascade on Quotation
fix(migrations): drop duplicate index on orders.user_id
refactor(domain-services): migrate OrderService to delegate via Actions
perf(domain-scopes): use whereByType macro instead of repeated where()
test(domain-actions): cover CreateOrder transaction rollback paths
ci(build): bump PHP image to 8.3 in deploy workflow
docs(localization): document translations:check pre-push hook
chore(deps): bump laravel/framework to 12.4

Squash merging

PRs are squashed before merging to master:

  • Multiple iteration commits on the branch collapse into a single Conventional Commit.
  • That commit's subject is the PR title — ensure the title is well-formed before clicking merge.
  • The linked Linear issue auto-closes once the squash lands.
  • Force-pushing to the feature branch during review is fine; the squash erases it anyway.
Never force-push or rebase master. Iteration noise belongs on the feature branch and gets erased by the squash. master history is permanent.

What not to commit

Never stage these:

  • .env, .env.* — environment files.
  • *-firebase-credentials.json and any other secret JSON.
  • Cloud provider keys, API tokens, private SSH keys.
  • Local debug output, storage/logs/*.log, generated PDFs, etc.

When staging, prefer naming specific files over git add -A or git add . — those can sweep in untracked secrets or large binaries by accident.

✅ Good:

git add src/Domain/Order/Actions/CreateOrder.php tests/Feature/CreateOrderTest.php

❌ Bad:

git add -A

The blanket form will happily stage the .env file you forgot to gitignore on a new machine.

The pre-push hook

The repo ships a pre-push hook (install via ./install-git-hooks.sh) that runs:

php artisan translations:check --changed

It validates translation integrity across all supported languages for the files in the push. If keys are missing in lang/en/ or lang/ar/, the push fails — add the missing translations and try again.

If the hook reports a missing key:

Missing translation: lang/ar/pages.php → your_resource

Add the key to the named file (and any other locale files reported), re-stage, commit, then push again. The hook only checks files in the current push, so isolated additions resolve quickly.

Do not bypass the hook with --no-verify. The missing keys will fail in CI anyway, and the next person to deploy gets stuck with a half-translated UI in production.