This document describes the five GitHub Actions workflows in
.github/workflows/, what each one asserts, where
its artifacts land, and the exact commands to reproduce each check locally.
It also documents the one-time repository setup the release workflow
depends on.
| Workflow | File | Triggers |
|---|---|---|
| Lint | lint.yml |
push to main, all pull requests |
| Schema drift | schema-drift.yml |
push to main, all pull requests |
| CI (test matrix) | ci.yml |
push to main, all pull requests |
| Container profile | container.yml |
push to main, all pull requests |
| Release | release.yml |
push of a tag matching v* |
All five run with permissions: contents: read and a per-branch/ref
concurrency group that cancels superseded runs (release does not
cancel-in-progress, since a publish must run to completion).
Triggers: push to main, pull requests.
What it asserts: reference/wcpd and conformance are ruff-clean —
both the lint rule set (ruff check) and formatting (ruff format --check). No artifacts are produced; a non-zero exit fails the job.
Local reproduction:
python -m pip install ruff
ruff check reference/wcpd conformance
ruff format --check reference/wcpd conformanceTo auto-fix formatting before committing:
ruff format reference/wcpd conformanceTriggers: push to main, pull requests.
What it asserts: the checked-in schemas/ directory (the manifest
schema and every per-operation schema under schemas/ops/) is byte-for-byte
what wcpd's live operation registry would generate. This catches the case
where someone edits an operation's params/result shape, scope, or
annotations without regenerating the published schema files.
Local reproduction:
cd reference/wcpd
python -m pip install -e ".[dev]"
python -m pytest tests/test_schema_export.py -qIf it fails because you intentionally changed an operation, regenerate
the schemas (see CONTRIBUTING.md)
and commit the diff under schemas/.
Triggers: push to main, pull requests.
What it asserts: the reference server and the conformance suite work
across the full support matrix — ubuntu-latest, macos-latest,
windows-latest, each on Python 3.11 and 3.12 (6 jobs, fail-fast: false
so one OS/version failing doesn't hide others). Each job:
- Installs both packages editable:
reference/wcpd[dev]andconformance[dev]. - Runs
reference/wcpd's unit tests with an OS-specific marker filter so platform-specific tests (Linux D-Bus adapters, macOS AppleScript adapters, Windows UIA adapters, anything needing a display) only run where they apply. - Boots a real
wcpdserver and runs the conformance suite against it end-to-end (not in-process), viascripts/run_conformance.shon POSIX runners orscripts/run_conformance.ps1on Windows. - Uploads the conformance report as an artifact regardless of outcome.
Environment used by the job: WCP_JWT_SECRET=ci-test-secret,
WCP_WORKSTATION_ID=ci-ws.
Artifacts: conformance-<os>-py<version> — the conformance-report.json
produced by the boot-and-run script, uploaded with
if-no-files-found: ignore (a job that fails before the script runs simply
has no artifact).
Local reproduction — unit tests, per OS marker filter:
cd reference/wcpd
python -m pip install -e ".[dev]"
# on Linux
python -m pytest . -q -m "not macos and not windows and not display"
# on macOS
python -m pytest . -q -m "not linux and not windows and not display"
# on Windows
python -m pytest . -q -m "not linux and not macos and not display"Local reproduction — end-to-end conformance run (boots the server for you):
# from the repo root, POSIX (Linux/macOS)
python -m pip install -e "reference/wcpd[dev]"
python -m pip install -e "conformance[dev]"
bash scripts/run_conformance.sh
# from the repo root, Windows
pwsh -File scripts/run_conformance.ps1Both scripts set sane defaults (WCP_JWT_SECRET=ci-test-secret,
WCP_WORKSTATION_ID=ci-ws, WCP_HOST=127.0.0.1, WCP_PORT=8977, a
temp WCP_SANDBOX_ROOT, an audit log inside it), start
uvicorn --factory wcpd.server:build_app, poll scripts/wait_for_http.py
until /mcp answers, run wcp-conformance, write
conformance-report.json in the current directory, tear the server down,
and propagate the conformance exit code.
Triggers: push to main, pull requests.
What it asserts: the shipped Dockerfile under reference/wcpd/
actually boots and is core-conformant, and that the headless container
profile covers at minimum the wcp.fs, wcp.exec, and wcp.system
capability families (the families a container with no display or window
manager can realistically expose). It also dumps docker logs wcpd on
failure for debugging.
Artifacts: container-conformance — container-conformance.json,
uploaded with if-no-files-found: ignore.
Local reproduction:
docker build -t wcpd:ci reference/wcpd
docker run -d --name wcpd \
-e WCP_JWT_SECRET=ci-test-secret \
-e WCP_WORKSTATION_ID=ci-ws \
-p 8977:8977 wcpd:ci
python -m pip install -e "conformance[dev]"
python scripts/wait_for_http.py http://127.0.0.1:8977/mcp 40
wcp-conformance \
--endpoint http://127.0.0.1:8977/mcp \
--jwt-secret ci-test-secret \
--workstation-id ci-ws \
--schemas-dir schemas \
--json container-conformance.json
python - <<'PY'
import json
rep = json.load(open("container-conformance.json"))
assert rep["core_conformant"], "container image is not core-conformant"
fams = rep["family_verdicts"]
for required in ("wcp.fs", "wcp.exec", "wcp.system"):
assert fams.get(required, False), f"{required} must pass in the container profile"
print("container profile OK:", sorted(fams))
PY
docker logs wcpd # inspect if something failed
docker rm -f wcpd # tear downTriggers: push of a tag matching v* (e.g. v0.1.0).
What it does:
build— builds sdists and wheels for bothreference/wcpdandconformancewithpython -m build, uploads them as thedistartifact.publish-pypi— downloadsdist, publishes both distributions to PyPI using PyPI trusted publishing (OIDC;id-token: writepermission, no long-lived secret). Runs in thepypiGitHub Environment.publish-image— logs in to GHCR with the built-inGITHUB_TOKEN(packages: writepermission — no extra secret needed), buildsreference/wcpd's Dockerfile, and pushesghcr.io/<owner>/wcpd:<tag>andghcr.io/<owner>/wcpd:latest.
Artifacts: dist (sdists + wheels for both packages), consumed by the
two publish jobs; nothing else is uploaded.
Publishing to PyPI via trusted publishing does not work out of the box;
it requires repository/organization configuration that must exist before
you push the first v* tag, or publish-pypi will fail:
- Register the trusted publisher on PyPI. On the PyPI project page
(Publishing → Add a new publisher), register this GitHub repository
(
kchemorion/WorkstationCapabilityProtocol), the workflow filename (release.yml), and the environment name (pypi). PyPI will only accept the OIDC token if these three match exactly. - Create a
pypiGitHub Environment in the repository settings (Settings → Environments → New environment →pypi). Thepublish-pypijob targetsenvironment: pypi; without the environment existing the job's OIDC claim won't match what PyPI expects, and you may also want to add required reviewers/protection rules here to gate who can trigger a real publish. - GHCR needs no extra setup.
publish-imageuses the automatically providedsecrets.GITHUB_TOKENwithpackages: write; no PAT or trusted-publisher registration is required, only that the repository allows Actions to write packages (default for repos with Packages enabled).
Until step 1 and 2 are done, do not push a v* tag expecting a successful
PyPI publish — the build and GHCR jobs will still succeed, but
publish-pypi will fail at the OIDC exchange.
Local reproduction (build only — nothing is published locally):
python -m pip install build
python -m build --outdir dist reference/wcpd
python -m build --outdir dist conformance