Modern Release Strategies with GitHub Actions (English)

Modern Release Strategies with GitHub Actions

Thursday, 04 December 2025

//

6 minute read

The core principle: make releases fast and painless, and you'll release more often. This is critical for agile development - rapid, reliable deployments create the fast feedback loop you need. When deploying is scary or slow, teams batch up changes, increasing risk. When deploying is boring and automatic, you ship small changes frequently.

This guide covers the release strategies I use in production, from simple tag-based deployments to multi-package monorepo publishing. All examples come from real GitHub Actions workflows in this repository.

Release Strategies at a Glance

Before diving into details, here's how common strategies compare:

Strategy Best For Complexity Common In
Tag-based Applications, explicit releases Low Startups, solo devs, mature teams
Branch-based Continuous deployment Low Startups, fast-moving teams
GitFlow Regulated releases, multiple versions High Enterprise, compliance-heavy
Trunk-based + Feature flags High-velocity teams Medium Big tech, mature startups
Monorepo multi-package Libraries, shared codebases Medium Platform teams, OSS projects

What Works Where

Startups & Small Teams: Start with tag-based or branch-based deployment. Keep it simple - you can always add complexity later. Trunk-based development with feature flags is popular at scale but overkill for small teams.

Enterprise: Often requires GitFlow or similar for compliance, audit trails, and release management. Multiple environments (dev, QA, staging, prod) with approval gates. Artifact attestations increasingly required for supply chain security.

Solo Developers: Tag-based is ideal. Push a tag, deployment happens. No ceremony, no overhead.

Platform/Library Teams: Need monorepo strategies with independent versioning per package. Semantic versioning is critical for downstream consumers.

Tag-Based Deployment

The simplest and most reliable strategy. Different tag prefixes route to different environments or trigger different actions.

Environment Targeting

on:
  push:
    tags:
      - 'release-*'  # Production
      - 'local-*'    # Dev environment
- name: Build Docker image
  run: |
    TAG_NAME=${GITHUB_REF#refs/tags/}
    if [[ "$TAG_NAME" == local-* ]]; then
      docker build -t myapp:local .
    else
      docker build -t myapp:latest -t myapp:$(date +%s) .
    fi

Why it works: Simple mental model, explicit deployments, easy rollback via timestamped tags.

# Deploy to production
git tag release-v2024.12.01 && git push origin release-v2024.12.01

# Deploy to dev
git tag local-feature-test && git push origin local-feature-test

Multi-Package Versioning

For monorepos with multiple publishable packages, use tag prefixes to identify which package to release:

# Different workflows, different tag patterns
# umami-net.yml
on:
  push:
    tags: ['umamiv*.*.*']  # e.g., umamiv1.0.5

# fetchextension.yml
on:
  push:
    tags: ['fetchextension-v*.*.*']  # e.g., fetchextension-v1.2.0

Extract the version and use it throughout:

- name: Extract version
  run: echo "VERSION=${GITHUB_REF#refs/tags/umamiv}" >> $GITHUB_OUTPUT

- name: Build & Pack
  run: |
    dotnet build -c Release -p:Version=${{ steps.version.outputs.VERSION }}
    dotnet pack -c Release -p:PackageVersion=${{ steps.version.outputs.VERSION }}

For automatic versioning, MinVer calculates versions from Git tags:

<PackageReference Include="MinVer" Version="6.0.0" PrivateAssets="all" />
<PropertyGroup>
  <MinVerTagPrefix>umamiv</MinVerTagPrefix>
</PropertyGroup>

Branch-Based Deployment

Deploy automatically when code lands on specific branches. Common in startups and fast-moving teams.

on:
  push:
    branches: [main]      # → Production
    # branches: [develop] # → Staging

Pros: No manual step, deploys on merge Cons: Accidental deploys possible, less explicit history

I use a hybrid approach - build on branch push (CI verification), but only publish on tags:

on:
  push:
    tags: ['scheduler-*']
    branches: [main, local]

jobs:
  build:
    # Always build
  publish:
    if: startsWith(github.ref, 'refs/tags/')  # Only publish on tags

Auto-Updates with Watchtower

For self-hosted deployments, Watchtower automatically pulls new images:

services:
  app:
    image: myapp:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 300  # Check every 5 minutes

Deployment flow: Push tag → GitHub Actions builds → Push to registry → Watchtower pulls → Container restarts. Total time: ~5 minutes.

Quality Gates

Fast deployments are useless if you deploy bugs. Every workflow should include gates:

- name: Run tests
  run: dotnet test --configuration Release

- name: Build
  run: dotnet build --configuration Release

# Tests or build fail → workflow stops → no publish

For multi-framework packages, build all targets:

- run: dotnet build -c Release --framework net8.0
- run: dotnet build -c Release --framework net9.0

Passwordless Publishing with OIDC

Modern approach - no secrets to rotate, no keys to leak:

permissions:
  id-token: write
  contents: read

- uses: NuGet/login@v1
  with:
    user: 'myusername'

- run: dotnet nuget push *.nupkg --api-key ${{ steps.login.outputs.NUGET_API_KEY }}

GitHub exchanges its OIDC token for a short-lived NuGet API key. Configure trust on NuGet.org to enable this.

Supply Chain Security

Artifact attestations prove your artifacts were built in CI, not tampered with. Increasingly required by enterprises.

permissions:
  id-token: write
  attestations: write

- uses: docker/build-push-action@v6
  id: push
  with:
    push: true
    tags: myapp:latest

- uses: actions/attest-build-provenance@v2
  with:
    subject-name: index.docker.io/myuser/myapp
    subject-digest: ${{ steps.push.outputs.digest }}
    push-to-registry: true

Consumers verify with: gh attestation verify oci://index.docker.io/myuser/myapp:latest --owner myuser

This achieves SLSA Level 2. For Level 3, use reusable workflows.

Preview Environments

Teams need stakeholders to preview changes before merging.

Enterprise approach - Branch-based preview environments:

on:
  pull_request:
    types: [opened, synchronize]

- run: |
    BRANCH=$(echo ${{ github.head_ref }} | sed 's/[^a-zA-Z0-9]/-/g')
    docker build -t myapp:preview-$BRANCH .
    # Deploy to k8s namespace, cloud platform, etc.

Startup/solo approach - Tunnel your local machine:

# Cloudflare Tunnel (free)
cloudflared tunnel run --url http://localhost:5000 my-preview
# → https://my-preview.cfargotunnel.com

Or use Wireguard VPN for internal team access to your dev machine.

Practical Tips

Tag cleanup: Git tags stick around forever.

git tag -d old-test-tag                      # Delete local
git push origin :refs/tags/old-test-tag      # Delete remote

Naming conventions: Be consistent.

  • release-YYYY.MM.DD for production
  • local-feature-name for dev
  • packagev1.2.3 for libraries

Secrets: Store in GitHub Secrets, never in code. Required secrets typically:

  • DOCKER_HUB_ACCESS_TOKEN
  • NUGET_API_KEY (or use OIDC)
  • NPM_TOKEN

Monorepo development: Use project references during development, switch to package references for deployment verification:

<ProjectReference Include="..\MyLib\MyLib.csproj" />
<!-- <PackageReference Include="MyLib" Version="1.0.0" /> -->

Moving to Kubernetes

Same principles, different tools:

Docker Compose Kubernetes
Watchtower ArgoCD / Flux
docker-compose.yml environments Namespaces
Container restart Rolling deployment
Manual rollback GitOps rollback

Summary

  1. Start simple: Tag-based deployment covers most needs
  2. Add complexity when needed: Branch-based, multi-environment, attestations
  3. Automate quality gates: Tests must pass before publish
  4. Enable rollback: Timestamped tags or GitOps history
  5. Secure your pipeline: OIDC for secrets, attestations for provenance
  6. Match your context: Solo dev ≠ startup ≠ enterprise

The workflows shown here run in this repository - check .github/workflows/ for complete implementations.

Remember: Rapid, reliable deployments are the foundation of agile development. Invest in your release pipeline early.

logo

© 2025 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.