feat: add root-level action.yml GitHub Action for installing fledge in CI - #511
feat: add root-level action.yml GitHub Action for installing fledge in CI#5110xLeif wants to merge 2 commits into
Conversation
…n CI CorvidLabs/fledge had no action.yml on any branch, so `uses: CorvidLabs/fledge@v1.7.2` failed with "Can't find 'action.yml'". Consumers fell back to curl-piping install.sh, whose latest-version lookup hits the unauthenticated, per-IP-rate-limited releases/latest API -- CorvidLabs/rune lost four CI runs to "could not determine latest version" from this. Adds: - action.yml: composite action. A concrete `version` (e.g. v1.7.2) downloads the release asset directly with zero API calls; `version: latest` resolves via an authenticated API call (github.token by default). Verifies every download against its .sha256 sidecar (warns and skips only for releases that predate sidecars). Fails clearly on Windows/unsupported arch before any network call. Every curl call retries transient network flake. - .github/workflows/test-action.yml: exercises the action on every push, ubuntu-latest and macos-latest, for both a pinned tag and latest (via `uses: ./` so it always tests the branch's own code), plus a dedicated windows-unsupported regression guard. - README.md: new GitHub Actions section, pinned form shown first. - CONTRIBUTING.md: documents moving the v1 tag as a manual post-release step. Also archives CHG-0007, which this change's own change-sequence.json bump staled (the same recurring upstream spec-sync bug fixed for five other records in c4b06f1/#506 -- an accepted, already-merged change stuck unable to reach `archived` because the archive staleness preflight is exactly what the bug breaks). Applied the identical remediation: snapshot state.json to accepted-state.json, move to .specsync/archive/changes/<date>-<id>, flip state to archived. No evidence altered -- approvals.json/verification.json verified byte-identical to git HEAD via SHA-256 before the move. Taken through the full verified SDD change lifecycle (CHG-0008): definition approval, implementation, specsync change verify (fledge lanes run verify-native, green), closing approval, accept. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1Ts8qwUqfvZK8GtE21bhm
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
✅ Corvin says...
_
<(^\ .oO(Caw! ^v^)
|/(\
\(\\
" "\\
"Caw! Your code sparkles like a dropped french fry."
CI Summary
| Check | Status |
|---|---|
| Dependency Audit | ✅ Passed |
| Integration (3 OS) | ✅ Passed |
| Lint (fmt + clippy) | ✅ Passed |
| Spec Validation | ✅ Passed |
| Tests (3 OS) | ✅ Passed |
Powered by corvid-pet
|
Heads-up on two governance collisions between this PR and the currently-open set — both are merge-order problems rather than defects in the change itself. 1.
Same sequence number, different slugs. Both branches were cut while the ledger on For reference, the other open PRs currently hold 0009 (#505) and 0010 (#509), so the next free number after this cluster is 0011. 2. This PR and #504/#509 resolve This PR archives it (moves to Worth noting the divergence is informative: If that holds, No action needed on the action.yml work, which looks good independently; this is purely about sequencing. |
0xGaspar
left a comment
There was a problem hiding this comment.
Reviewed the action script, the test workflow, and the docs. The shape of this is right — composite over JS/Docker, inputs passed via env: rather than interpolated into the run: block (which correctly avoids the classic script-injection hole), OS/arch rejection before any network call, set -euo pipefail, permissions: contents: read, uses: ./ so the workflow tests the branch's own code, and a dedicated regression job for the unsupported-platform path. The rate-limit motivation is real and the pinned-tag-makes-zero-API-calls design is the correct answer to it.
Two things I'd want changed before merge, one factual correction, and some nits.
Blocker 1 — version is unvalidated and reaches a URL that curl path-normalizes, ending in an executed binary
version goes straight into the download URL:
base="https://github.com/${repo}/releases/download/${version}"
curl -fsSL ... "${base}/${asset}" -o "${tmp}/fledge"curl resolves dot segments before sending, so .. in version escapes the repo namespace. Verified locally:
$ curl -sv "https://github.com/CorvidLabs/fledge/releases/download/../../../octocat/Hello-World/x"
> GET /CorvidLabs/octocat/Hello-World/x HTTP/2
With one more ../, version="../../../../attacker/repo/releases/download/v1" resolves to /attacker/repo/releases/download/v1/fledge-linux-x86_64. That download is then install -m 0755'd and executed by the final fledge --version step.
This only bites when a consumer feeds untrusted data into version, but that is not exotic — a pull_request_target workflow keyed off a branch name, label, or title is the standard example, and this is a public action inviting third-party use. Worth hardening at the boundary rather than relying on every consumer to sanitize.
Suggest validating before use:
if [ "$version" != "latest" ] && ! printf '%s' "$version" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.-]+)?$'; then
echo "::error::Invalid version '$version'. Expected a release tag like v1.7.2, or 'latest'." >&2
exit 1
fiinstall-dir is the same class (consumer-controlled path written with mode 0755) at much lower severity — worth a thought while you're in there.
Blocker 2 — checksum verification silently degrades to none, and the README promises otherwise
if curl -fsSL ... "${base}/${asset}.sha256" -o "${tmp}/fledge.sha256"; then
...verify...
else
echo "::warning::No checksum published for ${asset}; skipping verification. This release predates checksum sidecars."
fiThe else branch cannot distinguish "this release genuinely has no sidecar" from "that one request failed." Any transient failure, proxy interference, or selectively-blocked request downgrades a verified install to an unverified one, and a ::warning:: does not fail the build. Anyone positioned to tamper with the binary is positioned to fail the sidecar fetch.
Also, --retry 3 --retry-all-errors does not retry a 404, so the "predates sidecars" case is fast, but neither does it distinguish that 404 from anything else.
Options, roughly in order of preference:
- Capture the HTTP status (
curl -o file -w '%{http_code}'); treat 404 as the legitimate "no sidecar" case and any other outcome as fatal. - Require a checksum for any version at or above the first release that shipped sidecars, and only allow the skip below it.
- Add an explicit
allow-unverified: falseinput so skipping is opt-in.
Relatedly, README.md currently states:
every download is checksum-verified against the release's
.sha256sidecar
That is not true while the skip path exists. Either close the gap or soften the claim — right now the docs promise a guarantee the script doesn't deliver.
Correctness — Windows is refused, but the binary exists
gh release view v1.7.2 --json assets lists:
fledge-windows-x86_64.exe
fledge-windows-x86_64.exe.sha256
So the error text is misleading:
fledge publishes no binary for $RUNNER_OS via this action (Linux and macOS only)
"via this action" is carrying a lot of weight. Combined with the test job named "windows fails with a readable message" and README's "Linux and macOS runners (x86_64/aarch64) are supported," a reader reasonably concludes no Windows binary exists — when it ships in every release.
Either wire up Windows (the asset is right there; it mainly needs the .exe suffix and a $RUNNER_OS arm) or say plainly that the action doesn't support it yet, rather than implying the binary is unavailable. The regression job is a good idea either way — just make its message match reality.
Nits (non-blocking)
- JSON parsing.
grep -m1 '"tag_name"' | cut -d'"' -f4is fragile;jqis preinstalled on GitHub-hosted runners. Also the trailing|| truecollapses every failure mode — auth rejected, network down, rate-limited — into the same "Could not resolve the latest fledge release." Surfacing the HTTP status would make a rate-limit diagnosable, which is precisely the failure this PR exists to fix. - Token in argv.
auth=(-H "Authorization: Bearer ${INPUT_TOKEN}")puts the token in the process table. Low risk on single-tenant runners, butcurl --config -reading headers from stdin avoids it entirely. - Temp dir leaks on the failure paths —
mktemp -dis only cleaned on success. Ephemeral runners make this cosmetic; atrap 'rm -rf "$tmp"' EXITis a one-liner. test-action.ymlruns on every push to every branch (on: pushunfiltered) and on PRs to main, so branch pushes get two runs each. Given the concurrency group cancels in-progress, minor — but worth confirming it's intended.
On the governance side
I've left a separate comment about the CHG-0008 collision with #504 and the divergent CHG-0007 resolution, since that's sequencing rather than code review.
Happy to re-review quickly once the version validation and the checksum path are addressed — the rest is solid work and I'd like it to land, since the curl-pipe workaround it replaces is genuinely costing CI runs.
Addresses 0xGaspar's CHANGES_REQUESTED review on #511. Blocker 1 -- `version` was unvalidated and reached a URL curl path-normalizes. Confirmed against real github.com: requesting `.../releases/download/../../../octocat/Hello-World/x` sends `GET /CorvidLabs/octocat/Hello-World/x`, so a `..` in `version` pulls the binary from another repo, which the action then installs 0755 and executes. `version` is now allowlisted to `latest` or a release tag before any use, and the tag resolved from the API is validated the same way. `install-dir` gets the same treatment at lower severity: `..` segments and line breaks rejected (a newline would otherwise inject lines into $GITHUB_PATH/$GITHUB_OUTPUT). Blocker 2 -- checksum verification silently degraded to none. The sidecar fetch's else-branch warned and continued, which cannot distinguish a genuine absence from a suppressed request, and a ::warning:: does not fail a build. Verification is now mandatory: the fetch captures %{http_code} and any non-200 fails the step, with a 404 reported as "no sidecar published". Nothing real is lost -- surveyed all 39 releases: sidecars are universal from v0.9.1 on, v0.6.0-v0.9.0 publish no assets at all, only v0.3.0-v0.5.0 are now refused. `--retry 3` without `--retry-all-errors` on that fetch so a 404 fails in 0.35s rather than burning the backoff. README.md's checksum claim, which promised a guarantee the skip path did not deliver, now matches the implementation. Correctness -- the Windows message implied no Windows binary exists. Every release ships fledge-windows-x86_64.exe. The message now says the action does not support the platform *yet* and points at the binary that does; README.md and the regression job name say the same. Actually wiring Windows up needs .exe handling plus cygpath translation for $GITHUB_PATH and outputs.path, so it stays a deliberate follow-up rather than an untested add-on here. Nits -- jq instead of grep|cut, with the HTTP status surfaced so a rate limit is diagnosable (the failure this action exists to remove); the Bearer token moved from argv to a stdin curl config; `trap 'rm -rf "$tmp"' EXIT` so the temp dir is cleaned on failure paths; `on: push` narrowed to main so a branch push runs the workflow once rather than twice. test-action.yml also gains a `refuses-unsafe-install` job covering the traversal version and the sidecar-less v0.5.0 release, and passes step outcomes/outputs into assertion steps via `env:`. Recorded as CHG-0009: specsync freezes the definition of an already-applied change, and this response genuinely contradicts CHG-0008's REQ-setup-action-5 (warn-and-continue) and -6 (--retry-all-errors everywhere), so it belongs in its own workspace rather than as an edit to a frozen record. CHG-0008 keeps its original definition and had its evidence refreshed via an audited reopen. Both are accepted with `exact` evidence; `fledge lanes run pre-commit` and `fledge trust verify` are green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✅ Corvin says...
_
<(^\ .oO(Caw! ^v^)
|/(\
\(\\
" "\\
"Caw! Found a shiny new spec!"
CI Summary
| Check | Status |
|---|---|
| Dependency Audit | ✅ Passed |
| Integration (3 OS) | ✅ Passed |
| Lint (fmt + clippy) | ✅ Passed |
| Spec Validation | ✅ Passed |
| Tests (3 OS) | ✅ Passed |
Powered by corvid-pet
|
All four addressed in Blocker 1. You're right, and I reproduced it before fixing it. Blocker 2. Took the strongest of your three options and went one further: no skip path at all. I surveyed all 39 releases first, since the choice turns on what the skip actually protects — sidecars are universal from v0.9.1 on, v0.6.0–v0.9.0 publish no assets at all, so only v0.3.0–v0.5.0 are now refused. Your option 1 is still in there, but only to make the error accurate: the status is captured so a 404 reads "no sidecar is published there" and anything else names the status and curl's exit. Also dropped Windows. Correct on the facts, and the wording was doing exactly what you describe. Taking your second option: the message now says the action doesn't support the platform yet and points at Nits. All four. Recorded as CHG-0009 — |
|
Both real, thanks for catching them before they collided in the tree. CHG-0008. Not renumbering here — the record is accepted, so the definition approval's digest already covers the id, exactly as you note. This PR now also carries CHG-0009 for the review response (same reason: an applied definition can't be amended in place), so it collides with #505 too. Merge order it is; if #504/#505 land first I'll take the renumbering on this side, though it means rolling records back to draft and re-approving, so I'd rather we sequence than renumber where we can. CHG-0007. Staying with the archive, and I'd ask you to rebase #504/#509 onto that convention. Two reasons. #506 on Your digest-consistency hypothesis does hold, though, and this PR is another data point for it: |
The review fixes changed src/ under an already-accepted record, staling
CHG-0008. Healed via reopen -> verify -> accept (verify-native green,
4 requirements re-evidenced).
The fixes also introduced src/remote.rs and tests/isolation.rs, which
CHG-0008 did not cover. spec-sync refuses to widen the definition of an
already-applied change ("perform further spec changes in a new change
workspace"), so those land as CHG-0011 with its own remote delta and
REQ-remote-010 rather than by editing the accepted definition.
Allocated 0011 because 0008 (this PR and #511), 0009 (#505) and 0010
(#509) are all claimed on open branches.
Also documents four github exports the coverage gate flagged: remote_base
and remote_url were sharing one table row, and API_BASE_ENV /
REMOTE_BASE_ENV were undocumented.
`specsync check` exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuhwrJF2XFDVHhX7qzDuyy
Summary
CorvidLabs/fledgehas noaction.ymlon any branch, souses: CorvidLabs/fledge@v1.7.2fails with "Can't find 'action.yml'" — sibling repoCorvidLabs/spec-syncalready ships one at its root. Consumers currently curl-pipeinstall.sh, whoselatest_version()scrapes the unauthenticated, per-IP-rate-limitedreleases/latestAPI;CorvidLabs/runelost four CI runs tocould not determine latest versionfrom exactly this.action.yml(root, composite action, no JS/Docker): a concreteversion(e.g.v1.7.2) downloads the release asset directly with zero API calls;version: latestresolves via an authenticated API call (github.tokenby default). Verifies every download against its.sha256sidecar (warns and skips only for releases predating sidecars). Fails clearly on Windows/unsupported arch before any network call. Everycurlretries transient flake (--retry 3 --retry-all-errors)..github/workflows/test-action.yml: exercises the action on every push onubuntu-latest/macos-latestfor both a pinned tag andlatest(viauses: ./, so it always tests the branch's own code), plus a dedicatedwindows-unsupportedregression guard.README.md: new## GitHub Actionssection, pinned form shown first.CONTRIBUTING.md: documents moving thev1tag as a manual post-release step.Side-effect: CHG-0007 archived
Creating this change's spec-sync record bumped
.specsync/change-sequence.json, which staled CHG-0007's already-merged, already-accepted verification evidence — the same recurring upstreamspec-syncbug fixed for five other records inc4b06f1/#506 (an accepted, merged change stuck unable to reacharchivedbecause the archive staleness preflight is exactly what the bug breaks). This brokecargo test'scli_spec_check_succeeds_in_projectfor anyone onmain, not just this branch. Applied the identical, precedented remediation: archived CHG-0007 (evidence verified byte-identical togit show HEAD:via SHA-256 before the move — nothing altered).Follow-up (not in this PR, needs separate authorization)
Once merged, a maintainer creates and pushes the moving
v1tag pointing at the merge commit (documented in the new CONTRIBUTING.md section).CorvidLabs/runecan then replace its two pinned-curlInstall Fledgesteps in.github/workflows/ci.ymlwithuses: CorvidLabs/fledge@v1.Test Plan
action.ymlscript against the liveCorvidLabs/fledgev1.7.2 release: pinned success (checksum verified),latestsuccess (authenticated, real token), Windows/unsupported-arch fail cleanly before any network callaction.ymlandtest-action.ymlparse as valid YAMLfledge lanes run checkgreenfledge lanes run pre-commitgreen (fmt + lint + test + spec-check)specsync change verify(viaverify-native) green; full SDD lifecycle (CHG-0008) followed: definition approval → implement → verify → closing approval → accept🤖 Generated with Claude Code