The optimization & automation plan (Phases A–H)¶
Between January and May 2026 the project went through a structured eight-phase modernisation that took it from a hand-built CLI with minimal automation to a fully-gated, signed, multi-Python-tested package with auto-generated releases. This page is the canonical reference for what landed in each phase, why, and where to look for it.
If you're new to the codebase, read this top-to-bottom — every other page in the Contributing section assumes the tooling described here is in place.
Goals¶
The overarching goals across every phase:
- Make CI the source of truth. A green pipeline should mean the change is safe to ship; a red pipeline should mean it is not. No "ignore that one CI failure, it's flaky" exceptions.
- Automate everything that ships to a user. Versioning, changelog, PyPI
upload, EXE build, SBOM generation, signature attestation — all of it
triggered from
git merge, nevergit tagor localtwine upload. - Keep the contributor loop fast. One-liner commands (
just lint,just test,just install) and shared editor config so a new contributor gets a green local check in under five minutes. - Make the supply chain auditable. Every release ships a CycloneDX SBOM plus a license report, both signed with Sigstore so anyone can verify provenance offline.
Phase summary¶
| # | Phase | Focus | Commit |
|---|---|---|---|
| A | Foundation tooling | pyproject.toml, .editorconfig, .gitattributes, Dependabot, issue/PR templates, drop Py3.8/3.9 |
3eacf06 |
| B | Quality-gate CI | Pre-commit, ruff lint+format, mypy, pytest matrix (3.10–3.13 × Linux/Win), security scans, build-wheel, aggregate gate | ce29ab3 |
| C | Test coverage uplift | Encoding round-trip, header processing, helper coverage, COM mocks | 88564a4 |
| D | Reliability primitives | Typed exceptions, bounded-retry helper, structured logging, more tests, coverage floor → 25% | 5815429 |
| E | COM/IO hardening | COM error handling, threading correctness, BOM-aware decoding, CLI input validation | 61981eb |
| F | Release automation | release-please, PyPI Trusted Publishing, CycloneDX SBOM, license report, EXE attached to release | db5265b |
| G | UX polish | .visiowings.toml project config, visiowings init wizard, opt-out PyPI update check |
7bc6f05 |
| G+ | Docs site | MkDocs Material, mkdocstrings, GitHub Pages auto-deploy on push to main |
8a0b695 |
| H | Final polish | justfile, noxfile.py, Sigstore-signed SBOM/licenses, OpenSSF Scorecard, shared .vscode/ |
19dd67e |
| Docs | Phase H docs | development-environment.md, README badges, Sigstore + Scorecard docs |
826a068 |
The remainder of this page expands each phase: what problem it solved, what files it touched, and how to interact with the result.
Phase A — Foundation tooling and project metadata¶
Problem. The repo had a thin setup.py, no formatter config, no
contributor guidelines, supported EOL Pythons (3.8 and 3.9), and no
machine-readable hint about which line endings VBA files needed.
What landed.
pyproject.tomlis now the single source of truth: project metadata, dependencies, optional[dev]/[docs]extras, ruff/ruff-format/mypy/ pytest/coverage config, entry points.setup.pyis a 4-line shim kept only because some legacy build tools still look for it..editorconfigenforces LF + 4-space Python and CRLF for VBA files (*.bas,*.cls,*.frm) — VBA fails to re-import if line endings drift..gitattributesmirrors the same line-ending policy at the Git layer so Windows checkouts don't silently rewrite source files.- Dependabot (
pip+github-actions, weekly cadence, conventional-commit prefixes) so dependency PRs auto-flow. - Issue + PR templates,
CONTRIBUTING.md,SECURITY.md,CODE_OF_CONDUCT.md. - Python 3.8 and 3.9 dropped (both EOL); 3.10–3.13 declared.
pywin32made a Windows-only conditional dependency (pywin32>=305; sys_platform == 'win32') so the package installs on Linux for tests.py.typedmarker added — visiowings ships type information per PEP 561.
Where to look. pyproject.toml, .editorconfig, .gitattributes,
.github/dependabot.yml, .github/ISSUE_TEMPLATE/,
.github/PULL_REQUEST_TEMPLATE.md.
Phase B — Full quality-gate CI pipeline¶
Problem. The repo had a stub CI that ran a smoke test on one Python version. There was no formatter check, no type-check, no security scan, no build verification, and no aggregated "all checks passed" status check suitable for branch protection.
What landed.
.github/workflows/ci.ymlruns the following on every PR and push tomain:- Pre-commit — runs every hook over every file, identical to local.
- Lint (ruff) —
ruff check+ruff format --check. - Type-check (mypy) with the per-module strict overrides.
- Test matrix — pytest on Python 3.10/3.11/3.12/3.13 × Ubuntu/Windows
with
pytest-cov. Codecov upload on the canonical job (ubuntu-latest+ 3.12). - Security —
pip-audit --strict --requirement requirements.txt(vulnerability scan) +bandit -r visiowings/ -ll(static analysis). - Dependency Review — fails the PR if any new dependency carries a high-severity advisory.
- Build wheel + sdist —
python -m build+twine check, artefacts uploaded for inspection. - CI gate — a final job that aggregates the others. This is the single
required-status-check for branch protection on
main. .github/workflows/codeql.ymlruns CodeQL on push + weekly..pre-commit-config.yamlruns ruff, ruff-format, mypy, codespell, gitleaks (secret scanning), validate-pyproject, conventional-pre-commit, and actionlint.
Where to look. .github/workflows/ci.yml,
.github/workflows/codeql.yml, .pre-commit-config.yaml.
Phase C — Test coverage uplift¶
Problem. The codebase had a handful of integration tests that needed a real Visio install. There was effectively nothing testable on Linux.
What landed.
tests/_visio_mocks.py— a typed mock harness for the pywin32 surface area the codebase actually uses. Lets us run almost all logic on Linux.- New test modules covering encoding round-trips
(
tests/test_encoding_roundtrip.py), header processing (tests/test_header_processing.py), and Rubberduck folder integration. - The
windows_onlypytest marker so the integration tests are easy to skip on non-Windows runners; CI's Linux jobs run with-m "not windows_only".
Where to look. tests/_visio_mocks.py plus every tests/test_*.py.
Phase D — Typed exceptions, retries, structured logging¶
Problem. COM errors were caught as bare Exception and printed; transient
failures could blow up a long-running watcher session; logging was a mix of
print and ad-hoc logging.basicConfig calls.
What landed.
visiowings/exceptions.py— typed exception hierarchy (VisiowingsError→COMConnectionError,EncodingError,VBAImportError,VBAExportError).visiowings/_retry.py— bounded-retry helper with exponential backoff for COM operations. Deliberately notenacitydependency.visiowings/_logging.py— singlesetup_logging(debug: bool)entry point that configures a structured root logger; respectsVISIOWINGS_LOG_LEVEL.- New test modules for the three primitives (
test_retry.py,test_logging_setup.py, plus the existing exception tests). - Coverage floor raised from 15% → 25% in
pyproject.toml.
Where to look. visiowings/exceptions.py, visiowings/_retry.py,
visiowings/_logging.py, plus the matching tests.
Phase E — COM, threading, encoding hardening¶
Problem. Three classes of real bugs from production use: COM channels broke silently after Visio crashes, watchdog's threading model conflicted with COM apartments, and BOM-prefixed files could be silently mis-decoded.
What landed.
vba_import.VisioVBAImporter._ensure_connection— detects a dead COM channel via a sentinel attribute access, retries up to_MAX_RECONNECT_ATTEMPTS(3) before giving up with a typedCOMConnectionError.pythoncom.CoInitialize/CoUninitializecalls in every watchdog handler thread (the import path was previously implicit, breaking on some Visio versions).encoding.get_encoding_from_documentreads the document's LCID and maps it through theLCID_TO_CODEPAGEtable;resolve_encodingis the new user-facing entry point with a clear precedence (CLI flag → document → system defaultcp1252).- BOM detection in the file watcher so cross-platform editors (which often prepend a BOM) don't break VBA round-trips.
- CLI input validation: file existence, extension whitelist, output path resolution.
Where to look. visiowings/vba_import.py, visiowings/encoding.py,
visiowings/file_watcher.py, visiowings/cli.py.
Phase F — Release automation and supply-chain artefacts¶
Problem. Releases were manual: bump the version in three files, write the
changelog by hand, build locally, twine upload. Easy to make mistakes;
no provenance.
What landed.
- release-please (
.github/workflows/release-please.yml+release-please-config.json+.release-please-manifest.json). Reads Conventional Commits onmain, opens or updates a Release PR that bumps the version everywhere it lives (pyproject.toml,__init__.py,setup.py) and rewritesCHANGELOG.md. Merging that PR creates thevX.Y.Ztag and GitHub Release. - PyPI Trusted Publishing —
.github/workflows/publish.ymluses an OIDC token to upload to PyPI. No long-lived API token. One-time PyPI setup is documented in Releasing. - CycloneDX SBOM (
sbom.cdx.json) and a license report (licenses.json) are generated from the wheel and attached to every GitHub Release. - Standalone Windows EXE built via PyInstaller in
.github/workflows/build-exe.yml, smoke-tested with--version, and attached to the Release alongside the wheel and sdist.
Where to look. .github/workflows/publish.yml,
.github/workflows/release-please.yml, .github/workflows/build-exe.yml,
release-please-config.json.
Phase G — Project config, init wizard, update check¶
Problem. Repeat users were typing the same --file ./mydrawing.vsdm
--bidirectional --output ./vba flags every session. There was no way to know
a release shipped without checking PyPI by hand.
What landed.
visiowings/config.pyreads.visiowings.tomlfrom the project root and layers it under CLI flags. Documented in Configuration.visiowings initis a small interactive wizard that scaffolds a.visiowings.tomlfor the user. Detects open Visio documents on Windows and offers them as defaults.visiowings/_update_check.pydoes an opt-out, once-per-day check against PyPI's JSON API for a newer version. Cached in~/.cache/visiowings/update_check.json. Disable viaVISIOWINGS_NO_UPDATE_CHECK=1orupdate_check = falsein.visiowings.toml.- Coverage floor raised → 30%.
Where to look. visiowings/config.py, visiowings/_update_check.py,
visiowings/cli.py (the init subcommand).
Phase G+ — Documentation site¶
Problem. The README had grown into a 270-line wall of text. There was no discoverable place for a "how do I configure this?" answer.
What landed.
mkdocs.ymlconfigures MkDocs Material with mkdocstrings (Python handler) for auto-generated API docs.docs/tree split into Getting Started, Architecture, API Reference, Contributing, Changelog..github/workflows/docs.ymlbuilds with--stricton every push and deploys to GitHub Pages on push tomain. Live at https://twobeass.github.io/visiowings/.
Where to look. mkdocs.yml, docs/, .github/workflows/docs.yml.
Phase H — Final polish: dev experience and supply-chain¶
Problem. Three small remaining gaps: contributors had to memorise long shell incantations, the matrix tested four Pythons but you couldn't easily reproduce that locally, and the SBOM/license artefacts shipped unsigned.
What landed.
justfile— one-liner recipes for every common workflow (just install,just test,just lint,just fmt,just security,just docs-serve, …). Full table in Development environment.noxfile.py— cross-Python (3.10/3.11/3.12/3.13) sessions for tests + lint + type-check + docs + security. Reuses virtualenvs.- Sigstore signing —
publish.ymlnow signssbom.cdx.jsonandlicenses.jsonkeyless via OIDC. The.sigstorebundles are attached to the Release. Verification recipe in Releasing → Verifying a release. - OpenSSF Scorecard —
.github/workflows/scorecard.ymlruns weekly and on push tomain; SARIF results land in Security → Code scanning for dashboard tracking. Public viewer link in the README badge row. - Shared
.vscode/—settings.json(Ruff format-on-save, mypy daemon, pytest discovery, sane file defaults) +extensions.json(recommended extensions). Per-user state is still ignored via.gitignore— only the two committed files are tracked.
Where to look. justfile, noxfile.py,
.github/workflows/publish.yml (Sigstore step),
.github/workflows/scorecard.yml, .vscode/.
What didn't ship (and why)¶
A few things were considered and intentionally deferred:
- Strict ruleset for
BLE/TRY/SIM/PLruff families. Phase A enabled them; reality was that the legacy COM bridge invba_import.py/vba_export.py/visio_connection.pywas built aroundexcept Exceptionand lazyimport pywin32patterns, neither of which passes those rules. Re-enabling is gated on refactoring the COM layer behind a typed adapter — that's bigger than a CI cleanup. disallow_untyped_defs = trueglobally. Currently scoped to the primitives added in Phase D (encoding,_retry,exceptions). The legacy modules use COM objects which are inherentlyAny-typed; widening toAnyannotations would satisfy mypy but communicate nothing.- PyPI publishing of the standalone EXE. EXEs aren't a PyPI artefact
type; they're attached to GitHub Releases instead. The PyPI install path
remains
pipx install visiowings.
Repo-side prerequisites¶
Two settings live in the GitHub UI (not in this repo) and are worth calling out — without them, parts of the pipeline degrade:
- Dependency graph — Settings → Code security & analysis →
Dependency graph: Enabled. Required by the Dependency Review CI
job; without it, the action fails fast and the job is treated as
advisory (
continue-on-error: true). - PyPI Trusted Publisher — see Releasing → One-time setup. Without it, the first release upload to PyPI fails; subsequent releases are fine once the pending publisher is promoted.
Where the docs live now¶
| Topic | File |
|---|---|
| User-facing install + usage | docs/getting-started/, README.md |
.visiowings.toml |
docs/getting-started/configuration.md |
| Codepage matrix | docs/getting-started/codepages.md |
| API reference (auto-generated) | docs/components/cli.md, docs/components/document-manager.md |
| Contributor onboarding | CONTRIBUTING.md, docs/contributing/development-environment.md |
| Releasing + supply-chain verification | docs/contributing/releasing.md |
| Manual testing on real Visio (tutorial) | docs/contributing/manual-testing.md |
| Release UAT checklist (sign-off) | docs/contributing/uat.md |
| This page | docs/contributing/optimization-plan.md |
| Changelog | CHANGELOG.md, docs/changelog.md |