Table of Contents
- Background
- Required Tooling
- Getting Started
-
Walkthrough of the End-to-End Process
- Issue PROJ-123 is ready to be worked on
- Branch PROJ-123 is created on the git repository
- Assignee pushes to PROJ-123 branch
- Assignee creates Pull Request for PROJ-123 branch
- Team reviews Pull Request for PROJ-123 branch
- Assignee squashed and mergeds PROJ-123 branch to main branch
- Assignee creates tag for PROJ-123 release
- Feature released in PROJ-123 causes an issue in production
- Recommended Enhancements to this Process
- Conclusion
Background
The first job I held right after graduating college was chaotic. Leadership was the key factor in all of its issues; the company had experienced a large turnover right before I was hired to fill in the gaps. But we lacked real tech leadership who could mentor and guide us on how to effectively develop in teams. I was very fortunate that my second job did offer that leadership and expertise for effectively solving problems.
Often, I've fantasized, "what would I do differently at my first job knowing what I know now?" And the #1 answer that always comes out on top: I wish we used git
.
We had no version control at the time, and the problems we ran into were so obvious. There was no central codebase, so every member of our (albeit small) team was keeping copies of all of the code we had written. I would have dozens of ProductDisplayPageFINAL_FINAL_JUNE10_FINAL.js
on my machine in case a teammate of mine's changes would save over mine. Only our CTO had access to the codebase and all of the backups of files pushed into the server. So much time was spent asking him to go in and revert changes. Coworkers would be berated for "not checking" when other people had altered the code, even though there were no formal internal notifications set up. All of our code was being pushed up with entirely manual testing, and it frequently encountered bugs or issues. There was no concept of team-based code reviews. We were all isolated to our own work without knowing what other people were building and pushing.
It was night-and-day just finding out how git
and good process could solve all of these problems. I often tell people that if it weren't for having gone through the headaches and hurt of working in places such as my first job that the lessons I learned at my second job would not have stuck. I believe that my career to this point has all served a purpose in instructing me on proper software practices.
This is the essential software development process. I believe that any process that relies on code should start with this backbone and then be elaborated on over time in order to make processes more efficient.
Required Tooling
There are two key tools that are needed for this process.
Issue Tracker
New feature requests, update requests, and bug reports need to be collected, prioritized, and assigned. No matter what platform is selected, ensure that Issues being reported can be linked to using an identifier (ideally a short one).
Examples:
-
Jira by Atlassian. Jira Issue IDs are of the format
PROJECTID-###
, i.e.JIRA-123
. - Asana. Asana Tasks will need to be configured with a Custom ID field, as ticket IDs via the API are all long UUIDs.
- If using only GitHub:
- GitHub offers "Issues" on its repositories. Consider though if using this feature whether issues being reported could be affecting code across other repositories if the code is split out. This may mean setting up a central "issue reporting" repo. The ticket ID would the the Issue # (i.e. "1", but should be used in branch names later as "issue-1").
- GitHub also offers a "Project" management feature
Git Platform
A web platform should be used to make it easy to view code and navigate version history without solely relying on the git
tool. For this article, I will be using GitHub as the key facilitator. But all features here should be available across all other platforms as well.
Examples:
Getting Started
Setting up the Issue Tracker
As mentioned, whatever issue tracker platform is used, all tickets need to have a short unique identifier for each task. This will be easier to use in Git and to verbally relay to others for what is being worked (saying "I'm working on the requested change to the email notifications to make the font size bigger" vs. saying "I'm working on PROJ-123").
All issues should contain the full documentation for what change needs to be performed, and all issues should be reviewed as a group by the development team. I recommend an "Acceptance Criteria" checklist at the bottom of every task that clearly defines what steps need to be accomplished before an issue is marked as done. This checklist should remove any ambiguity when code changes need to be performed.
Setting up the Git Repository
For this article, the git repository will be referred to as git-project-repo
. This repo should have a default branch main
whose codebase is treated as "stable".
Note: all of the git
diagrams in this article were made using Mermaid and its gitGraph syntax. Dev.to does not support Mermaid markdown at this time, so all diagrams were saved as PNGs and are posted here with their accompanying Mermaid snippet.
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
This repo has had 2 features created for it. The production implementation should also rely on the "tag"s for the commits and not just relying on what's in main
. Production in this case is pointed to tag 1.1.0
.
Additionally, the git repository should be set up with a Pull Request Template (if using GitHub, this should be a markdown file stored in .github/PULL_REQUEST_TEMPLATE.md
; this will automatically populate all PR base comments with this file). This Template should contain the following sections:
Link to Issue: [PROJ-123](https://issue-tracking.platform.com/PROJ-123)
# Motivation
Why is this change needed?
# Changes Made
- High-level checklist of changes performed
- Could be the list of commits if change is small
# How to Verify
Procedure for either the Issue assignee to self-validate their work or for PR reviewers to review the work here, in addition to reviewing the code changes themselves.
# Release Steps
- [ ] Checklist of steps to perform for releases
- [ ] Could be a link to a full procedure document
Walkthrough of the End-to-End Process
Issue PROJ-123 is ready to be worked on
An Issue with short unique ID PROJ-123
, as mentioned in the Setting up the Issue Tracker section, contains the complete request that needs to be actioned. This issue has been reviewed and agreed-upon by the whole developer team and could be theoretically picked up by any member to action. The team has now agreed that PROJ-123
is ready to start now.
Branch PROJ-123 is created on the git repository
The assignee of PROJ-123
creates a new branch on the git-project-repo
repository. The branch name is the same as the Issue's ID.
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
branch "PROJ-123"
checkout "PROJ-123"
Assignee pushes to PROJ-123 branch
The Issue itself should be broken down into the separate steps needed to finish the "Acceptance Criteria". For example, a development task may be broken down into:
- Refactor a large function into smaller functions
- Update one of the smaller functions to take in a new argument
- Update unit-tests to pass
- Call that updated function in a different part of the codebase
- Add new unit-tests for new functionality
Each of these will be separate commits pushed by the developer into the Issue's branch. I recommend prefixing all commits with the Issue's ID as well, in case there are PRs that end up containing a handful of requests (i.e. if an Issue PROJ-234
is a small typo that needs to be fixed that would be more convenient to tackle at the same time as this request).
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
branch "PROJ-123"
checkout "PROJ-123"
commit id:"PROJ-123 | Refactor large func into smaller funcs"
commit id:"PROJ-123 | Update smaller func with new arg"
commit id:"PROJ-123 | Update unit-tests to pass"
commit id:"PROJ-123 | Added call to updated function"
commit id:"PROJ-123 | Add new unit-tests"
commit id:"PROJ-234 | Fix typo in docs"
Assignee creates Pull Request for PROJ-123 branch
By now, all steps of the "Acceptance Criteria" are now addressed, and all local validations by the assignee are passing. The assignee is now ready to get the change reviewed by the rest of the team. The assignee now creates a Pull Request to request to merge changes from the PROJ-123
branch into the main
branch.
For this example, the Pull Request's top comment will contain the following filled-in template:
Link to Issue: [PROJ-123](https://issue-tracking.platform.com/PROJ-123)
Also addresses: [PROJ-234](https://issue-tracking.platform.com/PROJ-234)
# Motivation
New feature request to change the size of the logo from 40 pixels tall to 50 pixels. This requires refactoring a part of the codebase in order to accept a new parameter. But this will let us make it easier to adjust this size in the future via a config update instead of a code change.
Also fixes a typo in the documentation reported in [PROJ-234](https://issue-tracking.platform.com/PROJ-234).
# Changes Made
- Refactor a large function into smaller functions
- Update one of the smaller functions to take in a new argument
- Update unit-tests to pass
- Call that updated function in a different part of the codebase
- Add new unit-tests for new functionality
- Fixed typo in documentation
# How to Verify
- [x] `make tests` runs all unit-tests locally
- [x] Go to https://internal-test.platform/feature-3 to see the changes in our test environment
# Release Steps
Current tag: `1.1.0`. New tag: `1.2.0`
- [ ] Send email to internal@company.com "Incoming release"
- [ ] Squash and merge PR into "main" branch
- [ ] Create new tag `1.2.0`
- [ ] Manually navigate into https://internal.platform/admin and change the tag to `1.2.0` and press "restart platform"
- [ ] Validate changes in Prod environment
- [ ] If rollback is required, follow the [Rollback Procedure](https://github.com/git-project-repo/README.md#rollback-procedure)
The content of this PR template is important because:
- All code changes should be clearly tied to a "why we need this change" and a "how this change was done" for historical references
- Reviewers are given a clear set of instructions for reviewing work
- Assignees are given a clear set of instructions for releasing work
Any part of this process that is ambiguous will turn into delays (because people are afraid of pushing code or performing a change that will cause problems). Clearly defined procedures (that can be expanded on if there are problems that arise) will remove this worry and hesitation.
The original Issue Tracking Ticket should also be updated with a link to the new Pull Request and indicate that the work is now ready to review.
Team reviews Pull Request for PROJ-123 branch
I personally recommend going simple with this. I like to use the "labels" feature of GitHub and allow users to tag PRs with a "needs review" label that can be filtered against all repositories in a GitHub organization. The benefits of this strategy:
- PRs can be started and refined until they're properly ready to review. For example, an assignee might ensure that the "diff" doesn't contain any accidental changes after making a proper PR.
- Using the label adequately communicates the status of this work. PRs that have a lot of feedback that may warrant significant rework may be untagged with the "needs review" label before it is re-added again.
- All members of the team can see all PRs that are tagged for review and can theoretically receive feedback from anyone.
If your team is large enough, another strategy is to randomly assign all PRs to dedicated reviewers. This also has the added benefit of ensuring that knowledge is being properly shared across the team and prevents a small handful of team members from becoming the sole reviewers. But I have not run into issues with the label-only strategy that couldn't be fixed by asking members of the team to slow down with code reviews to see if that forces others on the team to boost their contributions.
By the end of this step, any PR that is tagged for review should ideally have a minimum of 2 approvals if possible. Just having 1 approval is enough to get another pair of eyes on changes being pushed, but 2 will also allow for adequate knowledge transfer.
Assignee squashed and mergeds PROJ-123 branch to main branch
In the Pull Request template, the PR for PROJ-123
contains the following checklist:
# Release Steps
Current tag: `1.1.0`. New tag: `1.2.0`
- [ ] Send email to internal@company.com "Incoming release"
- [ ] Squash and merge PR into "main" branch
- [ ] Create new tag `1.2.0`
- [ ] Manually navigate into https://internal.platform/admin and change the tag to `1.2.0` and press "restart platform"
- [ ] Validate changes in Prod environment
- [ ] If rollback is required, follow the [Rollback Procedure](https://github.com/git-project-repo/README.md#rollback-procedure)
This is an example of what steps might be needed for a full push to production. Your team may want to expand on these steps where necessary (like if there are any database backup commands that need to be performed before releases). If, for example, a pre-production environment should be first used to validate the work in a semi-live environment, I recommend expanding that process to still rely on git
's versioning:
# Release Steps
Current tag: `1.1.0`. New tag: `1.2.0`
- [ ] Send email to internal@company.com "Incoming release"
- [ ] Squash and merge PR into "main" branch
- [ ] Create new tag `1.2.0`
+- [ ] Manually navigate into https://internal-preprod.platform/admin and change the tag to `1.2.0` and press "restart platform"
+- [ ] Validate changes in Pre-Prod environment
- [ ] Manually navigate into https://internal.platform/admin and change the tag to `1.2.0` and press "restart platform"
- [ ] Validate changes in Prod environment
- [ ] If rollback is required, follow the [Rollback Procedure](https://github.com/git-project-repo/README.md#rollback-procedure)
Now the PR is ready to be squashed and merged into the main
branch. From the PR, the commit history will look like this full diagram:
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
branch "PROJ-123"
checkout "PROJ-123"
commit id:"PROJ-123 | Refactor large func into smaller funcs"
commit id:"PROJ-123 | Update smaller func with new arg"
commit id:"PROJ-123 | Update unit-tests to pass"
commit id:"PROJ-123 | Added call to updated function"
commit id:"PROJ-123 | Add new unit-tests"
commit id:"PROJ-234 | Fix typo in docs"
checkout "main"
merge "PROJ-123" id:"PROJ-123 | Feature 3"
But from main
(and after the PROJ-123
branch is deleted now that its work is complete), the history will appear more simply as:
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
commit id:"PROJ-123 | Feature 3"
Deleting the branch does not mean the history is gone forever. In GitHub, any commit on main
resulting from a PR will contain a link to the original PR in its title. Additionally, the git blame feature will enable us to easily check any line of code to find out any "why" and "how" the change was performed.
Assignee creates tag for PROJ-123 release
Looking at the git graph, note that a new version has not yet been tagged for the new commit. There should be no live environments who are using this code in their implementation. As noted in the example "Release Steps" checklist:
- [x] Squash and merge PR into "main" branch
- [ ] Create new tag `1.2.0`
- [ ] Manually navigate into https://internal.platform/admin and change the tag to `1.2.0` and press "restart platform"
- [ ] Validate changes in Prod environment
Tagging is essential in order to easily revert or release changes together without needing to work through code reversion PRs. With the "pre-production" example noted in the Assignee squashed and mergeds PROJ-123 branch to main branch section, tagging makes it easy to understand what environments are viewing which codebase at-a-glance.
Tags should follow the Semantic Versioning standard because it is easy-to-follow and well-documented.
The assignee now tags the new release with "1.2.0" because it is a new feature that is not backwards-incompatible with the existing codebase.
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
commit id:"PROJ-123 | Feature 3" tag:"1.2.0"
The assignee now navigates into the production environment and switches the tag for its code reference from "1.1.0" to "1.2.0".
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0"
commit id:"PROJ-123 | Feature 3" tag:"1.2.0" type:HIGHLIGHT
By now, all the steps for releasing PROJ-123
are complete. The original Issue Tracking Ticket should also be updated with a comment on the release's completed status and any outcomes encountered.
Feature released in PROJ-123 causes an issue in production
Oh no! The release that was pushed ended up surfacing an incident in production! This issue may be something that is noticed immediately or results in a critical bug issue with ID PROJ-124
.
Incidents like these should be planned as a matter of when and not if; code is written and maintained by humans. Having procedures in place in advance is crucial and necessary and will instill trust in the team moving forward.
There are two strategies that can be applied here: fail-forward or rollback.
Fail-forward means that the root cause of the issue was quickly located in the codebase. Maybe there's a small typo in a config name that wasn't caught with linting. Fail-forward means whoever is addressing this fire creates a PR with this changes and gets it reviewed and approved following all the steps in the Walkthrough of the End-to-End Process again.
Rollback means either that the root cause of the issue can't be located quickly enough to go through with failing-forward or that the fire being caused is too great to allow for time to investigate the root cause right away. Luckily, rollback is easy with our current configuration by switching the tag back to the last "stable" version of the codebase. There may be other steps as well for rolling back, like if an older version of a backend database needs to be restored.
gitGraph
commit id:"initial commit"
commit id:"PROJ-121 | Feature 1" tag:"1.0.0"
commit id:"PROJ-122 | Feature 2" tag:"1.1.0" type:HIGHLIGHT
commit id:"PROJ-123 | Feature 3" tag:"1.2.0"
Whatever issue is encountered here, we are luckily able to effectively figure out these problems because:
- Version control makes it fast for us to switch between "safe" and "new" releases.
- All releases will be small, modular changes with clearly outlined
diff
s. Releases should not be a signficant amount of alterations that make it difficult to pin-point the culprit. - Every part of this process has a documented procedure that can be altered or expanded to cover any gap that was missing where this original issue was introduced (like if an extra unit-test or check would have caught this culprit when the PR was in review).
Recommended Enhancements to this Process
The Walkthrough of the End-to-End Process now covers all of the basics for using git
as part of any code change process. Here are all the ways that this backbone can be streamlined.
Add as many programmatic checks as possible
I firmly believe that any code or machine-readable specifications should be stored in version control and reviewed by all members of a team. This includes Markdown documentation as well as XML-as-code files (such as any no-code software services that saves its "code" as XML).
For any work being version controlled, I recommend looking into tooling to automate:
- Formatting: Automatically re-structure content to a single standard (i.e. forcing code from being tab-indented to space-indented).
- Linting: Checking if content adheres to a set of rules that require manual fixes (i.e. checking that all functions defined in code contain a top-level comment explaining what the function does and what its arguments mean).
- Unit-testing: Run a full suite of automated checks against a code implementation. This ensures both backwards-compatibility as well as that new features match expected behavior.
More programmatic checks that are set up against repositories should also result in faster and more thoughtful code reviews. Human reviews should not be wasted on trivial checks like "this file is missing a newline at the end of it" and should instead be thoughtful and along the lines of "maybe it would be more maintainable for us to do things this way instead of that way".
Every time there is an issue like what takes place in Feature released in PROJ-123 causes an issue in production, consider whether a new programmatic check or an expansion to an existing programmatic check would have caught the issue during code reviews.
Automate programmatic checks and releases with CI/CD Tooling
Now that more programmatic checks are set up, it's time to fully automate them into this process using CI/CD tooling.
CI/CD tooling can be used for both the "code review" phase (to automate all the "How to Verify" checkboxes) as well as the "release" phase (to automate all the "Release Steps" checkboxes).
Examples:
I am a personal fan of navigating through DevOps with the following mindset:
- Write a manual procedure for accomplishing a task and follow it enough times until it's set in stone. The procedure can now function as pseudocode for any automation implementation.
- Convert the manual procedure to a streamlined programmatic command
- Invoke the programmatic command in a CI/CD workflow
This mindset ensures that time isn't wasted troubleshooting the programmatic or CI/CD configuration until the team fully knows whether its useful to incorporate it into their processes (or to know if the process is too time consuming to continue doing manually).
Conclusion
The process outlined here is not a reinvented wheel. For further reading, I recommend reading any guide about Trunk-Based Development, the actual terminology for this process (the "trunk" is the main
branch on a git repository).
Software development by now is a solved science, and it's just a matter of understanding and utilizing these best practices. I'm thankful for having had the opportunity to experience the pain of a software development team that greatly struggled without having a system like this in place.
By following this process, I can personally guarantee that the following issues will be resolved:
- No more asking why a line of code was added or when it was added
- No more worry about having your work wiped out by someone else's code change. Team members can work against the same file in their own branches without worrying about stepping on someone else's toes (this will require navigating a merge conflict, but a merge conflict is way better than losing your work entirely).
- All members of the team are aware of all changes being worked on across all codebases and have the opportunity to ask questions and keep open communication during code reviews. No more knowledge silos during software development.
- All review, release, and rollback processes are documented and can be followed by anyone on the team without having a single-person-of-failure.
- No more worry about performing rollbacks. All changes being pushed are easy to investigate for root causes.
- Any team can become a well-oiled machine.
Top comments (0)