Uh oh!
There was an error while loading. Please reload this page.
ci: publish to public PyPI on release + TestPyPI dry-run workflow - #6
Conversation
Add public PyPI publishing alongside the existing private Azure Artifacts feed, via OIDC Trusted Publishing (no stored token): - release.yml: expose the release job's `version` as an output and upload the built wheel+sdist as an artifact; add an environment-gated `publish-pypi` job (`needs: release`, `id-token: write`, GitHub-hosted runner) that downloads that artifact and publishes to pypi.org. The Azure feed and GHCR image steps are unchanged. - publish-testpypi.yml: new manually-dispatched dry run that stamps a throwaway `.dev<run#>` version and publishes to test.pypi.org, so the build + OIDC path can be validated before a real release. All third-party actions are pinned to full commit SHAs (Dependabot's github-actions ecosystem keeps them current). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
Claude encountered an error after 39s —— View job Code Review in Progress |
The pinned v1.0.134 drops thinking blocks across tool_use turns, so Bedrock rejects the reviewer request with `Expected thinking or redacted_thinking, but found tool_use`. Bump to the latest release to pick up the fix; SHA-pinned, Dependabot keeps it current. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude finished @uipreliga's task in 1m 29s —— View job PR Review: PyPI Publishing CI/CD |
The release (bump + build + Azure feed + GHCR) job ran on the self-hosted `uipath-ubuntu-latest` pool, so cutting a release was blocked whenever that pool was unavailable -- and publish-pypi (needs: release) with it. Move it to GitHub-hosted ubuntu-latest so PyPI releases are independent of the self-hosted runners. semantic-release, uv, twine, and docker buildx -> GHCR all run there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two pre-existing CI infra bugs: 1. `.venv` cache exit 127 -- the Linux cache blocks cached `.venv` keyed only on major.minor (`py3.13`), so a hosted-Python patch bump left `.venv/bin/python` a dangling symlink and every `.venv/bin/*` failed with "cannot execute: required file not found". Drop `.venv` from the three Linux cache paths (Quality Gate / E2E / Live); `uv sync --frozen` rehydrates it from the cached wheels, matching what the Windows job already does. 2. Codex live test `duplicate column name: thread_id` -- addopts pins `-n auto`, so xdist workers shared one ~/.codex and raced the Codex SQLite state migration. Run the Codex live suite serially with `-n0`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The self-hosted `uipath-ubuntu-latest` pool is unavailable to this repo, so every job pinned to it (Quality Gate, No-Extra Install, E2E Smoke, CodeQL, docker-publish, publish-dev) sat queued indefinitely. Move them all to GitHub-hosted `ubuntu-latest` so CI runs without depending on the pool. Note: this drops the self-hosted `SAFE_CHAIN_MINIMUM_PACKAGE_AGE` supply-chain check on uv installs (a no-op on GitHub-hosted); the env is kept so parity returns if a job ever moves back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:6
Scope: pr:6 · branch ci/pypi-publishing · 0b19b26 · 2026-07-09T09:45Z · workflow variant
Change class: complex — adds public-PyPI publishing via OIDC Trusted Publishing (new environment-gated publish-pypi job) plus a TestPyPI dry-run workflow, and moves the release job from the self-hosted uipath-ubuntu-latest pool to GitHub-hosted ubuntu-latest; job dependency/gating (needs + if), OIDC scoping, artifact hand-off, and dev-version stamping require correctness reasoning
coder_eval is in excellent health (9.8/10) with flawless type safety, security, architecture, and harness quality; every open finding is confined to the new CI/release workflows, where the only real risk is that a partially-failed public-PyPI publish is unrecoverable for that version, so the bottom line is: ship after making the PyPI publish idempotent and tightening a few release-doc/reproducibility nits.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.7 / 10 | 0 | 0 | 0 | 3 | Guard if: steps.release.outputs.version != '' copy-pasted across 9 steps in the release job |
| 2. Type Safety | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 3. Test Health | 9.9 / 10 | 0 | 0 | 0 | 1 | setup-uv is pinned to a SHA in publish-testpypi.yml but floats as @v4 in release.yml (version divergence) |
| 4. Security | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Architecture & Design | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 6. Error Handling & Resilience | 9.4 / 10 | 0 | 0 | 1 | 1 | Real PyPI publish lacks skip-existing, making a partially-failed upload unrecoverable for that version |
| 7. API Surface & Maintainability | 9.4 / 10 | 0 | 0 | 1 | 1 | Release workflow header comment omits the new public-PyPI publish target (operator-facing doc is stale) |
| 8. Evaluation Harness Quality | 10 / 10 | 0 | 0 | 0 | 0 | — |
Overall Score: 9.8 / 10 · Weakest Axis: Error Handling & Resilience at 9.4 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 2 · 🔵 6 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
- [Axis 6] Real PyPI publish lacks skip-existing, making a partially-failed upload unrecoverable for that version (
.github/workflows/release.yml:217) — The newpublish-pypijob'sPublish to PyPIstep (lines 217-222) ends withpackages-dir: dist/and NOskip-existing, whereas the sibling TestPyPI publish (publish-testpypi.yml) deliberately setsskip-existing: true(# A re-run at the same dev version is a no-op rather than a hard fail.). Eachworkflow_dispatchof release.yml cuts a NEW version (semantic-release forces a bump), so the ONLY way to recover a failed PyPI publish for version X is GitHub's 'Re-run failed jobs' onpublish-pypi. gh-action-pypi-publish uploads the wheel and sdist as separate files; if the first attempt uploads one file then dies (network blip mid-batch), the re-run hits PyPI's 'File already exists' and hard-fails permanently — you cannot delete files from PyPI nor re-dispatch the same version, so that version is left half-published and unrecoverable. Addskip-existing: trueto thePublish to PyPIstep to match TestPyPI and make job re-runs idempotent. - [Axis 7] Release workflow header comment omits the new public-PyPI publish target (operator-facing doc is stale) (
.github/workflows/release.yml:4) — The header comment still enumerates only two publish destinations — line 3-4:# ...build, and publish to the/# UiPath \coder_eval` Azure Artifacts feed (wheel) and GHCR (agent image).— but this PR adds a whole newpublish-pypijob (lines 193-222) that pushes the same wheel+sdist to PUBLIC PyPI (an irreversible, publicly-visible action). An operator reading the header to understand a release's blast radius would not know it now publishes to public PyPI. Update the header (line 4) to add public PyPI, e.g.# UiPath `coder_eval` Azure Artifacts feed (wheel), public PyPI (OIDC Trusted Publishing), and GHCR (agent image).`
Nits
- [Axis 1] Guard
if: steps.release.outputs.version != ''copy-pasted across 9 steps in the release job (.github/workflows/release.yml:109) — The exact stringif: steps.release.outputs.version != ''is repeated on 9 separate steps (lines 109, 122, 126, 133, 141, 158, 163, 167, 175). This is a DRY/readability smell: a reader must re-verify each step carries the guard, and dropping it from one of the docker steps would silently push an image on a no-version run. The PR itself already demonstrates the cleaner pattern one level down --publish-pypiis a separateneeds: releasejob gated ONCE withif: needs.release.outputs.version != ''(line ~213). Consider likewise splitting the wheel-publish + versioned-image build/push steps into a secondneeds: releasejob gated once, eliminating the 9 per-step guards. Same-file inconsistency: PyPI publish is a gated job, but the near-identical 'publish an artifact only on a real release' logic for the wheel/Docker image stays inline with repeated guards. - [Axis 1] Brittle dual-templated heredoc: shell
${DEV_VERSION}interpolated into an unquoted Python<<PYheredoc (.github/workflows/publish-testpypi.yml:76) — Line 67 opens the heredoc aspython3 - <<PY(unquoted), so the shell expands${DEV_VERSION}into the Python source before Python parses it. Line 76 then mixes both templating layers in one expression:p.write_text(re.sub(pat, f'{key} = "${DEV_VERSION}"', p.read_text(), count=1))--${DEV_VERSION}is shell-substituted while{key}is a Python f-string field. It works only because DEV_VERSION is a controlled<current>.dev<run#>string; any shell-special or quote char injected via the version would corrupt the generated Python. Cleaner and robust: use a quoted heredoc<<'PY'and read the value inside Python viaos.environ['DEV_VERSION'](the step already setsRUN_NUMBERvia env at line 61, so this is the established pattern) rather than string-splicing it into source. - [Axis 1] Near-duplicate build+publish step sequence across release.yml and publish-testpypi.yml must be kept in sync by hand (
.github/workflows/publish-testpypi.yml:48) — The 'Checkout / Set up Python 3.13 / Install uv / uv build / pypa/gh-action-pypi-publish' sequence in publish-testpypi.yml (lines ~48-91) is a near-copy of the corresponding steps in release.yml, including the shared pypa/gh-action-pypi-publish SHA pined0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0that now lives in two files and must be bumped in lockstep. GitHub Actions has limited step reuse, so this is a minor note rather than a defect, but a shared composite action (or at minimum a comment cross-referencing the two pin sites) would reduce the risk of the TestPyPI dry-run drifting from the real publish path it is meant to validate. - [Axis 3] setup-uv is pinned to a SHA in publish-testpypi.yml but floats as @v4 in release.yml (version divergence) (
.github/workflows/publish-testpypi.yml:55) — The dry-run pinsuses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0(publish-testpypi.yml:55) while the real release floatsuses: astral-sh/setup-uv@v4(release.yml:82). Becauseuv buildproduces the artifact in both paths, a future@v4uv release that regresses wheel/sdist metadata would break the real release while the pinned dry-run keeps passing -- the build tool is not held identical across the two paths, weakening the dry-run's value as a build smoke test. Pin both to the same SHA (or float both) so the dry-run reproduces the real build environment. - [Axis 6] New public-PyPI publish extends a non-atomic release: main is tagged/pushed (irreversible) before the wheel reaches PyPI (
.github/workflows/release.yml:198) — The newpublish-pypijob (lines 198-222) hasneeds: release, so it runs only AFTER the release job has already executed the irreversiblegit push origin main "v${{ steps.release.outputs.version }}"(line 123, pushed with the app token that bypasses branch protection) and after the Azure feed + GHCR image publishes. A failure in the newpublish-pypijob therefore leaves main permanently tagged at v with the package on the Azure feed and GHCR but absent from public PyPI — a partial release with no automatic compensation/rollback. The push-before-publish ordering itself pre-exists on origin/main (not in this diff), but this PR adds public PyPI — the most user-visible artifact — as the LAST, separate-job step past that irreversible push, widening the dangling-release window. Total-failure of publish-pypi is recoverable by re-running the failed job (therelease-distartifact is retained), so severity is Low; document the manual recovery (re-runpublish-pypi) or, longer term, move thegit pushof the tag to AFTER a successful publish of all artifacts (or use a staging tag promoted on success). - [Axis 7] Real-PyPI Trusted-Publisher one-time setup is undocumented in release.yml (asymmetric with publish-testpypi.yml) (
.github/workflows/release.yml:220) — The publish-pypi step comment (lines 220-221) only asserts# Trusted Publisher is configured on pypi.org for this repo + / # workflow (release.yml) + environment (pypi); no password needed.— it gives no steps to (re)create that publisher. publish-testpypi.yml documents the full one-time pending-publisher config in its header (lines 12-19: Project name / Owner / Repository / Workflow name / Environment). Mirror that block in release.yml so the higher-stakes real-PyPI publisher can be reconstructed if the repo/env is ever recreated; right now the setup for the TEST instance is documented but the setup for the PRODUCTION instance is not.
What's Missing
Parallel paths:
- 🔵 The TestPyPI dry-run stamps the version with a hand-rolled Python regex over pyproject.toml + init.py (publish-testpypi.yml:67-77), whereas the real release writes the version via semantic-release (release.yml:89-106). So the dry-run validates the build + OIDC publish path but NOT the version-writing mechanism it exists to de-risk — a semantic-release version-write regression would pass the dry-run and only surface at real release time. (trigger: .github/workflows/publish-testpypi.yml)(restates: Axis 3: setup-uv pinned to SHA in publish-testpypi.yml but floats @v4 in release.yml)
- 🔵 The new dry-run job omits the SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS env that release.yml (53-58) and docker-publish.yml (50) both set — release.yml's own comment says it is 'Kept (matching pr-checks.yml) so parity is preserved if the job ever moves back to the self-hosted pool.' The new parallel publish path silently breaks that stated parity convention. (trigger: .github/workflows/publish-testpypi.yml)
Tests:
- 🔵 Nothing asserts the version-stamp substitution actually matched: re.sub(..., count=1) at publish-testpypi.yml:76 returns the text UNCHANGED (no error) if the regex misses, so a future pyproject.toml / init.py format drift would build an un-stamped wheel with no failure signal. Add a post-stamp check that the emitted version equals DEV_VERSION. (trigger: .github/workflows/publish-testpypi.yml)(restates: Axis 1: Brittle dual-templated heredoc splicing ${DEV_VERSION} into Python source)
- 🔵 No mechanical guard ties the OIDC Trusted-Publisher config to reality: the publisher is registered against a specific workflow filename + environment name (release.yml:204-206 -> env 'pypi'; publish-testpypi.yml:39-41 -> env 'testpypi'), but nothing (actionlint / a CEnnn-style check) fails if the workflow file is renamed or the environment key drifts — the publish would silently 403 only at real release time. Genuinely hard to fully statically verify (the publisher lives on pypi.org), so at minimum note this coupling. (trigger: .github/workflows/release.yml)
Daily/nightly:
- 🟡 The PR adds public-PyPI publishing (an irreversible, publicly-visible artifact + a claimed public 'coder-eval' name) but states no cross-repo blast radius: whether the downstream coder-eval-uipath / eval-runner consumers should now install from public PyPI vs. the private Azure Artifacts feed the release job still populates, and what the nightly/pipeline install source becomes. The unstated blast radius on the cross-repo install contract is itself the gap. (trigger: .github/workflows/release.yml)
Harness & Lint Improvements
Static checks (lint / type):
- [ce-lint] Add a CE025 workflow-pinning rule. This requires a small framework extension: the ce-lint runner currently only rglob's
*.pyandast.parses them, so teachcheck_pathsto also load.github/workflows/*.ymlvia pyyaml and dispatch a YAML-node rule pass. CE025 then enforces two invariants over everyuses:step: (a) every third-party action (anything notactions/*first-party, at reviewer discretion) must pin a full 40-char commit SHA — rejectingastral-sh/setup-uv@v4; and (b) any action referenced in more than one place must resolve to a single SHA repo-wide. This runs inmake lint/CI and fails before human review. Prevents: Finding 4 (setup-uv pinned to a SHA in publish-testpypi.yml:55 but floating@v4in release.yml:82 — the@v4float fails invariant (a)) and Finding 3 (the duplicatedpypa/gh-action-pypi-publishSHA pin living in two files that must be bumped in lockstep — a future drift fails invariant (b)). - [ce-lint] In the same CE025/CE026 workflow YAML pass, add a rule requiring every step whose
uses:ispypa/gh-action-pypi-publishto explicitly set awith.skip-existingkey (any value). Forcing the key to be present makes the author consciously choose idempotent-vs-strict upload behavior for each publish step rather than silently inheriting the non-idempotent default. Prevents: Finding 5 (release.yml:217-222Publish to PyPIomitsskip-existing, leaving a partially-failed upload of version X permanently unrecoverable, while the sibling TestPyPI step deliberately setsskip-existing: true). The rule surfaces the omission at lint time.
Harness improvements (not statically reachable):
- Add
actionlint(which bundles shellcheck forrun:blocks) tomake lint/ CI so the GitHub Actions workflows and their embedded shell/heredoc scripts are linted alongside the Python tree. Why not static: The repo's four static gates (ce-lint AST over*.py, ruff, pyright, bandit) are Python-only and never parse.github/workflows/*.ymlor the bash/Python heredocs embedded inrun:steps, so none can see a shell variable being spliced into script source. actionlint+shellcheck is a shell-aware analyzer the repo does not currently run. Prevents: Finding 2 (publish-testpypi.yml:67-76 opens an unquotedpython3 - <<PYheredoc and interpolates the shell${DEV_VERSION}directly into the generated Pythonf'{key} = "${DEV_VERSION}"'— shellcheck-class heredoc/quoting analysis flags this dual-templating splice). - Consider adding a dedicated GitHub Actions auditor (e.g.
zizmor) to CI for broader supply-chain hardening beyond the two CE025 invariants — itsunpinned-usescheck independently corroborates the SHA-pinning rule and it audits workflow-permission / injection surfaces the custom CE rule does not cover. Why not static: No workflow-aware static tool exists in the repo's gates today; this is a new CI dependency rather than a tweak to an existing check, and it targets YAML the Python-only gates cannot read. Listed as optional to avoid redundancy with CE025's pinning invariant. Prevents: Reinforces Finding 4 (unpinnedastral-sh/setup-uv@v4) and provides defense-in-depth for future workflow-security regressions the point fixes do not address.
Top 5 Priority Actions
- Add
skip-existing: trueto thePublish to PyPIstep (release.yml:217) so a mid-batch upload failure is recoverable via job re-run instead of leaving that semantic-release-bumped version half-published and permanently stuck — highest-impact resilience fix. - Make the release atomic-ish by moving the irreversible
git push origin main/tag (release.yml:123) to after all artifact publishes succeed, or at minimum document that recovery from a failedpublish-pypijob (release.yml:198) is a re-run of that job, closing the dangling-release window this PR widens by adding public PyPI as the last step. - Pin
astral-sh/setup-uvto the same SHA in both paths — the dry-run pins@…af3a # v4.2.0(publish-testpypi.yml:55) while the real release floats@v4(release.yml:82) — so the TestPyPI dry-run actually reproduces the real build environment it exists to validate. - Update the release.yml header comment (line 4) to add the new public-PyPI (OIDC Trusted Publishing) target so operators can gauge a release's true blast radius.
- Mirror publish-testpypi.yml's one-time Trusted-Publisher setup block into release.yml (near line 220) and de-duplicate the copy-pasted per-step guards / shared pypa-publish SHA pin so the higher-stakes production publisher is documented and the two workflows can't silently drift.
Stats: 0 🔴 · 0 🟠 · 2 🟡 · 6 🔵 across 8 axes reviewed.
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:6
Scope: pr:6 · branch ci/pypi-publishing · 0b19b26 · 2026-07-09T09:44Z · workflow variant
Change class: complex — introduces a new public-PyPI OIDC Trusted-Publishing path (new gated publish-pypi job + TestPyPI dry-run workflow with a regex version-stamping script) and moves the release job off the self-hosted runner; correctness/security requires reasoning about permissions, OIDC scoping, and supply-chain pinning
coder_eval's application code is exemplary (perfect Type Safety and Architecture, 9.8/10 overall) and every confirmed finding is confined to the newly-added release/PyPI CI workflows — a widened supply-chain blast radius plus a handful of publish-path fidelity gaps — so the bottom line is a healthy codebase whose only real risks live in the tag-pinned, non-atomic release pipeline that now reaches public PyPI.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.9 / 10 | 0 | 0 | 0 | 1 | SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS is dead/no-op config after the move to ubuntu-latest |
| 2. Type Safety | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 3. Test Health | 9.9 / 10 | 0 | 0 | 0 | 1 | TestPyPI dry-run diverges from the real publish path (e.g. setup-uv pin), so a green dry-run does not faithfully rehearse / de-risk the real PyPI publish |
| 4. Security | 9.4 / 10 | 0 | 0 | 1 | 1 | Public-PyPI wheel is built in the release job through a tag-pinned (mutable) setup-uv and docker actions — widened supply-chain blast radius |
| 5. Architecture & Design | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 6. Error Handling & Resilience | 9.4 / 10 | 0 | 0 | 1 | 1 | TestPyPI version-stamp uses re.sub (not re.subn+count check), so a version-line format drift silently no-ops under set -euo pipefail (publish-testpypi.yml:76; set -euo is line 63, not 62) |
| 7. API Surface & Maintainability | 9.9 / 10 | 0 | 0 | 0 | 1 | release.yml top-of-file header is stale after adding the public-PyPI publish path (still lists only Azure feed + GHCR) |
| 8. Evaluation Harness Quality | 9.9 / 10 | 0 | 0 | 0 | 1 | Public-PyPI publish is coupled into the internal multi-registry release job (needs: release), making the release non-atomic and entangled with Azure/GHCR steps |
Overall Score: 9.8 / 10 · Weakest Axis: Security at 9.4 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 2 · 🔵 6 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
- [Axis 4] Public-PyPI wheel is built in the release job through a tag-pinned (mutable) setup-uv and docker actions — widened supply-chain blast radius (
.github/workflows/release.yml:82) — The PR correctly SHA-pins every NEW action, but the wheel/sdist that is now published to PUBLIC PyPI is built byuv buildinside thereleasejob, which installs uv via an unpinned mutable tag: line 82uses: astral-sh/setup-uv@v4(contrast publish-testpypi.yml:55 which pins the SAME action toastral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a). The docker steps are likewise tag-pinned: line 164uses: docker/setup-buildx-action@v3, line 168uses: docker/login-action@v3, line 176uses: docker/build-push-action@v6. Before this PR a compromise ofsetup-uv@v4(repointed tag / upstream account takeover) could only poison the private Azure feed + GHCR; this PR uploads that samedist/asrelease-dist(line 134) and publishes it to public PyPI (publish-pypi job), so a tampered build now reaches everypip install coder-evaluser — and the same job holdsRELEASE_APP_PRIVATE_KEY(line 66, bypasses main branch protection) and the Azure PAT. Pin all four to full 40-char commit SHAs (matching the file's own convention# vX.Y.Zcomment) to close the widened blast radius. CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:N - [Axis 6] TestPyPI version-stamp uses re.sub (not re.subn+count check), so a version-line format drift silently no-ops under set -euo pipefail (publish-testpypi.yml:76; set -euo is line 63, not 62) (
.github/workflows/publish-testpypi.yml:76) — Line 76 stamps the dev version withp.write_text(re.sub(pat, f'{key} = "${DEV_VERSION}"', p.read_text(), count=1)).re.subreturns the input unchanged (NO exception) whenpatdoes not match, soset -euo pipefailat line 62 cannot catch a missed stamp. I verified empirically: on the current double-quoted lineversion = "0.8.2"the substitution count is 1 (works today), but on any format drift (e.g. single quotesversion = '0.8.2', or a move to a dynamic/attr version) the count is 0 and the text is returned untouched with rc=0. A silent miss means the wheel/sdist carries the BASE version instead of<base>.dev<run#>, which then collides with an already-uploaded file on TestPyPI — a collision masked byskip-existing: true(line 92) — so the whole point of the dry run (validating the dev-version + Trusted-Publishing path) passes green while having uploaded nothing / the wrong version. Fix: usere.subn(...)and fail loud when the count is 0 for either file, e.g.new, n = re.subn(pat, ...); assert n == 1, f'version pattern did not match {path}'; p.write_text(new)so a format drift aborts the step instead of publishing a stale/colliding artifact.
Nits
- [Axis 1] SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS is dead/no-op config after the move to ubuntu-latest (
.github/workflows/release.yml:58) — Line 58SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS: "openai-codex-cli-bin,openai-codex"is, per its own comment (lines 54-57:... on GitHub-hosted runners this is a no-op. Kept (matching pr-checks.yml) so parity is preserved if the job ever moves back to the self-hosted pool.), a no-op now thatruns-on: ubuntu-latest. It is retained purely as a speculative hedge against a hypothetical future move back touipath-ubuntu-latest(YAGNI). It is dead config on the job today; consider removing it (git history + pr-checks.yml preserve the pattern) or leaving only if the parity hedge is genuinely load-bearing. Informational nit — the comment already prevents it from misleading. - [Axis 3] TestPyPI dry-run diverges from the real publish path (e.g. setup-uv pin), so a green dry-run does not faithfully rehearse / de-risk the real PyPI publish (
.github/workflows/publish-testpypi.yml:92) — publish-testpypi.yml is the pre-release test harness for release.yml'spublish-pypijob, but three divergences mean a green dry-run does not exercise the exact real path: (1)skip-existing: true(publish-testpypi.yml:92,# A re-run at the same dev version is a no-op rather than a hard fail.) is ABSENT from the real publish-pypi step (release.yml:222 has onlypackages-dir: dist/); if the inline regex stamp ever silently no-ops (leavingversion = "0.8.2") and that base version is already on TestPyPI, the upload is silently skipped and the job still reports green — a false pass that proves no publish occurred, whereas the real path would hard-fail on the duplicate. (2) setup-uv is SHA-pinned toastral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0(publish-testpypi.yml:55) while the release build job floatsastral-sh/setup-uv@v4(release.yml:82), so the dry-run builds with a different, pinned-older uv than the real build. (3) The dry-run builds and publishes in a single job (uv buildat :81 → publish at :84), while the real path builds inrelease, hands off viaactions/upload-artifact(release.yml:134) and publishes from a separatepublish-pypijob viaactions/download-artifact(release.yml:212) — so the artifact upload/download handoff the real publish depends on is never exercised by the dry-run. Severity is Low because the OIDC-critical publish step is identical (samepypa/gh-action-pypi-publish@ed0c... # v1.13.0SHA and trusted-publishing shape at publish-testpypi.yml:84 and release.yml:218) and the failure mode is visible and recoverable (tag/Azure feed/docker all succeed first; only the public-PyPI job fails). To close the gap: drop or justifyskip-existing, add a post-publish assertion that a file was uploaded, align the setup-uv ref with the release job, and/or split the dry-run into a build job + download-artifact publish job mirroring release.yml. - [Axis 4] TestPyPI version-stamp uses an unquoted heredoc with a shell-expanded value inlined into the Python source (
.github/workflows/publish-testpypi.yml:67) — Line 67 opens the heredoc with an UNQUOTED delimiterpython3 - <<PY, so the shell expands${DEV_VERSION}into the python source text before python runs (line 76:p.write_text(re.sub(pat, f'{key} = "${DEV_VERSION}"', p.read_text(), count=1))).DEV_VERSION="${CURRENT_VERSION}.dev${RUN_NUMBER}"(line 66) is built fromgithub.run_number(integer, passed safely via the RUN_NUMBER env var — good) and CURRENT_VERSION read from pyproject.toml. run_number is not attacker-controlled, so this is not exploitable in the normal flow, but the pattern is fragile: a"or newline in the pyproject version string would break out of the f-string and inject arbitrary python. Harden by quoting the delimiter (<<'PY') and reading the value inside python viaos.environ["DEV_VERSION"]instead of shell-expanding it into the source, so no repo-content value is ever spliced into executable text. CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:N - [Axis 6] Real publish-pypi job omits skip-existing (present on the TestPyPI sibling), making a partial-upload release version unrecoverable via re-run (
.github/workflows/release.yml:217) — ThePublish to PyPIstep (lines 217-222) passes onlypackages-dir: dist/with noskip-existing, whereas the parallel dry-run job setsskip-existing: true(publish-testpypi.yml line 92). gh-action-pypi-publish uploads the wheel and sdist as separate files; if one uploads and the other fails transiently (network/PyPI blip), a GitHub 're-run failed jobs' re-executes this step and twine hard-fails with 400 (file already exists) on the already-uploaded file — so that release version can never be completed on public PyPI without cutting a brand-new version bump. The normal (nothing-uploaded) failure re-runs fine, so this only bites on a partial upload, but the inconsistency with the TestPyPI job is real. Noteskip-existing: truedoes NOT overwrite an existing file (it skips it), so it preserves the 'never silently overwrite a real release' intent while making partial-upload retries recoverable; adding it here is the safe, consistent choice. If the hard-fail-on-any-existing behavior is intentional, add a comment stating why the two publish jobs diverge. - [Axis 7] release.yml top-of-file header is stale after adding the public-PyPI publish path (still lists only Azure feed + GHCR) (
.github/workflows/release.yml:4) — This PR adds a newpublish-pypijob (lines 193-222) that publishes the wheel+sdist to public PyPI, but the workflow's own summary header at lines 3-4 still reads# Manually-triggered release: bump the version, tag, build, and publish to the/# UiPath \coder_eval` Azure Artifacts feed (wheel) and GHCR (agent image).— it never mentions public PyPI, so a maintainer reading the file header gets an incomplete picture of where a release now goes. Separately, the new job documents its trusted-publisher requirement only as a 2-line inline comment (lines 220-221:# Trusted Publisher is configured on pypi.org for this repo +/# workflow (release.yml) + environment (pypi); no password needed.), whereas the sibling publish-testpypi.yml carries a full one-time-setup block enumerating Project name / Owner / Repository / Workflow name / Environment. The higher-stakes production path is thus less documented than the dry run. Fix: addpublic PyPI (pypi.org, OIDC Trusted Publishing)to the line-3/4 header summary, and add an operator setup block for the real publisher (Project name: coder-eval, Owner: UiPath, Repository: coder_eval, Workflow name: release.yml, Environment: pypi) mirroring publish-testpypi.yml. Note: the setup values that ARE stated (workflow release.yml, environment pypi) correctly match the workflow'senvironment.name: pypi` and filename, so this is a completeness gap, not a mismatch. - [Axis 8] Public-PyPI publish is coupled into the internal multi-registry release job (needs: release), making the release non-atomic and entangled with Azure/GHCR steps (
.github/workflows/release.yml:198) —publish-pypi(line 198) runs as a separateneeds: releasejob that fires only AFTER thereleasejob has already pushed the tag, uploaded the wheel to the Azurecoder_evalfeed (line 140Publish to coder_eval feed), and pushed the versioned GHCR image (line 174Build and push versioned agent image). Ifpublish-pypi'sPublish to PyPIstep (line 217) then fails (OIDC/Trusted-Publisher misconfig, PyPI outage), the release is partial:v<version>exists on the Azure feed and GHCR but not on public PyPI, with no rollback (Azure/GHCR are already published and PyPI artifacts cannot be recalled). This is inherent to adding a second public registry and is recoverable by re-running only the failedpublish-pypijob (the built dist is retained as therelease-distartifact, so the same wheel republishes without a rebuild), which keeps severity Low. To reduce the window, consider gating the Azure feed publish behind the same environment/OIDC readiness as PyPI, or documenting the 're-run publish-pypi job' recovery step in the workflow header. Note also that unlike TestPyPI (skip-existing: true), the real PyPI step has noskip-existing, so a re-run after a partial upload that actually reached PyPI will surface a 400 'file already exists' — acceptable/intended for the real index but worth being aware of during recovery.
What's Missing
Parallel paths:
- 🟡 publish-testpypi.yml SHA-pins astral-sh/setup-uv (@38f3f10... v4.2.0) but the release.yml build step that produces the wheel actually shipped to public PyPI floats astral-sh/setup-uv@v4 (line 82) — the two supposedly-parallel install paths diverge, and the mutable one is on the production side. (trigger: .github/workflows/release.yml)(restates: Axis 4: tag-pinned setup-uv/docker widens supply-chain blast radius)
- 🔵 The dry-run builds and publishes in one job, while the real path splits build (release job, upload-artifact release-dist at line 134) from publish (publish-pypi job, download-artifact at line 212) — the artifact upload/download handoff the real publish depends on is a parallel path the TestPyPI rehearsal never exercises. (trigger: .github/workflows/publish-testpypi.yml)(restates: Axis 3: TestPyPI dry-run diverges from the real publish path)
Tests:
- 🔵 Both new/changed production workflows ship with no actionlint (or any YAML/shell) CI gate — the heredoc python, the regex version-stamp, and the OIDC job wiring are un-linted, so malformed workflow syntax or shell issues surface only at dispatch time against a real index; an actionlint job in make verify/CI would push this class of defect left. (trigger: .github/workflows/publish-testpypi.yml)
- 🟡 No guard asserts the dev-version stamp regex actually matches the current pyproject.toml / init.py line format; a quote/format drift makes re.sub a silent no-op (rc=0) under set -euo pipefail, so a green dry-run can prove no correct publish occurred — a re.subn+count check is the missing mechanical test. (trigger: .github/workflows/publish-testpypi.yml)(restates: Axis 6: re.sub version-stamp silently no-ops on format drift)
Display & mapping dicts:
- 🔵 release.yml's top-of-file summary header (lines 3-4) still enumerates only the Azure feed + GHCR as publish targets and was not extended when the new public-PyPI target was added — the 'what this workflow publishes' map falls out of date. (trigger: .github/workflows/release.yml)(restates: Axis 7: release.yml header stale after adding public-PyPI path)
- 🔵 The publish-pypi job carries only a 2-line inline trusted-publisher comment, while the sibling publish-testpypi.yml carries a full one-time pending-publisher setup block (Project/Owner/Repository/Workflow/Environment); the operator setup 'map' was not extended for the higher-stakes production publisher. (trigger: .github/workflows/release.yml)(restates: Axis 7: release.yml header stale after adding public-PyPI path)
Daily/nightly:
- 🔵 The PR does not state the cross-repo/distribution blast radius: coder-eval now resolves from public PyPI for any consumer installing off the default index (e.g. docs/tutorials/02-ci-pipeline.md 'uv pip install --system coder-eval', plus external eval-runner / coder-eval-uipath pipelines). Publishing the same version to BOTH the private Azure feed and public PyPI makes index precedence/coexistence load-bearing and unstated. (trigger: .github/workflows/release.yml)
- 🔵 The release path is now non-atomic across three registries (Azure feed + GHCR + public PyPI): a publish-pypi failure leaves v live on Azure/GHCR but absent from PyPI with no rollback — a production-release blast radius introduced by adding the second public registry, recoverable only by re-running the failed job. (trigger: .github/workflows/release.yml)(restates: Axis 8: PyPI publish coupled into the internal multi-registry release job)
Harness & Lint Improvements
Static checks (lint / type):
- [bandit-codeql] Wire a GitHub Actions security linter into CI/
make lint— CodeQL's Actions query pack (actions/security-and-quality, which ships an unpinned-tag query) orzizmor(itsunpinned-usesaudit) — to fail the build on anyuses:that references a mutable tag (@vN/ branch) instead of a full 40-char commit SHA. This is the load-bearing gate: it turns 'SHA-pin every action' from a reviewer's memory into a mechanical check that fires before merge. Prevents: MEDIUM finding 4 (release.yml:82 setup-uv@v4, :164/:168/:176 docker actions on the now-public-PyPI build path) and the setup-uv-pin sub-point of finding 2 (publish-testpypi.yml:55 pins the identical action, release.yml:82 floats it). - [bandit-codeql] Enable the template/expression-injection audit in the same Actions linter (zizmor
template-injection, or CodeQL's Actions injection queries) to flagrun:scripts that splice untrusted or repo-content values into an interpreter — including an unquoted heredoc delimiter (<<PY) whose body inlines a shell${VAR}into inline Python. The safe form is a quoted delimiter (<<'PY') + reading viaos.environ. Prevents: LOW security finding 5 (publish-testpypi.yml:67 unquotedpython3 - <<PYinlining${DEV_VERSION}into the Python source at :76). - [ce-lint] New CE025 'workflow actions must be SHA-pinned' as an in-repo alternative/complement to the CodeQL route: extend
tests/lint/runner.py(currently Python-AST-only —check_pathsrglobs*.py,check_filerunsast.parse) to also feed.github/workflows/*.ymlto a new text/regex rule family, then have CE025 flag anyuses: …@<ref>where<ref>is not 40 hex chars. Slots intotests/lint/rules/ce025_workflow_actions_sha_pinned.py+ALL_RULESintests/lint/runner.py(next free CE number after CE024). Note the framework extension is real work — the runner has no YAML/text-rule path today. Prevents: Same class as the two bandit-codeql items — finding 4 (release.yml:82/164/168/176) and finding 2's unpinned-setup-uv divergence — kept entirely insidemake verifyif a third-party actions linter isn't desired.
Harness improvements (not statically reachable):
- Collapse the TestPyPI dry-run and the real publish onto ONE shared build+publish seam so drift is impossible by construction: extract the
uv buildstep and thepypa/gh-action-pypi-publishstep into a reusable workflow (.github/workflows/_build-and-publish.yml) or composite action that bothrelease.yml(publish-pypi) andpublish-testpypi.ymlcall with only the index URL /skip-existingdiffering. This makes the dry-run rehearse the exact real path — same setup-uv ref, same build→upload-artifact→download-artifact→publish handoff — instead of a single-job shortcut. Why not static: A static check can grep for an unpinned tag but cannot verify that two separate workflows exercise the same runtime path (artifact handoff, job graph, publish shape) — that's execution-semantics parity, not a textual pattern. Prevents: Finding 2 (dry-run diverges from real publish: skip-existing, setup-uv pin, single-job vs build→artifact→publish) and finding 8 (publish-pypi coupled into the internal release job) — the reusable seam is the single source of truth for the OIDC publish. - Add a post-stamp verification step to publish-testpypi.yml (and any real path that stamps a version): after rewriting pyproject.toml/init.py, assert the substitution actually occurred — e.g.
re.subn(pat, …, count=1)returningn == 1per file, or a grep-back that the file now contains<base>.dev<run#>— and fail the job loudly if not, instead ofre.subsilently returning the text unchanged underset -euo pipefail. Why not static: Whether the version-line regex matches depends on the runtime content of pyproject.toml (format drift like single-quotes or a dynamic version), so the miss is only observable at execution time; a lint rule broadly banningre.subin stamping scripts would be too false-positive-prone to be the gate. Prevents: MEDIUM finding 6 (re.sub no-ops on version-line format drift, masked green by skip-existing) and, indirectly, the skip-existing masking called out in findings 2 and 7. - Reconcile the two publish steps'
skip-existingbehavior deliberately rather than by accident: either addskip-existing: trueto release.yml:217 (so a partial wheel/sdist upload is re-runnable) or add an inline comment on both steps stating why the real index intentionally hard-fails on duplicates while the dry run skips — and, if kept divergent, add a one-line release-recovery note ('re-run only the publish-pypi job; the built dist is retained as the release-dist artifact') to the workflow header. Why not static: This is a semantic parity + operability decision between two jobs and a documentation choice; there is no textual pattern that distinguishes an intentional divergence from a forgotten one. Prevents: LOW finding 7 (real publish-pypi omits skip-existing → partial-upload release unrecoverable via re-run) and the operability half of finding 9 (non-atomic release recovery).
Top 5 Priority Actions
- Pin the four mutable tag-referenced actions in release.yml to full 40-char commit SHAs —
astral-sh/setup-uv@v4(release.yml:82),docker/setup-buildx-action@v3(:164),docker/login-action@v3(:168),docker/build-push-action@v6(:176) — since this job now builds the wheel published to public PyPI while holding RELEASE_APP_PRIVATE_KEY and the Azure PAT, giving a repointed upstream tag a path to everypip install coder-evaluser. - Replace
re.sub(..., count=1)withre.subn(...)plus anassert n == 1per file at publish-testpypi.yml:76 so a version-line format drift aborts loudly instead of silently shipping a stale/colliding base-version artifact thatskip-existingthen masks as a green dry-run. - Quote the heredoc delimiter (
<<'PY') at publish-testpypi.yml:67 and read the value viaos.environ["DEV_VERSION"]instead of shell-splicing${DEV_VERSION}into the Python source, removing the injection-prone pattern where a stray quote/newline in the version string could execute arbitrary code. - Add
skip-existing: true(or a comment justifying its absence) to the realPublish to PyPIstep at release.yml:217 to match the TestPyPI sibling, so a partial wheel/sdist upload can be recovered by a job re-run rather than forcing a brand-new version bump. - Close the remaining release-fidelity/documentation gaps — align the dry-run's
setup-uvpin and build-then-publish artifact handoff with the real path (publish-testpypi.yml:55/81-84), and refresh the stale release.yml header (lines 3-4) plus add a full trusted-publisher setup block for the public-PyPI job (release.yml:193-222).
Stats: 0 🔴 · 0 🟠 · 2 🟡 · 6 🔵 across 8 axes reviewed.
…stamp
Two medium findings from the PR review:
- [security] The wheel/sdist now published to PUBLIC PyPI is built in the
release job, which pulled actions via mutable tags (setup-uv@v4,
docker/{setup-buildx,login,build-push}@vn). A repointed tag / upstream
takeover would reach every `pip install coder-eval` user, in a job that
also holds RELEASE_APP_PRIVATE_KEY and the Azure PAT. Pin all four to
full commit SHAs (setup-uv now matches publish-testpypi.yml).
- [resilience] The TestPyPI dev-version stamp used re.sub, which returns
the text unchanged (rc=0) on a version-line format drift, silently
publishing the base version -> TestPyPI collision masked by
skip-existing, so the dry run passes green having validated nothing.
Switch to re.subn and SystemExit when the match count != 1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Uh oh!
There was an error while loading. Please reload this page.
Follow-up to 27fe263's mechanical `runs-on:` swap, fixing what a multi-model review of it found. Correctness: - `SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS` was read from a secret that does not exist at repo or org level (verified via `gh secret list` and `actions/organization-secrets`), so all six `${{ secrets.… }}` references resolved to an empty exclusion list. Every site now uses `secrets.… || 'openai-codex-cli-bin,openai-codex'` — literal as the operative value, secret as a future override — and pr-checks.yml declares it once at the workflow level instead of six per-job copies, which is how several installing jobs had ended up with no exclusions at all. - docs.yml had no `timeout-minutes` (the only such job in the repo) above an unattended `git push --force`, so a hang would hold a shared-pool slot for GitHub's 6h default. Security: - `quality-gate`, `no-uipath-extra` and `evalboard` fall back to stock `ubuntu-latest` for fork PRs. All three execute the PR's own lockfile install hooks and test files, and the repo is public, so untrusted code no longer runs on the shared pool image. Same-repo PRs, push and merge_group use the pool. Consumer contract: - `action-dogfood` stays on `ubuntu-latest`. It is the executable proof behind the published Marketplace Action, and docs/tutorials/02-ci-pipeline.md — the one consumer snippet naming a runner — says `ubuntu-latest`; nothing else in CI exercises that image. - docker-publish.yml sets no safe-chain env: nothing installs on the host there, and the Docker build does not inherit job env. A value would be dead config. Prose: - Three release.yml comments asserted the release jobs were GitHub-hosted specifically to avoid this pool, directly above the `runs-on:` lines 27fe263 rewrote. Corrected, and the comment now records that this KNOWINGLY reverses PR #6, which had moved the release job off the pool because "cutting a release was blocked whenever that pool was unavailable". - CONTRIBUTING gains a short § CI runners; those false comments were previously the repo's only prose about the pool. Also: `.github/actionlint.yaml` declares the three custom labels (advisory — nothing in `make verify` runs actionlint); converge docs.yml's lone `actions/checkout` pin (v4.4.0/node20) and pr-checks.yml's outlier `actions/setup-python` pin (v5.6.0) onto the repo-standard SHAs; pin the live-test pytest steps to `-n 4` so real-API concurrency stops varying with the runner's vCPU count. A runner-label registry lint rule was written for this and reverted as out of proportion to the change; it is recorded in .claude/harness-candidates.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to 27fe263's mechanical `runs-on:` swap, fixing what a multi-model review of it found. Correctness: - `SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS` was read from a secret that does not exist at repo or org level (verified via `gh secret list` and `actions/organization-secrets`), so all six `${{ secrets.… }}` references resolved to an empty exclusion list. Every site now uses `secrets.… || 'openai-codex-cli-bin,openai-codex'` — literal as the operative value, secret as a future override — and pr-checks.yml declares it once at the workflow level instead of six per-job copies, which is how several installing jobs had ended up with no exclusions at all. - docs.yml had no `timeout-minutes` (the only such job in the repo) above an unattended `git push --force`, so a hang would hold a shared-pool slot for GitHub's 6h default. Security: - `quality-gate`, `no-uipath-extra` and `evalboard` fall back to stock `ubuntu-latest` for fork PRs. All three execute the PR's own lockfile install hooks and test files, and the repo is public, so untrusted code no longer runs on the shared pool image. Same-repo PRs, push and merge_group use the pool. Consumer contract: - `action-dogfood` stays on `ubuntu-latest`. It is the executable proof behind the published Marketplace Action, and docs/tutorials/02-ci-pipeline.md — the one consumer snippet naming a runner — says `ubuntu-latest`; nothing else in CI exercises that image. - docker-publish.yml sets no safe-chain env: nothing installs on the host there, and the Docker build does not inherit job env. A value would be dead config. Prose: - Three release.yml comments asserted the release jobs were GitHub-hosted specifically to avoid this pool, directly above the `runs-on:` lines 27fe263 rewrote. Corrected, and the comment now records that this KNOWINGLY reverses PR #6, which had moved the release job off the pool because "cutting a release was blocked whenever that pool was unavailable". - CONTRIBUTING gains a short § CI runners; those false comments were previously the repo's only prose about the pool. Also: `.github/actionlint.yaml` declares the three custom labels (advisory — nothing in `make verify` runs actionlint); converge docs.yml's lone `actions/checkout` pin (v4.4.0/node20) and pr-checks.yml's outlier `actions/setup-python` pin (v5.6.0) onto the repo-standard SHAs; pin the live-test pytest steps to `-n 4` so real-API concurrency stops varying with the runner's vCPU count. A runner-label registry lint rule was written for this and reverted as out of proportion to the change; it is recorded in .claude/harness-candidates.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: change to centralized managed GitHub pool 8 workflow file(s) modified, 4 action(s) pinned Runners migrated: ubuntu-latest ubuntu-24.04 ubuntu-22.04 ubuntu-24.04-arm ubuntu-22.04-arm ubuntu-slim ubuntu-18.04 ubuntu-20.04 windows-latest Co-Authored-By: Claude <noreply@anthropic.com> * fix(ci): correct defects in the uipath runner migration Follow-up to 27fe263's mechanical `runs-on:` swap, fixing what a multi-model review of it found. Correctness: - `SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS` was read from a secret that does not exist at repo or org level (verified via `gh secret list` and `actions/organization-secrets`), so all six `${{ secrets.… }}` references resolved to an empty exclusion list. Every site now uses `secrets.… || 'openai-codex-cli-bin,openai-codex'` — literal as the operative value, secret as a future override — and pr-checks.yml declares it once at the workflow level instead of six per-job copies, which is how several installing jobs had ended up with no exclusions at all. - docs.yml had no `timeout-minutes` (the only such job in the repo) above an unattended `git push --force`, so a hang would hold a shared-pool slot for GitHub's 6h default. Security: - `quality-gate`, `no-uipath-extra` and `evalboard` fall back to stock `ubuntu-latest` for fork PRs. All three execute the PR's own lockfile install hooks and test files, and the repo is public, so untrusted code no longer runs on the shared pool image. Same-repo PRs, push and merge_group use the pool. Consumer contract: - `action-dogfood` stays on `ubuntu-latest`. It is the executable proof behind the published Marketplace Action, and docs/tutorials/02-ci-pipeline.md — the one consumer snippet naming a runner — says `ubuntu-latest`; nothing else in CI exercises that image. - docker-publish.yml sets no safe-chain env: nothing installs on the host there, and the Docker build does not inherit job env. A value would be dead config. Prose: - Three release.yml comments asserted the release jobs were GitHub-hosted specifically to avoid this pool, directly above the `runs-on:` lines 27fe263 rewrote. Corrected, and the comment now records that this KNOWINGLY reverses PR #6, which had moved the release job off the pool because "cutting a release was blocked whenever that pool was unavailable". - CONTRIBUTING gains a short § CI runners; those false comments were previously the repo's only prose about the pool. Also: `.github/actionlint.yaml` declares the three custom labels (advisory — nothing in `make verify` runs actionlint); converge docs.yml's lone `actions/checkout` pin (v4.4.0/node20) and pr-checks.yml's outlier `actions/setup-python` pin (v5.6.0) onto the repo-standard SHAs; pin the live-test pytest steps to `-n 4` so real-API concurrency stops varying with the runner's vCPU count. A runner-label registry lint rule was written for this and reverted as out of proportion to the change; it is recorded in .claude/harness-candidates.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: uipreliga <tomasz.religa@uipath.com>

What
Adds public PyPI publishing alongside the existing private Azure Artifacts feed, using OIDC Trusted Publishing (no long-lived token stored).
release.yml(edited)releasejob'sversionas a job output.release-distartifact.publish-pypijob:needs: release, only runs when a version was cut, on GitHub-hostedubuntu-latest, withid-token: writescoped to just this job. Downloads the same artifact and publishes to pypi.org.publish-testpypi.yml(new).dev<run#>version so repeated runs never collide; does not bump the version, tag, or touchmain/ the Azure feed.Security
UiPath/coder_eval+ workflow filename + environment (pypi/testpypi).id-token: writeis isolated to the dedicated publish jobs (least privilege), not the broad release job.github-actionsecosystem keeps them current.Required setup before this can publish
release.yml, environmentpypi✅ (configured)pypi✅ (created)publish-testpypi.yml, environmenttestpypi— needed only if using the TestPyPI dry run.Testing
End-to-end OIDC can only be exercised once merged to
main:patch/minor/major.🤖 Generated with Claude Code