iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Tagging / SemVer

Git tags drive most modern release pipelines: push a vX.Y.Z tag, CI builds an artifact, signs it, publishes it, and updates the changelog. The shape is the same in GitHub Actions, GitLab CI, and CircleCI; only the trigger syntax differs. Get this right once and "ship" stops being a manual checklist.

Tag-triggered release pipeline with provenance

EXAMPLE
# .github/workflows/release.yml
name: release
on:
  push:
    tags: ['v*.*.*']

permissions:
  contents: write          # for creating a GitHub Release
  packages: write          # for GHCR or GH Packages
  id-token: write          # for OIDC to a cloud provider

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
      sha:     ${{ github.sha }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }                # needed for changelogs

      - id: meta
        name: Parse tag
        run: |
          v="${GITHUB_REF_NAME#v}"
          echo "version=$v" >> "$GITHUB_OUTPUT"
          echo "VERSION=$v" >> "$GITHUB_ENV"

      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }

      - run: npm ci
      - run: npm test
      - run: npm run build

      # 1) Build a Docker image, tag with both 'vX.Y.Z' and 'latest'
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:v${{ env.VERSION }}
            ghcr.io/${{ github.repository }}:latest

  publish-github-release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      # 2) Auto-generate release notes from PRs since the previous tag
      - id: notes
        run: |
          prev="$(git tag --list 'v*.*.*' --sort=-version:refname | sed -n '2p')"
          if [ -z "$prev" ]; then range="$GITHUB_SHA"; else range="${prev}..HEAD"; fi
          {
            echo 'notes<<EOF'
            git log "$range" --no-merges --pretty='* %s (%h)'
            echo 'EOF'
          } >> "$GITHUB_OUTPUT"

      # 3) Publish a GitHub Release with the notes
      - uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name: Release ${{ github.ref_name }}
          body: ${{ steps.notes.outputs.notes }}
          draft: false
          prerelease: ${{ contains(github.ref_name, '-rc') }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with: { role-to-assume: ${{ secrets.DEPLOY_ROLE_ARN }}, aws-region: ap-southeast-2 }

      - name: Deploy to ECS
        run: |
          aws ecs update-service --cluster shop-prod --service api \
            --task-definition shop-app:${{ github.ref_name }} --force-new-deployment
          aws ecs wait services-stable --cluster shop-prod --services api

# ===== Conventions to make this work cleanly =====
# 1) Annotated tags only (git tag -a vX.Y.Z -m 'Release vX.Y.Z')
# 2) Semantic versioning — vX.Y.Z; -rc.N for release candidates
# 3) Tag from main only; pre-release branches use vX.Y.Z-rc.N
# 4) NEVER delete or move a published tag — issue a new patch instead
# 5) Combine with a changelog generator (release-please, changesets) if you want
#    the PRs to drive the version bump automatically

Why it matters

Tag-triggered pipelines turn "release" from a checklist into a deterministic event: cut the tag, watch CI ship the binary, deploy the image, write the release notes. Keep the tag immutable, build provenance into the artifact (signed image + SBOM), and the audit trail comes for free.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
git tag v1.4.0 && git push --tags
# Use SemVer: MAJOR.MINOR.PATCH.
# Automate via conventional commits → release-please / semantic-release.
Try it Yourself »

Exercise

Push a new git tag to the remote.

git push --

Discussion

Loading…