All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 6s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 6s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 6s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 6s
WAI operation catalogue / does this change touch it (pull_request) Successful in 6s
WAI conformance / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 5s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 6s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 7s
wai-ledger service / does this change touch it (pull_request) Successful in 6s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m25s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 6s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 6s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 6s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 6s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m57s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m7s
WAI Studio / does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 6s
wai-web wasm32 / does this change touch it (pull_request) Successful in 6s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 29s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 2m1s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 2m59s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m1s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 26s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m19s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m4s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m15s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 5m15s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 6m19s
wai-fleet service / service · macos (pull_request) Successful in 1m2s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m46s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 1m43s
wai-ledger service / service · macos (pull_request) Successful in 1m24s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m2s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m18s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m19s
wai-pack / wai-pack · macos (pull_request) Successful in 3m8s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 30s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m49s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 3m23s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m39s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m9s
wai-fleet service / service · linux (pull_request) Successful in 2m5s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 3m28s
wai-ledger service / service · linux (pull_request) Successful in 1m43s
wai-pack / wai-pack · linux (pull_request) Successful in 1m55s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m0s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m10s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m12s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 11m32s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 4m51s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 13m12s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m2s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m3s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m34s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m40s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 17m22s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m44s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m46s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 34s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 17s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 9m28s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m7s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 14m49s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m34s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m16s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 11m4s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 13m6s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m58s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 10m55s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 9m51s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 10m15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 9m21s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m14s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h4m7s
Every change to WAI added an entry at the head of wai/CHANGELOG.md, and every new module appended to the hand-kept lists of .github/workflows/wai-conformance.yml. So any two branches conflicted on those lines and had to merge one after another. This moves both into files a change adds. - Changelog entries: a change adds wai/changes/<yyyy-mm-dd>-<slug>.md holding the entry text. scripts/changelog-assemble.py renders the entries into CHANGELOG.md's Unreleased section for a release, in a stable order. scripts/check-changelog-fragment.py, run by repo-checks on every pull request, fails a pull request that changes wai/ and adds or edits no entry file directly in wai/changes/ (a release passes, and so does a commit with the trailer line "Changelog: none"). A pull request opened before the check reached main passes as it is; one opened after it that adds its entry to the head of Unreleased fails and is told where the entry goes. wai breaking reads breaking changes from entry files too. - The conformance workflow: each per-module verifier step is a file in .github/wai-conformance/verifiers/, each corpus a file in corpora/, and the integer-kernel tests carry a tag line. scripts/wai-conformance-lists.py derives every list the workflow kept by hand (WAI_FEATURES, the union --bin and --test lists, the features-alone lists, the kernel and corpus steps) from those files when the workflow runs. Its `selftest --exact` holds the derived lists to the hand lists of the commit before this one, and its dry-run prints every command each job runs: the dry-runs before and after this change differ only in the gate's path list, which gains the two paths this change adds. - What stays shared, and how to insert there, is written in wai/CONTRIBUTING.md and wai/changes/README.md: wai-rs Cargo.toml features and [[test]] entries, lib.rs, registry rows, wai/.gitattributes and the live corpus table take an insert beside the related entry, never at the end. Co-Authored-By: Claude Opus 5.5 <[email protected]>
154 lines
6 KiB
Python
Executable file
154 lines
6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""changelog-assemble.py: the changelog's entries, one a file, rendered into its Unreleased
|
|
section for a release.
|
|
|
|
A change adds its entry as a file of `wai/changes/` instead of editing the head of
|
|
`wai/CHANGELOG.md`, so that two changes' entries never edit the same line.
|
|
For a release this puts every entry at the head of `## Unreleased`, in a stable order, and
|
|
removes the files.
|
|
|
|
changelog-assemble.py [--changelog wai/CHANGELOG.md] [--fragments wai/changes]
|
|
[--check | --write]
|
|
|
|
With neither flag it prints the Unreleased section as it would be, changing nothing.
|
|
--check checks every file and exits 1 with one line a finding. --write writes the changelog
|
|
and removes the files it took.
|
|
|
|
A file is named `<yyyy-mm-dd>-<slug>.md` (the date it was written and a few words of its
|
|
title, lowercase, joined by hyphens) and holds exactly one entry in the changelog's form: a
|
|
`### ` title line, a blank line, and the entry's text, with no other heading outside a code
|
|
block. The entries go newest date first and, within a date, by file name, each separated
|
|
from the next by one blank line; the same files give the same bytes. `README.md` is not an
|
|
entry.
|
|
|
|
Standard library only.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
NAME = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$")
|
|
UNRELEASED = "## Unreleased"
|
|
|
|
|
|
def fragment_files(directory):
|
|
"""The entry files of `directory`, by name: every `*.md` but README.md."""
|
|
if not os.path.isdir(directory):
|
|
return []
|
|
return sorted(f for f in os.listdir(directory) if f.endswith(".md") and f != "README.md")
|
|
|
|
|
|
def check_fragment(name, text):
|
|
"""Every finding about one entry file: its name and its form."""
|
|
out = []
|
|
m = NAME.match(name)
|
|
if not m:
|
|
out.append(f"{name}: not named <yyyy-mm-dd>-<slug>.md (a date, then lowercase words joined by hyphens)")
|
|
else:
|
|
try:
|
|
datetime.date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
except ValueError:
|
|
out.append(f"{name}: {m.group(1)}-{m.group(2)}-{m.group(3)} is not a date")
|
|
if "\r" in text:
|
|
out.append(f"{name}: has carriage returns; lines end with \\n alone")
|
|
if not text.endswith("\n") or text.endswith("\n\n"):
|
|
out.append(f"{name}: does not end with exactly one newline")
|
|
lines = text.rstrip("\n").split("\n")
|
|
if not lines[0].startswith("### ") or not lines[0][4:].strip():
|
|
out.append(f"{name}: its first line is not the entry's title (`### <title>`)")
|
|
if len(lines) < 3 or lines[1] != "" or not "".join(lines[2:]).strip():
|
|
out.append(f"{name}: the title is not followed by a blank line and the entry's text")
|
|
fence = False
|
|
for n, line in enumerate(lines[1:], 2):
|
|
if line.lstrip().startswith("```"):
|
|
fence = not fence
|
|
continue
|
|
if not fence and re.match(r"^#{1,6} ", line):
|
|
out.append(f"{name}: line {n}: a heading inside the entry; an entry is one `### ` title and its text")
|
|
if line != line.rstrip():
|
|
out.append(f"{name}: line {n}: trailing white space")
|
|
if fence:
|
|
out.append(f"{name}: a code block is not closed")
|
|
return out
|
|
|
|
|
|
def order_key(name):
|
|
"""Newest date first; within a date, by name."""
|
|
m = NAME.match(name)
|
|
date = (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else (0, 0, 0)
|
|
return (tuple(-x for x in date), name)
|
|
|
|
|
|
def entries(directory):
|
|
"""[(name, text)] of the entry files, in the order they are rendered, and the findings."""
|
|
found, bad = [], []
|
|
for name in fragment_files(directory):
|
|
with open(os.path.join(directory, name), encoding="utf-8", newline="") as fh:
|
|
text = fh.read()
|
|
bad += check_fragment(name, text)
|
|
found.append((name, text))
|
|
found.sort(key=lambda e: order_key(e[0]))
|
|
return found, bad
|
|
|
|
|
|
def assemble(changelog_text, items):
|
|
"""The changelog with the entries at the head of its Unreleased section."""
|
|
lines = changelog_text.split("\n")
|
|
try:
|
|
at = lines.index(UNRELEASED)
|
|
except ValueError:
|
|
raise SystemExit(f"changelog-assemble: the changelog has no `{UNRELEASED}` line")
|
|
if not items:
|
|
return changelog_text
|
|
head = "\n".join(lines[: at + 1])
|
|
rest = "\n".join(lines[at + 1:]).lstrip("\n")
|
|
block = "\n".join(text.rstrip("\n") + "\n" for _, text in items)
|
|
return head + "\n\n" + block + ("\n" + rest if rest else "")
|
|
|
|
|
|
def main(argv):
|
|
args = list(argv)
|
|
def opt(name, default):
|
|
if name in args:
|
|
i = args.index(name)
|
|
v = args[i + 1]
|
|
del args[i:i + 2]
|
|
return v
|
|
return default
|
|
changelog = opt("--changelog", "wai/CHANGELOG.md")
|
|
directory = opt("--fragments", "wai/changes")
|
|
mode = args[0] if args else "print"
|
|
if mode not in ("print", "--check", "--write") or len(args) > 1:
|
|
print(__doc__)
|
|
return 2
|
|
items, bad = entries(directory)
|
|
if mode == "--check" or bad:
|
|
for b in bad:
|
|
print(f"{directory}/{b}")
|
|
if bad:
|
|
print(f"changelog-assemble: {len(bad)} finding(s); nothing was written")
|
|
return 1
|
|
print(f"changelog-assemble: every entry under {directory}/ is one entry in the changelog's form.")
|
|
return 0
|
|
with open(changelog, encoding="utf-8", newline="") as fh:
|
|
text = fh.read()
|
|
out = assemble(text, items)
|
|
if mode == "print":
|
|
lines = out.split("\n")
|
|
at = lines.index(UNRELEASED)
|
|
end = next((i for i in range(at + 1, len(lines)) if lines[i].startswith("## ")), len(lines))
|
|
sys.stdout.write("\n".join(lines[at:end]).rstrip("\n") + "\n")
|
|
return 0
|
|
with open(changelog, "w", encoding="utf-8", newline="") as fh:
|
|
fh.write(out)
|
|
for name, _ in items:
|
|
os.remove(os.path.join(directory, name))
|
|
print(f"changelog-assemble: {len(items)} entr{'y' if len(items) == 1 else 'ies'} written to {changelog}, and their files removed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|