Open Source Your Side Project: License, README, Automated Releases
Turn a private script into a public repo with an MIT license and semantic-release-driven versioning.
What you'll build
You'll take a script that lives on your laptop and turn it into a public GitHub repo with an MIT license, a README, a contribution guide, and a CI workflow that reads your commit messages and publishes versioned GitHub Releases on its own. No manual tagging, ever.
Prerequisites
- A GitHub account and
gitinstalled. - GitHub CLI 2.x, authenticated (
gh auth login). - macOS or Linux shell. On Windows, use WSL or Git Bash.
- No Node.js needed locally. CI installs it; semantic-release requires Node ≥ 22.14.0 there, and the workflow below handles that.
Verified against semantic-release 25.0.9, actions/checkout v7, actions/setup-node v7, and GitHub CLI 2.x in September 2026.
1. Start from your script
Substitute your own project; this stand-in makes the tutorial runnable end to end:
mkdir tidy-downloads && cd tidy-downloads
git init -b main
cat > tidy-downloads.sh <<'EOF'
#!/usr/bin/env bash
# Move files older than 30 days from ~/Downloads into ~/Downloads/archive
mkdir -p ~/Downloads/archive
find ~/Downloads -maxdepth 1 -type f -mtime +30 -exec mv {} ~/Downloads/archive/ \;
EOF
chmod +x tidy-downloads.sh
2. Add a license
A repo with no license is "all rights reserved" by default, so nobody can legally use, copy, or modify your code even though it's public. MIT is the standard permissive choice; compare options at choosealicense.com if you want copyleft instead.
Pull the official MIT text straight from GitHub's API:
gh api licenses/mit --jq .body > LICENSE
Open LICENSE and replace the [year] and [fullname] placeholders with the current year and your name.
3. Write the README and contribution guide
The README answers three questions in order: what is this, how do I install it, how do I run it.
cat > README.md <<'EOF'
# tidy-downloads
Moves files older than 30 days out of ~/Downloads into an archive folder.
## Install
curl -fsSLO https://raw.githubusercontent.com/YOUR_USER/tidy-downloads/main/tidy-downloads.sh
chmod +x tidy-downloads.sh
## Usage
./tidy-downloads.sh
## License
MIT
EOF
The contribution guide is where the release automation gets its rules. semantic-release decides version numbers from commit messages in the Conventional Commits format, so contributors have to follow it:
cat > CONTRIBUTING.md <<'EOF'
# Contributing
Commit messages must follow Conventional Commits, because releases
are cut automatically from them:
- `fix: ...` -> patch release (1.0.1)
- `feat: ...` -> minor release (1.1.0)
- `feat!: ...` or a `BREAKING CHANGE:` footer -> major release (2.0.0)
- `docs:`, `chore:`, `ci:` -> no release
Open an issue before large changes.
EOF
4. Configure semantic-release
By default semantic-release also tries to publish to npm. This project isn't an npm package, so list only the three plugins you need:
cat > .releaserc.json <<'EOF'
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}
EOF
Analyzer reads the commits, notes generator writes the changelog text, and the GitHub plugin creates the tag and release. Dropping @semantic-release/npm also means you don't need an npm token or trusted-publishing setup.
5. Add the release workflow
mkdir -p .github/workflows
cat > .github/workflows/release.yml <<'EOF'
name: Release
on:
push:
branches: [main]
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # push the version tag and create the release
issues: write # comment on issues closed by a release
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # full history, so the last release tag is visible
- uses: actions/setup-node@v7
with:
node-version: "lts/*"
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release@25
GITHUB_TOKEN is injected by Actions automatically. You create no secrets at all.
6. Commit and publish
The commit type matters here: chore: initial commit would produce no release, while feat: triggers 1.0.0.
git add .
git commit -m "feat: initial release"
gh repo create tidy-downloads --public --source=. --push
The push kicks off the workflow immediately.
Verify it works
Watch the run finish, then list releases:
gh run watch
gh release list
Expected output:
TITLE TYPE TAG NAME PUBLISHED
v1.0.0 Latest v1.0.0 about 1 minute ago
In the Actions log you'll see the confirmation line Published release 1.0.0 on default channel, and the repo's Releases page now shows v1.0.0 with generated notes. From here on, push a fix: commit and CI cuts v1.0.1 with no action from you.
Troubleshooting
There are no relevant changes, so no new version is released.
Your commits don't match Conventional Commits, or they're all non-releasing types like chore:. Amend the message (git commit --amend -m "fix: ...") or push a new fix:/feat: commit.
ENOGHTOKEN No GitHub token specified.
The env: block with GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} is missing from the release step, or a typo renamed the variable. semantic-release only reads GH_TOKEN or GITHUB_TOKEN.
EGITNOPERMISSION Cannot push to the Git repository.
The job can't push the version tag. Usually the contents: write permission is missing from the workflow, or a branch protection ruleset blocks the github-actions bot. Add the permission, or exempt the bot in the ruleset's bypass list.
Next steps
Add @semantic-release/changelog and @semantic-release/git to also maintain a CHANGELOG.md in the repo, or @semantic-release/npm with trusted publishing once you're ready to ship to a registry. Enforce commit format locally with commitlint, and add issue templates plus a CODE_OF_CONDUCT.md when contributors show up.
Sources & further reading
- Using semantic-release with GitHub Actions — semantic-release.gitbook.io
- semantic-release Configuration — semantic-release.gitbook.io
- Conventional Commits 1.0.0 — conventionalcommits.org
- gh repo create — cli.github.com
- actions/checkout releases — github.com
- actions/setup-node — github.com
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 3
semantic-release is solid for javascript projects, but how do you handle the case where your tool isn't a node package—like a go binary or python script? do you just vendor the release logic into your workflow, or is there a cleaner pattern you're using in practice?
sounds nice in theory, but i burned a week once debugging semantic-release on a monorepo where conventional commits weren't enforced upstream — devs just... weren't following the format, and nothing published for three sprints. the automation only works if you actually have the discipline around it. how do you keep the commit message format from drifting once the novelty wears off?
the semantic-release automation piece definitely saves headaches—we went through a few manual release cycles on one of our internal tools before wiring this up, and the number of times someone forgot to bump the patch version or tag inconsistently was embarrassing. the real win though is just not having to think about it once the workflow is running. makes handing off maintenance way less painful.