Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
b34718a
ci(release): guard prepare-release against accidental major bumps
MarTrepodi Oct 13, 2026
1717519
ci(ci): reject the bang shorthand for breaking changes in commitlint
MarTrepodi Oct 13, 2026
93e05eb
docs: document the accidental major bump gotcha in commit messages
MarTrepodi Oct 13, 2026
78c48b3
build(deps): Bump typing-extensions from 4.15.0 to 4.16.0
dependabot[bot] Oct 13, 2026
da0031d
ci(ci): pin commitlint once and run the same hook in CI
MarTrepodi Oct 13, 2026
8440cf3
ci(ci): group Dependabot updates and stop linting its own commits
MarTrepodi Oct 13, 2026
27cce7b
ci: run ruff from uv.lock in CI and pre-commit
MarTrepodi Oct 13, 2026
4edfb76
refactor(helpers): narrow TB reward table ids to str in get_tb_missio…
MarTrepodi Oct 13, 2026
0cdc467
fix(parity): pass strict=True to zip in the list comparators
MarTrepodi Oct 13, 2026
a605e79
style(examples): apply ruff format to the Sync and Async examples
MarTrepodi Oct 13, 2026
2c35cf5
style(tests): end the JSON fixtures with a newline
MarTrepodi Oct 13, 2026
d0e2590
ci: check formatting of examples and parity, and enforce it
MarTrepodi Oct 13, 2026
f7f2b18
style(examples): format the Async README code block for ruff 0.17
MarTrepodi Oct 13, 2026
7a20532
Merge pull request #184 from swgoh-utils/fix/tb-reward-table-ids
MarTrepodi Oct 13, 2026
95c05f3
Merge pull request #174 from swgoh-utils/ci/guard-major-bump
MarTrepodi Oct 13, 2026
1e7f831
Merge pull request #181 from swgoh-utils/ci/pin-commitlint
MarTrepodi Oct 13, 2026
51e7f7b
Merge branch 'develop' into ci/dependabot-groups
MarTrepodi Oct 13, 2026
8d823a5
Merge pull request #182 from swgoh-utils/ci/dependabot-groups
MarTrepodi Oct 13, 2026
4a72730
Merge branch 'develop' into dependabot/uv/develop/typing-extensions-4…
MarTrepodi Oct 13, 2026
01ac8f1
Merge pull request #179 from swgoh-utils/dependabot/uv/develop/typing…
MarTrepodi Oct 13, 2026
1ee2236
build(deps-dev): Bump pytest from 9.0.3 to 9.1.1
dependabot[bot] Oct 13, 2026
e14a93e
build(deps-dev): Bump ty from 0.0.56 to 0.0.86
dependabot[bot] Oct 13, 2026
489f40e
Merge pull request #176 from swgoh-utils/dependabot/uv/develop/pytest…
MarTrepodi Oct 13, 2026
325de73
build(deps-dev): Bump mkdocs-material from 9.7.6 to 9.7.7
dependabot[bot] Oct 13, 2026
c595fb8
Merge pull request #177 from swgoh-utils/dependabot/uv/develop/mkdocs…
MarTrepodi Oct 13, 2026
7614fed
build(deps-dev): Bump git-changelog from 2.9.4 to 2.10.0
dependabot[bot] Oct 13, 2026
b404c93
Merge pull request #178 from swgoh-utils/dependabot/uv/develop/git-ch…
MarTrepodi Oct 13, 2026
db01de6
Merge branch 'develop' into dependabot/uv/develop/ty-0.0.86
MarTrepodi Oct 13, 2026
bb36d9b
Merge pull request #175 from swgoh-utils/dependabot/uv/develop/ty-0.0.86
MarTrepodi Oct 13, 2026
aa41563
Merge branch 'develop' into ci/ruff-single-source
MarTrepodi Oct 13, 2026
d31b3d2
Merge pull request #183 from swgoh-utils/ci/ruff-single-source
MarTrepodi Oct 13, 2026
81dfe05
Merge pull request #185 from swgoh-utils/fix/pre-commit-all-files
MarTrepodi Oct 13, 2026
b509dd1
build(deps-dev): Bump ruff from 0.15.15 to 0.17.0
dependabot[bot] Oct 13, 2026
55c9ad0
Merge branch 'develop' into dependabot/uv/develop/ruff-0.17.0
MarTrepodi Oct 13, 2026
4e60472
Merge pull request #180 from swgoh-utils/dependabot/uv/develop/ruff-0…
MarTrepodi Oct 13, 2026
ed7c473
ci(release): restate closing keywords in the release PR
MarTrepodi Oct 13, 2026
c41eb8d
ci: comment on issues when their fix merges into develop
MarTrepodi Oct 13, 2026
82a42dc
ci: ignore fenced code indented inside list items
MarTrepodi Oct 13, 2026
8d9db20
Merge pull request #186 from swgoh-utils/backmerge/main-to-develop-38…
MarTrepodi Oct 13, 2026
3e081b7
Merge pull request #187 from swgoh-utils/ci/close-issues-on-release
MarTrepodi Oct 13, 2026
3e66987
style(examples): remove unused imports and a dead variable
MarTrepodi Oct 13, 2026
5e70872
ci(examples): lint examples in CI and pre-commit
MarTrepodi Oct 13, 2026
b9990ed
Merge pull request #188 from swgoh-utils/ci/lint-examples
MarTrepodi Oct 13, 2026
df8dc90
docs(api): order client pages for first-time readers
MarTrepodi Oct 14, 2026
9c71be1
Merge pull request #189 from swgoh-utils/docs/client-page-ordering
MarTrepodi Oct 14, 2026
2a467f6
ci(docs): publish versioned docs with mike
MarTrepodi Oct 17, 2026
ed3d204
docs: transport errors arrive as SwgohComlinkException
MarTrepodi Oct 17, 2026
038999d
ci(docs): deploy each release/* branch as its own docs version
MarTrepodi Oct 17, 2026
4298ae2
Merge pull request #191 from swgoh-utils/docs/versioned-pages-2.x
MarTrepodi Oct 17, 2026
d8c1994
ci(release): dispatch the docs deploy on the staged release branch
MarTrepodi Oct 18, 2026
aa4b44f
Merge pull request #193 from swgoh-utils/ci/prepare-release-dispatch-…
MarTrepodi Oct 18, 2026
0615e1b
build(deps-dev): Bump ruff from 0.17.0 to 0.17.1
dependabot[bot] Oct 18, 2026
f00a1cd
build(deps-dev): Bump ty from 0.0.86 to 0.0.88
dependabot[bot] Oct 18, 2026
d61be79
Merge pull request #194 from swgoh-utils/dependabot/uv/develop/ruff-0…
MarTrepodi Oct 19, 2026
523fb27
Merge pull request #195 from swgoh-utils/dependabot/uv/develop/ty-0.0.88
MarTrepodi Oct 19, 2026
38b44cd
feat(helpers): add get_era_units for an Era's unit roster
MarTrepodi Oct 22, 2026
a33e7c3
Merge pull request #196 from swgoh-utils/feat/era-units
MarTrepodi Oct 22, 2026
c19921b
fix(statcalc): build units that start above one star
MarTrepodi Oct 22, 2026
2ace9ad
Merge pull request #197 from swgoh-utils/fix/statcalc-units-without-o…
MarTrepodi Oct 22, 2026
8155a04
chore(changelog): update for v2.6.0
github-actions[bot] Oct 22, 2026
a09d7a4
Merge remote-tracking branch 'origin/main' into release/v2.6.0
MarTrepodi Oct 22, 2026
cc59bc9
ci(ci): skip commits already on develop when linting release PRs
MarTrepodi Oct 22, 2026
f406128
Merge pull request #199 from swgoh-utils/ci/commitlint-skip-develop-c…
MarTrepodi Oct 22, 2026
2ab3356
Merge remote-tracking branch 'origin/develop' into release/v2.6.0
MarTrepodi Oct 22, 2026
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
1 change: 1 addition & 0 deletions .commitlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"version", "release", "changelog", "deps", "ci",
"docs", "examples", "tests"
]],
"subject-exclamation-mark": [2, "never"],
"subject-max-length": [1, "always", 100],
"header-max-length": [0, "always"],
"body-max-line-length": [0, "always"],
Expand Down
21 changes: 21 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
version: 2

# Dependabot reads this file from the DEFAULT branch (main), not from target-branch, so
# edits take effect only once they are released to main.
#
# Updates are grouped (one PR per group per week, not one per dependency), and the commit
# prefix is pinned so Dependabot doesn't guess a type/scope from repo history.
updates:
- package-ecosystem: "github-actions"
directory: "/"
Expand All @@ -8,6 +14,11 @@ updates:
labels:
- "dependencies"
- "ci"
commit-message:
prefix: "chore(deps)"
groups:
github-actions:
patterns: ["*"]

# "uv" (not "pip") so updates also refresh uv.lock
- package-ecosystem: "uv"
Expand All @@ -18,3 +29,13 @@ updates:
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"
groups:
# ruff and ty bumps routinely bring new lint/type diagnostics that need code fixes;
# keep them apart so they can't hold up the routine bumps. A dependency joins the
# FIRST group it matches, so this must stay above the catch-all.
astral-tools:
patterns: ["ruff", "ty"]
python:
patterns: ["*"]
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
- [ ] I have updated the docs (`README.md`, `docs/`, `examples/`) if user-facing behavior changed
- [ ] I have added unit tests that cover my changes (mocked, not requiring a live comlink service)
- [ ] Tests pass (`uv run pytest tests/`)
- [ ] Ruff linter and formatter pass (`uvx ruff check src/ tests/` and `uvx ruff format --check src/ tests/`)
- [ ] Ruff linter and formatter pass (`uv run ruff check src/ tests/` and `uv run ruff format --check src/ tests/`)
- [ ] Type check passes (`uv run ty check src/swgoh_comlink/`)
- [ ] I have not bundled unrelated changes in this PR

Expand Down
31 changes: 31 additions & 0 deletions .github/release-pr/v2.6.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Automated release prep for **v2.6.0**. CHANGELOG.md regenerated via git-changelog.

Merging promotes `develop` -> `main` with the changelog already in place.
Afterwards, run the **release** workflow on `main` to build, publish, and tag v2.6.0.

_No open issues are referenced by the 22 pull requests merged since v2.5.0._

---

<a name="v2.6.0"></a>

## [v2.6.0](https://github.com/swgoh-utils/comlink-python/compare/v2.5.0...v2.6.0) (2026-10-22)

### Features

- **helpers:** add get_era_units for an Era's unit roster ([38b44cd](https://github.com/swgoh-utils/comlink-python/commit/38b44cdb3d736338387bdc61fbde697c60dd4b60))

### Bug Fixes

- **statcalc:** build units that start above one star ([c19921b](https://github.com/swgoh-utils/comlink-python/commit/c19921bdffb9122a31c5ff0cacfad7fd3b64a04f))
- **parity:** pass strict=True to zip in the list comparators ([0cdc467](https://github.com/swgoh-utils/comlink-python/commit/0cdc467162faa7d16ac816eee63db1334e16dda0))

### Code Refactoring

- **helpers:** narrow TB reward table ids to str in get_tb_mission_scores ([4edfb76](https://github.com/swgoh-utils/comlink-python/commit/4edfb76eb87167edb244f6b4a4ca1aec47fafb5b))

### Docs

- transport errors arrive as SwgohComlinkException ([ed3d204](https://github.com/swgoh-utils/comlink-python/commit/ed3d20418725cb3c8f5d4ce564eb3f9d3c297f8e))
- **api:** order client pages for first-time readers ([df8dc90](https://github.com/swgoh-utils/comlink-python/commit/df8dc90ba027d81ae9fb7acb7de23aafc145dcee))
- document the accidental major bump gotcha in commit messages ([93e05eb](https://github.com/swgoh-utils/comlink-python/commit/93e05eb57e1dbd13c562678350980184b0f94d72))
130 changes: 130 additions & 0 deletions .github/tools/breaking_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Explain, and police, a major version bump computed by git-changelog.

The angular convention marks a commit as breaking when ANY line of its message matches
``^break(s|ing changes?)?[ :].+$`` (case-insensitive), so a body line that merely wraps
onto "Breaking ..." or "breaks ..." silently bumps the major version. This script reuses
git-changelog's own parse (same commits, same regex) to list every line that triggered
the bump, and writes them to the GitHub job summary.

Usage (from the repo root)::

uv run python .github/tools/breaking_changes.py --bump auto --next-tag v3.0.0

Exits 1 only when ``--bump auto`` produced a major and no commit carries the intentional
``BREAKING CHANGE: <description>`` footer; an explicit ``--bump major`` is never blocked.
"""

from __future__ import annotations

import argparse
import os
import re
import sys
from dataclasses import dataclass

from git_changelog import Changelog, Commit, read_config

# The Conventional Commits footer, exactly: upper-case, single space, colon, description.
# Anything else BREAK_REGEX accepts ("Breaking changes ...", "breaks the ...") is prose.
FOOTER_REGEX = re.compile(r"^BREAKING CHANGE: \S")


@dataclass(frozen=True)
class Trigger:
"""One message line that made git-changelog treat its commit as breaking."""

commit: Commit
line: str

@property
def is_footer(self) -> bool:
return bool(FOOTER_REGEX.match(self.line))


def major(tag: str) -> int:
return int(tag.lstrip("v").split(".", 1)[0])


def code(text: str) -> str:
"""Render ``text`` as an inline code span, even if it contains backticks."""
return f"`` {text} ``" if "`" in text else f"`{text}`"


def find_triggers(changelog: Changelog) -> tuple[str | None, list[Trigger]]:
"""Return the last released tag and every breaking line in the unreleased commits."""
unreleased = changelog.versions_list[0]
if unreleased.tag: # HEAD is already tagged: nothing to bump.
return unreleased.tag, []
previous = unreleased.previous_version.tag if unreleased.previous_version else None
regex = changelog.convention.BREAK_REGEX
triggers = [
Trigger(commit, line)
for commit in unreleased.commits
if commit.convention["is_major"]
for line in [commit.subject, *commit.body]
if regex.match(line)
]
return previous, triggers


def render(previous: str, next_tag: str, bump: str, triggers: list[Trigger], blocked: bool) -> str:
out = [
f"## ⚠️ Major version bump: {previous} → {next_tag}",
"",
f"`bump={bump}`. git-changelog's angular convention treats a commit as breaking when"
" **any** line of its message matches `^break(s|ing changes?)?[ :].+$`"
" (case-insensitive). These lines triggered it:",
"",
]
if not triggers:
out.append("_No commit since the last tag matches — the major came from the bump input._")
for t in triggers:
sha = t.commit.hash[:7]
ref = f"[`{sha}`]({t.commit.url})" if t.commit.url else f"`{sha}`"
merge = " (merge)" if len(t.commit.parent_hashes) > 1 else ""
kind = "✅ footer" if t.is_footer else "⚠️ prose"
out.append(f"- {ref}{merge} {t.commit.subject} \n {kind}: {code(t.line)}")
out.append("")
if blocked:
out += [
"### ❌ Blocked: no intentional `BREAKING CHANGE:` footer",
"",
"None of these lines is the exact `BREAKING CHANGE: <description>` footer, so this major"
" is almost certainly accidental. Re-run **prepare release** with `bump=minor` (or"
" `patch`); if the break is real, re-run with `bump=major`.",
"",
]
return "\n".join(out)


def main() -> int:
parser = argparse.ArgumentParser(description="Explain and police a major bump from git-changelog.")
parser.add_argument("--bump", required=True, choices=["auto", "patch", "minor", "major"])
parser.add_argument("--next-tag", required=True, help="the tag git-changelog computed")
args = parser.parse_args()

config = read_config()
changelog = Changelog(".", convention=config["convention"], versioning=config["versioning"])
previous, triggers = find_triggers(changelog)

if previous is None or major(args.next_tag) <= major(previous):
print(f"{previous} → {args.next_tag}: not a major bump, nothing to check.")
return 0

blocked = args.bump == "auto" and not any(t.is_footer for t in triggers)
report = render(previous, args.next_tag, args.bump, triggers, blocked)
print(report)
if summary := os.environ.get("GITHUB_STEP_SUMMARY"):
with open(summary, "a", encoding="utf-8") as fh:
fh.write(report + "\n")
if blocked:
print(
"::error title=Accidental major bump?::bump=auto computed a major without a"
" 'BREAKING CHANGE:' footer; re-run with bump=minor/patch, or bump=major if intended."
)
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
168 changes: 168 additions & 0 deletions .github/tools/closing_issues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""Find the issues that merged pull requests resolve, for the develop -> main release flow.

GitHub only acts on closing keywords ("Closes #12") in a pull request that merges into the
DEFAULT branch (main). Work here merges into develop, so the keywords are recorded but never
fire, and the release PR that later carries the fix to main has no keywords of its own. This
script reads the keywords back out of the merged pull requests (descriptions and commit
messages) so they can be:

* ``release``: re-stated in the release PR body, so GitHub closes the issues when the release
merges into main.
* ``develop``: announced on each still-open issue as soon as its fix merges into develop.

Matching follows GitHub's rules: a keyword (close/closes/closed, fix/fixes/fixed,
resolve/resolves/resolved, optional colon) directly before ONE reference — ``#12``,
``owner/repo#12`` or an issue URL in this repository. "Closes #1, #2" closes only #1, as on
GitHub. HTML comments (the PR template's hints) and code are ignored.

Usage (from the repo root, with ``gh`` authenticated)::

python3 .github/tools/closing_issues.py release --since v2.5.0
python3 .github/tools/closing_issues.py develop --sha <merge commit> [--dry-run]
"""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys

KEYWORD_REGEX = re.compile(
r"(?<![\w/#-])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?[ \t]+"
r"(?:(?P<repo>[\w.-]+/[\w.-]+)?#(?P<number>\d+)"
r"|https://github\.com/(?P<url_repo>[\w.-]+/[\w.-]+)/issues/(?P<url_number>\d+))\b",
re.IGNORECASE,
)
# Text GitHub does not scan for keywords: HTML comments, fenced code blocks (including ones
# indented inside list items), and inline code.
IGNORED_REGEX = re.compile(
r"<!--.*?-->|^[ \t]*(?P<fence>```|~~~).*?^[ \t]*(?P=fence)|`[^`\n]*`", re.DOTALL | re.MULTILINE
)
MERGE_SUBJECT_REGEX = re.compile(r"^Merge pull request #(\d+) from ")
# Marks comments this script posted, so a re-run never posts the same note twice.
MARKER = "<!-- closing-issues: pr={pr} -->"


def gh(*args: str) -> str:
return subprocess.run(["gh", *args], check=True, capture_output=True, text=True).stdout


def referenced_issues(text: str, repo: str) -> list[int]:
"""Issue numbers in ``repo`` that ``text`` closes, in order of first mention."""
text = IGNORED_REGEX.sub(" ", text)
numbers: dict[int, None] = {}
for m in KEYWORD_REGEX.finditer(text):
ref_repo = m["repo"] or m["url_repo"] or repo
if ref_repo.lower() == repo.lower():
numbers[int(m["number"] or m["url_number"])] = None
return list(numbers)


def pull_request_text(repo: str, pr: int) -> str:
"""The PR description plus every commit message in it."""
body = gh("api", f"repos/{repo}/pulls/{pr}", "--jq", '.body // ""')
messages = gh("api", "--paginate", f"repos/{repo}/pulls/{pr}/commits", "--jq", ".[].commit.message")
return f"{body}\n\n{messages}"


def is_open_issue(repo: str, number: int) -> bool:
"""True for an open issue; False for a closed one, a pull request, or a missing number."""
try:
issue = json.loads(gh("api", f"repos/{repo}/issues/{number}"))
except subprocess.CalledProcessError:
return False
return issue["state"] == "open" and "pull_request" not in issue


def issues_by_pr(repo: str, prs: list[int]) -> dict[int, list[int]]:
"""Map each still-open issue to the merged PRs that resolve it."""
found: dict[int, list[int]] = {}
for pr in prs:
for number in referenced_issues(pull_request_text(repo, pr), repo):
found.setdefault(number, []).append(pr)
return {n: found[n] for n in sorted(found) if is_open_issue(repo, n)}


def refs(prs: list[int]) -> str:
return ", ".join(f"#{pr}" for pr in prs)


def release(repo: str, since: str) -> int:
"""Print the release PR section that re-states the closing keywords merged since ``since``."""
subjects = subprocess.run(
["git", "log", "--merges", "--format=%s", f"{since}..HEAD"], check=True, capture_output=True, text=True
).stdout.splitlines()
prs = sorted({int(m[1]) for s in subjects if (m := MERGE_SUBJECT_REGEX.match(s))})
found = issues_by_pr(repo, prs)
if not found:
print(f"_No open issues are referenced by the {len(prs)} pull requests merged since {since}._")
return 0
# One keyword per line: GitHub closes only the first reference after each keyword.
print("### Issues resolved by this release\n")
print("Merging this PR into `main` closes:\n")
for number, by in found.items():
print(f"- Closes #{number} (via {refs(by)})")
return 0


def develop(repo: str, sha: str, dry_run: bool) -> int:
"""Comment on each open issue resolved by the PR(s) that ``sha`` merged into develop."""
prs = [
int(n)
for n in gh(
"api",
f"repos/{repo}/commits/{sha}/pulls",
"--jq",
'.[] | select(.merged_at != null and .base.ref == "develop") | .number',
).split()
]
if not prs:
print(f"{sha[:7]} is not a pull request merge into develop; nothing to do.")
return 0
summary = []
for number, by in issues_by_pr(repo, prs).items():
for pr in by:
marker = MARKER.format(pr=pr)
existing = gh("api", "--paginate", f"repos/{repo}/issues/{number}/comments", "--jq", ".[].body")
if marker in existing:
summary.append(f"- #{number}: already noted for #{pr}")
continue
body = (
f"{marker}\nAddressed by #{pr}, which is now merged into `develop`. This issue will close"
" automatically when that change is released to `main`."
)
if not dry_run:
gh("api", "-X", "POST", f"repos/{repo}/issues/{number}/comments", "-f", f"body={body}")
summary.append(f"- #{number}: {'would comment' if dry_run else 'commented'} (resolved by #{pr})")
report = "\n".join(summary) or f"No open issues are referenced by {refs(prs)}."
print(report)
if path := os.environ.get("GITHUB_STEP_SUMMARY"):
with open(path, "a", encoding="utf-8") as fh:
fh.write(f"## Issues addressed by {refs(prs)}\n\n{report}\n")
return 0


def main() -> int:
parser = argparse.ArgumentParser(description="Carry closing keywords from develop merges through to main.")
sub = parser.add_subparsers(dest="command", required=True)
rel = sub.add_parser("release", help="print the release PR's 'Closes #N' section")
rel.add_argument("--since", required=True, help="the last release tag")
dev = sub.add_parser("develop", help="comment on open issues resolved by a merge into develop")
dev.add_argument("--sha", required=True, help="the merge commit pushed to develop")
dev.add_argument("--dry-run", action="store_true", help="report without commenting")
args = parser.parse_args()

repo = os.environ.get("GITHUB_REPOSITORY") or gh(
"repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"
)
repo = repo.strip()
if args.command == "release":
return release(repo, args.since)
return develop(repo, args.sha, args.dry_run)


if __name__ == "__main__":
sys.exit(main())
Loading
Loading