GitHub Releases: Stop Shipping Code Into the Void ๐
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:
- Every PR updates the
[Unreleased]section - On release day, rename
[Unreleased]to the version + date - Copy that section into your GitHub Release notes
- 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. ๐