Automatic release isn't complete if deployment is still manual. In this episode we align semantic-release with GitOps: version tags trigger ArgoCD or Flux synchronization, and the staging environment promotes to production through a branch. The version tag becomes the single source of truth.

In episode 18 we monitored releases: version tags, release dashboards, and post-release validation. But there's one link in the chain that often breaks: the version ships automatically, while deployment to the cluster is still done manually by humans. This is where GitOps comes in. Its principle is simple but powerful — Git is the single source of truth, and changes that enter Git are automatically synchronized to the environment by an operator such as ArgoCD or Flux.
In this episode we cover:
GitOps, a pattern popularized by Weaveworks and adopted by both ArgoCD and Flux, stands on three core principles:
Semantic-release is a natural partner for GitOps. Release automation decides the version, GitOps decides where that version goes. Both work from the same source: Git.
The complete flow becomes one straight line without human intervention:
staging.v1.4.0-rc.1 on the staging branch.main.v1.4.0, then the deployment to production runs.The one key: the version semantic-release releases must be identical to the version of the deployed image. Don't let the deploy pick its own version different from the release tag. Tag the image exactly like the version tag:
docker build -t ghcr.io/acme/api:"${RELEASE_VERSION}" .
docker push ghcr.io/acme/api:"${RELEASE_VERSION}"The RELEASE_VERSION variable is taken directly from semantic-release's output, not guessed. That way you can always trace "version X is running in the cluster" back to the exact commit and changelog.
The easiest-to-understand model: one branch for one environment.
| Branch | Nature | Example version | Environment |
|---|---|---|---|
staging | prerelease | 1.4.0-rc.1 | Staging |
main | stable | 1.4.0 | Production |
Promotion isn't about copying artifacts; it's promoting an already-verified version. The code on staging has been tested, so merging staging to main only inherits a version that's already known to be good. semantic-release takes care of the version bumps:
fix commit on staging → 1.4.0-rc.2.main → stable 1.4.0 without a suffix.feat commit on staging → 1.5.0-rc.1.The most popular ArgoCD pattern: the target application is managed through a manifest repository (e.g. acme/deploy-configs) containing a kustomize or helm chart. The release workflow writes the new version into that manifest, then ArgoCD syncs it to the cluster.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api
namespace: argocd
spec:
project: default
destination:
namespace: production
server: https://kubernetes.default.svc
source:
repoURL: https://github.com/acme/deploy-configs
targetRevision: production
path: apps/api
syncPolicy:
automated:
prune: true
selfHeal: trueThe release workflow replaces the image tag in the manifest then pushes to that repository:
#!/usr/bin/env bash
set -euo pipefail
VERSION="${1:?tag versi wajib diisi}"
MANIFEST_REPO="git@github.com:acme/deploy-configs.git"
git clone --depth 1 --branch production "$MANIFEST_REPO" /tmp/deploy
cd /tmp/deploy
sed -i "s|image: ghcr.io/acme/api:.*|image: ghcr.io/acme/api:${VERSION}|" apps/api/deploy.yaml
git add apps/api/deploy.yaml
git commit -m "chore: promote api ${VERSION} ke production"
git pushThe advantage of this pattern: every deployment change is a commit that can be reviewed and reverted. Rollback is just git revert v1.3.0 to the previous version — and ArgoCD automatically returns the cluster to that state.
The second pattern needs no extra workflow at all. ArgoCD Image Updater or Flux ImagePolicy monitors the container registry, finds new version tags that match the semver rules, then updates the manifest automatically.
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: api
spec:
imageRepositoryRef:
name: api
filterTags:
pattern: '^v(?P<semver>\d+\.\d+\.\d+)$'
policy:
semver:
range: '>=1.0.0'The policy above only picks up stable version tags without the rc suffix, so the image updater will never deploy a release candidate to production. You can verify the active policy with flux get imagepolicy api.
Warning
The image updater pattern hands the "which version is running" decision to the GitOps operator. Make sure its semver filter is strict — a prerelease suffix is easy to miss and ends up in production. The manifest repo pattern is more explicit and easier to audit, because every change is a commit with a trace.
How does the deployment know a release was just published? The two most common triggers:
release event — runs when a GitHub Release is published by semantic-release.workflow_run trigger — runs when another release workflow finishes successfully.An example release-event-based trigger:
name: Deploy Production
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout manifest repo
uses: actions/checkout@v4
with:
repository: acme/deploy-configs
- name: Promote to production
run: ./promote.sh "${{ github.event.release.tag_name }}"An example trigger based on another finished workflow:
name: Deploy Staging
on:
workflow_run:
workflows: ["Release Candidate"]
types: [completed]
branches: [staging]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Get the version from the latest release
run: |
VERSION="${{ github.event.workflow_run.head_branch }}"
echo "deploying the release result on branch $VERSION"Tip
Use a single trigger. If you trigger the deploy from the release event
and from a tag push at the same time, the same release will fire the
deployment twice — wasting runner minutes and risking a race condition.
Make the release event the single trigger, because it only happens once
per release.
Tip
Make the version tag the source of truth. Tag v1.4.0 is immutable: it
always points to the same commit, so rollback always has a definite
reference point. Don't rely on the moving latest label — with latest,
you'll never know which version is actually running in production. Check
with git tag -l to make sure only version tags are published.
In this episode we connected release automation with deployment automation:
staging for rc, main for stable.release event or workflow_run — one trigger, not two.Release and deployment now live in a single automatic flow. In episode 20 we'll make sure the whole team can follow this standard — through documentation, onboarding, and code review. See you there!