Skip to content
Closed
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
16 changes: 16 additions & 0 deletions .github/workflows/build-loremaster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- "docs/**"
- "UI_Spin_qeynos_LO1.ini"
- "README.md"
- "CHANGELOG.md"
# Governs the bytes every other path is checked out with, so a change
# here can alter what gets packaged without touching a packaged file.
- ".gitattributes"
Expand All @@ -34,6 +35,7 @@ on:
- "docs/**"
- "UI_Spin_qeynos_LO1.ini"
- "README.md"
- "CHANGELOG.md"
# Governs the bytes every other path is checked out with, so a change
# here can alter what gets packaged without touching a packaged file.
- ".gitattributes"
Expand Down Expand Up @@ -87,6 +89,16 @@ jobs:
- name: Verify change classifier
run: python tools/ci_change_scope.py --self-test

- name: Verify release notes tooling
run: python tools/release_notes.py --self-test

- name: Require a changelog entry for the release being published
if: github.event_name == 'workflow_dispatch' && inputs.publish_release
# Checked here rather than at the publish step, which is twenty
# minutes of building away: a release with nothing to say about
# itself should cost seconds to find out.
run: python tools/release_notes.py --version '${{ inputs.release_tag }}' --check

- name: Classify changed paths
id: scope
shell: pwsh
Expand Down Expand Up @@ -537,12 +549,16 @@ jobs:
# marker has to sit at column zero and so cannot be indented inside
# this step.
$version = $tag -replace '^v', ''
$changelog = python tools/release_notes.py --version $tag
if ($LASTEXITCODE -ne 0) { throw "no changelog entry for $tag" }
$notes = @()
if ($prerelease) {
$notes += "> **Release candidate.** Install it by hand and run it. The in-app updaters ignore prereleases, so nobody is offered this build and the current release stays where it is."
$notes += ""
}
$notes += @(
$changelog,
"",
"## Installing",
"",
"**Linux** -- download ``Loremaster-$version-x86_64.AppImage``, ``chmod +x`` it, and run it. There is no self-update on Linux, so new builds always come from this page.",
Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Changelog

What each release is for. Written in the pull request that makes the change,
so the release itself carries it: `tools/release_notes.py` reads the entry
matching the version being published, and a candidate and the release it is
promoted to resolve to the same entry.

## 0.4.0

First build carrying the upstream work merged since 0.3.4, plus the fork fixes
that came with it.

### From upstream ([itsspin/spinips](https://github.com/itsspin/spinips))

- **Native themes** — Vellum & Ember, and Midnight Frost for Glass, applied
consistently across every window and remembered between sessions.
- **Adventure journal** — loot and encounter history kept in a local database,
so a session's kills and drops survive a restart.
- **Raid context from the log** — instance and difficulty are read from log
evidence instead of being asked for, so kills are credited without a prompt
where the log already says enough.
- **Item intelligence** and a rebuilt gear plan import.
- **Combat frames** — a foreground attack perimeter, a brighter auto-attack
pulse, and tightened player progression bars.
- **Pet illusions restored**, and alert sounds you can point at your own files.
- **SpinTexture** companion project for sharper world textures.

### Fork-specific

- **Updates check this fork.** Both updaters previously asked
`itsspin/spinips` what the newest build was — which never carries the Linux
AppImage, and would install upstream's skins over this fork's. They now
follow this repository.
- **Every raid kill awaiting a difficulty is kept.** A single pending slot
discarded the first kill when two raid targets died before you confirmed a
difficulty, which is exactly what happens when a raid clears several at
once. Each kill now keeps its own zone, character and clear time.

### Removed

- **Alt+Z instance lockout OCR.** Upstream retired the feature and this fork
follows them; raid context now comes from the log rather than from scanning
the Instance Information window. `Ctrl+Shift+Z` is no longer claimed.
24 changes: 24 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ workflow, run manually (Actions → Run workflow). Three inputs matter:
| `release_tag` | The tag to create or update. |
| `prerelease` | On by default. Publishes a release candidate. |

## Saying what a release is for

`CHANGELOG.md` holds one entry per version, and the release carries it. Write
the entry in the pull request that makes the change, while you still know why
you made it -- not at release time, when it has to be reconstructed from a
diff.

## 0.4.0

What this release is for, in prose.

### Fork-specific
- ...

A candidate and the release it is promoted to read the **same** entry:
`0.4.0-rc.1`, `0.4.0-rc.2` and `0.4.0` all resolve to `## 0.4.0`. So the
narrative you write once is carried by every build that ships it, and nothing
has to be re-typed into a release page.

Publishing a version with no entry fails in the first job, seconds in, rather
than after a twenty-minute build. Beneath the entry each release also gets
install instructions naming the real artifacts, and the list of pull requests
merged since the last full release, generated by GitHub.

## Release candidates

A candidate is published as a GitHub pre-release, which is what keeps it out of
Expand Down
91 changes: 91 additions & 0 deletions tools/release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Read one version's entry out of CHANGELOG.md.

The release workflow prepends this to a release's notes, so what a change was
for is written once, in the pull request that makes it, and is then carried by
every build that ships it -- the candidate and the release it is promoted to
both read the same entry, because both resolve to the same base version.
"""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parents[1]
CHANGELOG = REPO / "CHANGELOG.md"
HEADING = re.compile(r"^## +(?P<version>\S+)", re.MULTILINE)


def base_version(version: str) -> str:
"""Strip a prerelease suffix: 0.4.0-rc.1 and 0.4.0 share one entry."""
return re.sub(r"[-+].*$", "", str(version).strip().lstrip("v"))


def entry(text: str, version: str) -> str:
"""The body under ``## <version>``, or "" when there is no such heading."""
wanted = base_version(version)
matches = list(HEADING.finditer(text))
for index, match in enumerate(matches):
if base_version(match.group("version")) != wanted:
continue
start = match.end()
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
return text[start:end].strip("\n").strip()
return ""


def _self_test() -> int:
sample = "\n".join((
"# Changelog", "",
"## 0.4.0", "",
"First entry.", "",
"- a bullet", "",
"## 0.3.4", "",
"Older entry.",
))
assert entry(sample, "0.4.0").startswith("First entry.")
assert "- a bullet" in entry(sample, "0.4.0")
# A candidate reads its own release's entry, which is the whole point.
assert entry(sample, "0.4.0-rc.1") == entry(sample, "0.4.0")
assert entry(sample, "v0.4.0-rc.2") == entry(sample, "0.4.0")
# The next version's entry must not leak into this one.
assert "Older entry." not in entry(sample, "0.4.0")
assert entry(sample, "0.3.4") == "Older entry."
# A missing entry is reported, never guessed at.
assert entry(sample, "9.9.9") == ""
# The last entry in the file runs to the end of it.
assert entry("## 1.0.0\n\nOnly entry.\n", "1.0.0") == "Only entry."
print("release notes extractor: ALL PASS")
return 0


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", help="release version or tag, e.g. v0.4.0-rc.1")
parser.add_argument("--check", action="store_true",
help="exit non-zero when the entry is missing, print nothing")
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
if args.self_test:
return _self_test()
if not args.version:
parser.error("--version is required")
if not CHANGELOG.is_file():
print(f"{CHANGELOG.name} does not exist", file=sys.stderr)
return 1
found = entry(CHANGELOG.read_text(encoding="utf-8"), args.version)
if not found:
print(
f"CHANGELOG.md has no '## {base_version(args.version)}' entry, so this "
"release would ship without saying what it is for. Add one and run again.",
file=sys.stderr)
return 1
if not args.check:
print(found)
return 0


if __name__ == "__main__":
raise SystemExit(main())
4 changes: 4 additions & 0 deletions tools/release_quality_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,10 @@ def run_source_selftests() -> None:
"test_*.py",
],
)
run_command(
"release notes extractor --self-test",
[sys.executable, str(REPO / "tools" / "release_notes.py"), "--self-test"],
)
run_command(
"SpinUI installer --selftest",
[
Expand Down