Organization-level defaults and shared automation for the dcc-mcp organization.
A GitHub release and the matching PyPI upload are produced by two separate steps, so a release can exist on GitHub while the package never reaches PyPI. Nothing in the current gates compares the two, which lets a broken publish stay green indefinitely.
.github/workflows/release-integrity.yml closes that gap: for every repository/package
pair it compares the newest GitHub release tag (leading v stripped) against
https://pypi.org/pypi/<package>/json and fails on a mismatch.
release-integrity.yml- the reusable check (workflow_call+workflow_dispatch).release-integrity-nightly.yml- runs the check daily over the built-in manifest: every dcc-mcp repository that publishes to a project that exists on PyPI (36 repositories / 38 packages, includingdcc-mcp-core, which also publishesdcc-mcp-serveranddcc-mcp-core-semantic). The manifest is written out twice, as therepositoriesdefault of both theworkflow_calland theworkflow_dispatchinputs inrelease-integrity.yml; update both copies and keep them identical, because the nightly schedule only reads theworkflow_callone.scripts/check_release_integrity.py- the comparison itself; it can be run locally.
A mismatch is tolerated (reported as a warning instead of a failure) when the release was
published less than grace_minutes ago, because a PyPI upload lands a few minutes after
the GitHub release, or when PyPI is already ahead of the newest stable release, which is
normal for a pre-release upload. Everything else fails.
python scripts/check_release_integrity.py \
--repository dcc-mcp/dcc-mcp-maya --package dcc-mcp-maya
python scripts/check_release_integrity.py --manifest '[
{"repository": "dcc-mcp/dcc-mcp-core",
"packages": ["dcc-mcp-core", "dcc-mcp-server", "dcc-mcp-core-semantic"]}
]'Exit codes: 0 consistent, 1 inconsistent, 2 the check could not run.
jobs:
release-integrity:
uses: dcc-mcp/.github/.github/workflows/release-integrity.yml@main
with:
repositories: '[{"repository":"dcc-mcp/dcc-mcp-maya","packages":["dcc-mcp-maya"]}]'
grace_minutes: "30"Omit repositories to use the built-in manifest of every PyPI-publishing dcc-mcp
repository. Set strict: true to fail on tolerated mismatches as well.
tests/test_release_integrity_manifest.py asserts the two copies stay identical, so a
half-applied manifest edit fails the Profile contract workflow instead of silently
leaving the nightly gate on an outdated list.
Release integrity answers "did the release reach PyPI?". This gate answers the question that comes before the merge: has CI actually run for this release pull request?
Those are not the same question, and the second one is harder than it looks. A pull
request whose CI is parked in GitHub's manual approval queue has no check runs at
all: gh pr checks prints nothing, statusCheckRollup is empty (or one all-null
entry), and mergeable is MERGEABLE. Nothing tells you CI is waiting — it looks
exactly like a pull request that has no CI. A release window that trusts mergeable
then publishes a version no CI ever validated, which is how dcc-mcp-premiere v0.6.2
shipped.
The runs behind this are all authored by github-actions[bot]: release-please opens
the pull request with secrets.GITHUB_TOKEN, GitHub does not treat that bot as a
trusted collaborator, so its pull_request runs go to the approval queue. The same
branch pushed from an account with write access runs immediately — which is why some
repositories never see this. dcc-mcp-houdini proves the fix: its release.yml passes
secrets.PERSONAL_ACCESS_TOKEN, so its release pull requests are authored by a real
collaborator and their CI runs at once.
A second silent shape is worse, because it inverts the signal. Merging a pull request
that still has a run in the approval queue makes GitHub flip that run to
conclusion=failure — while it still has zero jobs. So "CI is red" on a release
pull request frequently means "an approval request was invalidated by the merge", not
"a test failed". Treating it as red is as wrong as treating it as green, and it sends
the next reader looking for a test failure that does not exist.
scripts/check_release_ci_gate.py therefore never trusts conclusion. It counts
jobs, and returns one of four verdicts:
| Verdict | Meaning | Mergeable |
|---|---|---|
green |
every run completed successfully and at least one job ran | yes |
red |
a run executed jobs and did not succeed | no |
pending |
a run has not finished yet | no |
no_evidence |
no run produced a single job | no |
no_evidence carries a reason: awaiting_approval (a run sits in the approval queue),
approval_invalidated (a zero-job failure — a workflow that never ran, usually an
approval overtaken by the merge), or no_runs (nothing was ever triggered). Partial
evidence is not green either: if CI passed but E2E produced a run that executed zero
jobs, the verdict is no_evidence, because merging ships a version only part of the
suite saw.
Be precise about the limit of that rule, because the difference matters to anyone
writing automation against this gate. What is caught is a suite that produced a run
which executed no jobs. A suite that was never triggered at all is invisible
here — if a pull request has only a CI run and no E2E run object exists, the verdict is
green. Telling those two apart needs a per-repository list of which suites are
expected, which this check does not have. Do not read green as "every suite in the
repository passed"; read it as "every run that exists produced jobs and succeeded".
This is what a release window runs before merging anything:
python scripts/check_release_ci_gate.py --open-prs
python scripts/check_release_ci_gate.py --open-prs --repositories dcc-mcp/dcc-mcp-maya,dcc-mcp/dcc-mcp-nukepython scripts/check_release_ci_gate.py --pr dcc-mcp/dcc-mcp-premiere#19The verdict is always against the exact head commit, never the merge commit. When the pull request is already merged the report also shows the push runs of the merge commit, as labelled context: it tells you whether an already-published release has to be rolled back, and it is never a substitute for the head-commit verdict.
python scripts/check_release_ci_gate.py --org dcc-mcp --format jsonAdd --approve to approve the parked runs the sweep finds. It is off by default:
approving only lets an already-queued run execute, but it is a live action and should
be deliberate.
Exit codes: 0 no finding, 1 at least one finding at or above --fail-on
(default no_evidence), 2 the check could not be performed.
release-ci-gate.yml- the reusable check (workflow_call+workflow_dispatch).release-ci-gate-nightly.yml- sweeps every open release pull request in the organization once a day and fails on anyno_evidenceverdict, so a parked run stops being silent.
jobs:
release-ci-gate:
uses: dcc-mcp/.github/.github/workflows/release-ci-gate.yml@mainThe gate stops the merge; it does not stop the queue. To stop runs from parking in the
first place, give release-please a token that belongs to an account with write access
instead of secrets.GITHUB_TOKEN:
- uses: googleapis/release-please-action@v5
with:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}That is the single change that removes the failure mode, and it is per-repository:
dcc-mcp-houdini already works this way. Until every repository does, the gate is what
keeps a parked run from being read as "no CI, therefore fine".
Two machine-readable contracts share one checker, scripts/check_repo_contract.py, which
loads whichever one you point --contract at. The rules and their severities live in the
JSON under contract/, never in the script.
| Contract | Ids | Applies to | Docs |
|---|---|---|---|
contract/repo_contract.json |
R0xx |
every repository | docs/repo-contract.md |
contract/adapter_contract.json |
A0xx |
the 50 repositories shipping a Python package | docs/adapter-contract.md |
The 50 dcc-mcp repositories that ship a Python package had converged on nothing: five
spellings of the Install SOP report's schema_version field, three line-length values,
a 54-patch spread of dcc-mcp-core floors, and 6 of 50 with a pre-commit config. The
adapter contract turns that into a gate.
adapter-contract-nightly.yml- sweeps every repository incontract/adapter_repositories.jsononce a day, with--profile strict --fail-on error, so the two baseline rules block regressions and the four convergence rules are emitted as the stock-take list.contract/adapter_contract.json- the rules.A001(no reference to Core's deprecatedINSTALL_SOP_SCHEMA_VERSION) andA003(a declared Core dependency pins a lower bound) are errors inbaseline;A002,A004,A005andA006are warnings instrict.
A001 is an error from day one because every existing reference already sits behind a
try/except ImportError fallback, so deleting it needs no Core floor bump. Which fix applies
depends on what the value means, and the two cases need opposite edits: where the value is the
schema artifact revision only the name is deprecated, so rename it to
INSTALL_SOP_SCHEMA_REVISION; where the value feeds a report's schema_version, or is a
vestigial import, delete the reference and read the field from
install_sop_report_schema_version(). What is never allowed is a rename that only makes the
gate go green: where the value reaches a report's schema_version, that keeps the artifact
revision flowing into a field the schema pins at 1, which is the defect
PIP-4047/4048/4049/4050/4051 fixed one repository at a time. The full rule, with the counts per
case, is in docs/adapter-contract.md.
Run it locally against any adapter:
python scripts/check_repo_contract.py \
--root /path/to/dcc-mcp-maya \
--contract contract/adapter_contract.json \
--profile strict --format textprofile/README.md is the organization profile. profile-contract.yml validates it on
every change, and scripts/check_profile_contract.py is the check behind it.