From 886c618450c3beb986f2b43b2cd5b9df5e46d134 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Thu, 6 Aug 2026 16:10:46 -0400 Subject: [PATCH] Fix changelog ordering for modpublisher's own tag/release publishing modpublisher's `changelog = file("CHANGELOG.md")` (Archie/build.gradle.kts) reads that file straight off disk at publish time - it has no idea about git tags or PRs. release-notes.yaml is reactive: it only generates a release's changelog entry *after* seeing its tag pushed, landing it via a PR that needs a human merge. If modpublisher creates the tag as part of the same `./gradlew publish*` invocation that reads `changelog`, that's a hard ordering conflict, not a race that sometimes loses - the entry can't exist yet. And if that invocation runs in CI under the default GITHUB_TOKEN, the tag it creates won't even fire release-notes.yaml at all, per GitHub's anti-recursion rule for that token. Adds a `generateChangelog` Gradle task that runs generate_release_notes.py synchronously, writing Archie/CHANGELOG.md right before any publish task reads it - `publishCurseforge`/`publishModrinth`/`publishGitHub`/`publishMod` all now `dependsOn` it. No GitHub Actions dependency, so it works locally or in CI regardless of token/trigger semantics. Since the release tag doesn't exist yet at this point, the script gains `--range-end` (defaults to `--new-tag`, matching the existing post-tag behavior) so a real ref like HEAD can be walked while `--new-tag` carries the not-yet-real intended version for display/heading purposes. `--posts-dir` is now optional - the Gradle task only regenerates the changelog, not the news post, which has no such ordering requirement and stays release-notes.yaml's job. Verified via `./gradlew publishCurseforge/publishModrinth/publishGitHub/ publishMod --dry-run` (generateChangelog appears once, correctly ordered before each) and a real `./gradlew generateChangelog` run against this repo's actual full history (no tag exists yet, so it exercised the root-commit fallback too) - reverted the resulting CHANGELOG.md back to its stub afterward since that was a verification run, not a real release. Co-Authored-By: Claude Sonnet 5 --- .github/scripts/generate_release_notes.py | 70 ++++++++++++++++------- AGENTS.md | 13 ++++- Archie/build.gradle.kts | 21 +++++++ 3 files changed, 79 insertions(+), 25 deletions(-) diff --git a/.github/scripts/generate_release_notes.py b/.github/scripts/generate_release_notes.py index 0768f8c3f..26cc2fe05 100644 --- a/.github/scripts/generate_release_notes.py +++ b/.github/scripts/generate_release_notes.py @@ -1,14 +1,26 @@ #!/usr/bin/env python3 -"""Generates a CHANGELOG.md section and a mkdocs-material news/blog post for a tagged release. - -Walks the first-parent history between the previous tag (auto-detected if not given) and the -new tag. Each entry on that line is either a merged pull request (its title, author, and URL are -looked up via `gh pr view`) or a commit pushed directly to the branch (its own subject line is -used as-is). Entries are parsed for a Conventional Commits prefix (`feat:`, `fix:`, ...) and -grouped into sections; anything that doesn't parse lands in "Other Changes" rather than being -dropped, since pre-adoption history won't be Conventional-Commits-shaped. - -Requires `git` (full history - the caller must checkout with fetch-depth: 0) and the `gh` CLI +"""Generates a CHANGELOG.md section and/or a mkdocs-material news/blog post for a release. + +Walks the first-parent history between the previous tag (auto-detected if not given) and +--range-end (a real ref - defaults to --new-tag, but see below). Each entry on that line is +either a merged pull request (its title, author, and URL are looked up via `gh pr view`) or a +commit pushed directly to the branch (its own subject line is used as-is). Entries are parsed for +a Conventional Commits prefix (`feat:`, `fix:`, ...) and grouped into sections; anything that +doesn't parse lands in "Other Changes" rather than being dropped, since pre-adoption history won't +be Conventional-Commits-shaped. + +Two call shapes: + - Post-tag (.github/workflows/release-notes.yaml): --new-tag is a real, already-pushed tag; used + as both the git ref to end the walk at and the version display string. Writes both the + changelog section and a news post. + - Pre-publish (Archie/build.gradle.kts's `generateChangelog` task): the tag doesn't exist yet at + this point, so pass --new-tag as the *intended* version (e.g. `v1.2.0`, not yet a real ref) + together with --range-end HEAD (or another real ref) to walk up to. Only pass --changelog-path, + not --posts-dir, in this mode - modpublisher's `changelog = file(...)` needs CHANGELOG.md + correct *before* it tags/publishes; the news post has no such ordering requirement and stays + the reactive post-tag workflow's job. + +Requires `git` (full history - the caller must checkout/clone with full depth) and the `gh` CLI (authenticated via GH_TOKEN) for PR metadata lookups. See .github/workflows/release-notes.yaml. """ from __future__ import annotations @@ -209,19 +221,30 @@ def write_news_post(posts_dir: Path, repo: str, version_display: str, date: str, def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--repo", required=True, help="owner/name") - parser.add_argument("--new-tag", required=True) + parser.add_argument("--new-tag", required=True, + help="version identity for headings/filenames - a real tag ref in the " + "post-tag flow, or the not-yet-created intended tag in the " + "pre-publish flow (see --range-end)") + parser.add_argument("--range-end", default=None, + help="real git ref to end the walk at; defaults to --new-tag. Set this " + "explicitly (e.g. HEAD) when --new-tag isn't a ref that exists yet") parser.add_argument("--prev-tag", default=None, help="auto-detected if omitted") - parser.add_argument("--changelog-path", required=True, type=Path) - parser.add_argument("--posts-dir", required=True, type=Path) + parser.add_argument("--changelog-path", type=Path, default=None) + parser.add_argument("--posts-dir", type=Path, default=None, + help="omit to skip news-post generation (e.g. the pre-publish flow)") parser.add_argument("--dry-run", action="store_true", help="print instead of writing files") args = parser.parse_args() - prev_tag = args.prev_tag or detect_previous_tag(args.new_tag) + if not args.dry_run and not args.changelog_path and not args.posts_dir: + parser.error("nothing to do - pass --changelog-path and/or --posts-dir, or --dry-run") + + range_end = args.range_end or args.new_tag + prev_tag = args.prev_tag or detect_previous_tag(range_end) start = range_start(prev_tag) - records = walk_first_parent(start, args.new_tag) + records = walk_first_parent(start, range_end) if not records: - print(f"No commits between {prev_tag or '(root)'} and {args.new_tag} - nothing to generate.") + print(f"No commits between {prev_tag or '(root)'} and {range_end} - nothing to generate.") return entries = [resolve_entry(args.repo, sha, subject, parents) for sha, subject, parents in records] @@ -237,12 +260,15 @@ def main() -> None: print(body) return - update_changelog(args.changelog_path, f"[{version_display}]", date, body) - post_path = write_news_post( - args.posts_dir, args.repo, version_display, date, prev_tag, body, len(entries), - ) - print(f"Updated {args.changelog_path}") - print(f"Wrote {post_path}") + if args.changelog_path: + update_changelog(args.changelog_path, f"[{version_display}]", date, body) + print(f"Updated {args.changelog_path}") + + if args.posts_dir: + post_path = write_news_post( + args.posts_dir, args.repo, version_display, date, prev_tag, body, len(entries), + ) + print(f"Wrote {post_path}") if __name__ == "__main__": diff --git a/AGENTS.md b/AGENTS.md index d05d1a846..5ad2bdc42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,9 +47,16 @@ All commands below are run from inside `Archie/` (`cd Archie` first). ## Dependency and integration touchpoints - Versions and plugin IDs are centralized in `gradle/libs.versions.toml` (repo root); update there first. - Packaging/publishing is configured at the `Archie/` build root via `modfusioner` (`fusejars`) and - `modpublisher` (CurseForge/Modrinth IDs and required deps) in `Archie/build.gradle.kts` - `modpublisher` - reads its changelog text straight from `Archie/CHANGELOG.md`, the same file the release-notes workflow - maintains. + `modpublisher` (CurseForge/Modrinth/GitHub IDs and required deps, tasks `publishCurseforge`/ + `publishModrinth`/`publishGitHub`/`publishMod`) in `Archie/build.gradle.kts` - `modpublisher` reads its + changelog text straight off disk from `Archie/CHANGELOG.md` when a publish task runs, so those four + tasks `dependsOn` a `generateChangelog` task (same file, same script, same `Archie/build.gradle.kts`) + that regenerates it synchronously first. This is deliberately *not* left to the reactive, tag-triggered + `release-notes.yaml` workflow: if `modpublisher` auto-tags as part of the same `./gradlew publish*` + invocation, that workflow can't possibly have generated this release's entry yet by the time + `changelog` is read, and if that invocation runs in CI under the default `GITHUB_TOKEN`, the tag it + creates won't even fire the workflow (GitHub's anti-recursion rule for that token). `release-notes.yaml` + still owns the `Archie/docs/news/posts/` blog entry, which has no such ordering requirement. - Mixins are split by scope: loader mixins in `Archie/fabric/src/main/resources/archie.mixins.json` and `Archie/neoforge/src/main/resources/archie.mixins.json`, common mixin config in `Archie/common/src/main/resources/archie-common.mixins.json`. diff --git a/Archie/build.gradle.kts b/Archie/build.gradle.kts index 15c3d1a98..1cc02c162 100644 --- a/Archie/build.gradle.kts +++ b/Archie/build.gradle.kts @@ -265,4 +265,25 @@ tasks { workingDir = rootDir commandLine("mike", "deploy", "--push", "--update-aliases", tag, "latest") } + // modpublisher's `changelog = file("CHANGELOG.md")` (see the `publisher { }` block above) + // reads that file straight off disk when a publish task runs - it doesn't know about git tags + // or PRs. .github/workflows/release-notes.yaml (reactive, post-tag) can't help here: by the + // time it would generate this release's entry, the publish task attached to the tag has + // already read (and shipped) whatever was on disk before. This task closes that gap by + // generating CHANGELOG.md synchronously, right before any publish task reads it - see + // .github/scripts/generate_release_notes.py's module docstring for the two call shapes. + register("generateChangelog") { + group = "publishing" + workingDir = rootDir + commandLine( + "python3", "../.github/scripts/generate_release_notes.py", + "--repo", "mod_source".prop!!.removePrefix("https://github.com/"), + "--new-tag", "v${project.version}", + "--range-end", "HEAD", + "--changelog-path", "CHANGELOG.md", + ) + } + listOf("publishCurseforge", "publishModrinth", "publishGitHub", "publishMod").forEach { + named(it) { dependsOn(getByName("generateChangelog")) } + } }