Automate Release with GitHub Actions

Automate releases using GitHub Actions — Git Tutorial.

Focus: automate releases using github actions

Sponsored

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 push of tags that match a pattern like v*.
  • 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 build or 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/checkout or softprops/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 matching v* → 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:

  1. You create an annotated tag and push it to GitHub.
  2. GitHub Actions detects the tag push (thanks to the on: push: tags: ['v*'] trigger).
  3. The workflow checks out the repository at that tag using actions/checkout@v4.
  4. It runs the build and test suite (e.g., npm test or pytest).
  5. It builds the distributable artifacts (e.g., a zip file, a Docker image, or a Python wheel).
  6. It generates a changelog from the commits or PRs since the last release — often with a tool like git-cliff or by querying the GitHub API.
  7. It creates a GitHub Release, attaching the artifacts and the changelog.
  8. 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.

  1. Create the workflow file at .github/workflows/release.yml.
  2. Define the trigger: on push of tags matching v*.
  3. Set up the job with a name like release.
  4. 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 with v (e.g., v1.0.0, v2.3.4).
  • permissions: contents: write gives the workflow permission to create a release. See the troubleshooting section if you get a 403 error.
  • The softprops/action-gh-release action handles the heavy lifting of creating the release and attaching files. The generate_release_notes: true option 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 using on: [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: write to 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 repo scope, 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-artifact before creating the release (then download and attach them in a later step). Usually not needed for typical projects.

Version not extracted correctly

  • Cause: GITHUB_REF format is refs/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/checkout and softprops/action-gh-release.
  • You must set permissions: contents: write to allow the workflow to create a release.
  • Using generate_release_notes: true gives 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 push without 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.ref instead of github.ref_name to get the version, leading to strings like refs/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

  1. Use semantic-release to automate version bumps based on commit messages, eliminating manual tag creation.
  2. Combine GitHub Actions with a changelog generator like git-cliff for more detailed release notes than the built-in auto-generated ones.
  3. 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.0 tag, 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 .zip archive 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.3 the only manual step.
  • The workflow file lives in .github/workflows/release.yml and uses on: push: tags: ['v*'] to filter triggers.
  • Include permissions: contents: write to allow the workflow to create GitHub Releases.
  • Use softprops/action-gh-release to attach build artifacts and generate_release_notes: true for 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.