From 6cf973b0179eb99277cc94d9072ea55e668e1aaf Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Tue, 17 Mar 2026 14:30:08 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20WP-0006=20=E2=80=94=20packaging=20&=20d?= =?UTF-8?q?istribution=20infrastructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI matrix (Python 3.11/3.12) with pip cache and coverage job. PyPI publish workflow (OIDC trusted publishing, triggered on v*.*.* tags). Docker image for REST service with non-root user + ghcr.io push workflow. markidocx --version flag. Diagram optional extras (diagram-mermaid/graphviz/plantuml) and readme/urls in pyproject.toml. CHANGELOG.md (Keep a Changelog format) with retrospective v0.1.0 entry. docs/release-process.md with executable checklist. All 272 tests pass; ruff and mypy clean; twine check PASSED. Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 18 ++++++ .github/workflows/ci.yml | 29 ++++++++- .github/workflows/docker.yml | 36 +++++++++++ .github/workflows/publish.yml | 33 ++++++++++ CHANGELOG.md | 61 +++++++++++++++++++ CLAUDE.md | 2 + Dockerfile | 19 ++++++ docs/release-process.md | 58 ++++++++++++++++++ pyproject.toml | 12 ++++ src/markidocx/cli.py | 18 ++++++ .../MRKD-WP-0006-packaging-distribution.md | 10 +-- 11 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/publish.yml create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 docs/release-process.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5097607 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +tests +docs +workplans +specs +contrib +dist +*.egg-info +__pycache__ +*.pyc +*.pyo +.pytest_cache +.mypy_cache +.ruff_cache +.markidocx +requirements.txt +CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e510e4..6d7a92f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: "pip" - name: Install dependencies run: pip install -e ".[dev]" @@ -31,4 +32,30 @@ jobs: run: mypy src/ - name: Run tests - run: pytest + run: pytest --tb=short -q + + coverage: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + needs: test + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install dependencies + run: pip install -e ".[dev]" + + - name: Run tests with coverage + run: pytest --cov=markidocx --cov-report=xml --tb=short -q + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: coverage.xml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..70006cc --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,36 @@ +name: Docker + +on: + push: + tags: + - "v*.*.*" + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.ref_name }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..34bb02d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish to PyPI + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write # required for OIDC trusted publishing + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install build tools + run: pip install build twine + + - name: Build distribution + run: python -m build + + - name: Check distribution + run: twine check dist/* + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..710b20b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +All notable changes to markidocx are documented in this file. + +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [Unreleased] + +--- + +## [0.1.0] — 2026-03-17 + +### Added + +**Core pipeline (WP-0001)** +- `manifest.py` — project manifest parsing and validation (FR-100) +- `builder.py` — Markdown → DOCX conversion, LEVEL1 feature set: headings, lists, tables, + footnotes, images, hyperlinks (FR-200) +- `importer.py` — DOCX → Markdown round-trip import (FR-300, FR-400) +- `differ.py` — structural drift detection between original Markdown and re-imported result (FR-700) +- `templates.py` — document family management: `article`, `book`, `website` (FR-600) +- `evidence.py` — evidence and report assembly (FR-1400) + +**Service interfaces (WP-0002)** +- `cli.py` — Typer CLI: `build`, `import`, `compare`, `validate`, `serve`, `workflow`, + `mcp`, `template` commands +- `rest.py` — FastAPI REST service with structured `WarningRecord` / `FailureRecord` output + (FR-900, FR-1208) +- `mcp_server.py` — FastMCP server exposing same functional surface as CLI/REST (FR-1000) +- `errors.py` — `WarningRecord`, `FailureRecord`, `OutputState` error framework (FR-1201–1210) + +**LEVEL3 advanced features (WP-0003)** +- `level3.py` — LEVEL3 support detection and capability disclosure (FR-537–539) +- `xref.py` — cross-reference round-trip helpers (FR-531, FR-540) +- `figures.py` — numbered figure round-trip helpers (FR-532, FR-541) +- `diagrams.py` — auto-diagram source-only preservation (FR-533, FR-534) +- `bibliography.py` — citation and references section round-trip (FR-535, FR-536, FR-542) +- `workflows.py` — composite workflow orchestration: `single-file-roundtrip`, + `multi-file-roundtrip`, `release-regression`, `family-switch-build` (FR-1300) + +**Diagram renderer integration (WP-0005)** +- Pluggable `DiagramRenderer` protocol and `RendererResult` type +- `MermaidRenderer` — shells out to `mmdc` when available (FR-533) +- `GraphvizRenderer` — shells out to `dot` when available (FR-533) +- `PlantUMLRenderer` — shells out to `plantuml` when available (FR-533) +- Graceful source-only fallback with `WarningRecord(reason="renderer-unavailable")` when + tool is absent (FR-538) +- Alt-text source markers enable diagram round-trip after rendering (FR-534) +- Optional extras: `diagram-mermaid`, `diagram-graphviz`, `diagram-plantuml` + +**Packaging & distribution (WP-0006)** +- GitHub Actions CI: matrix test on Python 3.11 + 3.12, ruff, mypy, coverage +- PyPI publish workflow triggered on `v*.*.*` tags via OIDC trusted publishing +- Docker image for REST service (`ghcr.io`) +- `markidocx --version` command + +[Unreleased]: https://github.com/tegwick/marki-docx/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/tegwick/marki-docx/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index ae202c1..a501068 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -320,3 +320,5 @@ Then either: - MCP tool reference: `~/the-custodian/state-hub/mcp_server/TOOLS.md` - ADR-001 (workplan convention): `~/the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md` - Contribution convention: `~/the-custodian/canon/standards/contribution-convention_v0.1.md` +- Release process: `docs/release-process.md` +- Changelog: `CHANGELOG.md` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ea3eac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +# Create non-root user +RUN useradd --create-home --shell /bin/bash appuser + +WORKDIR /app + +# Install markidocx +COPY pyproject.toml ./ +COPY src/ ./src/ +RUN pip install --no-cache-dir . + +# Switch to non-root user +USER appuser + +EXPOSE 8080 + +ENTRYPOINT ["markidocx", "serve"] +CMD ["--host", "0.0.0.0", "--port", "8080"] diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..eff5968 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,58 @@ +# Release Process + +## Pre-release checklist + +Before tagging a release, verify every item: + +- [ ] All tests pass: `pytest --tb=short -q` +- [ ] Lint clean: `ruff check .` +- [ ] Type-check clean: `mypy src/` +- [ ] Corpus regression passes: `markidocx test` +- [ ] SBOM is current (re-run after any dependency change — see CLAUDE.md) +- [ ] `CHANGELOG.md` updated: move items from `[Unreleased]` to a new versioned section +- [ ] Version bumped consistently: + - `src/markidocx/__init__.py` — `__version__ = "X.Y.Z"` + - `pyproject.toml` — `version = "X.Y.Z"` +- [ ] Build artefacts clean: `python -m build && twine check dist/*` + +## Tagging and releasing + +```bash +# Verify version strings match +python -c "import markidocx; print(markidocx.__version__)" + +# Create a signed tag +git tag -s vX.Y.Z -m "Release vX.Y.Z" + +# Push tag — triggers publish.yml (PyPI) and docker.yml (ghcr.io) automatically +git push origin vX.Y.Z +``` + +Pushing the tag triggers two GitHub Actions workflows: +- **`publish.yml`** — builds and publishes to PyPI via OIDC trusted publishing +- **`docker.yml`** — builds and pushes Docker image to `ghcr.io` + +## Post-release + +- Create a GitHub release from the tag and paste the relevant CHANGELOG section +- Verify installation: `pip install markidocx==X.Y.Z && markidocx --version` +- Verify Docker image: `docker run --rm ghcr.io/tegwick/marki-docx:vX.Y.Z --version` + +## Versioning policy + +markidocx follows [Semantic Versioning](https://semver.org/): + +| Change type | Version bump | +|------------|-------------| +| Breaking change to CLI/API/round-trip contract | MAJOR | +| New feature, backwards-compatible | MINOR | +| Bug fix, docs, tooling | PATCH | + +## Setting up PyPI trusted publishing (one-time) + +In the PyPI project settings, add a trusted publisher: +- Publisher: GitHub Actions +- Repository owner: `tegwick` +- Repository name: `marki-docx` +- Workflow filename: `publish.yml` +- Environment name: (leave blank) diff --git a/pyproject.toml b/pyproject.toml index 95880c7..b5e0d90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "setuptools.build_meta" name = "markidocx" version = "0.1.0" description = "Markdown ↔ DOCX round-trip editing system" +readme = "README.md" requires-python = ">=3.11" dependencies = [ "python-docx>=1.1.0", @@ -27,6 +28,14 @@ dev = [ "types-PyYAML>=6.0", "httpx>=0.27", ] +diagram-mermaid = ["nodeenv>=1.8"] +diagram-graphviz = ["graphviz>=0.20"] +diagram-plantuml = [] + +[project.urls] +Homepage = "https://github.com/tegwick/marki-docx" +Source = "https://github.com/tegwick/marki-docx" +Tracker = "https://github.com/tegwick/marki-docx/issues" [project.scripts] markidocx = "markidocx.cli:app" @@ -37,6 +46,9 @@ where = ["src"] [tool.setuptools.package-data] markidocx = ["templates/*.docx"] +[tool.setuptools] +license-files = ["LICENSE"] + [tool.ruff] line-length = 100 target-version = "py311" diff --git a/src/markidocx/cli.py b/src/markidocx/cli.py index a5ab621..b79554a 100644 --- a/src/markidocx/cli.py +++ b/src/markidocx/cli.py @@ -10,6 +10,8 @@ import typer from rich.console import Console from rich.table import Table +from markidocx import __version__ + app = typer.Typer( name="markidocx", help="Markdown ↔ DOCX round-trip editing system.", @@ -18,6 +20,22 @@ app = typer.Typer( template_app = typer.Typer(help="Template family management.") app.add_typer(template_app, name="template") + +def _version_callback(value: bool) -> None: + if value: + print(f"markidocx {__version__}") + raise typer.Exit() + + +@app.callback() +def _main( + version: Annotated[ + bool, + typer.Option("--version", callback=_version_callback, is_eager=True, help="Show version and exit."), + ] = False, +) -> None: + pass + console = Console() err_console = Console(stderr=True) diff --git a/workplans/MRKD-WP-0006-packaging-distribution.md b/workplans/MRKD-WP-0006-packaging-distribution.md index f7ea95f..544864b 100644 --- a/workplans/MRKD-WP-0006-packaging-distribution.md +++ b/workplans/MRKD-WP-0006-packaging-distribution.md @@ -3,7 +3,7 @@ id: MRKD-WP-0006 type: workplan domain: markitect repo: marki-docx -status: active +status: done state_hub_workstream_id: 7e255145-8d18-4f22-b1ca-31f02944b890 created: 2026-03-16 updated: 2026-03-16 @@ -25,7 +25,7 @@ release artefact production. ```task id: MRKD-WP-0006-T01 -status: todo +status: done priority: high state_hub_task_id: cfca0094-b5ae-45ec-80c9-e7705f37bd12 ``` @@ -54,7 +54,7 @@ Deliverable: `.github/workflows/ci.yml` present; CI passes on a clean push to `m ```task id: MRKD-WP-0006-T02 -status: todo +status: done priority: high state_hub_task_id: 44f60455-b144-48e3-93a0-74869018c2ea ``` @@ -84,7 +84,7 @@ returns the correct version string. ```task id: MRKD-WP-0006-T03 -status: todo +status: done priority: medium state_hub_task_id: 05ff77b0-6347-4c96-9fcd-c2b2baaf0fce ``` @@ -121,7 +121,7 @@ Deliverable: `docker build . && docker run -p 8080:8080 ` starts the REST ```task id: MRKD-WP-0006-T04 -status: todo +status: done priority: medium state_hub_task_id: 177e4861-d153-4b1d-85d4-a272da14bfe5 ```