Split version history out of PROJECT.org into CHANGELOG.org
This commit is contained in:
25
AGENTS.md
25
AGENTS.md
@ -1,23 +1,12 @@
|
||||
This is a Django-based web application that has an API, but primarily functions
|
||||
with traditional Django views with HTML templates to display data that mostly
|
||||
constitutes "scrobbled" items. The app started as a way to track a user's
|
||||
watched videos via a Jellyfin server, but has since grown to keep track of a
|
||||
number of media types: music tracks, tasks, videos, web pages, food, life
|
||||
events, sports events, podcasts, video games, board games, beers, brick (lego)
|
||||
sets, puzzles, books and geolocations.
|
||||
This is a Django-based web application that has an API, but primarily functions with traditional Django views with HTML templates to display data that mostly constitutes "scrobbled" items. The app started as a way to track a user's watched videos via a Jellyfin server, but has since grown to keep track of a number of media types: music tracks, tasks, videos, web pages, food, life events, sports events, podcasts, video games, board games, beers, brick (lego) sets, puzzles, books and geolocations.
|
||||
|
||||
The project is written in Python and prefers to use "fat" models where logical
|
||||
methods are contained in either instance methods on instatiated data models, or
|
||||
classmethods on the Django model class itself. When logic grows too complex,
|
||||
helper functions should be pulled out into utils.py files and the model instance
|
||||
ro class method should call the utility function.
|
||||
The project is written in Python and prefers to use "fat" models where logical methods are contained in either instance methods on instatiated data models, or classmethods on the Django model class itself. When logic grows too complex, helper functions should be pulled out into utils.py files and the model instance ro class method should call the utility function.
|
||||
|
||||
Be sure to check pyproject.toml for project defaults. Specifically for black and
|
||||
isort expectations.
|
||||
Be sure to check pyproject.toml for project defaults. Specifically for black and isort expectations.
|
||||
|
||||
Imports in python files should always be top level if possible.
|
||||
|
||||
All tasks live in the PROJECT.org file and include an org ID that is a uuid to make them unique.
|
||||
All tasks live in the PROJECT.org file and include an org ID that is a uuid to make them unique. Release history lives in CHANGELOG.org.
|
||||
|
||||
In local development, environment variables for various sensitive values live in a .envrc file
|
||||
|
||||
@ -27,10 +16,8 @@ Care should be taken when using .envrc that we do not spam services we use in pr
|
||||
|
||||
USING SESH COMMIT
|
||||
|
||||
This repository should use the `sesh` command to capture agentic session notes in git notes. You can
|
||||
read the help next by running `sesh` by itself, but make sure that we use capture any reasoning
|
||||
or prompts used for the work.
|
||||
This repository should use the `sesh` command to capture agentic session notes in git notes. You can read the help next by running `sesh` by itself, but make sure that we use capture any reasoning, tools or prompts used for the work. Any time we say "commit" in this project, we mean "sesh commit"
|
||||
|
||||
When asking to "commit" we should always assume we're using `sesh commit` and capturing session notes.
|
||||
|
||||
Release commits (`[release] Bump to version X.Y`) are handled by the user. Never auto-commit release steps; only prepare the changes (e.g. the PROJECT.org DONE/version entry and pyproject.toml bump) and leave them for the user to commit.
|
||||
Release commits (`[release] Bump to version X.Y`) are handled by the user. Never auto-commit release steps; only prepare the changes (e.g. the PROJECT.org DONE entry, a new CHANGELOG.org version entry) and leave them for the user to commit.
|
||||
|
||||
2930
CHANGELOG.org
Normal file
2930
CHANGELOG.org
Normal file
File diff suppressed because it is too large
Load Diff
3101
PROJECT.org
3101
PROJECT.org
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,11 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_FILE = Path("PROJECT.org")
|
||||
CHANGELOG_FILE = Path("CHANGELOG.org")
|
||||
PYPROJECT_FILE = Path("pyproject.toml")
|
||||
|
||||
BACKLOG_RE = re.compile(r"^\* Backlog\s+\[(\d+)/(\d+)\](.*)$")
|
||||
VERSION_RE = re.compile(r"^\* Version\s+(\d+\.\d+)\s+\[\d+/\d+\]")
|
||||
DONE_HEADER_RE = re.compile(r"^(\*\* DONE\s+)(.*)$")
|
||||
ITEM_HEADER_RE = re.compile(r"^\*\* ")
|
||||
|
||||
|
||||
@ -39,6 +39,35 @@ def bump_version(current_major, current_minor, kind):
|
||||
raise ValueError(f"Unknown bump kind: {kind}")
|
||||
|
||||
|
||||
def collect_done_items(backlog_lines):
|
||||
"""Split Backlog into items; return done items and kept items.
|
||||
|
||||
Each item is a list of lines (without the section header).
|
||||
"""
|
||||
# Split Backlog into items at each ** line (skip the section header)
|
||||
items = [] # list of (start_idx, end_idx, is_done)
|
||||
item_start = None
|
||||
for i in range(1, len(backlog_lines)):
|
||||
if ITEM_HEADER_RE.match(backlog_lines[i]):
|
||||
if item_start is not None:
|
||||
items.append(
|
||||
(item_start, i, backlog_lines[item_start].startswith("** DONE"))
|
||||
)
|
||||
item_start = i
|
||||
if item_start is not None:
|
||||
items.append(
|
||||
(
|
||||
item_start,
|
||||
len(backlog_lines),
|
||||
backlog_lines[item_start].startswith("** DONE"),
|
||||
)
|
||||
)
|
||||
|
||||
done_items = [backlog_lines[s:e] for s, e, is_done in items if is_done]
|
||||
kept_items = [backlog_lines[s:e] for s, e, is_done in items if not is_done]
|
||||
return done_items, kept_items
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in ("major", "minor"):
|
||||
print(f"Usage: {sys.argv[0]} <major|minor>", file=sys.stderr)
|
||||
@ -46,56 +75,25 @@ def main():
|
||||
|
||||
kind = sys.argv[1]
|
||||
|
||||
lines = PROJECT_FILE.read_text().splitlines(keepends=True)
|
||||
changelog_lines = CHANGELOG_FILE.read_text().splitlines(keepends=True)
|
||||
project_lines = PROJECT_FILE.read_text().splitlines(keepends=True)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 1. Identify top-level sections
|
||||
# 1. Parse current version from the first * Version header in CHANGELOG.org
|
||||
# ---------------------------------------------------------------
|
||||
section_starts = []
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("* ") and not line.startswith("** "):
|
||||
section_starts.append(i)
|
||||
section_starts.append(len(lines))
|
||||
|
||||
backlog_idx = None
|
||||
version_idx = None
|
||||
|
||||
for idx, start in enumerate(section_starts[:-1]):
|
||||
header = lines[start].strip()
|
||||
if header.startswith("* Backlog"):
|
||||
backlog_idx = idx
|
||||
if header.startswith("* Version"):
|
||||
version_idx = idx # last occurrence wins
|
||||
|
||||
if backlog_idx is None:
|
||||
print("ERROR: no Backlog section found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if version_idx is None:
|
||||
print("ERROR: no Version section found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
backlog_start = section_starts[backlog_idx]
|
||||
backlog_end = section_starts[backlog_idx + 1]
|
||||
|
||||
# Find the newest Version section (first after Backlog) that matches
|
||||
# our expected format (e.g. "37.0" not "0.11.4").
|
||||
version_start = None
|
||||
for idx in range(backlog_idx + 1, version_idx + 1):
|
||||
header = lines[section_starts[idx]].strip()
|
||||
if VERSION_RE.match(header):
|
||||
version_start = section_starts[idx]
|
||||
for i, line in enumerate(changelog_lines):
|
||||
if VERSION_RE.match(line.strip()):
|
||||
version_start = i
|
||||
break
|
||||
|
||||
if version_start is None:
|
||||
print("ERROR: no parseable Version header found", file=sys.stderr)
|
||||
print(
|
||||
"ERROR: no parseable Version header found in CHANGELOG.org", file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
version_header = lines[version_start].strip()
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 2. Parse current version from the newest * Version header
|
||||
# ---------------------------------------------------------------
|
||||
vm = VERSION_RE.match(version_header)
|
||||
vm = VERSION_RE.match(changelog_lines[version_start].strip())
|
||||
current_version = vm.group(1)
|
||||
major_str, minor_str = current_version.split(".")
|
||||
current_major = int(major_str)
|
||||
@ -104,73 +102,80 @@ def main():
|
||||
new_version = f"{new_major}.{new_minor}"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 3. Collect ** DONE items from the Backlog section
|
||||
# 2. Identify the Backlog section in PROJECT.org
|
||||
# ---------------------------------------------------------------
|
||||
backlog_lines = lines[backlog_start:backlog_end]
|
||||
section_starts = []
|
||||
for i, line in enumerate(project_lines):
|
||||
if line.startswith("* ") and not line.startswith("** "):
|
||||
section_starts.append(i)
|
||||
section_starts.append(len(project_lines))
|
||||
|
||||
# Split Backlog into items at each ** line (skip the section header)
|
||||
items = [] # list of (start_idx, end_idx, is_done)
|
||||
item_start = None
|
||||
for i in range(1, len(backlog_lines)):
|
||||
if ITEM_HEADER_RE.match(backlog_lines[i]):
|
||||
if item_start is not None:
|
||||
items.append((item_start, i, backlog_lines[item_start].startswith("** DONE")))
|
||||
item_start = i
|
||||
if item_start is not None:
|
||||
items.append((item_start, len(backlog_lines), backlog_lines[item_start].startswith("** DONE")))
|
||||
backlog_idx = None
|
||||
for idx, start in enumerate(section_starts[:-1]):
|
||||
if project_lines[start].strip().startswith("* Backlog"):
|
||||
backlog_idx = idx
|
||||
|
||||
done_items = [(s, e) for s, e, is_done in items if is_done]
|
||||
kept_items = [(s, e) for s, e, is_done in items if not is_done]
|
||||
if backlog_idx is None:
|
||||
print("ERROR: no Backlog section found in PROJECT.org", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
backlog_start = section_starts[backlog_idx]
|
||||
backlog_end = section_starts[backlog_idx + 1]
|
||||
backlog_lines = project_lines[backlog_start:backlog_end]
|
||||
|
||||
done_items, kept_items = collect_done_items(backlog_lines)
|
||||
|
||||
if not done_items:
|
||||
print("No DONE items found in Backlog — nothing to release.")
|
||||
sys.exit(1)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 4. Build the new Version section text
|
||||
# 3. Build the new Version section text
|
||||
# ---------------------------------------------------------------
|
||||
version_section_lines = [f"* Version {new_version} [{len(done_items)}/{len(done_items)}]\n"]
|
||||
for s, e in done_items:
|
||||
version_section_lines.extend(backlog_lines[s:e])
|
||||
version_section_lines = [
|
||||
f"* Version {new_version} [{len(done_items)}/{len(done_items)}]\n"
|
||||
]
|
||||
for item in done_items:
|
||||
version_section_lines.extend(item)
|
||||
version_section_lines.append("\n")
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 5. Build updated Backlog section
|
||||
# 4. Build updated Backlog section
|
||||
# ---------------------------------------------------------------
|
||||
backlog_header_line = backlog_lines[0]
|
||||
bm = BACKLOG_RE.match(backlog_header_line.strip())
|
||||
if not bm:
|
||||
print(f"ERROR: could not parse backlog header: {backlog_header_line!r}", file=sys.stderr)
|
||||
print(
|
||||
f"ERROR: could not parse backlog header: {backlog_header_line!r}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
done_count = int(bm.group(1))
|
||||
total_count = int(bm.group(2))
|
||||
tags = bm.group(3)
|
||||
|
||||
new_done = done_count - len(done_items)
|
||||
new_total = total_count - len(done_items)
|
||||
new_done = 0
|
||||
new_total = len(kept_items)
|
||||
new_backlog_header = f"* Backlog [{new_done}/{new_total}]{tags}\n"
|
||||
|
||||
backlog_body = []
|
||||
for s, e in kept_items:
|
||||
backlog_body.extend(backlog_lines[s:e])
|
||||
for item in kept_items:
|
||||
backlog_body.extend(item)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 6. Assemble the new file
|
||||
# 5. Assemble the new PROJECT.org (Backlog + DONE items removed)
|
||||
# ---------------------------------------------------------------
|
||||
before_backlog = lines[:backlog_start]
|
||||
after_backlog = lines[backlog_end:version_start]
|
||||
before_backlog = project_lines[:backlog_start]
|
||||
output_project = before_backlog + [new_backlog_header] + backlog_body
|
||||
PROJECT_FILE.write_text("".join(output_project))
|
||||
|
||||
# Everything from the first Version section onwards
|
||||
from_version = lines[version_start:]
|
||||
|
||||
output = (
|
||||
before_backlog
|
||||
+ [new_backlog_header]
|
||||
+ backlog_body
|
||||
# ---------------------------------------------------------------
|
||||
# 6. Insert the new Version section into CHANGELOG.org after the title
|
||||
# ---------------------------------------------------------------
|
||||
output_changelog = (
|
||||
changelog_lines[:version_start]
|
||||
+ version_section_lines
|
||||
+ ["\n"]
|
||||
+ after_backlog
|
||||
+ from_version
|
||||
+ changelog_lines[version_start:]
|
||||
)
|
||||
CHANGELOG_FILE.write_text("".join(output_changelog))
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 7. Update pyproject.toml
|
||||
@ -183,19 +188,14 @@ def main():
|
||||
count=1,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 8. Write files
|
||||
# ---------------------------------------------------------------
|
||||
PROJECT_FILE.write_text("".join(output))
|
||||
PYPROJECT_FILE.write_text(pyproject)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 9. Build commit body from done item titles
|
||||
# 8. Build commit body from done item titles
|
||||
# ---------------------------------------------------------------
|
||||
commit_lines = []
|
||||
for s, e in done_items:
|
||||
title = parse_done_line(backlog_lines[s])
|
||||
for item in done_items:
|
||||
title = parse_done_line(item[0])
|
||||
if title:
|
||||
commit_lines.append(f"- {title}")
|
||||
|
||||
@ -203,14 +203,17 @@ def main():
|
||||
commit_message = f"[release] Bump to version {new_version}\n\n{commit_body}"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 10. Git commit + tag
|
||||
# 9. Git commit + tag
|
||||
# ---------------------------------------------------------------
|
||||
subprocess.run(["git", "add", str(PROJECT_FILE), str(PYPROJECT_FILE)], check=True)
|
||||
subprocess.run(
|
||||
["git", "add", str(PROJECT_FILE), str(CHANGELOG_FILE), str(PYPROJECT_FILE)],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
||||
subprocess.run(["git", "tag", new_version], check=True)
|
||||
|
||||
print(f"\nReleased v{new_version} — tag {new_version} created.")
|
||||
print(f"Moved {len(done_items)} DONE item(s) from Backlog to Version section.")
|
||||
print(f"Moved {len(done_items)} DONE item(s) from Backlog to CHANGELOG.org.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user