Um momento
0xD0Lesson 14 of 14

Capstone: build the release bot

Turn merged pull requests into a version number, release notes, closed issues and contributor thanks - the bot Dungeon Dash runs on every release.

45 min 6-question quiz 3 code exercises
By the end of this lesson you can
  • Classify merged pull requests from their conventional-commit titles
  • Generate grouped Markdown release notes with links and authors
  • Combine versioning, notes, closed issues and contributor thanks into one release

You’ve come a long way: from your first pull request to reviewing others’, wiring up CI, and hardening the project’s workflows. The other maintainers have one last request: automate releases. Writing release notes by hand takes an afternoon, and somebody always forgets to thank a contributor.

The bot reads the last release tag and the pull requests merged since (as the API returns them - squash-merged, so each title is a conventional commit), and produces:

1v1.5.0
2
3## v1.5.0
4
5### Features
6- boss battles (#128) @mira
7- **audio:** victory music (#134) @ana
8
9### Fixes
10- **physics:** stop wall clipping after a dash (#131) @kai
11- faster dungeon generation (#137) @mira
12
13### Other changes
14- explain the controls (#133) @sam-sprites
15- bump pillow to 10.3.0 (#136) @dependabot[bot]
16
17Closes: #42, #43, #30, #57, #61
18New contributors: @ana, @kai

The plan

Everything here is a piece you’ve already built:

  1. Classify each PR title (conventional commits lesson): type, optional scope, ! or a BREAKING CHANGE: note in the body.
  2. Version from the most significant change (semver lesson).
  3. Notes: group entries under headings, with the PR number and author (Markdown lesson).
  4. Closed issues from closing keywords in the PR bodies (issues lesson).
  5. Contributors: thank everyone, and welcome first-timers - people not in the list of earlier contributors.

Build it in that order in the three exercises: classify and version, then notes, then the whole release.

classify.py
1import re
2
3TITLE = re.compile(r"(?P<type>\w+)(?:\((?P<scope>[\w-]+)\))?(?P<bang>!)?: (?P<subject>.+)")
4for title in ["feat(audio): victory music", "fix!: drop the old save format", "Update README"]:
5    match = TITLE.match(title)
6    print(match.groupdict() if match else f"not conventional: {title}")
Output
{'type': 'feat', 'scope': 'audio', 'bang': None, 'subject': 'victory music'}
{'type': 'fix', 'scope': None, 'bang': '!', 'subject': 'drop the old save format'}
not conventional: Update README

Named groups ((?P<type>...)) make the parsing readable, and titles that don’t follow the convention fall into “Other changes” rather than crashing the bot - real repositories always have a few.

Running it on GitHub

Once the script works, a workflow runs it at release time: a maintainer clicks Run workflow, the bot computes the version and notes, and gh release create publishes them. Notice the permissions and pinned versions from the security lesson:

.github/workflows/release.yml
1name: Release
2on:
3  workflow_dispatch:
4
5permissions:
6  contents: write          # to create the tag and release
7  pull-requests: read
8
9jobs:
10  release:
11    runs-on: ubuntu-latest
12    environment: release   # requires a maintainer's approval
13    steps:
14      - uses: actions/checkout@v4
15        with:
16          fetch-depth: 0   # full history, so the last tag is available
17      - name: Collect merged pull requests
18        env:
19          GH_TOKEN: ${{ github.token }}
20        run: |
21          last_tag=$(git describe --tags --abbrev=0)
22          echo "$last_tag" > input.txt
23          gh pr list --state merged --search "merged:>$(git log -1 --format=%cI "$last_tag")" \
24            --json number,title,author,body --jq '.[] | {number, title, author: .author.login, body}' -c >> input.txt
25      - name: Build the release
26        run: python scripts/release_bot.py < input.txt > release.md
27      - name: Publish
28        env:
29          GH_TOKEN: ${{ github.token }}
30        run: gh release create "$(head -n 1 release.md)" --notes-file <(tail -n +3 release.md)

Where to go next

  • Contribute for real: find a project you use, read its CONTRIBUTING.md, and pick a good first issue.
  • Automate your own repos: add CI, Dependabot and a release workflow to a project of yours.
  • Go deeper: reusable workflows and composite actions, GitHub Apps, Codespaces for cloud dev environments, and GitHub Copilot.
  • Release tooling: release-please and semantic-release do what your bot does, for many ecosystems.

Key takeaways

  • Conventional PR titles make versions and release notes computable.

  • Group notes by kind, link PRs and credit authors - and handle titles that don’t follow the rules.

  • Closing keywords and contributor lists come straight from the merged PRs.

  • Run release automation in a workflow with least-privilege permissions and an approval gate.

Lesson quiz

6 questions · pass with 5 correct · up to 50 XP

Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.

Practice: automate GitHub chores with Python

Real GitHub work involves lots of small automation: matching CODEOWNERS, expanding build matrices, bumping versions, reading the API. Write those helpers in Python and run them against sample inputs - locally in your browser, with no GitHub account needed.

Exercise 1

Classify the merged PRs

+25 XP

The first input line is the last release tag; each following line is a merged PR as JSON with number, title, author and body. For each PR print #128 feature: boss battles (kinds: breaking for a ! or a BREAKING CHANGE: in the body, feature for feat, fix for fix and perf, otherwise other; non-conventional titles are other with the whole title). Then print next: v1.5.0, or next: none when nothing is releasable.

  • Spring release
  • Big release
  • Nothing to release
main.py
Loading editor…

Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.

Exercise 2

Write the release notes

+25 XP

Using the same input, print Markdown release notes: ## v1.5.0, then - each preceded by a blank line - sections ### ⚠️ Breaking changes, ### Features, ### Fixes and ### Other changes in that order, skipping empty ones. Each entry is - subject (#N) @author, with **scope:** before the subject when there is a scope. If nothing is releasable, print No release needed. instead.

  • Spring release
  • Big release
  • Nothing to release
main.py
Loading editor…

Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.

Exercise 3

Ship the whole release

+25 XP

Put it all together. The input is the same, plus a final line known: mira sam-sprites ... listing everyone who contributed before. Print:

  1. the version on its own line, then a blank line, then the release notes (as in the last exercise);
  2. a blank line, then Closes: #42, #43 - every issue closed by a closing keyword in a PR body (keyword before each reference), in order, without duplicates - or Closes: none;
  3. New contributors: @ana, @kai - authors not in the known list, sorted, ignoring bots (names ending in [bot]) - or New contributors: none.

If nothing is releasable, print only No release needed.

  • Spring release
  • Big release
  • Nothing to release
main.py
Loading editor…

Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.

Questions about this lesson

Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.

Loading posts…

Gostou da aula? 😆👍
Apoie nosso trabalho com uma doação: