A Singapore Government Agency Website

Source Code Management

IM8 Policy: SC-1: Code Repository
Manage the codebase in a central code repository with version control.

Treating Everything as Code means it is documented as text that can be kept in the same place, versioned and tools for manipulating text can be used to collaborate on it as a team. This goes for the source code of our applications - but also for configuration files (for tools and environments), setup scripts and documentation.

All related files for a project can be stored in the same repository of a Source Code Management (SCM) system, like git as used by SHIP-HATS GitLab. This becomes the single source of truth for everything to do with our application. Commits to this SCM repo can then be used to detect changes and trigger relevant actions, such as CI/CD pipelines or infrastructure deployments.

Committing to Version Control

Atomic Commits

Small, frequent commits to version control help to manage the sharing of changes and track down any problems that might arise. Commits to the SCM can be made as soon any atomic change is complete, and all dependent changes should be committed at the same time.

Quality Metric: Commit Frequency
This depends on the size of the changes that go together – but if you’re committing to version control from every few minutes to multiple times a day on average, that’s in the right range.

A good time to commit is when all tests for a change are passing. This way the commit can be shared without breaking a CI/CD pipeline or confusing a teammate, and builds the project up from one known good state to the next. Keeping these steps small contains the impact of any rollback or loss of work.

Moreover, systems like git provide ways to always get back to a previous state of your work – but only if it’s committed. The bigger a changeset you build up before committing, the more you stand to lose if something goes wrong.

Similarly, pushing commits to a central server such as GitLab frequently provides some redundancy. If you are practicing Trunk-Based Development, it also shares your changes with the team as soon as possible, so that they can avoid getting out of sync and potentially building upon a foundation that has shifted out from under them.

Descriptive Commit Messages

Version Control Systems like git provide ways of looking at the changes to the files associated with a commit, so there’s no need to exhaustively spell these out in the commit message. A short summary is useful, along with the reason why these changes were made. This will help answer questions from teammates (or future you) when looking through the git log or annotations for the changed lines of code.

Conventional Commits

Tags can be added to messages to identify the type of change the commit represents: a bugfix, a new feature, a breaking change. This enables automated semantic versioning, looking at the list of commits going into a given release to establish what the new major, minor and bugfix version numbers should be.

Repository strategy

Version control systems are logically separated into repositories, which are the root level of a hierarchy of files that can be checked out together.

One repo per project

As mentioned above, the repository houses all related files for a single project. This has the advantage of encapsulation, simplicity of not having to think about files from other projects and allowing any change to the repository to trigger the application’s CI/CD pipeline. Configuration files for the pipeline, development tooling and documentation can live at the root level of the repository.

If files within other projects need to be referenced, they should be included as libraries using a dependency management tool. This means that working on changes to the library involves publishing them and updating the dependency before they can be used in the app.

Branching strategy

SCMs like git allow developers to create branches of changes that diverge from each other. This is an attractive prospect because it prevents their work from conflicting with other people’s… for now. These branches must be merged back into the main trunk of development before they can be released though, at which time the potential conflicts that have built up will become real and have to be resolved. If this has been over many different changes, it can be quite confusing to determine what the correct ending state should be. There are mitigations for this risk, like frequently keeping the branch up-to-date with the trunk, but it will still be detached with changes that are not shared, which represents a kind of knowledge silo. For this reason, maintaining branches for a long time can be a painful development overhead.

Trunk-based development

In trunk-based development, developers deliver code to the trunk of development frequently and avoid maintaining long-running branches. This helps catch integration issues early, and ensures the team is always building on top of the latest code.

To make use of a merge-request code review process, developers create short-lived branches from the trunk and merge them back in when they have completed a chunk of work, sending the changes to other team members for review before they are added to the trunk. These branches will often map to the work done to implement a User Story, so these have to be kept small too.

Tagging Merge Requests

Merge Requests can also be tagged with the ID of a User Story or work-tracking backlog item that the change is a part of. This can further help give a reviewer context on the change, find related changes, and see which stories are going into a release. Some SCMs and work-tracking tools (like SHIP-HATS GitLab and JIRA) have integrations that enable a backlog item to be shown with all its associated commits.

Tidying up

Once the source branch has been merged in it can be deleted, to keep the repository clean, and another branch created for the next piece of work. If the branch is for a backlog item, its name can contain the ID, or otherwise describe the branch’s singular purpose.

Quality Metric: Branch lifetime
The shorter, the better - long enough to contain a coherent set of changes to form a merge request.

Keep main ready to release

If we can make sure the code on the main branch is always ready to be released from a technical perspective, then we can decide to do that whenever makes sense from a business perspective.

To achieve that, the code on the main branch must:

  • Trigger a successful CI pipeline run, passing all our tests and checks
  • Go through review before it is merged in
  • Contain vertical slices of functionality that don’t leave the system in a broken state, or use feature flags to hide half-completed features

Doing all this successfully enables Continuous Delivery.

Using hooks to enforce quality and security

VCS like git have lifecycle hooks that enable us to shift control even further left than CI and prevent certain classes of problems from even being committed, merged or shared across the team.

Pre-commit hooks can be used to run linting tools, check that tests have passed, or check for sensitive keywords and block the commit.

Post-receive hooks on the server end can enforce conventions configured by the team or organisation and reject a push when they are violated.

Commit signing

One important push rule for keeping the software supply chain secure is to only allow commits signed with the author’s SSH key to be pushed to the server. This helps prevent a malicious actor insert their own changes into the codebase and taking advantage of deployment automation.

IM8 Control: SC-2: Commit Signing
Configure the code repository to reject unsigned commits.

These hook scripts can be configured on the server or checked into the codebase to be shared across the team, and libraries (such as Husky in the JavaScript ecosystem) can automatically insert them into the development workflow.

Chat with me!
Bot Launcher