This repo provides tooling to manage releases for Agile sprint periods.
The preferred way to create a release is the guided release-wizard.py script — see Release Wizard (Recommended) below. It walks you through the entire process end to end (pick a sprint → review in-scope PRs → create release candidates → merge them → verify → generate release notes → publish to Confluence), confirming at every step.
The wizard orchestrates a set of underlying scripts. You don't need to run these directly for a normal release, but they remain fully available and documented for finer control or for performing a single step manually (see Manual Usage (Individual Scripts)):
-
main.py- Outputs merged PRs from the last 2-week sprint for every repo within the given organization. Shows PR details including commit IDs and tags. -
unmerged-prs.py- Finds open PRs that were approved within a sprint window but not yet merged. Useful for identifying work that is ready to ship but hasn't been released. -
create-release-candidate.py- Creates release candidate PRs from dev to release branches for repositories with unreleased changes. Automatically determines the correct RC version number. -
create-release-notes.py- Generates markdown release notes for a sprint version by analyzing merged RC PRs and merge commits on release branches. -
create-confluence-page.py- Publishes a release notes markdown file to Confluence as a new page under the appropriate year page.
Only repositories tagged with the topic from DEPLOYABLE_TOPIC in .env are considered deployable (defaults to deployer-php). If you do not already have a .env file configured for this repo, one will be created and configured for you.
These are required before running any of the scripts, including the wizard:
-
Dependencies installed —
pip install httpx python-dotenv markdown(see Dependencies for details). -
A GitHub Personal Access Token to authenticate with GitHub.
- Generate one at GitHub Personal Access Tokens with the permissions you need:
- Read-only (e.g. viewing sprint PRs with
main.py):read:organdrepo(read access) - Creating PRs (the wizard and
create-release-candidate.py):read:organd fullrepo
- Read-only (e.g. viewing sprint PRs with
- On first run you'll be prompted for the token and a
.envwill be created automatically; you can also do this manually viacp .env.example .envand paste the token in yourself.
- Generate one at GitHub Personal Access Tokens with the permissions you need:
-
A configured
.env— setORG_NAME(and optionalTEAM_NAME). To publish release notes to Confluence, also setCONFLUENCE_EMAILandCONFLUENCE_API_TOKEN.
release-wizard.py is the primary, recommended way to cut a release. It ties the whole process together into an interactive, step-by-step flow with an explicit confirmation at every gate. It is a thin orchestrator: it reuses the sprint-date logic from src/github_utils.py and runs the underlying scripts under the hood (parsing their output), so their individual behavior is unchanged.
For a normal release you only need this script — you do not need to run the individual scripts yourself.
First-time setup: make sure you've met the Prerequisites above — dependencies installed and your .env configured (GitHub token, org/team, and Confluence credentials for the publish step).
Usage: python3 release-wizard.py [org=<org name>] [team=<team name>]
Arguments:
[org=<org name>](optional) - GitHub organization name. If omitted,ORG_NAMEfrom.envis used. The wizard aborts immediately if neither is set.[team=<team name>](optional) - Filter by team. If omitted,TEAM_NAMEfrom.envis used when present.
The guided flow:
- Select a sprint - choose previous, current, or next; each is shown with its
vYYYY.WWversion and date window. The current sprint is the default (press Enter to select it). - Review PRs in scope - runs
main.py format=consoleand shows its output so you can see exactly what merged this sprint. If the sprint has no PRs (common for the next sprint), you can pick a different one. - Create release candidates - once you confirm, the wizard generates the JSON input (
output/vYYYY.WW.json), previews with a dry run, asks for explicit confirmation, then runscreate-release-candidate.pyand lists the created RC PR links. - Merge the RC PRs (manual) - the wizard prints the PR links and waits. You open and merge each one yourself in GitHub; this friction is intentional.
- Verify - re-runs the dry run to confirm which repos are now fully merged versus still have an open, un-merged RC PR. If any remain un-merged, it loops back so you can merge them.
- Generate release notes - runs
create-release-notes.py --output(writingoutput/vYYYY.WW-release-notes.md) and previews the result for your confirmation. - Publish to Confluence (optional) - if
CONFLUENCE_EMAIL/CONFLUENCE_API_TOKENare set and you confirm, runscreate-confluence-page.py(with an optional--draft). If the Confluence credentials are missing, this step is skipped gracefully and the notes file is left inoutput/.
You can abort at any prompt by typing q (or quit/abort); no further actions are taken.
Resuming an interrupted run: the wizard records its progress to output/.wizard-progress.json (git-ignored). If you quit, are interrupted, or close the terminal partway through, the next run detects the unfinished release and offers to resume it — either at the creating/merging release candidates stage or the release notes stage. Resuming re-checks the live state on GitHub (which RC PRs exist and which are merged), so it continues correctly regardless of how far the previous run got. You can also decline and either discard the saved progress or start a different sprint. Progress is cleared automatically once a release completes.
Example:
python3 release-wizard.py org=my-orgNote: This script reuses the underlying scripts, so it relies on the same .env configuration (GitHub token, org/team, and Confluence credentials for the publish step).
For most releases you should use the Release Wizard above, which runs these scripts for you in the correct order. The sections below document the individual scripts for finer control, troubleshooting, or running a single step on its own.
This script retrieves merged PRs from the last 2-week sprint and outputs details including commit IDs and tags.
It also surfaces nested feature-branch merges: if a PR was merged into another feature branch and that branch was later merged to dev via its own PR, the inner PR is included in the output even though its base branch is not dev. These entries are annotated with the dev PR that carried them into the release (Note: merged into dev via #<n> in console/text/markdown, merged_via_pr in JSON). This relies on standard Merge pull request #N merge commits; feature-to-feature PRs that were squash- or rebase-merged are not detected.
Repository order in the output follows the optional RELEASE_REPO_ORDER key in .env (same rules as create-release-notes.py). When unset, repositories are ordered alphabetically by full name.
Usage: python3 main.py [org=<org name>] [team=<team name>] [name=<repo name>] [format=console|text|json|markdown] [branch=all|release|dev] [week=YYYY.WW|YYYY.WW-WW|YYYY.WW-YYYY.WW] [offset=<Nh|Nd|Nw>]
Arguments:
[org=<org name>](optional) - GitHub organization name. If omitted,ORG_NAMEfrom.envis used.[team=<team name>](optional) - Filter by specific team. If omitted,TEAM_NAMEfrom.envis used when present.[name=<repo name>](optional) - Filter output to one repository. Supports repository name (for examplemy-service) or full name (for examplemy-org/my-service). If no exact match is found, the closest repository name is used.[format=console|text|json|markdown](optional) - Output format. Defaults tojson.consoleoutputs colored text for the terminal.textoutputs the same layout without ANSI colors.markdownproduces a table-based layout suitable for saving to a.mdfile.[branch=all|release|dev](optional) - Branch filter. Defaults todev.[week=YYYY.WW|YYYY.WW-WW|YYYY.WW-YYYY.WW](optional) - Week filter. Single week uses sprint behavior and must be even (end week). Range values include all dates from Monday of the start week through Sunday of the end week, interpreted inDATE_RANGE_TZ_OFFSET.[week-offset=<Nh|Nd|Nw>]or[offset=<Nh|Nd|Nw>](optional) - Shifts the date filter window by the offset. Examples:1d,1w,12h,1.5h.week=...is required whenname=<repo name>is provided.
Optional .env keys for main.py:
ORG_NAME=<your-org>TEAM_NAME=<your-team>DEPLOYABLE_TOPIC=<repo-topic>(defaults todeployer-php)DATE_RANGE_TZ_OFFSET=<timezone-offset>(defaults to0; examples:-8,5.5,+05:30)
Examples:
python3 main.py org=my-org– JSON output, dev branch only, current sprintpython3 main.py org=my-org format=console– text output, dev branch only, current sprintpython3 main.py org=my-org team=my-team format=json branch=dev– JSON output for specific team, dev branch onlypython3 main.py format=json– usesORG_NAMEfrom.env, outputs JSONpython3 main.py org=my-org format=console week=2026.08– text output for sprint ending in week 8 of 2026python3 main.py org=my-org team=my-team format=json week=2025.52– JSON output for specific team and sprintpython3 main.py org=my-org name=my-service format=console week=2026.08– text output for one repo in the specified sprint windowpython3 main.py org=my-org name=my-service format=console week=2026.07-11– one repo for ISO week range within 2026python3 main.py org=my-org name=my-service format=console week=2025.50-2026.05– one repo for ISO week range across yearspython3 main.py org=my-org name=my-service format=console week=2026.07-11 offset=12h– same range shifted by 12 hourspython3 main.py org=my-org format=markdown > sprint.md– markdown output saved to a file
Note: Scripts can also be run directly without python3:
./main.py org=my-orgIf that doesn't work, your Python installation may require python instead of python3.
Tip: When using format=console, pipe through less -R to preserve ANSI colors while scrolling:
./main.py org=my-org format=console | less -RThis script finds open PRs that received an approval within the sprint window but have not yet been merged. It takes the same arguments as main.py.
Usage: ./unmerged-prs.py [org=<org name>] [team=<team name>] [name=<repo name>] [format=console|text|json|markdown] [branch=all|release|dev] [week=YYYY.WW|YYYY.WW-WW|YYYY.WW-YYYY.WW] [offset=<Nh|Nd|Nw>] [--output]
Arguments: Same as main.py — see above.
Examples:
./unmerged-prs.py org=my-org– JSON output, dev branch only, current sprint./unmerged-prs.py org=my-org format=console week=2026.08– colored output for a specific sprint./unmerged-prs.py org=my-org format=console week=2026.08 --output– write tooutput/v2026.08-unmerged-prs.txt
How it works:
- Fetches open PRs sorted by
updated_atand stops when PRs predate the sprint window - For each candidate PR, concurrently fetches its reviews and its mergeability status
- Uses each reviewer's latest review within the sprint to determine their final state — so an approve followed by a request for changes counts as not approved
- Includes a PR only if at least one reviewer's final sprint review is
APPROVEDand the PR has no merge conflicts - PRs where GitHub hasn't computed mergeability yet (
null) are included to avoid missing forgotten PRs - Output shows the latest approval timestamp, PR details, and the list of approvers
- A summary at the end lists per-repo counts for both unmerged and merged approved PRs; in
consoleformat the unmerged count is bold yellow (action may be needed) and "no approved unmerged PRs" is bold green (all clear) - Repos with only approved merged PRs (no unmerged) appear in the summary for accuracy confirmation, but not in the main PR details
- JSON output includes
repositories_scanned,repositories_with_prs,total_approved_count,total_approved_merged_countat the top level andapproved_count/approved_merged_countper repository
This script takes the JSON output from main.py and creates release candidate PRs from dev to release branches (main/master) for repositories with unreleased changes. It automatically determines the correct RC version number by checking existing PRs.
Usage: python3 create-release-candidate.py <json_file> [--dry-run]
Arguments:
<json_file>(required) - JSON output file from main.py[--dry-run](optional) - Preview what would happen without creating PRs
Examples:
- Generate JSON from main.py:
python3 main.py org=my-org format=json > sprint-prs.json - Preview release candidates (dry run):
python3 create-release-candidate.py sprint-prs.json --dry-run
- Create release candidate PRs:
python3 create-release-candidate.py sprint-prs.json
How it works:
- Reads the sprint version from the JSON (e.g.,
v2026.10) - For each repository in the JSON input:
- Detects the release branch (
mainormaster) - Compares the
devbranch against the release branch to check for unreleased commits
- Detects the release branch (
- For repositories where
devis ahead of the release branch:- Checks for existing release candidate PRs (e.g.,
v2026.10-rc0,v2026.10-rc1) - Creates a new PR with the next RC number (e.g.,
v2026.10-rc2) - Creates PR from
devbranch to release branch (mainormaster)
- Checks for existing release candidate PRs (e.g.,
- Skips repositories where
devhas no commits ahead of the release branch - Works correctly regardless of whether the release branch uses regular or squash merges
Requirements:
- Your GitHub token must have full
repopermissions to create PRs - Repositories must have a
devbranch
This script generates markdown release notes for a release version (vYYYY.WW) across deployable repositories (topic from DEPLOYABLE_TOPIC, default deployer-php). It finds merged RC PRs for the target version and builds notes from commits tied to those RC PRs.
Usage: python3 create-release-notes.py [org=<org name>] [team=<team name>] [name=<repo name>] [week=YYYY.WW] [--output]
Arguments:
[org=<org name>](optional) - GitHub organization name. If omitted,ORG_NAMEfrom.envis used.[team=<team name>](optional) - Filter repositories by team. If omitted,TEAM_NAMEfrom.envis used when present.[name=<repo name>](optional) - Filter output to one repository. Supports repository name or full name. If no exact match is found, the closest repository name is used.[week=YYYY.WW](optional) - Target release version week (must be even). If omitted, current even week is used, or the most recent even week if the current week is odd.[--output](optional) - Write output tooutput/vYYYY.WW-release-notes.mdinstead of stdout.week=YYYY.WWis required whenname=<repo name>is provided.
Examples:
- Current/most recent release notes for an organization:
python3 create-release-notes.py org=my-org
- Notes for a specific team:
python3 create-release-notes.py org=my-org team=my-team
- Notes for a specific release week:
python3 create-release-notes.py org=my-org week=2026.08
- Team + specific release week:
python3 create-release-notes.py org=my-org team=my-team week=2026.08
- One repository + specific release week:
python3 create-release-notes.py org=my-org name=my-service week=2026.08
- Write release notes to file:
python3 create-release-notes.py org=my-org week=2026.08 --output
Output format:
- Markdown grouped by repository
- Repository order is controlled by the optional
RELEASE_REPO_ORDERkey in.env(see below). When unset, repositories are ordered alphabetically by full name - Includes release branch, current RC PR, and previous RC PR (if any)
- Each entry includes a PR link (
#<number>linking to the GitHub PR), PR title, and first non-empty paragraph from the PR body - Markdown formatting in that paragraph that would break the layout of the (markdown) release notes — headings, blockquotes, and emphasis (bold/italic) — is stripped. Bullet lists, links (
[text](url)), and inline code spans (`code`) are preserved; bullet lists render as a nested list under the entry - Any
PBT-XXXXreferences (4 digits) in rendered text are converted to GitLab issue links:https://pblgssgitlab01.aws.pointblue.org/point-blue-engineering-team/point-blue-tech/-/issues/XXXX
How it works:
- Reads organization/team repositories and keeps deployable repos only (topic from
DEPLOYABLE_TOPIC, defaultdeployer-php) - Detects the release branch (
mainormaster) - Looks for merged RC PRs matching the target version (for example
v2026.08-rc1) - Uses commits between the previous RC merge commit and the current RC merge commit
- If there is no previous RC, uses commits contained in the current RC PR
- Excludes RC commits and formats merged PR data as release notes
Repository ordering (RELEASE_REPO_ORDER):
- Optional
.envkey. A comma-separated list of patterns, highest priority first - A pattern containing
*is matched as a glob (e.g.*auth*matches any repo whose name containsauth); any other pattern is an exact match (e.g.my-org/api,my-login-app) - Each pattern is tested against both the repo's full name (
org/repo) and short name (repo), case-insensitively - Repos matching an earlier pattern are listed before repos matching a later pattern; repos matching no pattern are listed last. The first matching pattern wins for a repo that could match several
- Within any single group, repos are ordered alphabetically by full name
- Example:
RELEASE_REPO_ORDER='*dblib*,my-org/api,*auth*,my-login-app'listsdblib-named repos first (alphabetically), thenmy-orf/api, then any repo containingauth, thenmy-login-app, then everything else
This script reads a release notes markdown file (produced by create-release-notes.py) and creates a new Confluence page for that sprint release under the appropriate year page.
Usage: python3 create-confluence-page.py <release-notes-file> [--draft]
Arguments:
<release-notes-file>(required) - Path to the markdown file, e.g.output/v2026.10-release-notes.md[--draft](optional) - Create the page as an unpublished draft. Skips the duplicate page check. Useful for testing formatting before the real page exists.
Examples:
- Publish release notes for a sprint:
python3 create-confluence-page.py output/v2026.10-release-notes.md
- Create an unpublished draft to preview formatting:
python3 create-confluence-page.py output/v2026.10-release-notes.md --draft
- Full workflow — generate then publish:
python3 create-release-notes.py org=my-org week=2026.10 --output python3 create-confluence-page.py output/v2026.10-release-notes.md
How it works:
- Parses the version (e.g.
v2026.10) and year from the filename - Page title is set to
v2026.10 Release - Aborts with an error if a page with that title already exists in Confluence
- If the year page (e.g.
v2026 Releases) doesn't exist yet, it is created automatically under the main releases page - Converts the markdown to Confluence storage format and creates the sprint page under the year page
Required .env keys:
CONFLUENCE_EMAIL— your Atlassian account emailCONFLUENCE_API_TOKEN— generate one by going to your Atlassian account settings → Security → API tokens
- httpx
- python-dotenv
- markdown (required for
create-confluence-page.py)
In order to run this program, you will need to have installed the proper dependencies.
You can do this by opening an IDE and running the command pip install {example_module} for each dependency you are missing.