Skip to content

Git

Commit messages

All commit messages should follow Conventional Commits guidelines.

Use the imperative mood for commit messages to describe what the commit does when applied rather than what you did to make that happen. In other words, phrase such messages as commands (e.g., "Add feature" instead of "Added feature").

Usually, the commit will relate to a specific item of work. If the work involves a Jira ticket, always include the Jira reference in square brackets as follows:

feat: [JIRA-123] Add feature.

For very large projects, consider adding a "scope" as follows:

shell
feat(api): [JIRA-123] Add API endpoint.

Fast-forward merges

Use fast-forward merges rather than merge commits whenever possible to maintain clear and concise commit history. This often involves rebasing each branch before merging it.

For new projects, ensure that GitLab merge requests are configured to use fast-forward merges under Settings > Merge Requests.

Basic workflow

All projects should have a protected main branch. For simple projects, this branch should always reflect the state of the production environment.

For small/uncomplicated items of work, it may be sufficient to commit and push changes directly to the main branch.

Otherwise, create a new branch from main for each item of work. Name the branch with a reference to the corresponding Jira ticket, followed by a brief, 'sluggified' description of the work to be done. For example:

shell
git switch -c JIRA-123/add-a-feature

When working on such "feature" branches, be sure to commit and push changes frequently throughout the day. It is perfectly acceptable, even encouraged, to create and push multiple "wip" commits (or repeatedly amend and force-push a single "wip" commit if you prefer) as the work progresses.

NEVER allow work to reside solely on your local machine overnight.

When the work is complete, create a merge request in GitLab for the branch to be incorporated into main. Ensure the branch is deleted following the merge.

In most cases, you should "squash" multiple commits into a single commit before (fast-forward) merging. Ensure that the squashed commit message follows Conventional Commits guidelines, as mentioned earlier.

Quality-assured workflow

Some projects require a process with additional guarantees and guardrails for each new release.

INFO

As this workflow is more complex and introduces overhead, it should only be used when the following criteria apply:

  • Each "release" undergoes a full testing cycle
  • All stakeholders 'buy in' to the release process
  • There is a commercial imperative, i.e.
    • The project is active with a healthy budget
    • There is a significant business or financial risk with deploying a new release

A project may start out using the 'basic' workflow and later switch to the 'quality-assured' workflow as circumstances require.

As with the basic workflow, all quality-assured projects have a default main branch. Create feature branches from main and name them according to the related Jira ticket. For example:

shell
git switch main
git pull
git switch -c JIRA-123/add-a-feature

Conventional changelog

Install marcocesarato/php-conventional-changelog as a "dev" requirement to aid with the release process. (See also morphsites/laravel-standards.)

shell
composer require --dev marcocesarato/php-conventional-changelog

Always create a .changelog file with the following contents:

php
<?php

return [
    'packageBump' => false,
    'tagPrefix' => '',
];

This ensures that (1) the release commands don't modify composer.json or package.json unnecessarily and (2) version tags are not prefixed with "v" or similar.

Add the following commands to the "scripts" section of composer.json:

json
{
    "scripts": {
        "changelog": "conventional-changelog",
        "release:major": "@php vendor/bin/conventional-changelog --major --commit-all --annotate-tag",
        "release:minor": "@php vendor/bin/conventional-changelog --minor --commit-all --annotate-tag",
        "release:patch": "@php vendor/bin/conventional-changelog --patch --commit-all --annotate-tag --merged"
    }
}

Note our use of annotated tags, which is not the default behaviour. This ensures that git describe outputs a semantic version number.

Run composer changelog to generate a new CHANGELOG.md file. For existing projects, you should remove extraneous entries from this file by hand before committing it for the first time.

Preparing a release

Create a Jira release for each new major/minor version and link "code complete" Jira tickets to the corresponding release by means of the "Fix versions" field.

For the feature branch corresponding to each Jira ticket, create a merge request targetting the main branch. Rebase and fast-forward merge each in turn.

Once the release is ready for testing, tag the new version:

shell
git switch main
git pull

composer release:minor # or release:major
git push --follow-tags

Occasionally, you may use composer release:major instead of composer release:minor to create a new major release. This is often a "political" rather than a technical decision.

Environment branches

An environment branch should exist for each deployment environment. Multiple sites in Laravel Forge may reference the same environment branch. Name environment branches with an 'environment' prefix, followed by the distinct name of the environment. For example:

shell
git switch -c environment/production
git switch -c environment/stage
git switch -c environment/test

After tagging a release, reset the appropriate environment branch:

shell
git switch environment/test
git reset --hard 1.0.0
git push --force-with-lease

Perform the deployment via either CI or a manual trigger.

Protect environment branches under Settings > Repository using the environment/* wildcard expression, but with the "Allowed to force push" option enabled.

Patching a release

During QA testing and UAT, bugs and other issues may arise.

For each Jira "bug" ticket, create a branch from main as usual. For example:

shell
git switch main
git pull
git switch -c JIRA-234/fix-a-thing

When the fix is ready to be tested, create a merge request targetting the main branch, then rebase and fast-forward merge it. Do the same in turn for each bug fix branch ready for testing.

Tag a new patch version:

shell
git switch main
git pull

composer release:patch
git push --follow-tags

This process may be repeated several times during the test cycle.

Deploying to production

Once a release is ready for production, reset the production environment branch to the latest release tag:

shell
git switch environment/production
git reset --hard 1.0.5
git push --force-with-lease

Perform the deployment via CI or a manual trigger.

Patching a release in production ("hot fixing")

In an emergency where a fix must be made in production, it is acceptable to apply the changes directly to the production environment branch:

shell
git switch environment/production
# stage changes for commit
git commit -m "fix: ..."
git push

Once the immediate situation has been resolved, create a patch as soon as possible by fast-forward merging environment/production onto main and tagging it, than advance environment/production to match main:

shell
git switch main
git merge --ff-only environment/production

composer release:patch
git push --follow-tags

git switch environment/production
git merge --ff-only main
git push

In situations where the production environment does not track main prior to the hot fix (i.e., there is a new release under test or else main is being used to construct a new release), see the following section instead.

Concurrent releases

Occasionally, it may be necessary to maintain multiple releases at the same time. For example, 1.1 may be on a staging site for UAT whilst 1.2 undergoes QA on a test site whilst 1.3 is under active development.

TIP

It is usually wise to create a 'maintenance' release after one or more 'feature' releases to ensure that dependencies are upgraded regularly. Such 'maintenance' releases only require regression testing and should never be delayed in favour of more features.

In this situation, create a branch for each release not yet in production, except for the latest release. The branch name should match the name of the Jira release, but with a 'release' prefix. For example:

shell
# after using `composer release:minor` to create the 1.1 release…
git switch main
git switch -c release/1.1
git push

# later, after using `composer release:minor` to create the 1.2 release…
git switch main
git switch -c release/1.2
git push

# work on the next release (i.e., 1.3) begins…

Protect release branches under Settings > Repository using the release/* wildcard expression. Never enable the 'Allowed to force push' option for release branches.

In this example, the main branch should either track the most recent release (e.g., 1.3) or else serve as the staging area for constructing the next release if the project is under active development.

Once a given release is in production, the corresponding release branch should be deleted. (This must be done manually in Gitlab due to branch protection rules.)

Bug fixes should be incorporated into the corresponding release by means of merge requests, but targetting that release branch instead of the main branch.

Changes to earlier releases should then be 'forward-ported' to all subsequent releases using 'traditional' merge commits. For example:

shell
git switch release/1.1
git commit -m "fix: ..."

composer release:patch
git push --follow-tags

git switch release/1.2
git merge release/1.1
# resolve merge conflicts, e.g., in CHANGELOG.md, and git commit if needed

composer release:patch
git push --follow-tags

INFO

This is the one situation where a merge commit (as opposed to a fast-forward merge) is unavoidable. You should never rewrite history on release branches.

If the main branch tracks the latest release (e.g., 1.3), follow the same process to forward-port such changes.

shell
git switch main
git merge release/1.2
# resolve merge conflicts, e.g., in CHANGELOG.md, and git commit if needed

composer release:patch
git push --follow-tags

If the main branch currently serves as the staging area for a new release still under construction, it should instead be rebased on to the previous release branch and force-pushed to Gitlab.

shell
git switch main
git rebase --onto=release/1.2 1.2.0
git push --force-with-lease

A senior developer should action all such rebases as it involves "rewriting history" to some degree. Note that you must temporarily enable the "Allow to force push" option for the main branch (under Settings > Repository > Protected branches) before force-pushing to Gitlab.

The above also applies when merging a "hot fix" from production. Be sure to merge the fix from the production environment through all release branches and ultimately the main branch.

shell
# merge hot fix to 1.1 (staged for UAT)
git switch release/1.1
git merge environment/production
# resolve merge conflicts, e.g., in CHANGELOG.md, and git commit if needed

composer release:patch
git push --follow-tags

# merge hot fix to 1.2 (under test)
git switch release/1.2
git merge release/1.1
# resolve merge conflicts, e.g., in CHANGELOG.md, and git commit if needed

composer release:patch
git push --follow-tags

# merge hot fix to 1.3 (next in line for testing)
git switch main
git merge release/1.2
# resolve merge conflicts, e.g., in CHANGELOG.md, and git commit if needed

composer release:patch
git push --follow-tags

However, if the release on main is still under contruction, it should be rebased onto the previous release branch to which the fix has already been merged, as mentioned earlier.