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]>
310 lines
14 KiB
Python
Executable file
310 lines
14 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""check-changelog-fragment.py: a pull request that changes WAI says what it changed, in an
|
|
entry of its own.
|
|
|
|
check-changelog-fragment.py <base-sha> <head-sha>
|
|
|
|
A pull request whose diff (`git diff base...head`) changes anything under `wai/` must add or
|
|
edit an entry file directly in `wai/changes/` (one entry a file; see wai/changes/README.md),
|
|
in the changelog's form (scripts/changelog-assemble.py). A file in a subdirectory of
|
|
`wai/changes/` is refused: the assembler and `wai breaking` read only the directory itself,
|
|
so such an entry would never reach the changelog.
|
|
|
|
A change with nothing for the changelog to say (a lockfile bump, a fix to a test) says so
|
|
with the trailer line `Changelog: none`, exactly, in the trailer block of one of its commit
|
|
messages (the message's last paragraph, after a blank line, every line of it a `Key: value`
|
|
trailer), and passes.
|
|
|
|
A release's change, which renders the entry files into `wai/CHANGELOG.md` and removes them
|
|
(`changelog-assemble.py --write`), passes without one.
|
|
|
|
The transition. A pull request opened before this check landed passes as it is: it may edit
|
|
the head of `wai/CHANGELOG.md` instead, or carry no entry at all. The time it was opened is
|
|
the pull request's `created_at` in the event payload ($GITHUB_EVENT_PATH). The time the check
|
|
landed is when it reached the base branch: the committer time of the first commit on the
|
|
first-parent history of <base-sha> (the pull request's base, the tip of main) whose tree
|
|
holds this file. When the check comes in by a merge commit, that is the merge's own time,
|
|
not the time of the branch commit that added the file. Until the check is on the base branch
|
|
at all, every pull request counts as opened before it landed. One opened since it landed
|
|
that adds an entry to the head of `## Unreleased` in `wai/CHANGELOG.md` fails, and is told
|
|
to add the entry as a file of `wai/changes/` instead. With no event payload (a local run),
|
|
the change is taken as opened now.
|
|
|
|
Exit 0 when the change carries its entry, says it needs none, is a release's, was opened
|
|
before the check landed, or touches nothing under wai/; 1 otherwise, saying what to do. The
|
|
check never passes unchecked: a clone too shallow to diff is deepened first, and when the
|
|
two commits still cannot be compared, or git fails, or the times above cannot be read (a
|
|
shallow clone whose first-parent history stops before the answer is known), it fails and
|
|
says why (the workflow checks out the whole history, so this is a fault).
|
|
|
|
Standard library only; runs on Python 3.9.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import datetime
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import posixpath
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
WAI = "wai/"
|
|
CHANGELOG = "wai/CHANGELOG.md"
|
|
FRAGMENTS = "wai/changes"
|
|
README = "wai/changes/README.md"
|
|
SELF = "scripts/" + os.path.basename(__file__)
|
|
UNRELEASED = "## Unreleased"
|
|
TRAILER = "Changelog: none"
|
|
TRAILER_LINE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*:\s")
|
|
ISO = re.compile(r"^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})$")
|
|
|
|
|
|
class Fault(Exception):
|
|
"""Something the check needs and cannot read: it fails, saying what."""
|
|
|
|
|
|
def git(*args, cwd=None):
|
|
return subprocess.run(["git", *args], capture_output=True, text=True, cwd=cwd)
|
|
|
|
|
|
def must(*args, cwd=None):
|
|
r = git(*args, cwd=cwd)
|
|
if r.returncode != 0:
|
|
raise Fault(f"`git {' '.join(args)}` failed: {r.stderr.strip() or 'exit ' + str(r.returncode)}")
|
|
return r.stdout
|
|
|
|
|
|
def assembler():
|
|
spec = importlib.util.spec_from_file_location("changelog_assemble", os.path.join(HERE, "changelog-assemble.py"))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def comparable(base, head):
|
|
"""The merge base of base and head, fetching history once if the clone lacks it."""
|
|
mb = git("merge-base", base, head)
|
|
if mb.returncode == 0:
|
|
return mb.stdout.strip()
|
|
if git("rev-parse", "--is-shallow-repository").stdout.strip() == "true":
|
|
git("fetch", "-q", "--no-tags", "--unshallow", "origin")
|
|
for sha in (base, head):
|
|
if git("cat-file", "-e", f"{sha}^{{commit}}").returncode != 0:
|
|
git("fetch", "-q", "--no-tags", "origin", sha)
|
|
mb = git("merge-base", base, head)
|
|
return mb.stdout.strip() if mb.returncode == 0 else None
|
|
|
|
|
|
def trailers(message):
|
|
"""The lines of a commit message's trailer block: its last paragraph, when that is not
|
|
the subject and every line of it is a `Key: value` trailer (or a continuation line of
|
|
one, which starts with white space); otherwise none."""
|
|
paras = [p for p in re.split(r"\n[ \t]*\n", message.strip("\n")) if p.strip()]
|
|
if len(paras) < 2:
|
|
return []
|
|
out = []
|
|
for line in paras[-1].split("\n"):
|
|
if line[:1] in (" ", "\t") and out:
|
|
continue
|
|
if not TRAILER_LINE.match(line):
|
|
return []
|
|
out.append(line.rstrip())
|
|
return out
|
|
|
|
|
|
def says_none(mb, head):
|
|
log = must("log", "--format=%B%x00", f"{mb}..{head}")
|
|
return any(TRAILER in trailers(m) for m in log.split("\x00"))
|
|
|
|
|
|
def epoch(text, what):
|
|
m = ISO.match(text or "")
|
|
if not m:
|
|
raise Fault(f"{what} `{text}` is not an ISO 8601 time with its zone")
|
|
y, mo, d, h, mi, s, z = m.groups()
|
|
if z == "Z":
|
|
tz = datetime.timezone.utc
|
|
else:
|
|
z = z.replace(":", "")
|
|
sign = 1 if z[0] == "+" else -1
|
|
tz = datetime.timezone(sign * datetime.timedelta(hours=int(z[1:3]), minutes=int(z[3:5])))
|
|
return int(datetime.datetime(int(y), int(mo), int(d), int(h), int(mi), int(s), tzinfo=tz).timestamp())
|
|
|
|
|
|
def when(t):
|
|
return datetime.datetime.fromtimestamp(t, datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
|
|
def landed(base):
|
|
"""(commit, its committer time) of the first commit on the first-parent history of base
|
|
whose tree holds this file: when the check reached the base branch. None when no commit
|
|
of that history holds it: the check is not on the base branch yet. A shallow clone's
|
|
history stops at a commit whose parents it lacks, so when that commit already holds the
|
|
file, or the file is found nowhere before it, the answer is unknown and the check fails."""
|
|
out = must("log", "--first-parent", "--reverse", "--format=%H %ct", base)
|
|
rows = [l.split() for l in out.split("\n") if l.strip()]
|
|
if not rows or any(len(r) != 2 for r in rows):
|
|
raise Fault(f"`git log --first-parent {base}` listed no commits, so the time this check landed is unknown")
|
|
query = "".join(f"{sha}:{SELF}\n" for sha, _ in rows)
|
|
r = subprocess.run(["git", "cat-file", "--batch-check"], input=query, capture_output=True, text=True)
|
|
held = r.stdout.split("\n")[:len(rows)]
|
|
if r.returncode != 0 or len(held) != len(rows) or any(not l.strip() for l in held):
|
|
raise Fault(f"`git cat-file --batch-check` could not say which commits of {base}'s first-parent history hold {SELF}: "
|
|
f"{r.stderr.strip() or 'exit ' + str(r.returncode)}")
|
|
first = next((i for i, l in enumerate(held) if not l.endswith(" missing")), None)
|
|
shallow = must("rev-parse", "--git-path", "shallow").strip()
|
|
if os.path.isfile(shallow):
|
|
with open(shallow, encoding="utf-8") as fh:
|
|
stops = set(fh.read().split())
|
|
oldest = rows[0][0]
|
|
if oldest in stops and first in (None, 0):
|
|
found = (f"the first commit of it that holds {SELF}, {oldest[:12]}, is where this shallow clone's history stops"
|
|
if first == 0 else f"this shallow clone's history stops at {oldest[:12]} before any commit of it holds {SELF}")
|
|
raise Fault(f"on the first-parent history of {base[:12]}, {found}, so the time this check landed is unknown; "
|
|
f"check out the whole history (fetch-depth: 0)")
|
|
if first is None:
|
|
return None
|
|
sha, t = rows[first]
|
|
return sha, int(t)
|
|
|
|
|
|
def opened():
|
|
"""The pull request's creation time, from the event payload, or None with no payload."""
|
|
path = os.environ.get("GITHUB_EVENT_PATH")
|
|
if not path:
|
|
return None
|
|
try:
|
|
with open(path, encoding="utf-8") as fh:
|
|
event = json.load(fh)
|
|
created = event["pull_request"]["created_at"]
|
|
except (OSError, ValueError, KeyError, TypeError) as e:
|
|
raise Fault(f"the pull request's creation time cannot be read from the event payload {path} ({type(e).__name__}: {e})")
|
|
return epoch(created, "the pull request's created_at")
|
|
|
|
|
|
def unreleased_titles(text):
|
|
"""The `### ` titles of the changelog's Unreleased section."""
|
|
lines = text.split("\n")
|
|
if UNRELEASED not in lines:
|
|
return []
|
|
at = lines.index(UNRELEASED)
|
|
end = next((i for i in range(at + 1, len(lines)) if lines[i].startswith("## ")), len(lines))
|
|
return [l for l in lines[at + 1:end] if l.startswith("### ")]
|
|
|
|
|
|
def show(rev, path):
|
|
r = git("show", f"{rev}:{path}")
|
|
return r.stdout if r.returncode == 0 else ""
|
|
|
|
|
|
def check(base, head):
|
|
mb = comparable(base, head)
|
|
if mb is None:
|
|
raise Fault(f"{base} and {head} cannot be compared in this clone, even after fetching its history; the entry was not checked")
|
|
d = must("diff", "--name-status", "--no-renames", f"{mb}..{head}")
|
|
changed = [l.split("\t", 1) for l in d.splitlines() if "\t" in l]
|
|
wai = [p for _, p in changed if p.startswith(WAI)]
|
|
if not wai:
|
|
print("changelog: this change touches nothing under wai/, so it needs no entry")
|
|
return 0
|
|
|
|
bad, written, removed = [], [], []
|
|
for s, p in changed:
|
|
if not p.startswith(FRAGMENTS + "/") or p == README:
|
|
continue
|
|
if s == "D":
|
|
if posixpath.dirname(p) == FRAGMENTS and p.endswith(".md"):
|
|
removed.append(p)
|
|
elif posixpath.dirname(p) != FRAGMENTS:
|
|
bad.append(f"{p}: an entry file goes directly in {FRAGMENTS}/, not in a subdirectory: "
|
|
f"the assembler and `wai breaking` read only {FRAGMENTS}/ itself, so this entry would never reach the changelog")
|
|
elif not p.endswith(".md"):
|
|
bad.append(f"{p}: not an entry file: an entry is {FRAGMENTS}/<yyyy-mm-dd>-<slug>.md")
|
|
else:
|
|
written.append(p)
|
|
if written:
|
|
asm = assembler()
|
|
for p in written:
|
|
bad += [f"{FRAGMENTS}/{b}" for b in asm.check_fragment(posixpath.basename(p), must("show", f"{head}:{p}"))]
|
|
|
|
# Entries added to the head of Unreleased by hand, rather than rendered there from the
|
|
# files this change removes (a release).
|
|
by_hand, release = [], False
|
|
if any(p == CHANGELOG for _, p in changed):
|
|
after = show(head, CHANGELOG)
|
|
new = collections.Counter(unreleased_titles(after)) - collections.Counter(unreleased_titles(show(mb, CHANGELOG)))
|
|
titles = [show(mb, p).split("\n", 1)[0] for p in removed]
|
|
by_hand = sorted((new - collections.Counter(titles)).elements())
|
|
release = bool(titles) and all(t in after.split("\n") for t in titles)
|
|
|
|
state = {}
|
|
def before():
|
|
"""Whether the pull request was opened before this check landed on the base branch
|
|
(read once). Until it is on the base branch at all, every pull request was."""
|
|
if "before" not in state:
|
|
at = landed(base)
|
|
o = opened()
|
|
state["opened"] = "no event payload: taken as opened now" if o is None else when(o)
|
|
if at is None:
|
|
state["landed"] = None
|
|
state["before"] = True
|
|
else:
|
|
sha, t = at
|
|
state["landed"] = f"{when(t)}, {sha[:12]}"
|
|
state["before"] = o is not None and o < t
|
|
return state["before"]
|
|
|
|
if by_hand and not before():
|
|
titles = "; ".join(f"`{t}`" for t in by_hand[:3]) + ("; ..." if len(by_hand) > 3 else "")
|
|
bad.append(f"changelog: this change adds {len(by_hand)} entr{'y' if len(by_hand) == 1 else 'ies'} to the head of "
|
|
f"`{UNRELEASED}` in {CHANGELOG} ({titles}). Since this check landed on the base branch ({state['landed']}; this pull request: "
|
|
f"{state['opened']}), a change adds its entry as a file instead: move each to "
|
|
f"{FRAGMENTS}/<yyyy-mm-dd>-<slug>.md, holding it exactly as the changelog would, and leave {CHANGELOG} as it was "
|
|
f"(see {README}).")
|
|
if bad:
|
|
for b in bad:
|
|
print(f"::error::{b}")
|
|
return 1
|
|
if written:
|
|
print(f"changelog: this change's entry is {', '.join(written)}")
|
|
return 0
|
|
if release:
|
|
print(f"changelog: this change renders {len(removed)} entr{'y' if len(removed) == 1 else 'ies'} of {FRAGMENTS}/ into {CHANGELOG} (a release)")
|
|
return 0
|
|
if says_none(mb, head):
|
|
print(f"changelog: a commit of this change says `{TRAILER}` in its trailers")
|
|
return 0
|
|
if before():
|
|
if state["landed"] is None:
|
|
print(f"changelog: this check is not on the base branch yet (no commit of the first-parent history of "
|
|
f"{base[:12]} holds {SELF}), so this pull request counts as opened before it landed and passes as it is; "
|
|
f"one opened after it lands adds its entry as a file of {FRAGMENTS}/")
|
|
else:
|
|
print(f"changelog: this pull request was opened ({state['opened']}) before this check landed on the base branch "
|
|
f"({state['landed']}), so it passes as it is; one opened since adds its entry as a file of {FRAGMENTS}/")
|
|
return 0
|
|
print(f"::error::changelog: this change touches {len(wai)} file(s) under wai/ ({', '.join(wai[:3])}{', ...' if len(wai) > 3 else ''}) "
|
|
f"and adds no entry. Add one file, {FRAGMENTS}/<yyyy-mm-dd>-<slug>.md, holding the entry as the changelog "
|
|
f"would (a `### ` title, a blank line, its text; see {README}), or, if there is nothing for the changelog "
|
|
f"to say, end a commit message with the trailer line `{TRAILER}` (in its last paragraph, after a blank line, "
|
|
f"with the other trailers).")
|
|
return 1
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 2 or not argv[0] or not argv[1]:
|
|
print("usage: check-changelog-fragment.py <base-sha> <head-sha>")
|
|
return 2
|
|
try:
|
|
return check(*argv)
|
|
except Fault as e:
|
|
print(f"::error::changelog: {e}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|