← Back to list

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…

Amit Kumar · 2026-04-16 15:18 · 12 claps · 6.6 min read
#releases #github #conventional-commits #gitops
Open on Medium ↗
Wiki topics: AGT · AI Agents 🔓 · Open Source

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:

  1. What changed? → Parse commit messages by type
  2. What should the new version be? → Apply semver rules based on types seen
  3. 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:

  1. Reads all commits since the last release
  2. Calculates the next semantic version based on commit types
  3. 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:

  1. Developer opens a PR with feat(auth): add SSO support
  2. commitlint validates the message in CI
  3. PR merges to main
  4. Release Please updates its Release PR (or creates one) — v1.4.0 is pending
  5. Team merges the Release PR when ready to ship
  6. Tag v1.4.0 is created automatically
  7. Docker image myapp:v1.4.0 is built and pushed
  8. Dev gets the new image in ~2 minutes (ArgoCD override)
  9. Staging gets a Helm values commit, ArgoCD syncs
  10. 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