Skip to content

Release 0.4.0: dependency minimums raised to the latest releases - #7

Merged
dvejsada merged 93 commits into
mainfrom
claude/tender-curie-iv97m6
Oct 28, 2026
Merged

dvejsada merged 93 commits into
mainfrom
claude/tender-curie-iv97m6

Conversation

@dvejsada

@dvejsada dvejsada commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Prepares the 0.4.0 release, the hardening release merged in #6.

Dependencies

Minimums raised to the latest releases, which the suite is run against:

Package Before After
markdown-it-py >=3.0,<5 >=4.2,<5
babel >=2.14,<3 >=2.18,<3
pyyaml >=6.0 >=6.0.3
legaldown-validator >=0.4.0,<0.5 unchanged (latest; pinned to one minor version)
dev: pytest / ruff / build >=8.0 / >=0.6 / >=1.2 >=9.2 / >=0.17 / >=1.6
build backend hatchling hatchling>=1.33

No code changes were needed.

Release

  • __version__ is 0.4.0, and CONFORMANCE.md names 0.4.0.
  • The roadmap marks v0.4 as this release.
  • The two HTML goldens change only in their generator meta line.

requires-python stays >=3.11. CI tests Python 3.13 only.

Test plan

  • pytest -q: 2149 passed with LEGALDOWN_SPEC_DIR set.
  • ruff check . is clean.
  • python -m build gives legaldown_render-0.4.0.tar.gz and legaldown_render-0.4.0-py3-none-any.whl, and twine check --strict passes on both.

After merge: publish a GitHub Release with tag v0.4.0. The publish workflow runs CI, checks that the tag matches __version__, and uploads to PyPI.

- S1: main() reconfigures stdout to UTF-8 and stderr to backslashreplace
  (guarded by hasattr(..., "reconfigure")); stdin is read as bytes and
  decoded as UTF-8. Czech text no longer crashes or is corrupted on cp1252.
- S4: a non-UTF-8 input file or stdin is "cannot read NAME: not valid UTF-8
  (byte 0x.. at offset N)", exit 1 (offset is in the file, not a buffer).
  A non-UTF-8 style file is a StyleError (style/loader.py _read only).
- S5: an OSError writing -o is "cannot write PATH: ...", exit 1.
- S6: -o with an extension no writer knows (x.docx, .pdf, .md) and no -f is
  a usage error (exit 2); no extension keeps the html default; -f wins.
- S2: StyleError is defined in errors.py as StyleError(RenderError,
  ValueError) and re-exported from style/loader.py, legaldown_render.style
  and the package. An unknown format raises the new OptionsError(RenderError,
  ValueError), exported from the package.
- S3: RenderRefused (now storing .reason) and StyleError define __reduce__ so
  they round-trip through pickle.
- S10 (CLI side): the argparse type _setting turns StyleError into
  ArgumentTypeError so its message reaches the user.

Docs: README (exit codes, encoding, -o extension, error classes),
docs/architecture.md (errors). Tests: tests/test_wp2_cli.py (new); no
existing test or golden changed.
…ed symbols (R1-R4)

R2: Babel calls for number/money/duration run in a decimal context whose
precision is max(28, len(value) + 8). Amounts of 27+ digits no longer raise
InvalidOperation and fractions over 28 digits are no longer rounded (§10.3).

R3: durations are formatted like number() (every written digit, locale
separators) and the plural category is taken from the displayed value
(Locale.plural_form on the Decimal: "1.0 days", cs "2,5 roku"). Babel's
format_unit, which rounds to 3 decimals, is no longer used.

R1: the CLDR pattern is looked up through one helper (Locale._data is the only
access; Babel 2.x has no public API): requested length, other lengths, the
language-only locale, then English. A raw "duration-day" is never printed.
Formatter.format_duration returns DurationText(text, english_fallback); the
resolver emits one render-locale-fallback Warning per (locale, unit) when
English stands in. Formatter.duration keeps returning str.

R4: an alphabetic currency symbol or code touching a digit on either side gets
U+00A0 in symbol and code mode ("CZK 5.00", "CHF 1.00"); "$5.00", "R$ 5,00",
"CA$5.00" and cs "5,00 Kč" are unchanged.

New: tests/test_wp1_values.py. No existing test or golden changed. Docs: ADR 0005
and docs/architecture.md.
…tives, dropped-comment spacing, {{term:}} text (B2, B3, B4, B8)

- B2: a list item whose model blocks are all comments/raw HTML (kind "html")
  is left out, after the whole list is traversed so item numbering and
  PlacedMarker.item stay aligned; a list left with no items and an ordinary
  quote with no built blocks are left out. A truly empty item and an empty
  drafting note keep their place. (build.py _list, list, quote)
- B3: every directive sentinel is wrapped in U+2E31 (category Po, accepted by
  markdown-it's isPunctChar) on both sides, as the braces it replaces are
  punctuation, so `_text_{{ref: a}}`, `{{ref: a}}_text_`, `__b__{{ref: a}}`
  and `*t*{{ref: a}}` parse as with `{x}`. sentinel_re, encoded_sentinel_re
  and _restore consume the wrappers; the hidden form's "h" tag is no longer
  needed. (build.py)
- B4: _merge_text strips the spaces before a dropped inline when a line break
  follows, so no double space is left.
- B8: Resolver._survey collects definition id -> plain rendered term text
  (first definition wins, via _title_text); {{term:}} uses it, falling back
  to the validator's definition_lookup.

Existing test changed: test_empty_list_items_keep_their_place is extended
(an empty item still keeps its place; a comment-only one goes). Goldens are
unchanged. Docs: CONFORMANCE.md, docs/architecture.md.
render_file() read the file with read_text, so a non-UTF-8 file raised a
bare UnicodeDecodeError. It now reads bytes and raises DocumentError with
the CLI's wording ("not valid UTF-8 (byte 0x.. at offset N)").

Behaviour change: DocumentError is not a ValueError, so code that caught
UnicodeDecodeError/ValueError from render_file for this case must catch
DocumentError (or RenderError). OSError for unreadable files is unchanged.

Test: tests/test_wp2_cli.py::test_render_file_raises_document_error_for_non_utf8.
money with a currency showed the larger of the currency's minor units and the
written digits only when the last written digit was non-zero: "1.500" EUR gave
"€1.50" and "1.1230" gave "€1.123". The fraction digit count is now fixed in the
pattern as max(minor units, written decimals) with currency_digits off, in
symbol, code and name mode, under _exact. "5" EUR is still "€5.00", JPY "5"
is "¥5", JPY "0.125" is "¥0.125". Tests added to tests/test_wp1_values.py; no
existing test or golden changed.
`"Self {{term: s}}" {{def: s}}`, and cycles through several definitions,
raised RecursionError in Resolver._term (already so before WP4, through
_plain_value of the validator's raw term). The term-text computation now
tracks the definitions it has open (the _survey pre-pass and the
_plain_value fallback, _plain_open): a {{term:}} to an id already open shows
its label=, else the definition id, and goes no further. The validator 0.4.0
reports nothing for such documents; the spec's definition-circular Warning
is not emitted by it, and no diagnostic is added here (CONFORMANCE.md notes
it).

Tests: direct and indirect cycles, at the definition site and at reference
sites, in the text and HTML writers.
A comment-only list item is now dropped only when the markers give it no
anchor identifier and no condition. The validator does not place a marker
on such an item, so this is defensive; the test drives the builder with a
made-up marker.
…es, template fields (R6, S7-S11)

- R6: Resolver._placeholder renders [TBD: id] when the effective
  placeholders.blank is empty or whitespace (spec §13.5 rule 6); no StyleError.
- S7: extends cycles are detected on resolved paths (Path.resolve), so
  ../dir/a.yaml, a second spelling, or a symlink no longer recurses until
  "File name too long".
- S8: dump_style drops `extends`, so a --print-style dump is self-contained and
  reloads from any directory.
- S9: overriding `version` or `extends` is a StyleError (it was silently
  ignored, and --print-style could print an unloadable style).
- S10 (loader side): a YAML error in a --set value raises StyleError naming
  the key and the YAML problem (with line/column) instead of resurfacing as a
  misleading type error; reaches stderr through WP2's argparse wrapper.
- S11: model.TemplateFields / template_settings inventory the {field} names of
  every text template setting (verified against numbering.fill,
  Resolver._ref, fill_condition); the loader reports unknown
  identifier-shaped fields and missing required ones, naming the setting
  (labels.condition must contain {condition}, §15.8). Other braces stay
  literal. Built-in styles and labels pass.

Existing test changed: tests/test_style.py::test_dump_round_trips now compares
with extends=None, because the flattened dump no longer records what the
style extended (S8). Docs: style-templates.md (field table, flat dump,
overrides), CONFORMANCE.md, architecture.md.
…back (S11 follow-up)

The required-field check no longer refuses an empty template for a setting
the resolver already replaces when empty: `labels.condition or "{condition}"`
(Resolver._condition) and `labels.attachment_file or "{file}"`. An empty
labels.condition shows the bare condition, which meets §15.8, and refusing it
would break existing styles for no gain. TemplateFields.empty_falls_back marks
exactly these two settings. Settings with no fallback (references.format and
the numbering/enumeration/paragraph refs) still require {designation}/{n};
unknown fields are checked in every non-empty template.

Tests: new tests for the empty condition label (renders the bare condition)
and for empty templates without a fallback. docs/style-templates.md updated.
…ty links (B6, W3, B9/W8)

B6 (§2.3): an image whose path has a URI scheme (data: included), starts with
/ or \ (//host too) or is a Windows drive path is replaced by its alt text in
the resolver (Resolver._resolve_inline) and reported as the Core Error
path-not-relative, which --strict refuses. The validator 0.4 does not check
image paths; the renderer's report is skipped when the validator names the same
path. ../ stays allowed (no default document root). The builder now keeps
javascript:/file:/data: links and images as nodes (markdown-it validateLink off)
so they can be reported and shown as text instead of as literal Markdown.

W3 (§8.7): the HTML allow-list becomes a deny-list (javascript, vbscript, file,
data) after the WHATWG normalisation; new urls.py shared by resolver and both
writers so they agree. A refused link is its text only in both writers.

B9/W8: an empty href is just the link text; a link with no text shows its URL;
the text writer omits <url> when it equals the text minus mailto:/tel:.

Changed goldens: features.{default,continental,outline}.txt and
features.default.html: the "[an unsafe one](javascript:alert(1))" source text is
now "an unsafe one" (the link is refused, its text kept).
… gate

An image with an empty or whitespace-only path is replaced by its alt text in
the resolver (no <img src="">, which requests the page itself) and reports
nothing: §2.3 has no rule for an empty path. Tests for both writers.

urls.py, html.py and docs/architecture.md no longer say markdown-it refuses
unsafe schemes while parsing: its validateLink is off in the builder, and the
deny-list is the gate for every href/src the HTML writer emits (Link, Image,
Value).
…ms, blank labels (W1, W4-W7)

W1: TextWriter takes the style's attachments.separator (api._writer hands
it over; TextWriter() keeps the default style's "page-break"). "none" prints
no separator; "rule" and "page-break" both print "---" before EACH
attachment (plain text has no pages). The "---" that used to precede the
"Attachments" heading is gone; the heading now stands directly after the body.
W4: "|" in a table cell is escaped as "\|"; the separator row carries
alignment colons from Table.align (":---", ":---:", "---:"), unpadded.
W5: _prefix gives a blank line the right-trimmed prefix, so one quote stays
one quote (">" and "> >" on blank lines). Blocks of a list item are separated
by a blank line; a nested list directly after a paragraph (or another list)
stays tight as before.
W6: an item's content indent is len(label or bullet) + 1, whatever its
condition label.
W7 (text only): a style-blanked label drops the "label: " prefix in the title
block and party details. HTML keeps its empty <dt>.

Existing tests changed because of W5:
- test_nested_lists_line_up_with_their_item_s_text: blank line between the
  item's paragraphs.
- test_a_later_paragraph_in_a_list_item_is_the_item_s: blank line before the
  item's second paragraph.
- test_a_heading_in_a_quote_or_item_is_a_bold_line_not_a_section: the
  two-block quotes printed as two quotes; now one quote with a ">" line.

Goldens changed (features.default/continental/outline.txt, each hunk read):
- W1: "---" moved from before the "Attachments" heading to before each of the
  two attachments.
- W4: the table separator row is now "|:---|---:|:---:|" (the document's
  table is left/right/center aligned).
template.default.txt is unchanged.
…ceholder inside it is kept

The term text of a {{term:}} was collected in Resolver._survey with a text-only
pass, before sections and anchors were registered and before inconsistent
placeholder types were known: a {{ref:}} in the term was dropped ("Fee per")
and a placeholder used with conflicting types showed a blank. _survey now
records only the first DefinitionSource per id; Resolver._term_text resolves its
children during inline resolution (all targets known), takes plain text and
caches it, under the existing _terms_open cycle guard. It restores used_anchors,
so a definition nested in a term keeps its document-order anchor; before targets
are known (a none-scheme heading) the text-only pass stands in, uncached.
_warn and _not_relative deduplicate, so no repeated diagnostics. Docs updated.
No existing test or golden changed.
R5: a list item in the preamble has no section, so `{section}` was filled
with "" and the continental label read ".1" while its designation was "1".
`numbering.fill` now removes `{section}` and one following "." when the
section is given as the empty string, so the label is "1". Items in a
section keep "1.1". The text and HTML output of a preamble list change
(".1 first pre" becomes "1 first pre"); no golden contains one.

W2: the print stylesheet broke before each `.ld-attachment`, so the
"Attachments" heading was stranded at the foot of the previous page. The
break now goes before the `.ld-attachments` container (its heading goes
with the first attachment), before each later attachment, and the heading
has `break-after: avoid`. Goldens features.default.html and
template.default.html changed only in those three CSS lines
(`--update-golden`, hunks inspected).

Tests: tests/test_wp7_small.py. Docs: CONFORMANCE.md (preamble label) and
docs/style-templates.md (`{section}` in the preamble).
…ck to 'other' (review: values)

Babel's plural operands convert the fraction digits with int(), which Python
limits to 4300 digits; {{duration:}} and name-mode {{money:}} with a longer
fraction raised a raw ValueError. values._plural_category catches it and uses
'other' (the CLDR category of a non-integer in nearly every locale); name-mode
money puts Babel's long-name pattern together with 'other' when its own lookup
would fail. sys.set_int_max_str_digits is process-wide and is not touched.
…ature labels (W1, W7 follow-up)

W1: with rule/page-break the text writer prints "---" before the
attachments heading (as it did originally) and between consecutive
attachments, not between the heading and the first attachment, so the
heading stays with what it heads. With none, no "---" at all. Updates the
TextWriter docstring, CONFORMANCE row and docs.
W7: the blank-label rule also covers the signature blocks (name, title, date,
place): a blanked label prints the value alone, and a row with neither label
nor value is omitted instead of a lone ":".

Goldens (features.default/continental/outline.txt): the "---" moves back
before the "Attachments" heading, relative to the previous commit; a "---"
now also separates the two attachments. Tests in tests/test_wp5_text.py
updated and extended for both changes.
…sage error (review: cli)

contract.v2, report.2025-10-27 and .out failed with exit 2 since the S6 fix. Now
only .docx .doc .odt .rtf .pdf .md .markdown .epub .tex (any case) need --format;
any other extension, or none, keeps the html default. -o and -f --help and the
README state the exact rule. tests/test_wp2_cli.py: the S6 parametrization grows
to the full list, and tests for the kept default, known extensions and help.
…LF everywhere (review: cli)

main() reconfigured sys.stdout/sys.stderr on every call, so an in-process
main([...]) changed the caller's streams for good. It now does so only when
argv is None (the console script and python -m call it so). Standard output is
reconfigured with newline="\n" and -o files are written with newline="\n", so
no platform translates LF to CRLF. README states both. tests/test_wp2_cli.py:
comments on the exact-byte comparisons (LF on every platform). New tests in
tests/test_wp9_review_fixes.py. No golden changed.
…ations (S13, S14, S15, B1, B5, B7, B10, R7/B11, W9/S12)

S13 labels (style/labels.py): the party identifier label is shown for §3.4
identification_number, which natural persons may have too. cs "IČO" ->
"Identifikační číslo", sk "IČO" -> "Identifikačné číslo", fr "Représentée par"
-> "Représenté(e) par"; pl "Reprezentacja" kept (no gendered form). Existing
test changed: tests/test_render.py French label assertion. No golden shows
these labels. New tests/test_wp8_labels.py.

S14: README and __init__ docstring examples open files with encoding="utf-8"
(and write with newline="\n"); the --locale example says it formats values and
README states labels follow the document language; README links are absolute
GitHub URLs for PyPI; CONFORMANCE.md added to the sdist (checked with
python -m build --sdist). docs: locale vs language wording in concepts.md and
style-templates.md.

S15: dropped the deprecated License classifier (license = "MIT" is SPDX);
.github/PUBLISHING.md recommends gating the publish workflow on the tests
(workflow file not changed).

DOC rows, each verified by running an example: CONFORMANCE.md Known
limitations for text after a comment (B1), directives in link/image
destinations (B5), the exact nesting-limit rule (B7), table cells beyond the
header (B10), parties block vs {{party:}} (R7/B11); architecture.md Security
rewritten for trusted style input (W9/S12), link-gate sentences de-duplicated.
…pe controls (review wave 2)

- urls.LINK_SCHEMES (http, https, mailto, tel, ftp, ftps, sftp, sip, sips, sms,
  urn, geo, news, plus scheme-less URLs) replaces the DENIED_SCHEMES deny-list:
  protocol handlers (ms-msdt:, vscode:, ...) and pseudo-schemes (C:\x,
  localhost:8080) render as their text. One predicate, is_link_target.
- resolver: a refused non-empty link is the Warning render-link-refused.
- urls.printable escapes C0/C1, DEL, Cf, U+2028/9 in quoted paths/URLs; used in
  path-not-relative and the new warning (no ESC/BEL/newline forged on stderr).
- path-not-relative dedupe matches the quoted '<path>'; message is generic.
- A link with no text shows the URL as written (unquoted), in both writers.
- tests/test_wp3_links.py: magnet: is no longer a link; 'a:b' is no longer
  a safe href (allow-list).
- template_settings: a ref is required to contain {n} only where it is read
  (numbering unless scheme none; enumeration when enabled or ordered:
  enumerate; paragraphs when numbered). Unknown fields are still flagged.
- Plain labels (labels.* but condition and attachment_file) substitute
  nothing, so braces in them are text and they are no longer checked.
- tests/test_wp6_style.py: labels.drafting_note/colon with fields are valid;
  paragraph format cases set paragraphs.numbered.
The assembler does not know the file it was given, so its findings
(answer-unknown, answer-invalid, answer-missing) had an empty file even
when RenderOptions.filename was set. _assemble now stamps the file name
on every diagnostic that names none, on the refusal and on the result
(§16.9). Tests extend the assembly test with the warning and the refusal.
A directive in a party's legal_name, label, address, identification_number,
date_of_birth, custom field or a representative's name or title was reported
at the party's first line. Each field is now resolved at its own key (the
signature blocks too); a value the source writes elsewhere, as a << merge
does, is at the line it is written. tests/test_diagnostic_lines.py: the
party/side test now expects the address key, not the party's line.
…ne order

The resolver works out the body before the title block, so a finding seen
first in the body and again in the title was reported at the later line.
diagnostics.record now keeps one finding (rule, level, message) at the
lowest line it was seen at, in Resolver._warn and Resolver._reference, and
api.render lists the renderer's own diagnostics in the order of their lines
(none last, stable) after the validator's. CONFORMANCE.md and
docs/architecture.md say so.
filename is documented as the name without its directory, but
render(filename='a/b.lgd') kept it on every diagnostic's file. render now
keeps the last path component, split at both / and \.
… root

A path with NUL or another control character (after the URL decoding that
applies to an image's source) is path-not-relative, so '..%00/x.png' and an
include of 'a<NUL>b' no longer pass as plain names. In the walk of a path
against the root a segment of '..' followed by dots or spaces counts as '..'
(Windows drops trailing dots and spaces), so '... /x' and '..../x' leave the
root; 'a..b.png' and '...hidden/x' are names as before. CONFORMANCE.md and
docs/architecture.md describe both, and the limitations that a file name with
a colon before any slash reads as a URI scheme (write ./report:v2.png) and
that an attachment's file is read as a URL only where it becomes a link.
…sted mode

A style value written into the stylesheet (fonts, sizes, colours, margins, page size,
max width) containing /*, */, </, a backslash, a control character or a line break is
now a StyleError naming the setting, in every mode: rejected rather than stripped,
since stripping one token can form another. html.extra_css containing </style (any
case) is refused. writers.html._css, the last line for a hand-built Style, now also
strips backslash, @, control characters and comment delimiters (a lone / stays so
calc(100% / 3) survives).

load_style(trusted=False) is for a style from somebody else: a built-in name, a
Style, a mapping or YAML text, never a file; extends only a built-in; strict grammars
per kind of value (declared as field metadata in style/model.py, enforced in
_convert, so a Style object and override objects are held to them too); no
html.extra_css; YAML text of at most 64 KiB without aliases; messages that name the
setting and never quote the style or a path.

RenderOptions.untrusted and --untrusted require a document root (OptionsError, exit
2) and load the style untrusted. Defaults are unchanged. No existing test or golden
changed.
README section on rendering documents you did not write, the Security and style
template chapters, and the conformance notes.
Keeps both: the none-scheme designation pass and the lowest-line rule for
repeated findings. The designation pass's diagnostics go through the same
Resolver._warn, and its rollback restores the diagnostics as they were
(a repeat may have lowered the line of one). CONFORMANCE.md's line-number
paragraph carries the designation findings' lines and the repeats rule.
The control-character check ran on the path before what a browser strips at
either end was removed, so a YAML block or folded scalar (which ends in a
newline), as in 'file: >' for an attachment, was a false path-not-relative.
The path is now read as before (tabs and newlines ignored, control characters
and spaces at either end stripped) and only a control character left inside it
(NUL, ESC, ...) is path-not-relative. Tests: block and folded scalars give no
finding; interior NUL and ESC, and '..%00/x.png' as an image, are still
refused.
…in its size

PyYAML's scanner takes time quadratic in the nesting depth of a flow collection:
a 40 KB text of nested brackets took 45 seconds to refuse in untrusted mode.
Styles are now read with a SafeLoader that refuses more than 32 levels (flow and
block) as the scanner reaches them, trusted or not; a mapping or Style passed in
is held to the same depth, and RecursionError stays only as a last resort. The
nesting test counts the scanner's work instead of timing it, which also brings
the suite back from 110 s to 60 s.
…e outermost term, keep the item piece of a cut reference

A heading read from the text of a term that the heading itself uses was
memoised with the term cut short in it, so a reference to it showed a text that
differed from the heading with no Warning. Its text is not kept now, it is
worked out again where the term is not open, and render-designation-cycle
reports it.

The nesting limit counted the levels the outer block already had, so a term
nested about 58 levels in a block, whose definition used another term, showed
the inner id with render-term-limit. It now counts from where the outermost
open term began (or from the heading, while designations are worked out).

A reference to an item or paragraph under a heading longer than 1,000
characters cuts the heading part and keeps the item piece, and says in its own
message that the reference shows the first 1,000 characters.

The validator's text of a term is searched for a reference only for a term
whose definition is not in the body. The docs state when designations are
worked out, that a ring of more than 32 headings is reported as a limit, that
a heading with 59 or more nested levels can reach the nesting limit after one
hop, and that a frontmatter value shown in a heading is not designated through.

Tests: the repro, the nesting at 58 and 98 levels in both schemes, the cut
reference, and every heading that reads differently from its designation being
reported, over random tangles of headings, items and terms.
YAML numbers, booleans and timestamps are read through guarded constructors: text
PyYAML cannot convert, or a number of more than 32 characters, is a style error
(it raised ValueError or KeyError quoting the text, or a traceback on the command
line). Enormous numbers in a mapping, as keys, values or overrides, are refused
without str(). Whatever else the loader raises on an untrusted style is reported
as one generic style error; a heading level out of range no longer quotes its key.

values.date: an untrusted style names short, medium, long or full; a trusted
pattern is formatted on fixed dates when the style loads, so one Babel refuses
(GGGGGGG, QQQQQQ) is a style error instead of an exception at every render.

An untrusted style's free-text settings are at most 200 characters (a long
references.format multiplied the output by the number of references), and NUL
and lone surrogates are refused in them and, in every mode, in the settings
written into the stylesheet. A message lists 20 problems at most.

--untrusted --print-style loads the style untrusted; the --untrusted help says a
style may be a name or YAML text. stylesheet() writes any </ in extra_css as <\/.
Docs describe all of it. No existing test changed.
…and PDF renumbered

v0.4 is now the hardening release; DOCX moves to v0.5 and PDF to v0.6.
The upstream prerequisites table records what the renderer now reads from the
validator (source positions) and what still waits on it (link reference
definitions, the path and definition-circular rules, wide table rows).
The Node 20 action majors triggered the runner deprecation warning on
every run. Move to the latest majors, which run on node24:

- actions/checkout v4 -> v7
- actions/setup-python v5 -> v7
- actions/upload-artifact v4 -> v7
- actions/download-artifact v4 -> v8

Inputs used here (checkout repository/ref/path, setup-python
python-version and cache, artifact name/path) are unchanged. The
download-artifact v8 digest check defaults to error; the artifacts are
produced and consumed by the same workflow run, so it is not affected.

pypa/gh-action-pypi-publish is a composite action and is left as is.
Tests now run on Ubuntu and Windows with Python 3.13. Lint, conformance and
validator-main also use 3.13. Update the PUBLISHING.md note that describes
the CI matrix to match.
@dvejsada
dvejsada merged commit 387e63e into main Oct 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant