brevier 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brevier-0.1.0/.githooks/commit-msg +32 -0
- brevier-0.1.0/.github/ISSUE_TEMPLATE/bug.md +52 -0
- brevier-0.1.0/.github/ISSUE_TEMPLATE/epic.md +39 -0
- brevier-0.1.0/.github/ISSUE_TEMPLATE/task.md +52 -0
- brevier-0.1.0/.github/ISSUE_TEMPLATE/user-story.md +71 -0
- brevier-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +59 -0
- brevier-0.1.0/.github/workflows/cross-os-proof.yml +133 -0
- brevier-0.1.0/.github/workflows/pip-install-proof.yml +220 -0
- brevier-0.1.0/.github/workflows/release.yml +45 -0
- brevier-0.1.0/.gitignore +29 -0
- brevier-0.1.0/AGENTS.md +172 -0
- brevier-0.1.0/CLAUDE.md +1 -0
- brevier-0.1.0/PKG-INFO +12 -0
- brevier-0.1.0/brevier/__init__.py +1 -0
- brevier-0.1.0/brevier/cli/__init__.py +129 -0
- brevier-0.1.0/brevier/css_layers/__init__.py +66 -0
- brevier-0.1.0/brevier/engine/__init__.py +53 -0
- brevier-0.1.0/brevier/engine/weasyprint_engine.py +57 -0
- brevier-0.1.0/brevier/pipeline/__init__.py +117 -0
- brevier-0.1.0/docs/PRD.md +793 -0
- brevier-0.1.0/docs/architecture/adr/001-install-channels.md +57 -0
- brevier-0.1.0/docs/architecture/adr/002-identical-output.md +133 -0
- brevier-0.1.0/docs/architecture/adr/003-component-architecture.md +98 -0
- brevier-0.1.0/docs/architecture/adr/004-product-name.md +35 -0
- brevier-0.1.0/docs/process/DEFINITION_OF_DONE.md +40 -0
- brevier-0.1.0/docs/process/DEFINITION_OF_READY.md +20 -0
- brevier-0.1.0/docs/process/METHODOLOGY.md +38 -0
- brevier-0.1.0/docs/process/NON_FUNCTIONAL_REQUIREMENTS.md +38 -0
- brevier-0.1.0/docs/process/PRODUCT_GOAL.md +31 -0
- brevier-0.1.0/docs/process/SCRUM_EVENTS.md +44 -0
- brevier-0.1.0/pyproject.toml +38 -0
- brevier-0.1.0/scripts/check_name.py +401 -0
- brevier-0.1.0/tests/architecture/test_engine_is_hidden.py +33 -0
- brevier-0.1.0/tests/cli/test_render_command.py +101 -0
- brevier-0.1.0/tests/compare_pdf_content.py +116 -0
- brevier-0.1.0/tests/cross_os_check.py +120 -0
- brevier-0.1.0/tests/css/__init__.py +0 -0
- brevier-0.1.0/tests/css/test_layer_order.py +128 -0
- brevier-0.1.0/tests/fixtures/hello-bundled.html +23 -0
- brevier-0.1.0/tests/fixtures/hello.css +47 -0
- brevier-0.1.0/tests/fixtures/hello.html +12 -0
- brevier-0.1.0/tests/packaging/check_pdf.py +41 -0
- brevier-0.1.0/tests/packaging/test_pyproject_pins.py +58 -0
- brevier-0.1.0/tests/pipeline/conftest.py +49 -0
- brevier-0.1.0/tests/pipeline/test_debug_output.py +108 -0
- brevier-0.1.0/tests/pipeline/test_emit_html_writes_both_files.py +77 -0
- brevier-0.1.0/tests/scripts/test_check_name.py +263 -0
- brevier-0.1.0/uv.lock +967 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Enforces AGENTS.md's commit conventions: subject must reference a real
|
|
3
|
+
# issue number, subject must be short, body must stay short (a one-line
|
|
4
|
+
# reason at most, not a rewritten copy of the issue). Installed via
|
|
5
|
+
# `git config core.hooksPath .githooks` (see README/CONTRIBUTING).
|
|
6
|
+
set -eu
|
|
7
|
+
|
|
8
|
+
MSG_FILE="$1"
|
|
9
|
+
SUBJECT=$(sed -n '1p' "$MSG_FILE")
|
|
10
|
+
SUBJECT_LEN=$(printf '%s' "$SUBJECT" | wc -c)
|
|
11
|
+
|
|
12
|
+
# Body = all non-blank lines after the first blank line following the subject.
|
|
13
|
+
BODY_LINES=$(awk 'NR==1{next} /^$/{seen_blank=1; next} seen_blank && NF{count++} END{print count+0}' "$MSG_FILE")
|
|
14
|
+
|
|
15
|
+
FAIL=0
|
|
16
|
+
|
|
17
|
+
if ! printf '%s' "$SUBJECT" | grep -qE '#[0-9]+'; then
|
|
18
|
+
echo "commit-msg: subject line must reference a real issue number, e.g. 'feat: add hyphenation ranking (#12)'" >&2
|
|
19
|
+
FAIL=1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ "$SUBJECT_LEN" -gt 73 ]; then
|
|
23
|
+
echo "commit-msg: subject line is $SUBJECT_LEN chars, keep it under ~72" >&2
|
|
24
|
+
FAIL=1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
if [ "$BODY_LINES" -gt 5 ]; then
|
|
28
|
+
echo "commit-msg: commit body has $BODY_LINES content lines — keep it to a short reason, not a rewritten issue. Long explanation belongs in the issue, not the commit." >&2
|
|
29
|
+
FAIL=1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
exit $FAIL
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug
|
|
3
|
+
about: Something is broken, with a known or suspected reproduction
|
|
4
|
+
title: '[BUG] '
|
|
5
|
+
labels: bug
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Before opening this issue:
|
|
9
|
+
Short sentences, plain words, facts and numbers. No filler.
|
|
10
|
+
Labels: one type (set by this template) and one priority, P0-critical
|
|
11
|
+
to P3-low. Assign the issue. The sprint is the milestone: sprint-N
|
|
12
|
+
labels are retired, do not add one.
|
|
13
|
+
Project board: set Status, Priority, Size, Estimate, Start date and
|
|
14
|
+
Target date. Size and Estimate come from the points or hours below. -->
|
|
15
|
+
|
|
16
|
+
## Bug
|
|
17
|
+
<!-- What is broken and how to see it: reproduction steps, a command, or a
|
|
18
|
+
log excerpt. Say whether it is confirmed live or only suspected. -->
|
|
19
|
+
|
|
20
|
+
## Root Cause
|
|
21
|
+
<!-- Fill in once known. Delete this section while still investigating. -->
|
|
22
|
+
|
|
23
|
+
## Severity vs. Priority
|
|
24
|
+
<!-- Two axes, set independently. A low-severity bug can be P0 when it
|
|
25
|
+
blocks a release. A Blocker-severity bug in dead code can be P3.
|
|
26
|
+
|
|
27
|
+
The priority written here and the P0-critical to P3-low label on the
|
|
28
|
+
issue must agree. -->
|
|
29
|
+
**Severity (technical impact):** Blocker / Critical / Major / Minor
|
|
30
|
+
**Priority (business urgency):** P0 / P1 / P2 / P3
|
|
31
|
+
|
|
32
|
+
## Acceptance Criteria
|
|
33
|
+
<!-- At most 5, each independently checkable, each naming the test or
|
|
34
|
+
command that proves it. A date is not a proof.
|
|
35
|
+
|
|
36
|
+
Two more rules for a Bug specifically:
|
|
37
|
+
"Fixed and verified live" means you drove the broken path and watched
|
|
38
|
+
it work, not that the diff looks right. If the bug is in generated
|
|
39
|
+
code, verify against the golden-project build, not a template unit
|
|
40
|
+
test alone.
|
|
41
|
+
The regression test is the criterion most often ticked without the
|
|
42
|
+
test existing. Name it, and break the fix once to watch it fail. -->
|
|
43
|
+
- [ ] Fixed, and the fixed path verified through the layer the bug was in
|
|
44
|
+
(`<test or command>`)
|
|
45
|
+
- [ ] Regression test added, seen failing before it passed (`<test name>`)
|
|
46
|
+
|
|
47
|
+
## Notes
|
|
48
|
+
<!-- Decisions, rescopes, follow-up issue numbers. Delete if there are
|
|
49
|
+
none. Not a running log.
|
|
50
|
+
|
|
51
|
+
Bugs sit outside the Epic to Story to Task hierarchy, so there is no
|
|
52
|
+
parent section here by design. -->
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Epic
|
|
3
|
+
about: A large body of work spanning several sprints, broken into Stories
|
|
4
|
+
title: '[EPIC] '
|
|
5
|
+
labels: epic
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Before opening this issue:
|
|
9
|
+
Short sentences, plain words, facts and numbers. No filler.
|
|
10
|
+
Labels: one type (set by this template) and one priority, P0-critical
|
|
11
|
+
to P3-low. Assign the issue. The sprint is the milestone: sprint-N
|
|
12
|
+
labels are retired, do not add one.
|
|
13
|
+
Project board: set Status, Priority, Size, Estimate, Start date and
|
|
14
|
+
Target date. Size and Estimate come from the points or hours below. -->
|
|
15
|
+
|
|
16
|
+
## Epic
|
|
17
|
+
<!-- The outcome this epic delivers. Name what exists at the end, not the
|
|
18
|
+
slot in the schedule it occupies. -->
|
|
19
|
+
|
|
20
|
+
## Business Value
|
|
21
|
+
<!-- Why it matters, in one or two sentences. -->
|
|
22
|
+
|
|
23
|
+
## Product Goal alignment
|
|
24
|
+
<!-- Which Product Goal (docs/process/PRODUCT_GOAL.md) this serves.
|
|
25
|
+
"Indirect: ..." is a valid answer for tooling and maintenance work.
|
|
26
|
+
Write that rather than deleting the section. -->
|
|
27
|
+
|
|
28
|
+
## Closing this Epic
|
|
29
|
+
<!-- Child Stories are the native sub-issue links and nothing else. Add
|
|
30
|
+
them from the Sub-issues panel on this Epic. There is no markdown list
|
|
31
|
+
here on purpose — it duplicates the panel and drifts.
|
|
32
|
+
|
|
33
|
+
Do not close this Epic while a sub-issue is open. Do not leave it open
|
|
34
|
+
once they are all closed. Tasks hang off Stories, never off an Epic. A
|
|
35
|
+
Story that was dropped rather than delivered gets one line in Notes. -->
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
<!-- Decisions, rescopes, follow-up issue numbers. Delete this section if
|
|
39
|
+
there are none. Not a running log. -->
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Task
|
|
3
|
+
about: A technical subtask under a Story, or standalone technical work
|
|
4
|
+
title: '[TASK] '
|
|
5
|
+
labels: task
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Before opening this issue:
|
|
9
|
+
Short sentences, plain words, facts and numbers. No filler.
|
|
10
|
+
Labels: one type (set by this template) and one priority, P0-critical
|
|
11
|
+
to P3-low. Assign the issue. The sprint is the milestone: sprint-N
|
|
12
|
+
labels are retired, do not add one.
|
|
13
|
+
Project board: set Status, Priority, Size, Estimate, Start date and
|
|
14
|
+
Target date. Size and Estimate come from the points or hours below. -->
|
|
15
|
+
|
|
16
|
+
## Task
|
|
17
|
+
<!-- What needs doing, and why, in a few sentences or bullets. -->
|
|
18
|
+
|
|
19
|
+
## No parent
|
|
20
|
+
<!-- Delete this whole section when the Task has a Story. The native
|
|
21
|
+
sub-issue link is the parent record; a markdown copy is a second thing
|
|
22
|
+
to keep in sync.
|
|
23
|
+
|
|
24
|
+
Keep it only for a deliberate "no parent" decision, replacing this
|
|
25
|
+
comment with the one-line reason.
|
|
26
|
+
|
|
27
|
+
Set the parent from the Story: Sub-issues, Add existing issue. -->
|
|
28
|
+
|
|
29
|
+
## Scope
|
|
30
|
+
<!-- Only when it is not obvious what is excluded. Name the sibling issue
|
|
31
|
+
that owns anything deliberately left out. Delete otherwise. -->
|
|
32
|
+
|
|
33
|
+
## Acceptance Criteria
|
|
34
|
+
<!-- At most 5, each independently checkable, each naming the test or
|
|
35
|
+
command that proves it. A date is not a proof.
|
|
36
|
+
|
|
37
|
+
- [ ] `npm run compile` succeeds with the new command registered
|
|
38
|
+
(`extension.activation.test.ts`)
|
|
39
|
+
|
|
40
|
+
Prefer a test CI already runs. If nothing can prove it, say so:
|
|
41
|
+
(manual: exact steps) or (no test: reason). One manual criterion per
|
|
42
|
+
issue at most. Do not close with a box unchecked. -->
|
|
43
|
+
- [ ] <criterion> (`<test or command>`)
|
|
44
|
+
- [ ] <criterion> (`<test or command>`)
|
|
45
|
+
|
|
46
|
+
## Estimate
|
|
47
|
+
**Hours:** <hours, not points. Points size a Story; hours size the work of
|
|
48
|
+
implementing one Task inside a sprint.>
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
<!-- Decisions, rescopes, follow-up issue numbers. Delete this section if
|
|
52
|
+
there are none. Not a running log. -->
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: User Story
|
|
3
|
+
about: A vertical slice of user-facing value, the real requirements unit
|
|
4
|
+
title: '[STORY] As a <role>, I want <goal> so that <benefit>'
|
|
5
|
+
labels: user-story
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Before opening this issue:
|
|
9
|
+
Short sentences, plain words, facts and numbers. No filler.
|
|
10
|
+
Labels: one type (set by this template) and one priority, P0-critical
|
|
11
|
+
to P3-low. Assign the issue. The sprint is the milestone: sprint-N
|
|
12
|
+
labels are retired, do not add one.
|
|
13
|
+
Project board: set Status, Priority, Size, Estimate, Start date and
|
|
14
|
+
Target date. Size and Estimate come from the points or hours below. -->
|
|
15
|
+
|
|
16
|
+
## User Story
|
|
17
|
+
**As a** <role>
|
|
18
|
+
**I want** <goal>
|
|
19
|
+
**So that** <benefit>
|
|
20
|
+
|
|
21
|
+
<!-- INVEST before this enters the backlog: Independent, Negotiable,
|
|
22
|
+
Valuable, Estimable, Small, Testable. If it fails Small or Estimable,
|
|
23
|
+
split it rather than forcing a sixth criterion. -->
|
|
24
|
+
|
|
25
|
+
## No parent
|
|
26
|
+
<!-- Delete this whole section when the Story has an Epic. The native
|
|
27
|
+
sub-issue link is the parent record; a markdown copy is a second thing
|
|
28
|
+
to keep in sync.
|
|
29
|
+
|
|
30
|
+
Keep it only for a deliberate "no parent" decision, replacing this
|
|
31
|
+
comment with the one-line reason.
|
|
32
|
+
|
|
33
|
+
Set the parent from the Epic: Sub-issues, Add existing issue. -->
|
|
34
|
+
|
|
35
|
+
## Acceptance Criteria (Given/When/Then)
|
|
36
|
+
<!-- At most 5, each independently checkable.
|
|
37
|
+
|
|
38
|
+
Every criterion names the test or command that proves it, in brackets
|
|
39
|
+
at the end:
|
|
40
|
+
|
|
41
|
+
- [ ] Given an invoice with 60 rows, when it renders, then the
|
|
42
|
+
table header repeats on page 2 (`test_table_header_repeats`)
|
|
43
|
+
|
|
44
|
+
A date is not a proof. Prefer a test CI already runs. A repeatable
|
|
45
|
+
command is acceptable when a test genuinely cannot cover it. If
|
|
46
|
+
nothing can prove it, say so: (manual: exact steps) or (no test:
|
|
47
|
+
reason). One manual criterion per issue at most.
|
|
48
|
+
|
|
49
|
+
Do not close with a box unchecked. Check each one against the code,
|
|
50
|
+
not against what this issue used to say. -->
|
|
51
|
+
- [ ] Given <context>, when <action>, then <outcome> (`<test or command>`)
|
|
52
|
+
- [ ] Given <context>, when <action>, then <outcome> (`<test or command>`)
|
|
53
|
+
|
|
54
|
+
## Definition of Ready
|
|
55
|
+
- [ ] Meets [Definition of Ready](https://github.com/liviuionesi/brevier/blob/develop/docs/process/DEFINITION_OF_READY.md)
|
|
56
|
+
|
|
57
|
+
## Story Points
|
|
58
|
+
**Estimate:** <Fibonacci: 1 / 2 / 3 / 5 / 8 / 13 / 21>
|
|
59
|
+
<!-- The sprint is the GitHub Milestone. There is no Sprint section here. -->
|
|
60
|
+
|
|
61
|
+
## Closing this Story
|
|
62
|
+
<!-- Technical Tasks are the native sub-issue links and nothing else, for
|
|
63
|
+
the same reason as the Epic template. Do not close this Story while a
|
|
64
|
+
child Task is open. -->
|
|
65
|
+
|
|
66
|
+
## Definition of Done
|
|
67
|
+
- [ ] Meets [Definition of Done](https://github.com/liviuionesi/brevier/blob/develop/docs/process/DEFINITION_OF_DONE.md)
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
<!-- Decisions, rescopes, follow-up issue numbers. Delete this section if
|
|
71
|
+
there are none. Not a running log. -->
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Pull Request
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
<!-- What changed, in a few lines. -->
|
|
5
|
+
|
|
6
|
+
## Related Issue
|
|
7
|
+
Closes #<!-- issue number -->
|
|
8
|
+
|
|
9
|
+
## Type of Change
|
|
10
|
+
- [ ] 🐛 Bug fix
|
|
11
|
+
- [ ] ✨ New feature
|
|
12
|
+
- [ ] 💥 Breaking change
|
|
13
|
+
- [ ] 📝 Documentation
|
|
14
|
+
- [ ] 🔧 Configuration
|
|
15
|
+
- [ ] ♻️ Refactoring
|
|
16
|
+
- [ ] ✅ Test improvement
|
|
17
|
+
|
|
18
|
+
## Area(s) Affected
|
|
19
|
+
- [ ] Composer (classes, template or PDF in, schema out)
|
|
20
|
+
- [ ] Preparer (data cleaning, hyphenation, binding)
|
|
21
|
+
- [ ] Engine (layout, pagination, PDF out)
|
|
22
|
+
- [ ] Themes (DIN 5008, web)
|
|
23
|
+
- [ ] PDF import
|
|
24
|
+
- [ ] Linter
|
|
25
|
+
- [ ] CLI
|
|
26
|
+
- [ ] Java client
|
|
27
|
+
- [ ] Packaging / CI
|
|
28
|
+
- [ ] Documentation
|
|
29
|
+
|
|
30
|
+
## Testing Performed
|
|
31
|
+
- [ ] Tests added or updated (`pytest`, `./gradlew test`)
|
|
32
|
+
- [ ] Golden render gate checked, if this can move a pixel
|
|
33
|
+
(`docs/process/DEFINITION_OF_DONE.md`)
|
|
34
|
+
- [ ] Output opened and looked at, not only asserted
|
|
35
|
+
|
|
36
|
+
## Code Quality
|
|
37
|
+
- [ ] `ruff check` and `ruff format --check` clean
|
|
38
|
+
- [ ] `mypy` clean for touched code
|
|
39
|
+
- [ ] Self-review done
|
|
40
|
+
- [ ] Docstrings / Javadoc per `AGENTS.md`
|
|
41
|
+
- [ ] No debug prints left behind
|
|
42
|
+
- [ ] Errors handled, not swallowed
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
- [ ] README updated, if needed
|
|
46
|
+
- [ ] `docs/PRD.md` updated with a version history row, if the product changed
|
|
47
|
+
- [ ] New architecture decision written as an ADR under `docs/architecture/adr/`
|
|
48
|
+
|
|
49
|
+
## Security
|
|
50
|
+
- [ ] No secrets in code, logs or debug output
|
|
51
|
+
- [ ] No customer data written anywhere by default (PRD risk R8)
|
|
52
|
+
- [ ] No font file extracted or redistributed (PRD risk R7)
|
|
53
|
+
- [ ] Dependency scan clean for new or changed dependencies
|
|
54
|
+
|
|
55
|
+
## Before Merge
|
|
56
|
+
- [ ] CI passing
|
|
57
|
+
- [ ] Review comments addressed
|
|
58
|
+
- [ ] Branch up to date with `develop`
|
|
59
|
+
- [ ] Issue resynced to real implementation state
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Does the engine actually render on Linux, macOS and Windows?
|
|
2
|
+
#
|
|
3
|
+
# ADR 002 commits Brevier to all three operating systems, but only Linux was
|
|
4
|
+
# ever tested. This workflow answers the question for the other two before
|
|
5
|
+
# the product is built on the assumption.
|
|
6
|
+
#
|
|
7
|
+
# Manual trigger only. macOS runner minutes bill at 10x and Windows at 2x on
|
|
8
|
+
# a private repository, so this must never fire on every push.
|
|
9
|
+
name: cross-os-proof
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
render:
|
|
16
|
+
name: render on ${{ matrix.os }}
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
strategy:
|
|
19
|
+
# One platform failing must not hide the result of the other two —
|
|
20
|
+
# the whole point is to learn where it breaks.
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.13"
|
|
31
|
+
|
|
32
|
+
# --- the native font stack, obtained the way each platform does it ---
|
|
33
|
+
|
|
34
|
+
- name: Linux — font stack from apt
|
|
35
|
+
if: runner.os == 'Linux'
|
|
36
|
+
run: |
|
|
37
|
+
sudo apt-get update -qq
|
|
38
|
+
sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 \
|
|
39
|
+
libharfbuzz0b libfontconfig1 fonts-dejavu-core
|
|
40
|
+
|
|
41
|
+
- name: macOS — font stack from Homebrew
|
|
42
|
+
if: runner.os == 'macOS'
|
|
43
|
+
run: |
|
|
44
|
+
brew install pango
|
|
45
|
+
echo "DYLD_FALLBACK_LIBRARY_PATH=$(brew --prefix)/lib" >> "$GITHUB_ENV"
|
|
46
|
+
|
|
47
|
+
- name: Windows — font stack from MSYS2
|
|
48
|
+
if: runner.os == 'Windows'
|
|
49
|
+
uses: msys2/setup-msys2@v2
|
|
50
|
+
id: msys2
|
|
51
|
+
with:
|
|
52
|
+
msystem: UCRT64
|
|
53
|
+
update: true
|
|
54
|
+
install: mingw-w64-ucrt-x86_64-pango
|
|
55
|
+
|
|
56
|
+
- name: Windows — tell WeasyPrint where the DLLs are
|
|
57
|
+
if: runner.os == 'Windows'
|
|
58
|
+
shell: bash
|
|
59
|
+
run: |
|
|
60
|
+
# WEASYPRINT_DLL_DIRECTORIES is WeasyPrint's own supported way to
|
|
61
|
+
# point at the native libraries, which is why Windows needs no
|
|
62
|
+
# equivalent of the Linux runtime hook in ADR 001.
|
|
63
|
+
echo "WEASYPRINT_DLL_DIRECTORIES=${{ steps.msys2.outputs.msys2-location }}\\ucrt64\\bin" >> "$GITHUB_ENV"
|
|
64
|
+
|
|
65
|
+
# --- the engine itself, pinned so the comparison means something ---
|
|
66
|
+
|
|
67
|
+
- name: Install WeasyPrint
|
|
68
|
+
shell: bash
|
|
69
|
+
run: python -m pip install --quiet "weasyprint==70.0" "pypdf>=5,<7"
|
|
70
|
+
|
|
71
|
+
- name: Fetch identical font files, pinned by commit
|
|
72
|
+
shell: bash
|
|
73
|
+
env:
|
|
74
|
+
# A fixed commit in google/fonts, so the font bytes cannot move.
|
|
75
|
+
FONTS_COMMIT: 8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5
|
|
76
|
+
SANS_SHA256: bfb7bb691513f12e734dc346c03a03f784912432d7e3fa8e56efcf906fe86b3d
|
|
77
|
+
SERIF_SHA256: 4d8e6761424656867019081a1a01336f3cb086982682698714054fc33f782713
|
|
78
|
+
run: |
|
|
79
|
+
mkdir -p tests/fixtures/fonts
|
|
80
|
+
base="https://raw.githubusercontent.com/google/fonts/$FONTS_COMMIT/ofl"
|
|
81
|
+
curl -sSL -o tests/fixtures/fonts/NotoSans.ttf "$base/notosans/NotoSans%5Bwdth%2Cwght%5D.ttf"
|
|
82
|
+
curl -sSL -o tests/fixtures/fonts/NotoSerif.ttf "$base/notoserif/NotoSerif%5Bwdth%2Cwght%5D.ttf"
|
|
83
|
+
# Prove the input was byte-identical here, so a difference in the
|
|
84
|
+
# output later cannot be blamed on a different font file.
|
|
85
|
+
digest() { python -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1"; }
|
|
86
|
+
sans=$(digest tests/fixtures/fonts/NotoSans.ttf)
|
|
87
|
+
serif=$(digest tests/fixtures/fonts/NotoSerif.ttf)
|
|
88
|
+
echo " NotoSans.ttf : $sans"
|
|
89
|
+
echo " NotoSerif.ttf: $serif"
|
|
90
|
+
test "$sans" = "$SANS_SHA256" || { echo "NotoSans differs on this runner"; exit 1; }
|
|
91
|
+
test "$serif" = "$SERIF_SHA256" || { echo "NotoSerif differs on this runner"; exit 1; }
|
|
92
|
+
echo "font input is identical on this runner"
|
|
93
|
+
|
|
94
|
+
- name: Render with the platform's own fonts
|
|
95
|
+
shell: bash
|
|
96
|
+
run: python tests/cross_os_check.py tests/fixtures/hello.html "out-system-${{ runner.os }}.pdf"
|
|
97
|
+
|
|
98
|
+
- name: Render with identical bundled fonts
|
|
99
|
+
shell: bash
|
|
100
|
+
run: python tests/cross_os_check.py tests/fixtures/hello-bundled.html "out-bundled-${{ runner.os }}.pdf"
|
|
101
|
+
|
|
102
|
+
- uses: actions/upload-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: pdf-${{ runner.os }}
|
|
105
|
+
path: out-*.pdf
|
|
106
|
+
|
|
107
|
+
compare:
|
|
108
|
+
name: compare the three PDFs
|
|
109
|
+
needs: render
|
|
110
|
+
if: always()
|
|
111
|
+
runs-on: ubuntu-latest
|
|
112
|
+
steps:
|
|
113
|
+
- uses: actions/download-artifact@v4
|
|
114
|
+
with:
|
|
115
|
+
path: pdfs
|
|
116
|
+
|
|
117
|
+
- name: Checksums across operating systems
|
|
118
|
+
run: |
|
|
119
|
+
for kind in system bundled; do
|
|
120
|
+
echo "=============================================================="
|
|
121
|
+
if [ "$kind" = system ]; then
|
|
122
|
+
echo "Each platform used ITS OWN fonts:"
|
|
123
|
+
else
|
|
124
|
+
echo "Every platform used the SAME font files:"
|
|
125
|
+
fi
|
|
126
|
+
find pdfs -name "out-$kind-*.pdf" -print0 | sort -z | xargs -0 sha256sum
|
|
127
|
+
count=$(find pdfs -name "out-$kind-*.pdf" | wc -l)
|
|
128
|
+
unique=$(find pdfs -name "out-$kind-*.pdf" -print0 | xargs -0 sha256sum | awk '{print $1}' | sort -u | wc -l)
|
|
129
|
+
echo "-> $count PDFs, $unique distinct checksum(s)."
|
|
130
|
+
echo
|
|
131
|
+
done
|
|
132
|
+
echo "Reminder: identical bytes need identical fonts AND identical"
|
|
133
|
+
echo "native library versions. See ADR 003."
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Does `pip install brevier` give a working `brevier` command?
|
|
2
|
+
#
|
|
3
|
+
# Epic #38's proof: in a folder holding only hello.html and hello.css,
|
|
4
|
+
# `brevier render hello.html -o hello.pdf` writes hello.pdf into that same
|
|
5
|
+
# folder. This workflow builds the wheel once, then runs that proof on
|
|
6
|
+
# Linux, macOS and Windows, on Python 3.10 (the floor) and 3.13, in a fresh
|
|
7
|
+
# virtual environment with nothing else from this repo. `brevier --version`
|
|
8
|
+
# alone does not count — it works even without Pango (ADR 001).
|
|
9
|
+
#
|
|
10
|
+
# It also renders hello-bundled.html (fonts pinned with @font-face, one
|
|
11
|
+
# Python version, one run per OS) and compares those three PDFs' content.
|
|
12
|
+
# hello.html cannot be compared this way: without a pinned font, each OS
|
|
13
|
+
# resolves "Noto Sans" to a different substitute and draws a different page
|
|
14
|
+
# (ADR 002, "Fonts decide the result") — that is real, not a bug, so the
|
|
15
|
+
# content check needs the fixture that removes it.
|
|
16
|
+
#
|
|
17
|
+
# `source: pypi` swaps the wheel for a real `pip install brevier` from
|
|
18
|
+
# PyPI, to prove a tagged release too, not just the local build.
|
|
19
|
+
#
|
|
20
|
+
# Manual trigger and version tags only: macOS runner minutes bill at 10x
|
|
21
|
+
# and Windows at 2x on a private repo (see cross-os-proof.yml).
|
|
22
|
+
name: pip-install-proof
|
|
23
|
+
|
|
24
|
+
on:
|
|
25
|
+
workflow_dispatch:
|
|
26
|
+
inputs:
|
|
27
|
+
source:
|
|
28
|
+
description: "Install brevier from the wheel this run builds, or from PyPI"
|
|
29
|
+
type: choice
|
|
30
|
+
options: [wheel, pypi]
|
|
31
|
+
default: wheel
|
|
32
|
+
push:
|
|
33
|
+
tags:
|
|
34
|
+
- "v*.*.*"
|
|
35
|
+
|
|
36
|
+
env:
|
|
37
|
+
# A fixed date, so two runs of the same input cannot differ by timestamp
|
|
38
|
+
# (ADR 002, rule 2).
|
|
39
|
+
SOURCE_DATE_EPOCH: "1700000000"
|
|
40
|
+
# pypdf pin shared with cross-os-proof.yml, for the same reason: only the
|
|
41
|
+
# check scripts use it, so an unpinned range should never be why a run
|
|
42
|
+
# fails.
|
|
43
|
+
PYPDF_SPEC: "pypdf>=5,<7"
|
|
44
|
+
|
|
45
|
+
jobs:
|
|
46
|
+
build:
|
|
47
|
+
name: build the wheel
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
|
|
52
|
+
- uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: "3.13"
|
|
55
|
+
|
|
56
|
+
- name: Build wheel and sdist
|
|
57
|
+
run: |
|
|
58
|
+
python -m pip install --quiet build
|
|
59
|
+
python -m build
|
|
60
|
+
|
|
61
|
+
- uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: wheel
|
|
64
|
+
path: dist/*.whl
|
|
65
|
+
if-no-files-found: error
|
|
66
|
+
|
|
67
|
+
install:
|
|
68
|
+
name: install on ${{ matrix.os }}, Python ${{ matrix.python-version }}
|
|
69
|
+
needs: build
|
|
70
|
+
runs-on: ${{ matrix.os }}
|
|
71
|
+
strategy:
|
|
72
|
+
# One combination failing must not hide the result of the other five.
|
|
73
|
+
fail-fast: false
|
|
74
|
+
matrix:
|
|
75
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
76
|
+
python-version: ["3.10", "3.13"]
|
|
77
|
+
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v4
|
|
80
|
+
|
|
81
|
+
- uses: actions/setup-python@v5
|
|
82
|
+
with:
|
|
83
|
+
python-version: ${{ matrix.python-version }}
|
|
84
|
+
|
|
85
|
+
# --- Pango, obtained the way each platform does it (ADR 001) ---
|
|
86
|
+
|
|
87
|
+
- name: Linux — font stack from apt
|
|
88
|
+
if: runner.os == 'Linux'
|
|
89
|
+
run: |
|
|
90
|
+
sudo apt-get update -qq
|
|
91
|
+
sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 \
|
|
92
|
+
libharfbuzz0b libfontconfig1 fonts-dejavu-core
|
|
93
|
+
|
|
94
|
+
- name: macOS — font stack from Homebrew
|
|
95
|
+
if: runner.os == 'macOS'
|
|
96
|
+
run: |
|
|
97
|
+
brew install pango
|
|
98
|
+
echo "DYLD_FALLBACK_LIBRARY_PATH=$(brew --prefix)/lib" >> "$GITHUB_ENV"
|
|
99
|
+
|
|
100
|
+
- name: Windows — font stack from MSYS2
|
|
101
|
+
if: runner.os == 'Windows'
|
|
102
|
+
uses: msys2/setup-msys2@v2
|
|
103
|
+
id: msys2
|
|
104
|
+
with:
|
|
105
|
+
msystem: UCRT64
|
|
106
|
+
update: true
|
|
107
|
+
install: mingw-w64-ucrt-x86_64-pango
|
|
108
|
+
|
|
109
|
+
- name: Windows — tell WeasyPrint where the DLLs are
|
|
110
|
+
if: runner.os == 'Windows'
|
|
111
|
+
shell: bash
|
|
112
|
+
run: |
|
|
113
|
+
# WEASYPRINT_DLL_DIRECTORIES is WeasyPrint's own supported way to
|
|
114
|
+
# point at the native libraries (ADR 001).
|
|
115
|
+
echo "WEASYPRINT_DLL_DIRECTORIES=${{ steps.msys2.outputs.msys2-location }}\\ucrt64\\bin" >> "$GITHUB_ENV"
|
|
116
|
+
|
|
117
|
+
# --- a fresh virtual environment, with nothing else from this repo ---
|
|
118
|
+
|
|
119
|
+
- name: Create the virtual environment
|
|
120
|
+
shell: bash
|
|
121
|
+
run: |
|
|
122
|
+
python -m venv .venv
|
|
123
|
+
# Windows' venv lays out Scripts/, not bin/ — record whichever
|
|
124
|
+
# this OS used, as an absolute path so it survives a later `cd`.
|
|
125
|
+
if [ -d "$PWD/.venv/bin" ]; then
|
|
126
|
+
echo "VENV_BIN=$PWD/.venv/bin" >> "$GITHUB_ENV"
|
|
127
|
+
else
|
|
128
|
+
echo "VENV_BIN=$PWD/.venv/Scripts" >> "$GITHUB_ENV"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# --- install brevier itself, from the wheel or from PyPI ---
|
|
132
|
+
|
|
133
|
+
- uses: actions/download-artifact@v4
|
|
134
|
+
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.source == 'pypi') }}
|
|
135
|
+
with:
|
|
136
|
+
name: wheel
|
|
137
|
+
path: dist
|
|
138
|
+
|
|
139
|
+
- name: Install brevier from the wheel this run built
|
|
140
|
+
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.source == 'pypi') }}
|
|
141
|
+
shell: bash
|
|
142
|
+
run: '"$VENV_BIN/python" -m pip install --quiet dist/*.whl "$PYPDF_SPEC"'
|
|
143
|
+
|
|
144
|
+
- name: Install brevier from PyPI
|
|
145
|
+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.source == 'pypi' }}
|
|
146
|
+
shell: bash
|
|
147
|
+
run: '"$VENV_BIN/python" -m pip install --quiet brevier "$PYPDF_SPEC"'
|
|
148
|
+
|
|
149
|
+
# --- the proof itself: render, in a folder holding only the fixtures ---
|
|
150
|
+
|
|
151
|
+
- name: Render in a folder holding only the two fixtures
|
|
152
|
+
shell: bash
|
|
153
|
+
run: |
|
|
154
|
+
mkdir -p run
|
|
155
|
+
cp tests/fixtures/hello.html tests/fixtures/hello.css run/
|
|
156
|
+
(cd run && "$VENV_BIN/brevier" render hello.html -o hello.pdf)
|
|
157
|
+
|
|
158
|
+
- name: Check the PDF is real, not just present
|
|
159
|
+
shell: bash
|
|
160
|
+
run: '"$VENV_BIN/python" tests/packaging/check_pdf.py run/hello.pdf'
|
|
161
|
+
|
|
162
|
+
# --- the font-pinned fixture, once per OS, for the content check below ---
|
|
163
|
+
|
|
164
|
+
- name: Fetch the pinned fonts hello-bundled.html embeds
|
|
165
|
+
# tests/fixtures/fonts/ is gitignored (cross-os-proof.yml fetches it
|
|
166
|
+
# the same way) — a checkout alone does not have these files.
|
|
167
|
+
if: matrix.python-version == '3.13'
|
|
168
|
+
shell: bash
|
|
169
|
+
env:
|
|
170
|
+
# Same commit and checksums as cross-os-proof.yml, so both
|
|
171
|
+
# workflows prove against the same font bytes.
|
|
172
|
+
FONTS_COMMIT: 8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5
|
|
173
|
+
SANS_SHA256: bfb7bb691513f12e734dc346c03a03f784912432d7e3fa8e56efcf906fe86b3d
|
|
174
|
+
SERIF_SHA256: 4d8e6761424656867019081a1a01336f3cb086982682698714054fc33f782713
|
|
175
|
+
run: |
|
|
176
|
+
mkdir -p tests/fixtures/fonts
|
|
177
|
+
base="https://raw.githubusercontent.com/google/fonts/$FONTS_COMMIT/ofl"
|
|
178
|
+
curl -sSL -o tests/fixtures/fonts/NotoSans.ttf "$base/notosans/NotoSans%5Bwdth%2Cwght%5D.ttf"
|
|
179
|
+
curl -sSL -o tests/fixtures/fonts/NotoSerif.ttf "$base/notoserif/NotoSerif%5Bwdth%2Cwght%5D.ttf"
|
|
180
|
+
digest() { "$VENV_BIN/python" -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1"; }
|
|
181
|
+
test "$(digest tests/fixtures/fonts/NotoSans.ttf)" = "$SANS_SHA256" || { echo "NotoSans differs on this runner"; exit 1; }
|
|
182
|
+
test "$(digest tests/fixtures/fonts/NotoSerif.ttf)" = "$SERIF_SHA256" || { echo "NotoSerif differs on this runner"; exit 1; }
|
|
183
|
+
|
|
184
|
+
- name: Render the font-pinned fixture, for the cross-OS content check
|
|
185
|
+
if: matrix.python-version == '3.13'
|
|
186
|
+
shell: bash
|
|
187
|
+
run: |
|
|
188
|
+
mkdir -p run-bundled/fonts
|
|
189
|
+
cp tests/fixtures/hello-bundled.html run-bundled/
|
|
190
|
+
cp tests/fixtures/fonts/*.ttf run-bundled/fonts/
|
|
191
|
+
(cd run-bundled && "$VENV_BIN/brevier" render hello-bundled.html -o hello-bundled.pdf)
|
|
192
|
+
|
|
193
|
+
- uses: actions/upload-artifact@v4
|
|
194
|
+
if: matrix.python-version == '3.13'
|
|
195
|
+
with:
|
|
196
|
+
name: bundled-pdf-${{ runner.os }}
|
|
197
|
+
path: run-bundled/hello-bundled.pdf
|
|
198
|
+
|
|
199
|
+
compare:
|
|
200
|
+
name: compare the font-pinned PDFs across operating systems
|
|
201
|
+
needs: install
|
|
202
|
+
if: always()
|
|
203
|
+
runs-on: ubuntu-latest
|
|
204
|
+
steps:
|
|
205
|
+
- uses: actions/checkout@v4
|
|
206
|
+
|
|
207
|
+
- uses: actions/setup-python@v5
|
|
208
|
+
with:
|
|
209
|
+
python-version: "3.13"
|
|
210
|
+
|
|
211
|
+
- run: python -m pip install --quiet "pypdf>=5,<7"
|
|
212
|
+
|
|
213
|
+
- uses: actions/download-artifact@v4
|
|
214
|
+
with:
|
|
215
|
+
pattern: bundled-pdf-*
|
|
216
|
+
path: pdfs
|
|
217
|
+
|
|
218
|
+
- name: Compare content across operating systems
|
|
219
|
+
shell: bash
|
|
220
|
+
run: python tests/compare_pdf_content.py pdfs/*/hello-bundled.pdf
|