0x55aa
โ† Back to Blog

GitHub Releases: Stop Shipping Code Into the Void ๐Ÿš€

โ€ข13 min read

GitHub Releases: Stop Shipping Code Into the Void ๐Ÿš€

Confession time: For the first two years of my open source journey, my release process looked exactly like this:

git tag v1.4.0
git push origin v1.4.0
# Done. Ship it. ๐Ÿšข

And then I'd sit back, waiting for the praise to pour in.

What my users actually experienced:

User: "Hey, what's new in v1.4.0?"
Me: "Uh... stuff."
User: "Should I upgrade?"
Me: "Probably?"
User: "Did you fix the bug from last month?"
Me: *checks git log frantically* "...maybe?"

As a full-time developer who contributes to open source, I've shipped hundreds of releases โ€” and learned the hard way that pushing a tag isn't a release. It's just a mystery box that happens to have a version number on it. ๐Ÿ“ฆ

GitHub Releases changed everything. Let me show you how to use them properly. ๐ŸŽฏ

What's Actually Wrong With Just Pushing Tags? ๐Ÿ’ฃ

Nothing is wrong with Git tags themselves โ€” they're great for marking versions in history. The problem is that a tag communicates almost nothing to the humans depending on your code.

What your users want to know when they see a new version:

โœ… What changed? (new features, improvements)
โœ… What broke? (bug fixes)
โœ… What did you REMOVE? (breaking changes!)
โœ… Should I upgrade right now, or wait?
โœ… Will my existing code break?
โœ… What's the minimum version of X I need?

What a raw tag gives them:

โŒ A commit hash
โŒ A date
โŒ Absolutely nothing else

Real situation I encountered: I shipped a "minor" version of a Laravel package that quietly renamed a config key. Zero documentation. Zero release notes. Just a tag. A week later I got 12 identical GitHub issues saying "everything broke after upgrading." ๐Ÿ˜ฌ

Balancing work and open source taught me this: Your time is precious, and so is your users' time. A good GitHub Release takes 10 minutes to write and saves your community HOURS of confusion. That's the best ROI in software. ๐Ÿ’ก

GitHub Releases 101: The Anatomy of a Good One ๐Ÿ“‹

The GitHub Releases page is NOT just a fancy tag list. It's a communication channel. Here's what a proper release looks like:

The Bad Release (You've Seen These) โŒ

## v2.3.0

Bug fixes and improvements.

Translation: "I didn't feel like writing anything. Good luck."

What this communicates:

  • I don't respect your time
  • I don't know what changed
  • I don't care if you upgrade or not
  • Please open 40 issues asking what changed

The Good Release โœ…

## v2.3.0 - The "Finally Fixed That Annoying Thing" Release ๐ŸŽ‰

### ๐Ÿšจ Breaking Changes
- `UserService::authenticate()` now returns `AuthResult` instead of `bool`.
  Update your callers: `$auth->authenticate($user)->isSuccessful()`

### โœจ New Features
- Added `withRetry()` method to API client for automatic retries (#234)
- Support for PHP 8.3 (#251)
- New `--dry-run` flag for artisan commands (#198)

### ๐Ÿ› Bug Fixes
- Fixed race condition in session handling under high concurrency (#287)
- Fixed memory leak when processing large datasets (#301)
- Corrected timezone handling in event scheduler (#245)

### ๐Ÿ“– Documentation
- Updated installation guide for Docker environments
- Added examples for async queue processing

### โฌ†๏ธ Dependencies
- Updated `guzzle/guzzle` to ^7.8 (security fix)
- Dropped support for PHP 7.4 (EOL)

**Full Changelog:** https://github.com/you/project/compare/v2.2.0...v2.3.0

**Upgrade Guide:** [Read the migration docs](https://docs.your-project.com/migrations/v2.3)

What this communicates:

  • I care about your upgrade experience
  • Here's EXACTLY what changed
  • Here's what you need to fix if anything breaks
  • I did the work so YOU don't have to dig through git log

Your users will love you for this. I've gotten GitHub stars and DMs just because people were impressed by release notes. That's... free marketing. ๐ŸŒŸ

How to Write Release Notes That Don't Suck ๐Ÿ“

The Changelog Categories That Actually Help

Use these sections (not all required, include what's relevant):

### ๐Ÿšจ Breaking Changes
(THE MOST IMPORTANT SECTION. Never bury this.)

### โœจ New Features
(Things that didn't exist before)

### ๐Ÿ› Bug Fixes
(Things that were broken and now aren't)

### ๐Ÿ”’ Security
(ALWAYS call these out explicitly)

### โšก Performance
(Things that got faster)

### ๐Ÿ“– Documentation
(Docs updates worth knowing about)

### โฌ†๏ธ Dependencies
(Library updates, especially security-related)

### ๐Ÿ—‘๏ธ Deprecated
(Features going away in a future version)

### ๐Ÿงน Internal / Housekeeping
(Refactoring, tests, CI changes โ€” users rarely care)

Pro tip: If you have a "Breaking Changes" section with entries in it, consider making it a major version bump. That's what semantic versioning is for! ๐Ÿ”ข

The Golden Rule of Release Notes: Write for the Upgrader

Every line in your changelog should answer the question:

"What do I need to DO because of this change?"

Bad entry:

- Refactored authentication module

Good entry:

- Refactored authentication module to use PSR-15 middleware.
  If you extended `AuthMiddleware`, update your class to implement
  `Psr\Http\Server\MiddlewareInterface` instead. (#312)

See the difference? One tells you what changed. The other tells you what you need to DO about it.

In the security community, we write vulnerability reports with clear impact statements and remediation steps. Same principle applies here. Your changelog is a remediation guide for version upgrades! ๐Ÿ”’

Always Include the Comparison Link

**Full Changelog:** https://github.com/you/project/compare/v1.2.0...v1.3.0

This auto-generates a beautiful diff view showing every commit between versions. Users who want the raw detail can get it. Users who just want the summary have your notes.

GitHub literally generates this URL automatically when you create a release โ€” just don't delete it. ๐Ÿ™

Automating Release Notes (The Lazy Engineer's Way) ๐Ÿค–

Here's where it gets fun. You don't have to write all this manually.

Option 1: GitHub's Built-In Auto-Generate

When creating a release in the GitHub UI, click "Generate release notes". GitHub will:

  • Pull all PRs merged since the last release
  • Group them by PR labels (features, bug fixes, etc.)
  • Generate a formatted changelog automatically

Setup .github/release.yml to control the format:

# .github/release.yml
changelog:
  exclude:
    labels:
      - ignore-for-release
      - dependencies
  categories:
    - title: ๐Ÿšจ Breaking Changes
      labels:
        - breaking-change
    - title: โœจ New Features
      labels:
        - enhancement
        - feature
    - title: ๐Ÿ› Bug Fixes
      labels:
        - bug
        - fix
    - title: ๐Ÿ”’ Security
      labels:
        - security
    - title: ๐Ÿ“– Documentation
      labels:
        - documentation
    - title: โฌ†๏ธ Dependencies
      labels:
        - dependencies

Now every PR label automatically sorts into the right changelog section. The magic is: you do the work WHEN YOU MERGE the PR (by labeling it), not when you release. By release day, the notes write themselves! ๐ŸŽฉ

Option 2: GitHub Actions Release Automation

Full automated release on tag push:

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

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for changelog

      - name: Generate Release Notes
        uses: actions/github-script@v7
        with:
          script: |
            const { data: release } = await github.rest.repos.generateReleaseNotes({
              owner: context.repo.owner,
              repo: context.repo.repo,
              tag_name: context.ref.replace('refs/tags/', ''),
              previous_tag_name: 'v1.0.0', // or auto-detect
            });

            await github.rest.repos.createRelease({
              owner: context.repo.owner,
              repo: context.repo.repo,
              tag_name: context.ref.replace('refs/tags/', ''),
              name: release.name,
              body: release.body,
              draft: false,
              prerelease: false,
            });

Now pushing git tag v2.1.0 && git push origin v2.1.0 automatically:

  • Creates the GitHub Release
  • Generates the changelog
  • Publishes it publicly

Zero manual steps. ๐Ÿ”ฅ

Option 3: The CHANGELOG.md Pattern

Some projects maintain a CHANGELOG.md file in the repo, following the Keep a Changelog format:

# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- New pagination helper

## [2.3.0] - 2026-02-28

### Breaking Changes
- `UserService::authenticate()` now returns `AuthResult`

### Added
- withRetry() method to API client

### Fixed
- Race condition in session handling

## [2.2.0] - 2026-01-15
...

The workflow:

  1. Every PR updates the [Unreleased] section
  2. On release day, rename [Unreleased] to the version + date
  3. Copy that section into your GitHub Release notes
  4. Create a new empty [Unreleased] section

What I do in my Laravel packages: I keep CHANGELOG.md in the repo AND paste it into GitHub Releases. Users can read it without even leaving their browser. Double distribution, zero extra effort! ๐Ÿ’ช

The Release Naming Spectrum ๐ŸŽจ

Technical version numbers are required. Funny codenames are optional but beloved.

The boring (but fine) approach:

v2.3.0

The engaging approach:

v2.3.0 - "Summer Cleanup"
v2.3.0 - "The One With Retry Logic"
v2.3.0 - "Security Hardening Release"

The legendary approach (see: Django, Ubuntu):

Django 5.0 "All Systems Go"
Ubuntu 24.04 LTS "Noble Numbat"

Naming releases adds personality. It makes release notes feel like something a human wrote, not a robot generated. Users remember "oh that was the retry release" instead of "was it 2.3 or 2.4 that added retries?" ๐Ÿ˜„

Pre-Releases and Release Candidates ๐Ÿงช

Don't go straight to stable for major changes!

GitHub Releases supports pre-release flags. Use them:

v3.0.0-alpha.1 โ†’ Early testing (breaking, unstable)
v3.0.0-beta.1  โ†’ Feature complete, bug hunting
v3.0.0-rc.1    โ†’ Release Candidate, final testing
v3.0.0         โ†’ Stable release ๐ŸŽ‰

When to use pre-releases:

โœ… Major version with breaking changes
โœ… Big architectural refactors
โœ… New dependencies or runtime requirements
โœ… Anything you'd want 10 brave people to test first

What I do for security-related changes: I ALWAYS do a release candidate for security fixes. I want other security researchers to review before it goes public. In the security community, we peer-review patches โ€” open source is no different! ๐Ÿ”’

The "Oops" Release: How to Handle Yanked Versions ๐Ÿ™ˆ

Sometimes you ship a release and immediately realize it's broken. Here's the professional way to handle it:

Step 1: Mark the broken release as a pre-release

  • Edit the release in GitHub UI
  • Check "Set as a pre-release"
  • This removes it from being listed as "latest"

Step 2: Ship a patch immediately

git tag v2.3.1  # hotfix
git push origin v2.3.1

Step 3: Write an honest release note

## v2.3.1 โ€” Hotfix ๐Ÿšจ

**This is an emergency patch for v2.3.0.** If you upgraded to v2.3.0,
please upgrade to v2.3.1 immediately.

### Fixed
- Critical bug in UserService::authenticate() causing all logins to fail
  when `remember_me` is true (#334) โ€” Introduced in v2.3.0

### Upgrade Notes
This is a drop-in replacement for v2.3.0. No migration needed.

**Sorry for the disruption!** We've added a regression test to prevent
this from happening again. ๐Ÿ™

Be honest. Be fast. Apologize. Users forgive mistakes. Users don't forgive silence.

I've shipped broken releases. It happens. The communities that rally around you are the ones where you communicate openly and fix things fast. ๐Ÿค

Setting Up Your Repository for Great Releases ๐Ÿ› ๏ธ

A quick checklist:

โ–ก Create .github/release.yml with changelog categories
โ–ก Create PR label conventions (bug, enhancement, security, breaking-change)
โ–ก Add CHANGELOG.md to the repo root
โ–ก Set up GitHub Actions release workflow (optional but recommended)
โ–ก Write a RELEASING.md document explaining your release process to contributors
โ–ก Pin your "latest" release in the GitHub sidebar
โ–ก Enable "Automatically delete head branches" (keeps repo clean)

RELEASING.md template:

# How We Release

## Versioning
We follow semantic versioning (semver.org):
- PATCH: Bug fixes, security patches (1.0.X)
- MINOR: New features, backwards compatible (1.X.0)
- MAJOR: Breaking changes (X.0.0)

## Process
1. Merge all PRs for this release to `main`
2. Update CHANGELOG.md (move [Unreleased] to new version)
3. Bump version in package.json/composer.json
4. Run: `git tag vX.Y.Z && git push origin vX.Y.Z`
5. GitHub Actions auto-creates the release
6. Review the generated release notes and enhance manually
7. Announce in GitHub Discussions / Discord

## Labels
PRs must be labeled before merging:
- `bug` โ†’ Bug Fixes section
- `enhancement` โ†’ New Features section
- `security` โ†’ Security section
- `breaking-change` โ†’ Breaking Changes section
- `ignore-for-release` โ†’ Not included in changelog

TL;DR โ€” Your Release Checklist ๐Ÿ“‹

Every release should have:

โ–ก Proper semver version number (v1.2.3)
โ–ก Breaking Changes section (if any โ€” never hide these!)
โ–ก New Features section
โ–ก Bug Fixes section
โ–ก Security section (if applicable)
โ–ก Dependency changes (especially security updates)
โ–ก Comparison link to previous release
โ–ก Upgrade notes (if anything requires user action)
โ–ก Link to full docs or migration guide (for major versions)

The 10-minute release habit:

5 min  โ†’ Write the changelog sections from PR list
3 min  โ†’ Review and enhance auto-generated notes
2 min  โ†’ Add any manual context maintainers forget
0 min  โ†’ Your users now have all the context they need

Shortcut: Label your PRs consistently when merging. By release day, most of your notes are already written. The discipline happens at merge time, not release time. ๐ŸŽฏ

Wrapping Up ๐ŸŽ‰

Tags are for machines. GitHub Releases are for humans.

Your users are real developers with real codebases depending on your project. When you ship a release without notes, you're making them do archaeology through your git log just to figure out whether they can safely upgrade.

Don't be that maintainer.

Write the notes. Label the PRs. Automate where you can. The 10 minutes you invest in a good release note pays dividends for every user who reads it instead of opening a confused issue.

As a full-time developer who contributes to open source, this is one of the highest-leverage habits I've built. Good release notes have:

  • Reduced my issue count by ~40% post-release
  • Generated genuine "thank you" messages from users
  • Made my projects look more professional and trustworthy
  • Helped other contributors understand the project's direction

Start with your next release. Even if it's just 5 bullet points โ€” that's infinitely better than nothing. Your users will notice. And they'll stick around because of it. ๐ŸŒ


Got a release process you're proud of? Show me on GitHub or connect on LinkedIn โ€” always curious how other maintainers handle this!

Now go ship something โ€” and this time, tell people what's in the box. ๐Ÿ“ฆโœจ


P.S. If you've been publishing releases for years with just "Bug fixes and improvements" โ€” no judgment. We've all been there. Just don't do it for the NEXT one. ๐Ÿ˜„

P.P.S. The single most impactful thing you can do RIGHT NOW: go add release.yml labels to your last 10 merged PRs and generate release notes for your latest tag. It'll take 20 minutes and your changelog will be immediately 100x better. ๐Ÿš€