Automate Release with GitHub Actions
Automate releases using GitHub Actions — Git Tutorial.
Focus: automate releases using github actions
You have just fixed that critical bug, bumped the version in package.json or pyproject.toml, and now you're dreading the next task: manually creating a git tag, writing release notes, and uploading artifacts to GitHub. Repeat that a few times a week and it's not just tedious — it's error-prone. You've tagged the wrong commit, forgotten the changelog, or pushed a broken asset more times than you care to admit. It's time to take back your evenings and let a machine do the dirty work.
In this lesson, you'll learn to automate releases using GitHub Actions — from firing a workflow when you push a tag to producing a polished GitHub Release with changelog and artifacts, completely hands-free. By the end, you'll be able to replace your manual release checklist with a single git push command.
The problem this lesson solves
Manual releases are a chain of repetitive, mistake-prone steps:
- You must remember the exact command to create an annotated tag (
git tag -a v1.2.3 -m "Release v1.2.3"). - You then have to write release notes — fighting with diffs and PR titles to figure out what actually changed.
- You have to build the project locally, upload binaries or packages, attach them to the GitHub Release, and then update documentation links.
Every one of those steps is a possibility to mess up. A forgotten tag, a mismatch between the local build and CI, a broken download link — we've all been there. Automating releases using GitHub Actions eliminates that entire class of human error. It also makes releases repeatable and auditable, because every step is defined in code and runs in a clean environment.
But automation isn't just about saving time. It enforces a consistent release process. When you manually create releases, you might forget to run the linter or skip the tests "just this once." An automated pipeline runs the exact same checks every time, catching regressions before your users do. It also frees you to focus on what matters: writing the release notes and announcing the new version, not juggling shell commands.
Core concept / mental model
Think of GitHub Actions as a robot that lives inside your repository and waits for a trigger. Triggers are events like a push to main or, as in our case, a push of a tag. When the trigger fires, GitHub spins up a virtual machine, checks out your code, and runs a series of steps defined in a YAML file.
The whole pipeline is declarative — you describe the what and GitHub Actions handles the how. Here's the mental model:
- Workflow: A YAML file in
.github/workflows/that defines one or more jobs. - Trigger: The event that starts the workflow. For releases, we typically trigger on
pushof tags that match a pattern likev*. - Job: A collection of steps that run on the same runner. Jobs can run in parallel or sequentially.
- Step: A single command or action — for example, running
python -m buildor uploading an artifact. - Runner: The virtual machine that executes the job (Ubuntu, Windows, macOS).
- Action: A reusable unit of code — either a built-in GitHub action or a community one from the Marketplace, like
actions/checkoutorsoftprops/action-gh-release.
The key insight: you trigger a workflow by pushing a tag, not by clicking a button. The tag name becomes the version number, and everything else — build, test, changelog, upload — is scripted. This makes the tag the single source of truth for the release.
Let's map that onto a diagram in words:
git push origin v1.2.3→ GitHub sees a new tag matchingv*→ workflow starts → checks out code → builds and tests → generates changelog → creates a GitHub Release with assets → triggers deployment (optional).
How it works step by step
Here's the logical sequence of events when you automate a release:
- You create an annotated tag and push it to GitHub.
- GitHub Actions detects the tag push (thanks to the
on: push: tags: ['v*']trigger). - The workflow checks out the repository at that tag using
actions/checkout@v4. - It runs the build and test suite (e.g.,
npm testorpytest). - It builds the distributable artifacts (e.g., a zip file, a Docker image, or a Python wheel).
- It generates a changelog from the commits or PRs since the last release — often with a tool like
git-cliffor by querying the GitHub API. - It creates a GitHub Release, attaching the artifacts and the changelog.
- Optionally, it triggers a deployment — e.g., publish to PyPI or npm, or notify a Slack channel.
Each step runs in a clean environment, so you can be confident your artifact matches your code. The whole process is idempotent — if a step fails, you fix the bug and re-tag; the workflow runs again from scratch.
Hands-on walkthrough
Let's build a release automation from scratch. We'll use a simple Python project as an example, but the pattern works for any language.
- Create the workflow file at
.github/workflows/release.yml. - Define the trigger: on push of tags matching
v*. - Set up the job with a name like
release. - Add steps: checkout, build, test, create release.
Here's a minimal but complete workflow:
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # needed to create a release
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build pytest
- name: Run tests
run: pytest
- name: Build package
run: python -m build
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: dist/*
generate_release_notes: true
Let's break it down:
- The
on: push: tags: ['v*']trigger means this workflow only runs when you push a tag that starts withv(e.g.,v1.0.0,v2.3.4). permissions: contents: writegives the workflow permission to create a release. See the troubleshooting section if you get a 403 error.- The
softprops/action-gh-releaseaction handles the heavy lifting of creating the release and attaching files. Thegenerate_release_notes: trueoption tells GitHub to auto-generate notes from merged PRs.
Now push a tag and watch the magic:
# Add a tag to your current HEAD
git tag -a v1.0.0 -m "Release v1.0.0"
# Push the tag to GitHub
git push origin v1.0.0
Go to the Actions tab on GitHub — you'll see your workflow running. Once green, check the Releases page. You'll find a new release v1.0.0 with auto-generated notes and your built .tar.gz and .whl files attached.
Adding conditional jobs
What if you want to run a separate job for publishing to Docker Hub or PyPI? You can split the workflow into multiple jobs with dependencies:
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.vars.outputs.version }}
steps:
# ... build steps ...
- name: Extract version
id: vars
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
publish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Publish to PyPI
run: echo "Publishing version ${{ needs.build.outputs.version }}"
The needs keyword ensures publish only runs after build succeeds. You can also pass data between jobs via outputs.
Compare options / when to choose what
There are several ways to automate releases on GitHub. Here's a comparison:
| Approach | Pros | Cons | Best when |
|---|---|---|---|
| GitHub Actions (our focus) | Native, free for public repos, fully customizable, integrates with GitHub Releases | YAML syntax can be finicky, community actions vary in quality | You want everything in one place, versioning via tags |
| Semantic Release | Automatically determines version from commit messages, no manual tags | Requires commit message convention, heavier learning curve | Your team follows Angular commit conventions, want automated version bumps |
| GitHub CLI + bash script | Full control, no dependency on third-party actions | Reimplements logic, harder to maintain | You prefer shell scripting over YAML |
When to choose GitHub Actions:
- You already use GitHub for code hosting.
- You want the release to be triggered by a simple
git push. - You need integration with other GitHub features (e.g., diffs, PRs, issues).
When to consider semantic-release: if your project follows Conventional Commits, semantic-release can automatically bump the version and create the release — no tags needed by hand. However, it also enforces a strict commit discipline.
For most beginner-to-intermediate projects, GitHub Actions with a tag trigger is the sweet spot: simple, transparent, and easy to debug.
Troubleshooting & edge cases
Workflow doesn't trigger
- Wrong trigger syntax: Double-check that your YAML has
on: push: tags: ['v*']with proper indentation. A common mistake is usingon: [push]without filtering tags. - Tag already exists: The workflow won't trigger again for the same tag. Delete the tag and recreate it (or use a new tag).
Permission denied "Resource not accessible"
- Fix: Add
permissions: contents: writeto the job (as shown in our example). If you're using GitHub Enterprise, the default token may have restricted scopes; check your app's permissions.
The release action fails with 'Not Found'
- Cause: Your token might not have the
reposcope, or the action is outdated. - Fix: Use the latest version of
softprops/action-gh-release, and ensure your repository is public (for free tier) or your runner has the necessary permissions.
Artifact uploading is slow or fails
- Cause: Large files or network issues.
- Fix: Split artifacts into smaller chunks, or use
actions/upload-artifactbefore creating the release (then download and attach them in a later step). Usually not needed for typical projects.
Version not extracted correctly
- Cause:
GITHUB_REFformat isrefs/tags/v1.2.3. Use${{ github.ref_name }}to get just the tag name — simpler and safer than string manipulation.
What you learned & what's next
You now know how to automate releases using GitHub Actions. Let's recap the essentials:
- A workflow in
.github/workflows/is triggered when you push a tag matching a pattern. - The workflow can build, test, generate release notes, and attach artifacts using actions like
actions/checkoutandsoftprops/action-gh-release. - You must set
permissions: contents: writeto allow the workflow to create a release. - Using
generate_release_notes: truegives you automatic changelogs from PRs. - Tags are the single source of truth for versioning.
The next step in your Git journey is exploring release branches and versioning strategies — how to manage maintenance releases and hotfixes alongside your main line of development. You'll learn how to use git flow or truncated branching to keep your release pipeline stable even as you ship multiple versions. Start by pushing a test tag to a repo and watching your workflow run — you'll soon wonder how you ever lived without it.
Practice recap
Create a new GitHub repository, add a minimal project (e.g., a Python file or a simple HTML page), commit and push it. Then add the release.yml workflow from this lesson, push a v1.0.0 tag, and verify the release appears on the Releases page. Next, try modifying the workflow to push an additional artifact like a zip file, and observe how the job logs show each step executing.
Common mistakes
- Triggering on
pushwithout a tag filter, so the workflow runs on every commit, causing duplicate releases. - Forgetting to set
permissions: contents: write, resulting in a 'Resource not accessible' error when the action tries to create a release. - Using
github.refinstead ofgithub.ref_nameto get the version, leading to strings likerefs/tags/v1.0.0. - Attaching artifacts from a local build instead of building inside the workflow, which can cause mismatches between your machine and CI.
- Pushing a new tag to the same commit after a failure, which doesn't retrigger the workflow because the tag already exists — you must delete and recreate it.
Variations
- Use
semantic-releaseto automate version bumps based on commit messages, eliminating manual tag creation. - Combine GitHub Actions with a changelog generator like
git-clifffor more detailed release notes than the built-in auto-generated ones. - Add a separate job that publishes your package to PyPI, npm, or Docker Hub after the release is created.
Real-world use cases
- Releasing a Python CLI tool: push a
v0.4.0tag, and the workflow builds a wheel, runs tests, and publishes it to PyPI with release notes. - Shipping a web frontend: every version tag triggers a build, deploys the static assets to CDN, and attaches a
.ziparchive for staging environments. - Open-source library maintenance: a tag push creates a GitHub Release with auto-generated changelog, notifying all watchers without manual effort.
Key takeaways
- GitHub Actions lets you automate releases from tag pushes, making
git push origin v1.2.3the only manual step. - The workflow file lives in
.github/workflows/release.ymland useson: push: tags: ['v*']to filter triggers. - Include
permissions: contents: writeto allow the workflow to create GitHub Releases. - Use
softprops/action-gh-releaseto attach build artifacts andgenerate_release_notes: truefor automatic changelogs. - All build steps run in a clean environment, ensuring your release artifacts exactly match the code at that tag.
- If a step fails, fix the issue, delete the tag, and re-push — the workflow is idempotent.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.