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
2 changes: 2 additions & 0 deletions .github/nightly-notes/2026-10-20-ci-nightly-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## New
- Nightly updates now list what changed, grouped into new, improved and fixed (#1330)
23 changes: 23 additions & 0 deletions .github/nightly-notes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Nightly notes

Every pull request with a change users can notice adds one file here. On each push to `main`, CI
merges all of them into the body of the rolling `nightly` release, which the in-app update screen
shows to nightly testers.

- Name the file `YYYY-MM-DD-<branch>.md`: the date the PR is opened, then the branch name in
lowercase with `/` replaced by `-`. One file per PR, and never edit another PR's file, so open
PRs never conflict.
- Use only `## New`, `## Improved` and `## Fixed`, each followed by one short `- ` bullet per change.
- Write for users, in one line per bullet, with issue numbers as `(#123)` and no em-dashes.
- CI, test and refactor-only PRs add no file.

```markdown
## Fixed
- Music no longer stays in audio offload after a long pause (#1364)
```

When a stable version is released, these notes are folded into `release-notes.md` and the
`app/src/main/assets/changelog/v*.txt` changelog, and every file here except this README is deleted.

`python3 .github/scripts/nightly_notes.py --check` validates the files; the Checks job runs it on
every PR.
84 changes: 84 additions & 0 deletions .github/scripts/nightly_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Checks the nightly notes, or merges them into the nightly release body.

Each file in .github/nightly-notes/ holds one PR's user-visible changes as `## <Section>` headings
followed by `- ` bullets. The in-app update screen (ReleaseNotes.kt) shows the intro paragraph, then
one group per `##` section, so the merged body keeps exactly that shape.

nightly_notes.py --check
nightly_notes.py --intro "Built from ..." > body.md
"""

import argparse
import pathlib
import re
import sys

SECTIONS = ("New", "Improved", "Fixed")
NOTES_DIR = pathlib.Path(__file__).resolve().parents[1] / "nightly-notes"
FILE_NAME = re.compile(r"\d{4}-\d{2}-\d{2}-[a-z0-9][a-z0-9.-]*\.md")


def read_note(path):
sections = {}
errors = []
current = None
for number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
line = raw.strip()
where = f"{path.name}:{number}"
if not line:
continue
if "—" in line:
errors.append(f"{where}: no em-dashes in user-facing copy")
if line.startswith("## "):
current = line[3:].strip()
if current not in SECTIONS:
errors.append(f"{where}: section '{current}' must be one of {', '.join(SECTIONS)}")
sections.setdefault(current, [])
elif line.startswith("- "):
if current is None:
errors.append(f"{where}: bullet before any '## Section' heading")
else:
sections[current].append(line[2:].strip())
else:
errors.append(f"{where}: expected a '## Section' heading or a '- ' bullet")
if not any(sections.values()):
errors.append(f"{path.name}: has no bullets")
return sections, errors


def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--check", action="store_true", help="only validate the notes")
parser.add_argument("--intro", default="", help="paragraph shown above the sections")
args = parser.parse_args()

notes = sorted(path for path in NOTES_DIR.glob("*.md") if path.name != "README.md")
merged = {section: [] for section in SECTIONS}
errors = []
# File names start with the date, so reversed order lists the newest changes first.
for path in reversed(notes):
if not FILE_NAME.fullmatch(path.name):
errors.append(f"{path.name}: name must be YYYY-MM-DD-<branch>.md in lowercase")
sections, problems = read_note(path)
errors += problems
for section, items in sections.items():
merged.setdefault(section, []).extend(items)

if errors:
print("\n".join(errors), file=sys.stderr)
return 1
if args.check:
print(f"{len(notes)} nightly notes are valid.")
return 0

lines = [args.intro] if args.intro else []
for section in SECTIONS:
if merged[section]:
lines += ["", f"## {section}", *(f"- {item}" for item in merged[section])]
print("\n".join(lines).strip())
return 0


if __name__ == "__main__":
sys.exit(main())
17 changes: 16 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ jobs:
git status --porcelain -- app/src/main/java/org/tartarus/snowball
test -z "$(git status --porcelain -- app/src/main/java/org/tartarus/snowball)"

# The nightly job publishes these as the release body the update screen shows.
- name: Check nightly notes
run: python3 .github/scripts/nightly_notes.py --check

- name: Check Kotlin formatting
run: >
./gradlew spotlessCheck
Expand Down Expand Up @@ -457,6 +461,14 @@ jobs:
cancel-in-progress: false

steps:
- name: Checkout nightly notes
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
sparse-checkout: |
.github/nightly-notes
.github/scripts

- name: Download nightly APK
uses: actions/download-artifact@v8
with:
Expand All @@ -483,9 +495,12 @@ jobs:
gh release delete-asset nightly "$old" --yes
fi
done
python3 .github/scripts/nightly_notes.py \
--intro "Built from ${GITHUB_SHA::7} by run ${GITHUB_RUN_NUMBER}. Installs next to the stable app as Flow Nightly and updates itself from here." \
> nightly-body.md
gh release edit nightly --prerelease --latest=false \
--title "Flow nightly ${GITHUB_RUN_NUMBER}" \
--notes "Built from ${GITHUB_SHA::7} by run ${GITHUB_RUN_NUMBER}. Installs next to the stable app as Flow Nightly and updates itself from here."
--notes-file nightly-body.md

release:
name: Create Release
Expand Down
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,16 @@ rows, bespoke empty/error states, bespoke badges, bespoke formatters, or a secon
3. Follow current Kotlin and Android best practices — when unsure, check official docs rather than guessing.
4. DO NOT edit the app's Room database schema without explicit instruction (schema changes require a version bump and migration, handled deliberately).
5. DO NOT bump the app version in any file — version bumps are done manually by the project owner.
6. Every PR with a change users can notice adds a nightly note: one new file
`.github/nightly-notes/YYYY-MM-DD-<branch>.md` (the date the PR is opened, the branch name in
lowercase with `/` replaced by `-`) holding `## New`, `## Improved` or `## Fixed` headings, each
with one short `- ` bullet per change, written for users, issues as `(#123)`, no em-dashes. One
file per PR, never edit another PR's file. CI, test and refactor-only PRs add none. CI publishes
every note as the nightly release body that the in-app update screen shows, and the Checks job
fails on a malformed note (`python3 .github/scripts/nightly_notes.py --check`). This is separate
from the release changelog in `app/src/main/assets/changelog/v*.txt`; at a stable release the
notes are folded into it and into `release-notes.md`, and the note files are deleted (keep the
folder's `README.md`).

## AI-only guidelines

Expand Down
Loading