Interviews / DevOps & Cloud Infrastructure / Releases are manual and error-prone. Automate with semantic versioning and changelogs.
A critical deployment workflow is failing intermittently. Debug and fix the issue.
100 repositories duplicate the same CI workflow. Design a reusable workflow architecture.
Workflows are consuming too many minutes and running slowly. Optimize for speed and cost.
Workflows use long-lived credentials that could be leaked. Implement secure authentication with OIDC.
GitHub-hosted runners don't meet our requirements. Configure self-hosted runners at scale.
We need to test across multiple OS, Node versions, and configurations. Implement efficient matrix builds.
A workflow is vulnerable to script injection attacks. Identify and fix the security issues.
Every workflow run downloads the same dependencies. Implement an effective caching strategy.
Multiple workflows share the same setup steps. Create a composite action for reuse.
Releases are manual and error-prone. Automate with semantic versioning and changelogs.
Design a deployment workflow with environment approvals, staging, and production rollbacks.
Our monorepo builds everything on every change. Implement efficient path-based workflows.
Questions
Releases are manual and error-prone. Automate with semantic versioning and changelogs.
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 hourStep 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.mdStep 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
| Tool | Best For | Versioning | Changelog |
|---|---|---|---|
| semantic-release | Full automation | Automatic | Generated |
| release-please | PR-based releases | Automatic | Generated |
| changesets | Monorepos | Manual + PR | Generated |
| Standard Version | Simple projects | Manual trigger | Generated |
Practice Question
In semantic versioning with Conventional Commits, which commit type triggers a MAJOR version bump?