Skip to content

Commit 2718225

Browse files
sync: merge master into main (deploys workflow self-counting fix)
Mirrors the main-sync step that auto-publish-stable.yml normally runs after a successful PyPI publish, but done manually here because the publish step failed earlier (PYPI_API_TOKEN secret unset). Bringing main forward now so: - The workflow self-counting fix (02b902a) is registered as the active version on the default branch, unblocking future dev → testing → review → master cycles. - The 0.1.21 version bump (03dcc56, c20a854) is reflected on main for parity with the publish-failed master state. When PyPI_API_TOKEN is configured and the failed run is re-tried, auto-publish-stable's main-sync will be a no-op (idempotent). Conflict strategy = -X theirs (master wins on every conflict).
2 parents 5b49f17 + c20a854 commit 2718225

13 files changed

Lines changed: 438 additions & 41 deletions

.github/workflows/auto-promote-review-to-stable.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ on:
4343
description: "The SHA on review being approved (40-char)"
4444
required: true
4545
type: string
46+
force:
47+
description: "Bypass the CI-green gate (use only when the gate itself is broken — e.g. self-counting wedge)"
48+
required: false
49+
default: "false"
50+
type: string
4651

4752
permissions:
4853
contents: write
@@ -195,15 +200,28 @@ jobs:
195200
env:
196201
GH_TOKEN: ${{ github.token }}
197202
SHA: ${{ steps.sha.outputs.sha }}
203+
FORCE: ${{ github.event.inputs.force }}
204+
SELF_RUN_ID: ${{ github.run_id }}
198205
run: |
199206
set -euo pipefail
207+
208+
if [[ "$FORCE" == "true" ]]; then
209+
echo "::warning::Force-promote requested — bypassing CI gate. Use only when the gate itself is wedged."
210+
exit 0
211+
fi
212+
200213
runs_json=$(gh run list \
201214
--commit "$SHA" \
202-
--json conclusion,status,name,workflowName,url \
215+
--json conclusion,status,name,workflowName,databaseId,url \
203216
--limit 200)
204217
205-
inflight=$(echo "$runs_json" | jq '[.[] | select(.status != "completed")] | length')
206-
failed_runs=$(echo "$runs_json" | jq '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]')
218+
# Exclude this workflow's own run from the in-flight count: when
219+
# triggered by pull_request_review / workflow_dispatch / push,
220+
# the current run shows up in the list keyed by the same SHA
221+
# with status=in_progress, which would otherwise gate against
222+
# itself forever.
223+
inflight=$(echo "$runs_json" | jq --argjson self "$SELF_RUN_ID" '[.[] | select(.status != "completed" and .databaseId != $self)] | length')
224+
failed_runs=$(echo "$runs_json" | jq --argjson self "$SELF_RUN_ID" '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped" and .databaseId != $self)]')
207225
failed_count=$(echo "$failed_runs" | jq 'length')
208226
209227
if (( inflight > 0 )); then

.github/workflows/auto-promote-testing-to-review.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ jobs:
132132
GH_TOKEN: ${{ github.token }}
133133
SHA: ${{ steps.sha.outputs.sha }}
134134
FORCE: ${{ github.event.inputs.force }}
135+
SELF_RUN_ID: ${{ github.run_id }}
135136
run: |
136137
set -euo pipefail
137138
@@ -146,8 +147,13 @@ jobs:
146147
# - completed + success/skipped → green
147148
# - completed + anything else → red
148149
# - !completed → in-flight (silent retry)
149-
inflight=$(echo "$runs_json" | jq '[.[] | select(.status != "completed")] | length')
150-
failed_runs=$(echo "$runs_json" | jq '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]')
150+
# Exclude this workflow's own run from the in-flight count: when
151+
# triggered by workflow_dispatch / pull_request_review / push,
152+
# the current run shows up in the list keyed by the same SHA
153+
# with status=in_progress, which would otherwise gate against
154+
# itself forever.
155+
inflight=$(echo "$runs_json" | jq --argjson self "$SELF_RUN_ID" '[.[] | select(.status != "completed" and .databaseId != $self)] | length')
156+
failed_runs=$(echo "$runs_json" | jq --argjson self "$SELF_RUN_ID" '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped" and .databaseId != $self)]')
151157
failed_count=$(echo "$failed_runs" | jq 'length')
152158
153159
echo "in-flight runs: $inflight"

.github/workflows/auto-publish-stable.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ jobs:
178178
- name: Sync project (build + dev deps)
179179
run: uv sync --all-extras --dev
180180

181+
- name: Add venv bin to PATH so release.sh's `git cliff` resolves
182+
# release.sh invokes `git cliff` to regenerate CHANGELOG.md.
183+
# `git cliff` works as a git subcommand only if the
184+
# `git-cliff` binary is on PATH. `uv sync --dev` puts it
185+
# under .venv/bin/, so we prepend that to PATH for the rest
186+
# of the job.
187+
run: echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH"
188+
181189
- name: Run release.sh --stable master --from-ci
182190
env:
183191
# Standard token for gh CLI (creates GitHub release, uploads).

CHANGELOG.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,142 @@ All notable changes to svg2fbf will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.21] - 2026-04-29
9+
10+
### Added
11+
12+
- HARD pre-publish gate — never release without Docker install verification
13+
- Add byte-exact E2E regression test for svg2fbf output
14+
- Add --skip-date and --auto-repair-viewbox CLI flags + E2E install path test
15+
- Scripts/release_all.sh — single-command release orchestrator
16+
- Pre-flight checks docker daemon is reachable, shows context
17+
- **justfile:** CI-green + approval gates on promote recipes
18+
- **justfile:** CI-green + approval gates on promote recipes
19+
- Fully automated promotion pipeline (TRDD-bbd4b1f0)
20+
- **ci:** Cross-platform wheel-install gate before PyPI publish
21+
22+
### Fixed
23+
24+
- Promote svg-text2path from optional to required runtime dependency
25+
- Release_all.sh pre-flight ignores gitignored runtime state
26+
- Docker tests must always match host CPU arch via --platform
27+
- Hard memory limits + watchdog to prevent host swap blow-up
28+
- Address security + correctness audit findings in src/
29+
- Harden release + validation scripts per audit findings
30+
- **auto-install:** Six Windows-specific bugs + Windows CI workflow
31+
- **auto-install,ci:** Windows UTF-8 stdout + tolerant uninstall step
32+
- **ci-windows:** Pre-extract shutil.which('node') to avoid pwsh escape mangle
33+
- **ci:** Install system chromium on amd64 for Puppeteer 22+
34+
- **auto-install:** Six Windows-specific bugs + Windows CI workflow
35+
- **auto-install,ci:** Windows UTF-8 stdout + tolerant uninstall step
36+
- **ci-windows:** Pre-extract shutil.which('node') to avoid pwsh escape mangle
37+
- **ci:** Install system chromium on amd64 for Puppeteer 22+
38+
- **review:** Restore testing's pyproject.toml + uv.lock, keep rc1 version
39+
- **cli:** Reconfigure stdout/stderr to UTF-8 on entry to cli()
40+
- **tests:** Force UTF-8 decoding in test_text2path_integration subprocess calls
41+
- **tests:** Force UTF-8 in Path.read_text() for windows compat
42+
- **master:** Forward svg-text2path-required pyproject + lock from review (keep 0.1.20)
43+
- **ci:** Add .venv/bin to PATH so release.sh can find git-cliff
44+
- **workflows:** Exclude self from in-flight CI count + add force input
45+
46+
### Other
47+
48+
- Merge dev into testing - feature complete, ready for testing
49+
- Merge dev into testing - feature complete, ready for testing
50+
- Zip-slip defense + stale docstring + workflow policy note
51+
- Opt-in Node 24 runtime + bounded portable-Node download + reconcile TRDD
52+
- Add pyright to dev deps for in-CI release-dry-run
53+
- Zip-slip defense + stale docstring + workflow policy note
54+
- Opt-in Node 24 runtime + bounded portable-Node download + reconcile TRDD
55+
- Merge dev into testing - feature complete, ready for testing
56+
57+
Resolves conflict in .github/workflows/windows-autoinstall.yml by
58+
taking testing's version (excludes dev from CI triggers, per TRDD
59+
"no CI on dev" policy). The dev side carried an older comment block
60+
arguing for Windows-only CI on dev pushes; that argument was
61+
superseded by the broader CI-on-dev-is-noise policy that landed on
62+
testing in commit ca33aa5 (2026-04-29). Net behavior: windows-autoinstall
63+
fires on testing/review/master/main as expected, never on dev.
64+
- Merge testing into review - bugs fixed, ready for RC
65+
66+
Forwards all of testing's recent work (auto-promotion pipeline,
67+
CODEOWNERS, Windows/Linux/macOS auto-install, Docker E2E suite,
68+
text→path tests, etc.) into review.
69+
70+
Conflicts resolved by keeping review's version artifacts:
71+
- pyproject.toml: keep version=0.1.20rc1 (review's rc > testing's b3)
72+
- CHANGELOG.md: keep review's rc1 changelog entries
73+
- uv.lock: keep review's lockfile (matches rc1's pinned deps)
74+
75+
This preserves the version progression rule: a single version can
76+
only exist in ONE stage at a time, and stage on review must be ≥
77+
stage on testing. The next bump (review → master = stable) will
78+
take this rc1 to 0.1.20 stable.
79+
- Merge dev into testing - feature complete, ready for testing
80+
- Merge dev into testing - feature complete, ready for testing
81+
- Merge testing into review - bugs fixed, ready for release candidate
82+
- Merge dev into testing - feature complete, ready for testing
83+
- Merge testing into review - bugs fixed, ready for release candidate
84+
- Merge dev into testing - feature complete, ready for testing
85+
- Merge testing into review - bugs fixed, ready for release candidate
86+
- Review → master (bootstrap of TRDD-bbd4b1f0 auto-promotion pipeline; approved by Emasoft)
87+
- Merge dev into testing - feature complete, ready for testing
88+
- Merge dev into testing - feature complete, ready for testing
89+
- Testing → review (all CI green at 2f44f7c858fd5683a2fdcbb3bb4193a0b7e50394)
90+
- **npm:** Bump puppeteer in /tests in the npm-all group ([#10](https://github.com/Emasoft/svg2fbf/issues/10))
91+
- Merge dev into testing - feature complete, ready for testing
92+
- Testing → review (all CI green at aeb8f2eda476650134dd0b7d362c280c0734ce3b)
93+
- Review → master (bootstrap; one-off manual merge to deploy workflow self-counting fix)
94+
95+
### Testing
96+
97+
- Wire T13 text→path Docker E2E harness + fixtures (TRDD-c2a3199d)
98+
- Add pin_fbf_frame_to_png calibration utility (TRDD-c2a3199d)
99+
- Regen e2e goldens for testing branch (v0.1.20b3 version pin)
100+
- Restore docker-flavored text_frames golden, only patch version (TRDD-c2a3199d)
101+
- Version-strip byte-exact tests + add Docker-bound regen for text golden (TRDD-c2a3199d)
102+
- Regen text_frames golden (CI amd64 environment) + HOST_ARCH override
103+
- Convert T12 from byte-exact to per-frame visual diff (<3%)
104+
105+
### Documentation
106+
107+
- Add TRDD-eb937ddf — adapt release.sh to PR-based promotion flow
108+
- Add TRDD-c2a3199d — text→path Docker E2E test plan
109+
- Annotate T13 + text→path E2E tooling across guides (TRDD-c2a3199d)
110+
- Add TRDD-ad386cb7 — CI/release pipeline review proposal
111+
- Add TRDD-bbd4b1f0 — fully-automated promotion pipeline
112+
- Add TRDD-bbd4b1f0 — fully-automated promotion pipeline
113+
- Cross-platform gate documentation + workflow fixes
114+
115+
### Miscellaneous
116+
117+
- Gitignore /reports/ and /reports_dev/ per agent-reports rule
118+
- Add CODEOWNERS — make Emasoft the code owner of everything
119+
120+
### CI/CD
121+
122+
- Add Linux + macOS auto-install workflows + release dry-run gate
123+
124+
## [0.1.20b3] - 2026-04-27
125+
126+
### Other
127+
128+
- Release beta 0.1.20b3
129+
130+
### Miscellaneous
131+
132+
- Update uv.lock for beta 0.1.20b3
133+
134+
## [0.1.20rc1] - 2026-04-27
135+
136+
### Other
137+
138+
- Release rc 0.1.20rc1
139+
140+
### Miscellaneous
141+
142+
- Update uv.lock for rc 0.1.20rc1
143+
8144
## [0.1.20] - 2026-04-27
9145

10146
### Fixed
@@ -16,6 +152,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16152
- Forward tomli-w dev dep to testing
17153
- Forward tomli-w dev dep to review
18154
- Forward tomli-w dev dep to master
155+
- Release stable 0.1.20
156+
157+
### Miscellaneous
158+
159+
- Update uv.lock for stable 0.1.20
19160

20161
## [0.1.20b2] - 2026-04-27
21162

CONTRIBUTING.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,21 @@ fonts and exercises every release-blocking path:
148148
```
149149

150150
It runs T1–T10 (Node/Puppeteer auto-install), T_setup/T11 (svg-bbox
151-
harness + `--text2path` end-to-end), T12 (byte-exact FBF compare against
152-
the golden) and T13 (per-frame visual diff of the FBF against the input
153-
fixtures, budget < 3 % `diffPercentage`).
151+
harness + `--text2path` end-to-end), T12 (per-frame visual diff of the
152+
freshly-produced FBF against the committed golden FBF, budget < 3 %
153+
`diffPercentage` per frame) and T13 (per-frame visual diff of the FBF
154+
against the input fixture SVGs, budget < 3 % `diffPercentage`).
155+
156+
T12 used to be a byte-exact compare, but the system DejaVu glyph table
157+
drifts by sub-pixel amounts across fresh `apt-get install fonts-dejavu`
158+
runs even on the same architecture, making byte-exact comparison
159+
non-deterministic. A pixel-level visual invariant ("the FBF still
160+
LOOKS like the golden") is the right invariant for this lane.
154161

155162
Relevant scripts and fixtures:
156163

157164
- `tests/fixtures/e2e/text_frames/` — 5 deterministic text fixture SVGs (DejaVu Sans/Serif + Liberation Mono only) and the golden `expected.fbf.svg`. The `*.fbf.svg` ignore rule has an explicit exception for this golden in `tests/.gitignore`; if you regenerate the golden, the exception is what keeps it tracked.
165+
- `scripts/visual_diff_fbf_vs_golden.py` — T12 harness (extracts each frame from BOTH the produced FBF and the golden FBF, then runs `sbb-compare --json` per pair).
158166
- `scripts/visual_diff_text_frames.py` — T13 harness (cwd-stages inputs and runs `sbb-compare --json`).
159167
- `scripts/extract_fbf_frame.py` — XML-level extraction of one frame from an FBF.
160168
- `scripts/pin_fbf_frame_to_png.py` — calibration utility: pins PROSKENION to frame N and renders the full FBF wrapper to PNG via `sbb-svg2png`. Useful when calibrating the T13 thresholds.

docs/FBF_FRAME_COMPARATOR.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,10 @@ The comparator shares infrastructure with `test_frame_rendering.py`:
257257
- **testrunner.py** - Session-based testing for svg2fbf
258258
- **scripts/extract_fbf_frame.py** - XML-level lift of `<g id="FRAME0000N">` + `<g id="SHARED_DEFINITIONS">` from an FBF.SVG into a fresh standalone SVG. Used by the T13 Docker E2E harness for per-frame comparison without playing back the animation timeline. Loses ancestor inheritance from `ANIMATION_BACKDROP > ANIMATION_STAGE > ANIMATED_GROUP > PROSKENION` and any `STAGE_FOREGROUND` / `OVERLAY_LAYER` content (empty by default).
259259
- **scripts/pin_fbf_frame_to_png.py** - Calibration utility. Mutates a copy of an FBF.SVG (pin PROSKENION's `xlink:href`, drop the `<animate>` child) and renders to PNG via `sbb-svg2png`. Preserves the full FBF wrapper — ancestor inheritance, foreground/overlay layers, root SVG attrs — so the rendered PNG matches what a viewer truly sees at frame N. First-class FBF→PNG frame export is tracked upstream as [Emasoft/SVG-BBOX#3](https://github.com/Emasoft/SVG-BBOX/issues/3); when that lands, the pin-and-render workaround in this script becomes obsolete.
260+
- **scripts/visual_diff_fbf_vs_golden.py** - T12 harness in the Docker E2E suite. Extracts each frame from BOTH the produced FBF and the committed golden FBF (via `extract_fbf_frame.py`), then runs `sbb-compare --json` per pair with the same defaults as T13. Pass requires every frame at or below the image-wide budget (default 3 %).
260261
- **scripts/visual_diff_text_frames.py** - T13 harness in the Docker E2E suite. Stages input fixtures into `--workdir` and runs `sbb-compare --json` with `cwd=workdir` (sbb tools v1.0.14 sandbox file paths to `process.cwd()` and have no `--allow-paths` flag). Defaults: per-pixel-channel threshold 32/256, image-wide diffPercentage budget 3 %.
261262
- **scripts/regen_e2e_reference.sh** - Regenerate the non-text byte-exact golden (`tests/fixtures/e2e/expected/animation.fbf.svg`) on the host. Use only when input frames or the CLI invocation in `tests/fixtures/e2e/expected/COMMAND.txt` change intentionally — version bumps are absorbed by the version-strip in the byte-exact test, so they don't require a regen.
262-
- **scripts/regen_text_frames_golden.sh** - Regenerate the text→path byte-exact golden (`tests/fixtures/e2e/text_frames/expected.fbf.svg`) **inside Docker**. The text→path golden is platform-dependent (Linux vs macOS DejaVu Sans have different glyph metric tables), so this script always builds the same `python:3.12-slim` + `fonts-dejavu` + `fonts-liberation` image as `test_release_clean.sh` and runs `svg2fbf --text2path` inside it, ensuring byte-identical glyph paths to what T12 compares against. Never regenerate this golden on the host directly.
263+
- **scripts/regen_text_frames_golden.sh** - Regenerate the text→path golden (`tests/fixtures/e2e/text_frames/expected.fbf.svg`) **inside Docker**. T12 is now visual-diff (not byte-exact), but the golden is still platform-pinned: a Linux-generated golden keeps the calibrated baseline near 0 % rather than the ~1-2 % cross-platform AA drift floor. The script always builds the same `python:3.12-slim` + `fonts-dejavu` + `fonts-liberation` image as `test_release_clean.sh` and runs `svg2fbf --text2path` inside it. Never regenerate this golden on the host directly.
263264

264265
## License
265266

0 commit comments

Comments
 (0)