DeployU
Interviews / DevOps & Cloud Infrastructure / Releases are manual and error-prone. Automate with semantic versioning and changelogs.

Releases are manual and error-prone. Automate with semantic versioning and changelogs.

practical Release Automation Interactive Quiz Code Examples

The Scenario

Your release process is painful:

Current Process:
1. Developer decides "it's time for a release"
2. Manually update version in package.json
3. Write CHANGELOG.md by looking at git log
4. Create git tag manually
5. Push tag, manually create GitHub release
6. Copy changelog to release notes
7. Hope nothing was missed

Problems:
- Inconsistent versioning (1.2.3 vs v1.2.3)
- Changelog often incomplete or wrong
- Releases at random times
- Breaking changes not clearly communicated
- Hours of manual work per release

The Challenge

Implement fully automated releases with semantic versioning based on commit messages, auto-generated changelogs, and proper release artifacts.

Wrong Approach

A junior engineer might just write a bash script to bump versions, manually decide what version to use, or skip changelogs entirely. These approaches don't scale, are inconsistent, and don't communicate changes to users.

Right Approach

A senior engineer implements Conventional Commits for standardized messages, uses semantic-release or release-please for automated versioning, generates changelogs automatically, and integrates with package registries for publishing.

Step 1: Adopt Conventional Commits

# Commit message format:
<type>(<scope>): <description>

[optional body]

[optional footer(s)]

# Types and their effect on versioning:
fix:      -> PATCH (1.0.0 -> 1.0.1)
feat:     -> MINOR (1.0.0 -> 1.1.0)
feat!:    -> MAJOR (1.0.0 -> 2.0.0)
BREAKING CHANGE: in footer -> MAJOR

# Examples:
fix(auth): resolve token refresh race condition
feat(api): add pagination support to list endpoints
feat!: redesign authentication flow

BREAKING CHANGE: OAuth tokens now expire after 1 hour

Step 2: Enforce Commit Messages

# .github/workflows/commitlint.yml
name: Lint Commits

on:
  pull_request:
    branches: [main]

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: Install commitlint
        run: npm install -g @commitlint/cli @commitlint/config-conventional

      - name: Validate commits
        run: |
          npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'build', 'revert']
    ],
    'scope-enum': [
      2,
      'always',
      ['api', 'auth', 'ui', 'db', 'config', 'deps', 'release']
    ]
  }
};

Step 3: Automated Release with semantic-release

# .github/workflows/release.yml
name: Release

on:
  push:
    branches: [main]

permissions:
  contents: write
  issues: write
  pull-requests: write
  id-token: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release
// release.config.js
module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    [
      '@semantic-release/changelog',
      {
        changelogFile: 'CHANGELOG.md'
      }
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true
      }
    ],
    [
      '@semantic-release/github',
      {
        assets: [
          { path: 'dist/**/*.js', label: 'Distribution files' },
          { path: 'CHANGELOG.md', label: 'Changelog' }
        ]
      }
    ],
    [
      '@semantic-release/git',
      {
        assets: ['package.json', 'CHANGELOG.md'],
        message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
      }
    ]
  ]
};

Step 4: Alternative - Release Please (Google’s Approach)

# .github/workflows/release-please.yml
name: Release Please

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    outputs:
      release_created: ${{ steps.release.outputs.release_created }}
      tag_name: ${{ steps.release.outputs.tag_name }}
    steps:
      - uses: google-github-actions/release-please-action@v4
        id: release
        with:
          release-type: node
          package-name: my-package

  publish:
    needs: release-please
    if: needs.release-please.outputs.release_created
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci
      - run: npm run build
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Step 5: Multi-Package Release (Monorepo)

# .github/workflows/release-monorepo.yml
name: Release Packages

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write
  id-token: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: npm ci

      - name: Build all packages
        run: npm run build --workspaces

      - name: Create Release PR or Release
        uses: google-github-actions/release-please-action@v4
        id: release
        with:
          command: manifest
          config-file: release-please-config.json
          manifest-file: .release-please-manifest.json

      - name: Publish packages
        if: steps.release.outputs.releases_created
        run: |
          # Get list of released packages
          RELEASES='${{ steps.release.outputs.paths_released }}'

          for path in $(echo $RELEASES | jq -r '.[]'); do
            echo "Publishing $path"
            cd $path
            npm publish --provenance --access public
            cd -
          done
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
// release-please-config.json
{
  "packages": {
    "packages/core": {
      "release-type": "node",
      "package-name": "@myorg/core"
    },
    "packages/cli": {
      "release-type": "node",
      "package-name": "@myorg/cli"
    },
    "packages/utils": {
      "release-type": "node",
      "package-name": "@myorg/utils"
    }
  },
  "changelog-sections": [
    { "type": "feat", "section": "Features" },
    { "type": "fix", "section": "Bug Fixes" },
    { "type": "perf", "section": "Performance Improvements" },
    { "type": "docs", "section": "Documentation" }
  ]
}

Step 6: Docker Image Releases

# .github/workflows/release-docker.yml
name: Release Docker

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/${{ github.repository }}
            docker.io/myorg/myapp
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha

      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Update Docker Hub description
        uses: peter-evans/dockerhub-description@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
          repository: myorg/myapp
          readme-filepath: ./README.md

Step 7: Complete Release Pipeline

name: Complete Release

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write
  packages: write
  id-token: write

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm test

  release:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      released: ${{ steps.release.outputs.release_created }}
      version: ${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}
    steps:
      - uses: google-github-actions/release-please-action@v4
        id: release
        with:
          release-type: node

  publish-npm:
    needs: release
    if: needs.release.outputs.released == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm run build
      - run: npm publish --provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  publish-docker:
    needs: release
    if: needs.release.outputs.released == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ needs.release.outputs.version }}

  notify:
    needs: [release, publish-npm, publish-docker]
    if: needs.release.outputs.released == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: slackapi/slack-github-action@v1
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "Released v${{ needs.release.outputs.version }}!",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*v${{ needs.release.outputs.version }}* has been released!\n<https://github.com/${{ github.repository }}/releases|View Release>"
                  }
                }
              ]
            }

Release Automation Comparison

ToolBest ForVersioningChangelog
semantic-releaseFull automationAutomaticGenerated
release-pleasePR-based releasesAutomaticGenerated
changesetsMonoreposManual + PRGenerated
Standard VersionSimple projectsManual triggerGenerated

Practice Question

In semantic versioning with Conventional Commits, which commit type triggers a MAJOR version bump?