Skip to content

refactor(action)!: eight inputs, none of them a CLI flag, plus an unlisted evalboard source for ad-hoc GH runs - #147

Draft
bai-uipath wants to merge 7 commits into
mainfrom
bai/action-inputs-and-gha-source
Draft

refactor(action)!: eight inputs, none of them a CLI flag, plus an unlisted evalboard source for ad-hoc GH runs#147
bai-uipath wants to merge 7 commits into
mainfrom
bai/action-inputs-and-gha-source

Conversation

@bai-uipath

@bai-uipath bai-uipath commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Upstream half of giving UiPath/skills' run-coder-eval dispatch a real evalboard link while moving it onto the published composite action. Along the way the action's input surface got rewritten rather than extended. The skills-side PR consumes it and is blocked on this one being merged and released, since uses: UiPath/coder_eval@v0 reads its inputs from the action at that tag.

Why the input surface changed instead of growing

The action had no real consumers, only this repo's own action-dogfood job and the synthetic e2e in verify-published-action.yml, so a bad release surfaced in a self-test rather than in someone's workflow. Making run-coder-eval a real dependent meant adding inputs for a suite in a subdirectory, an agent extra, and a plugin inside the environment the CLI runs from. Five new inputs took the count to fifteen, and at that point the shape of the thing was the problem, not the gaps.

coder-eval run has 21 flags. The action promoted five of them to named inputs with no principle behind the choice: --tags got one, its sibling --exclude-tags did not, and extra-args's own description admitted it covered "--tags exclusions". A forwarding input buys nothing and costs a lot, because GitHub silently ignores an input the referenced tag does not define. One that is mistyped, or newer than the consumer's pin, yields a run that measured something else and still exits 0. A wrong CLI flag is a hard error instead.

The surface: 15 inputs to 8, none of them a CLI flag

An input now exists only where the action does something with the value besides pass it along.

Kept Because
version, extras, extra-packages, install-flags compose the install spec, which the action owns
working-directory applied to the action's own steps; illegal on a uses: step, and job-level defaults.run does not reach into a composite
env exported into the action's own shell, so a forwarded secret never touches $GITHUB_ENV
args the single passthrough
run-dir read back for the outputs
Removed Where it went
tasks, tags, model, extra-args all fold into args
prerelease generalises to install-flags, which also covers --extra-index-url for a private index
junit-path derived as <run-dir>/junit.xml, which is where both existing consumers already put it
step-summary replaced by a run-md-path output. A consumer that has to redact the report first cannot undo a write that already happened, so the write is the consumer's call. cat "$RUN_MD" >> "$GITHUB_STEP_SUMMARY" is the whole of the default this replaces
minimum-task-score deleted, not relocated — see below

Four different encodings (whitespace-split, line-delimited, comma-separated, NAME=VALUE) collapse to one clean_lines parser, copied into both step scripts because they are separate bash processes, with a test asserting the copies stay byte-identical.

Folding task globs into args removes a hazard

They now reach the CLI unexpanded and expand_task_files handles them. Verified directly:

recursive glob : 48 files      # **/*.yaml — no globstar needed
flat glob      : 21 files
non-matching   : exits 1       # "No task files found!"

So three documented "sharp edges" are deleted rather than reworded: ** silently degrading with globstar off (which was silently dropping every top-level task), a non-matching glob reaching the CLI as a literal path, and the need for an explicit per-depth ladder.

The other correctness win is the one that motivated args in the first place, reproduced with one touch:

$ touch 'sandbox.docker.env_passthrough_extra=A'
# whitespace-split (the old extra-args):
  [-D] [sandbox.docker.env_passthrough_extra=A]
# args:
  [-D] [sandbox.docker.env_passthrough_extra=[AUTH_TOKEN,BASE_URL]]

[...] is a bash character class. Silent, and it changes what the run measures. That path no longer exists.

The one capability this removes

minimum-task-score and its ~50 lines of embedded Python are gone with no replacement. Both call sites set it to "0.0", a no-op, so it had no real user. It is also in the wrong place: a score floor is policy over run.json, so it belongs in the CLI as --min-task-score, where it is unit-testable and reachable from the ADO pipelines and the nightly VM, which cannot use a GitHub action at all and reimplement their own gating today. Small follow-up, deliberately not in this PR.

Evalboard side

A third source, gha -> container runs-gha, registered and deliberately unlisted: no tab, no listing page, no aggregate view. Registration and surfacing turn out to be independent (NAV is a hardcoded array that does not iterate SOURCES), and app/runs/[id] reads by id on demand, so a direct link resolves with no listing in existence. That is what lets this skip the one piece of evalboard work with a documented footgun.

Its own container rather than a prefix inside runs, because getAdhocRunListing loads per-run metadata for every non-date-shaped id before truncating to the display limit, and because the storage account now carries a 14-day expiry rule scoped to runs-gha/ that must never be able to reach nightly history.

Three stale surfaces found while trimming the comments

Each one described an input that no longer exists, and each was invisible to the pinning lint:

  • The ci skill still told the agent to pass an experiment via extra-args:, so an emitted workflow would have had its experiment silently ignored — exactly the forwarding-input failure this PR is about, aimed at the workflows the plugin writes. Now -e and the path as two args lines.
  • docs/CI_GATE.md carried "Extras and plugins" twice; the second copy predated the rewrite and still named prerelease. Its frontmatter and intro also still promised a per-task score floor and a job-summary write.
  • test_ci_skill_does_not_recommend_a_recursive_task_glob enforced the opposite of the new behaviour: it banned ** and required the skill to explain globstar. It was passing only because the rewritten skill contains the phrase "no globstar caveat". Removed — CE026's unknown-input check already covers a stale tasks: in a snippet.

After merging

Two dispatches, in this order, before the skills PR can do anything:

  1. Release (release.yml), which bumps the version, re-pins action.yml, and moves v0. Until v0 moves, a consumer passing these inputs gets them silently ignored. Note the reverse window too: between this merge and that release, verify-published-action.yml on main passes the new with: block to the old @v0, so its nightly runs the same single task on the default model instead of haiku. Cheap, not broken, but a reason to release promptly.
  2. deploy-evalboard.yml in coder_eval_uipath (manual dispatch, no post-deploy health check). The gha source only exists on the deployed site once that runs, so sequence it before the first upload or the emitted link 404s on arrival.

Verification

  • tests/test_action_inputs.py (37 cases) executes both step scripts pulled out of action.yml with uv/coder-eval stubbed to record argv, so it asserts the text that ships rather than a copy. Mutation-checked: flattening the junit derivation fails 5 of them.
  • The stub is written in bash, not Python, because Git Bash rewrites absolute-POSIX-looking arguments on the way to a native binary, so a python-shebang stub mangled /action-checkout on the Windows runner.
  • CE026, the repo's own lint rule for doc snippets passing inputs action.yml does not declare, caught a surface I had missed (plugins/coder-eval/skills/ci/SKILL.md). Worth noting the rule earned its keep.
  • action-dogfood covers what a unit test cannot: working-directory on a composite step, a plugin installed through extra-packages being discovered at runtime, and now the run-md-path summary-append recipe the docs tell consumers to use. The discovery probe is a coder-eval plan on a task naming the BYOA fixture's agent kind, which exits 1 with No agent registered for type 'byoa-demo' if the entry point was not found.
  • Full local sweep: 426 lint/action tests, 608 evalboard vitest cases, tsc --noEmit, next build, ruff and pyright clean. actionlint findings unchanged against baseline on every workflow touched.

🤖 Generated with Claude Code

bai-uipath and others added 2 commits August 31, 2026 14:23
…se and args inputs

The composite action had no way to run from a subdirectory, install an agent
extra, or put a plugin in the environment it invokes, which is what kept every
real consumer on a hand-rolled `uv pip install` + `coder-eval run` instead.

`working-directory` applies to both of the action's steps. It is the only way in:
GitHub rejects `working-directory:` on a `uses:` step, and a job-level
`defaults.run` does not reach inside a composite action.

`extras` composes into the requirement string rather than installing afterwards,
and `extra-packages` maps to `uv tool install --with`. Both exist because that
install builds an isolated environment whose shims shadow every other coder-eval
on PATH, so neither an extra nor a plugin added beside it is ever imported by the
CLI the action runs. `prerelease` passes `--prerelease=allow` for when either
needs a prerelease to resolve.

`args` takes one argument per line and appends each verbatim. `extra-args` is
deliberately word-split, which also means pathname-expanded, so a `-D` override
whose value is a bracketed list (`key=[A,B,C]`, a bash character class) was
intact only while no file in the working directory happened to match it. A single
file named `key=A` silently rewrote a three-name list to one name and the run
measured something other than what the workflow asked for.

tests/test_action_inputs.py executes both step scripts pulled straight out of
action.yml, with uv and coder-eval stubbed to record their argv, so the
assertions are about the text that ships rather than a copy of it. The
action-dogfood job then covers the two things a unit test cannot reach:
`working-directory` on a composite step, and a plugin installed via
`extra-packages` actually being discovered at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…runs

UiPath/skills' `run-coder-eval` workflow_dispatch produces debug runs that today
survive only as a downloadable artifact zip. This gives them a dashboard link,
without putting them anywhere they can be mistaken for nightly history.

Registered but deliberately unlisted: no tab, no listing page, no aggregate view.
`NAV` in app/layout.tsx is a hardcoded array and does not iterate `SOURCES`, so
registration and surfacing are independent, and app/runs/[id] reads by id on
demand — a fresh link resolves with no listing in existence. Registration in
`SOURCES` is still mandatory, because `sourceById` is the only path by which a
container becomes reachable, and it coerces an unknown id to the default source
rather than throwing, so without an entry `?src=gha` would quietly read the
skills nightly's container and 404.

Its own container rather than an `adhoc-` prefix inside `runs`, for two reasons.
`getAdhocRunListing` loads per-run metadata for every non-date-shaped id in a
container before truncating to the front page's limit, so a stream of dispatches
would bury the intentional ad-hoc runs and cost a blob load each. And the
storage account carries a lifecycle rule that deletes these after 14 days;
sharing a container would put months of nightly history behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bai-uipath and others added 3 commits August 31, 2026 14:44
…l-spec tests

`test_local_installs_the_action_checkout` failed on the Windows smoke job:
Git Bash converts an argument that looks like an absolute POSIX path into
Windows form on the way to a native binary, so `CE_ACTION_PATH=/action-checkout`
reached the recording stub as `C:/Program Files/Git/action-checkout`.

Nothing to do with the action — the same mangling would hit any test that
asserts on argv through `shell: bash` on a Windows runner. MSYS2_ARG_CONV_EXCL
and MSYS_NO_PATHCONV turn the conversion off, and are ignored on POSIX.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The argv-recording stub had a `#!/usr/bin/env python3` shebang, which makes every stub invocation cross the MSYS-to-native boundary on a Windows runner. Git Bash rewrites arguments that look like absolute POSIX paths on the way across it, so `/action-checkout` reached the stub as `C:/Program Files/Git/action-checkout`. Switching the conversion off with `MSYS2_ARG_CONV_EXCL` only moved the failure: the shebang launcher then could not hand python its own script path either, and all 21 tests in the file failed instead of one.

A bash stub never crosses that boundary, so argv arrives byte-for-byte on every platform and no environment switches are needed. argv is now recorded NUL-delimited rather than as JSON, so a value carrying a quote, a backslash or a space needs no escaping on the way out of bash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`coder-eval run` has 21 flags. The action promoted five of them to named inputs with no principle behind the choice: `--tags` got one, its sibling `--exclude-tags` did not, and `extra-args`'s own description admitted it covered "--tags exclusions". A forwarding input buys nothing and costs a lot, because GitHub silently IGNORES an input the referenced tag does not define, so one that is mistyped or newer than the consumer's pin yields a run that measured something else and still exits 0. A wrong CLI flag is a hard error instead.

So the surface is now eight inputs, none of which is a CLI flag. An input exists only where the action does something with the value besides pass it along: `version`/`extras`/`extra-packages`/`install-flags` compose the install spec, `working-directory` is applied to the action's own steps, `env` is exported into its shell, and `run-dir` is read back for the outputs.

Removed: `tasks`, `tags`, `model` and `extra-args` all fold into `args`. `prerelease` generalises to `install-flags`, which also covers a private index. `junit-path` is derived as `<run-dir>/junit.xml`, which is where both existing consumers already put it. `step-summary` and its write are replaced by a `run-md-path` output, because a consumer that has to redact the report first cannot undo a write that already happened. `minimum-task-score` and its embedded Python are gone; both call sites set it to `0.0`, a no-op, and a score floor is policy over `run.json` that belongs in the CLI where it is unit-testable and reachable from ADO.

Folding task globs into `args` removes a documented hazard rather than adding one. They now reach the CLI unexpanded and `expand_task_files` handles them, so `**` works without `globstar`, and a glob matching nothing exits 1 instead of arriving as a literal path. Three "sharp edges" in the docs describing the old shell-expansion behaviour are deleted.

The four line-list parsers collapse to one `clean_lines`, copied into both step scripts because they are separate bash processes, with a test asserting the copies stay byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bai-uipath bai-uipath changed the title feat(action): subdirectory, extras and plugin inputs, plus an unlisted evalboard source for ad-hoc GH runs refactor(action)!: eight inputs, none of them a CLI flag, plus an unlisted evalboard source for ad-hoc GH runs Sep 1, 2026
bai-uipath and others added 2 commits September 1, 2026 13:47
Comments only, no behavior change. action.yml goes 291 -> 245 lines (71 -> 41
comment lines): the `author:` justification and the boxed INPUT DESIGN banner go,
the forwarding-input rule keeps six lines, and every input description and inline
comment is cut to its contract. The dogfood `with:` block in pr-checks.yml had 30
comment lines around 20 lines of YAML, now 12. GHA_SOURCE's preamble drops to the
4-6 line shape the two sources above it already use.

Also reflows the ragged comment left in verify-published-action.yml by the
previous commit's edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three surfaces still described the deleted inputs, found while trimming comments:

- The `ci` skill told the agent to pass an experiment via `extra-args:`, so an
  emitted workflow would have had its experiment silently ignored — GitHub drops
  an input the tag does not declare. Now `-e` and the path as two `args` lines.
  The "a path containing a space is unsafe there" caveat goes with it: `args`
  entries are verbatim.
- docs/CI_GATE.md carried "Extras and plugins" twice, the second copy predating
  the rewrite and still naming `prerelease`. Its frontmatter and intro also still
  promised a per-task score floor and a job-summary write.
- test_ci_skill_does_not_recommend_a_recursive_task_glob enforced the opposite of
  the new behavior: it banned `**` in a `tasks:` value and required the skill to
  explain `globstar`. It passed only because the rewritten skill contains the
  phrase "no `globstar` caveat". Removed; CE026's unknown-input check already
  covers a stale `tasks:` in a snippet.

test_ci_skill_covers_experiments_and_pins asserted `"extra-args" in text`;
retargeted to a standalone `-e` token, since the substring also matches inside
"coder-eval".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant