Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 48 additions & 22 deletions .github/scripts/generate_release_notes.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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__":
Expand Down
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
21 changes: 21 additions & 0 deletions Archie/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Exec>("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")) }
}
}