Teach Your Git Log to Deploy Itself: A Conventional Way
In an era of agentic automation, most teams spend more time managing releases than building features. Here’s how one commit message format…
Teach Your Git Log to Deploy Itself: A Conventional Way

In an era of agentic automation, most teams spend more time managing releases than building features. Here’s how one commit message format eliminated that entirely, versioning, changelogs, and multi-environment deployments, all on autopilot.
If you've worked on a team for any length of time, you've seen a git log that looks like this:
fix thing
wip
asdfgh
FINALLY FIXED
revert of revert
Amit's changes
This isn’t just ugly — it’s expensive. Every release requires someone to manually figure out what changed, decide what the version should be, write a changelog, and then kick off the deployment. That person is usually the most senior engineer on the team, doing the most junior work.
Conventional commits solve this by making the commit message machine-readable. Once commits follow a predictable format, tooling can take over the rest.
What Are Conventional Commits?
The Conventional Commits specification is a lightweight convention on top of commit messages. The format looks like this:
<type>(<scope>): <description>
[optional body]
[optional footer]
Core Types:
| Type | What it means | Version bump |
|-------------|-------------------------------------|-------------------------|
| `fix:` | A bug fix | Patch (`1.0.0`→`1.0.1`) |
| `feat:` | A new feature | Minor (`1.0.0`→`1.1.0`) |
| `chore:` | Tooling, CI, non-user-facing | None |
| `docs:` | Documentation only | None |
| `refactor:` | Code change without behavior change | None |
| `perf:` | Performance improvement | Patch |
| `test:` | Adding or updating tests | None |
Breaking changes
Adding BREAKING CHANGE: in the footer, or ! after the type, signals a major version bump:
feat!: redesign authentication API
BREAKING CHANGE: /auth/login endpoint removed, use /v2/auth instead
This results in ‘1.0.0’ → ‘2.0.0’.
Scopes (optional but useful)
Scope narrows what area of the code was changed:
fix(auth): handle token expiry edge case
feat(dashboard): add CSV export button
chore(ci): upgrade Node.js to v20
Why This Matters: The Machine Takes Over: Here’s the key insight: if every commit follows this format, a tool can read your git history and answer three questions automatically:
- What changed? → Parse commit messages by type
- What should the new version be? → Apply semver rules based on types seen
- What goes in the changelog? → Group and format the relevant commits
This is exactly what tools like Release Please do.
Release Please: The Automation Layer
Release Please is a GitHub Action (also available as a CLI) that watches your main branch. Every time you push, it:
- Reads all commits since the last release
- Calculates the next semantic version based on commit types
- Opens a Release PR that contains:
- A ‘CHANGELOG.md’ update
- A version bump in ‘package.json’ (or whatever manifest you use)
When you merge that Release PR, it automatically creates a GitHub Release and a git tag (‘v1.2.3’).
Basic configuration
# release-please-config.json
{
"packages": {
".": {
"release-type": "node"
}
}
}
# .release-please-manifest.json
{
".": "1.0.0"
}
# Add it as a workflow:
# .github/workflows/release-please.yml
name: release-please
on:
push:
branches: [main]
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
Now your versioning is fully automated. Merge a fix: commit → patch bump. Merge a feat: commit → minor bump. The team never debates version numbers again.
The bootstrap-sha trick
If you’re setting this up on an existing repo with history, Release Please will try to walk all the way back through your commits to find a baseline — and can crash on old squash-merged PRs. Add a bootstrap-sha to tell it where to start:
{
"bootstrap-sha": "abc123...",
"packages": {
".": { "release-type": "node" }
}
}
Set it to your current HEAD SHA. Release Please will ignore everything before that point.
Enforcing the Convention with Commitlint
Automation only works if everyone actually follows the format. commitlint enforces it at commit time via a Git hook.
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional']
}
npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
Now if a developer tries to commit fix thing, the hook rejects it with a clear error:
- subject may not be empty [subject-empty]
- type may not be empty [type-empty]
You can also run commitlint in CI to catch anything that slipped through (e.g., commits made directly via the GitHub UI):
- name: Validate commit messages
uses: wagoid/commitlint-github-action@v6
with:
configFile: commitlint.config.js
Taking It Further: Connecting Tags to Deployments
Once Release Please creates a tag (v1.2.3), you can trigger a deployment workflow on that tag event. This is where the full GitOps pipeline comes together.
The overall flow
Developer pushes a conventional commit
↓
Release Please opens a Release PR
↓
Team merges the Release PR
↓
Release Please creates tag v1.2.3
↓
on-tag workflow fires
↓
Docker image is built and pushed (tagged v1.2.3)
↓
Helm chart values updated with new tag
↓
ArgoCD detects the change and syncs
↓
App is running v1.2.3 in staging/production
No manual steps. No deployment scripts. No “who knows what version is in prod.”
on-tag workflow structure
name: on-tag
on:
push:
tags: ['**']
jobs:
build:
steps:
- name: Get version from tag
run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
- name: Build and push Docker image
run: |
docker build -t myregistry.io/myapp:${{ env.VERSION }} .
docker push myregistry.io/myapp:${{ env.VERSION }}
deploy-dev:
needs: build
steps:
- name: Update ArgoCD image tag
# Override the image tag directly in ArgoCD (immediate, no Git commit needed for dev)
uses: some-argo-override-action@v1
with:
application: myapp-dev
value: image.tag=${{ env.VERSION }}
Multi-Environment Strategy
The three-branch model: A clean GitOps setup separates concerns across branches and repos:
application-ui (app source)
└── main branch → Release Please → tag v1.2.3
applicaiton-k8s-ui (Helm chart)
├── staging branch → values.yaml with tag v1.2.2
└── prod branch → values.yaml with tag v1.2.1
k8s-apps (ArgoCD app-of-apps)
├── values-dev.yaml → points to dev branch
├── values-staging.yaml → points to staging branch
└── values-prod.yaml → points to prod branch
Dev: immediate override: Dev is ephemeral and fast. Rather than committing to a Git branch, use ArgoCD’s parameter override API to inject the image tag directly. This gives you near-instant deploys without polluting your Git history.
- name: Override ArgoCD parameter
run: |
argocd app set myapp-dev \
--parameter image.tag=v1.2.3
Staging: GitOps proper
Staging is where you want a real Git trail. The on-tag workflow updates the Helm values file in your chart repo:
global:
image:
tagPrefix:
tag: v1.2.3
ArgoCD watches the staging branch of the Helm chart repo and syncs automatically when this file changes.
Prod: gated promotion
Prod never auto-deploys. A separate manual workflow (staging-to-prod) reads the current tag from the staging branch and promotes it to the prod values file:
name: staging-to-prod
on:
workflow_dispatch: # manual trigger only
jobs:
promote:
steps:
- name: Read current staging tag
id: version
run: echo "tag=$(yq eval '.global.image.tag' values-staging.yaml)" >> $GITHUB_OUTPUT
- name: Update prod values
run: |
yq eval '.global.image.tag = "${{ steps.version.outputs.tag }}"' \
-i values-prod.yaml
- name: Commit and push
run: |
git commit -am "chore: promote ${{ steps.version.outputs.tag }} to prod"
git push
ArgoCD then syncs prod from the updated values-prod.yaml. The deployment is traceable — you can git log to see exactly when and what was promoted.
Patch-Only Releases From Hotfix Branches
What if you’re on v1.3.0 in prod and you need a hotfix? You don’t want the accumulated feat: commits from main going out — you just want the patch.
This is where staging and release/* branches come in. Configure Release Please with a separate config for these branches:
// release-please-patch-config.json
{
"versioning": "always-bump-patch",
"bootstrap-sha": "abc123...",
"packages": {
".": { "release-type": "node" }
}
}
# In your release-please workflow
- uses: googleapis/release-please-action@v4
with:
config-file: >-
${{ contains('refs/heads/staging refs/heads/prod', github.ref)
&& 'release-please-patch-config.json'
|| 'release-please-config.json' }}
Now a fix: commit on the staging branch produces v1.3.1 not v1.4.0.
Observability: Deployment Events
GitHub has a Deployments API that lets you track what version is deployed to each environment. Wire it into your workflow:
- name: Create deployment event
uses: chrnorm/deployment-action@v2
id: deployment
with:
token: ${{ secrets.GITHUB_TOKEN }}
owner: my-org
repo: my-helm-chart-repo # separate owner/repo, not "org/repo" in one field
ref: staging-branch
environment: dev
initial-status: in_progress
- name: Run actual deployment
...
- name: Mark deployment success
if: success()
uses: chrnorm/deployment-status@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
state: success
Team Notifications
- name: Notify Teams — deploy starting
run: |
curl -s -H 'Content-Type: application/json' -d '{
"@type": "MessageCard",
"themeColor": "0078D7",
"summary": "Deploying to Dev",
"sections": [{
"activityTitle": "Deploying ${{ env.VERSION }} to Dev",
"facts": [
{ "name": "Version", "value": "${{ env.VERSION }}" },
{ "name": "Run", "value": "[View](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" }
]
}]
}' "${{ secrets.MS_TEAMS_WEBHOOK_URL }}"
What You Get At the End
After wiring all of this together, here’s what your team’s daily workflow looks like:
- Developer opens a PR with
feat(auth): add SSO support - commitlint validates the message in CI
- PR merges to main
- Release Please updates its Release PR (or creates one) —
v1.4.0is pending - Team merges the Release PR when ready to ship
- Tag
v1.4.0is created automatically - Docker image
myapp:v1.4.0is built and pushed - Dev gets the new image in ~2 minutes (ArgoCD override)
- Staging gets a Helm values commit, ArgoCD syncs
- Prod waits for manual promotion (one click, fully auditable)
The changelog writes itself. The version is always correct. Anyone on the team can look at a git tag and know exactly what’s in production.
The upfront cost is maybe a day of setup. The ongoing return is weeks per year of saved manual release work — and no more “who deployed what and when?”
— La fin
메타데이터
- post_id
- a2f60f75c8ed
- slug
- teach-your-git-log-to-deploy-itself-a-conventional-way-a2f60f75c8ed
- url
- https://medium.com/@hiamit.py/teach-your-git-log-to-deploy-itself-a-conventional-way-a2f60f75c8ed
- canonical_url
- https://medium.com/@hiamit.py/teach-your-git-log-to-deploy-itself-a-conventional-way-a2f60f75c8ed
- author_url
- https://medium.com/@hiamit.py
- status
- ok
- fetched_at
- 2026-08-11 23:18:45