Skip to content

CI: a tag release ends with a GitHub Release built from the version's milestone - #254

Merged
tae898 merged 2 commits into
mainfrom
ci/github-release-notes
Nov 5, 2026
Merged

tae898 merged 2 commits into
mainfrom
ci/github-release-notes

Conversation

@tae898

@tae898 tae898 commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #253

A tag release now ends with a GitHub Release whose notes come from the version's milestone, and a final release closes the milestone.

Design. release-notes.yml is a separate workflow on workflow_run of Release native (arcadedb) and Release JVM (arcadedb-jvm) (types completed), plus workflow_dispatch with a tag for a version that is already published. I chose this over a last job in each release workflow because the two workflows do not wait for each other, take different times, and either can need a publish-only re-run: a job inside one of them cannot know the other finished. workflow_run fires when each one completes, including after a re-run. Each firing looks up both workflows for the tag commit (head_sha) and continues only when both concluded success, so the first to finish exits quietly with a notice and the second creates the release. It is not a PyPI poll: a poll needs a deadline and a sleep loop, and a workflow already only concludes success after its own upload (and for the JVM package after the install from PyPI on every platform). PyPI is still asked once for both versions as the last gate, and a mismatch fails the run. If both firings run at once, one creates and the other edits the release (and a concurrency group serialises them), so the result is the same.

What each piece does.

  • release-notes.yml: gate (tag regex, both release runs success, both versions on PyPI), notes, gh release create --verify-tag (or gh release edit when it exists), then the milestone. A.B.C.devN is a pre-release, A.B.C and A.B.C.postN a normal release marked latest. Only a final A.B.C closes the milestone A.B.C, and before that it moves what is still open in it (issues and PRs) to the next milestone, logging every move. The next milestone is the open one with the earliest due date after this one. When there is none, it creates one named after the next upstream engine version (read from the upstream main pom, the command already in release.adoc; today 26.12.1), due 30 days after the closed one. When that version cannot be read, or equals the base, nothing is moved or closed and the run fails with a message telling the maintainer to create the next milestone with a due date and re-run the workflow. A.B.C.post1 is not used as a name because post releases have no milestone. CI: resemble ArcadeData/arcadedb automation (labels, milestones, in-progress, PR template, Dependabot merge) #252's auto-assignment on merge then picks the next milestone, since the closed one is no longer open. Permissions: contents: read at the top, the job has contents: write (release), issues: write (milestone), pull-requests: read (merge commits) and actions: read (the two runs). No new secret, GITHUB_TOKEN only. The checkout and the tag go through env, never into a script body. Actions are pinned like the other workflows (actions/checkout by full SHA, # v7.0.1).
  • scripts/release_notes.py TAG [--dry-run] [--output FILE]: standard library plus gh api, GET only. The milestone is named after the base (26.11.1 for 26.11.1, 26.11.1.dev2, 26.11.1.post1). It lists merged PRs of the milestone, grouped by first matching label (bug Bug fixes, performance Performance, enhancement Features, documentation Documentation, else Other), each line title (#N), then install lines, the engine and the PyPI pages. A PR is in a tag when its merge commit is an ancestor of the tag (compare API), so a PR merged while the release runs is not listed. A final lists the whole milestone; .devN lists only PRs not in dev(N-1), so a preview does not repeat the last one. .postN does not use a milestone, because the final release closes it: it lists every PR whose merge commit lies between the previous tag of the same base (the final or post(N-1)) and the tag (compare API for the commits, commits/{sha}/pulls for the PR of each), whatever milestone the PR has or none. A tag that does not exist yet is read as the tip of main, so --dry-run works before the tag. A missing milestone gives notes without a change list and a warning, not a failure.
  • Docs: release.adoc has a new section "GitHub Release and milestone" (what runs, which tags get what, how to preview with --dry-run, the move of open items and the failure case) and a --dry-run line in the commands; .github/ISSUE_TEMPLATE/release.md previews the notes at the go step and checks the release and the milestone at the end; ci.adoc and CLAUDE.md name the workflow. The README does not describe the release flow, so it is unchanged.

How it was checked.

  • uvx --from actionlint-py actionlint .github/workflows/release-notes.yml (the repo's .github/actionlint.yaml applies): no findings. uvx pre-commit run --files <changed> run twice: all hooks pass (the YAML formatter skips .github/, shfmt had no files).
  • YAML parses (yaml.safe_load), compile() and pyflakes on the script are clean.
  • python3 -I scripts/release_notes.py 26.11.1 --dry-run against the live repository lists the six merged PRs of the milestone (below); 26.11.1.dev2 (an existing tag) reads from the tag and lists nothing since dev1, and v26.11.1 exits 1.
  • The gate step's run: block was extracted and run read-only with INPUT_TAG=26.11.1.dev2: both release workflows success for 662851c, both versions on PyPI, ready=true.
  • The jq lookup of the milestone was run against the live repository (1 open 18). The move-and-close step was run read-only with a stub gh that executes GETs for real and only prints the PATCH and POST calls: with no next milestone it would create 26.12.1 (due 2026-12-10) and move the open items one by one; with an unreadable upstream pom it exits 1 with the message and writes nothing; for a base without a milestone it warns and exits 0.
  • 26.11.1.post1 was dry-run as a hypothetical tag: it lists the 73 PRs merged since 26.11.1.dev2 (78 commits), including ones with no milestone. No release, tag or milestone was created or changed. No pytest, docker or java was run, and I added no unit tests for the script.

Dry run for 26.11.1 today (the tag does not exist yet; #194 now has its corrected title):

### Bug fixes

- native: save() refuses to overwrite a record changed after it was read (#194)
- Native insert_many: date-times, Decimals, bytes, numpy scalars and pandas NA stay on the JSON bulk path; an uncarriable value moves its own row, not the chunk (#218)

### Performance

- Native find_nearest: send the query vector as one float32 array, not a Python list (#203)
- Native to_json_list: skip the walk for untagged results; RIDs and map keys JSON-safe (#223)
- Native GraphBatch: float32 vectors in a binary side buffer (arcadedb_n_gb_bin) (#224)
- Native transactions: no JSON decode for a status the caller does not read (#226)
- insert_many (JVM build): pause the cyclic garbage collector for a list or tuple input (#236)
- Native iter_json_batches reads the cursor one batch at a time (#242)

What the first real run will show. workflow_run workflows run the file on the default branch, so nothing happens until this is merged. The first real run is the next tag: the first firing logs a notice and creates nothing, the second creates the release. To try it earlier, run GitHub Release by hand (Actions, workflow_dispatch) with 26.11.1.dev2: that creates a real pre-release for an existing tag. Still to confirm on a real run: that head_branch is the tag name for a tag push (the docs say so), that GITHUB_TOKEN may edit the milestone with issues: write, and that the repository setting for Actions allows the workflow to write releases. After the final 26.11.1 the 18 items open today move to the next open milestone, or to a new 26.12.1 created by the run; the milestone step was exercised only with a stub gh that logs writes (see below), never against the live milestone.

🤖 Generated with Claude Code

… milestone (#253)

workflow_run on both release workflows, not a last job in each: the two do not wait for each other and either can need a publish-only re-run, so each firing checks both for the tag commit and the second to finish creates the release. A PyPI check for both versions is the last gate. Notes come from scripts/release_notes.py (merged PRs of the milestone named after the base, in the tag and not in the previous tag of the same base, grouped by label), devN is a pre-release, a final release closes its milestone. Only contents/issues write plus two read scopes, no secret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tae898 tae898 added the github_actions Pull requests that update GitHub Actions code label Nov 5, 2026
…en items to the next milestone (#253)

The final release closes the base milestone, so a postN cannot depend on it: its notes now come from the PRs whose merge commit lies between the previous tag of the same base and the tag, whatever milestone they carry. Closing a milestone with open items would strand them, so a final release first moves them to the open milestone due next; with none it creates one named after the next upstream engine version (due 30 days later), and when that cannot be read it moves and closes nothing and fails with a message, because guessing a name (A.B.C.post1) would be wrong under the no-milestone rule for post releases. Every move is logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tae898
tae898 merged commit 1630ec8 into main Nov 5, 2026
29 checks passed
@tae898
tae898 deleted the ci/github-release-notes branch November 5, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Releases end on PyPI: no GitHub Release, no notes, milestone closed by hand

1 participant