Skip to content

feat: add root-level action.yml GitHub Action for installing fledge in CI - #511

Open
0xLeif wants to merge 2 commits into
mainfrom
0xleif/feat/setup-fledge-action
Open

feat: add root-level action.yml GitHub Action for installing fledge in CI#511
0xLeif wants to merge 2 commits into
mainfrom
0xleif/feat/setup-fledge-action

Conversation

@0xLeif

@0xLeif 0xLeif commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

CorvidLabs/fledge has no action.yml on any branch, so uses: CorvidLabs/fledge@v1.7.2 fails with "Can't find 'action.yml'" — sibling repo CorvidLabs/spec-sync already ships one at its root. Consumers currently curl-pipe install.sh, whose latest_version() scrapes the unauthenticated, per-IP-rate-limited releases/latest API; CorvidLabs/rune lost four CI runs to could not determine latest version from exactly this.

  • action.yml (root, composite action, no JS/Docker): 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 predating sidecars). Fails clearly on Windows/unsupported arch before any network call. Every curl retries transient flake (--retry 3 --retry-all-errors).
  • .github/workflows/test-action.yml: exercises the action on every push on ubuntu-latest/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.

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 upstream spec-sync bug fixed for five other records in c4b06f1/#506 (an accepted, merged change stuck unable to reach archived because the archive staleness preflight is exactly what the bug breaks). This broke cargo test's cli_spec_check_succeeds_in_project for anyone on main, not just this branch. Applied the identical, precedented remediation: archived CHG-0007 (evidence verified byte-identical to git 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 v1 tag pointing at the merge commit (documented in the new CONTRIBUTING.md section). CorvidLabs/rune can then replace its two pinned-curl Install Fledge steps in .github/workflows/ci.yml with uses: CorvidLabs/fledge@v1.

Test Plan

  • Real end-to-end run of the extracted action.yml script against the live CorvidLabs/fledge v1.7.2 release: pinned success (checksum verified), latest success (authenticated, real token), Windows/unsupported-arch fail cleanly before any network call
  • Checksum match/mismatch/missing-sidecar logic verified in isolation
  • action.yml and test-action.yml parse as valid YAML
  • fledge lanes run check green
  • fledge lanes run pre-commit green (fmt + lint + test + spec-check)
  • specsync change verify (via verify-native) green; full SDD lifecycle (CHG-0008) followed: definition approval → implement → verify → closing approval → accept

🤖 Generated with Claude Code

…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
@0xLeif
0xLeif requested a review from a team as a code owner August 15, 2026 18:19
@0xLeif
0xLeif requested review from 0xGaspar, Kyntrin and tofu-ux August 15, 2026 18:19
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@0xLeif 0xLeif changed the title test title feat: add root-level action.yml GitHub Action for installing fledge in CI Aug 15, 2026
github-actions[bot]
github-actions Bot previously approved these changes Aug 15, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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

@0xGaspar

Copy link
Copy Markdown
Contributor

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. CHG-0008 is claimed twice.

Same sequence number, different slugs. Both branches were cut while the ledger on main sat below 8, so specsync change new handed each the same identity. Whichever lands second needs renumbering, and that has to happen while the record is still draft — once a definition approval exists, its digest covers the id, so renaming afterward invalidates the approval.

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 CHG-0007 in opposite directions.

This PR archives it (moves to .specsync/archive/changes/2026-08-07-…). #504 and #509 each re-accept it in place via reopen → verify → accept, so on those two branches the record is still under .specsync/changes/. Merging in either order leaves CHG-0007 present in both locations, or conflicts outright.

Worth noting the divergence is informative: reopen succeeded on those branches where it deadlocked on the five records fixed in #506. The difference is digest consistency — CHG-0007's closing-approval digest still matches its verification evidence, so reopen_change's equality check at src/change.rs:1784 passes and the record can be re-verified normally rather than needing the archive workaround. That supports the version-skew hypothesis in CorvidLabs/spec-sync#481: the deadlock isn't inherent to staleness, it's specific to records whose stored digest no longer reproduces.

If that holds, reopen → verify → accept is the preferable remediation for a record staled purely by a sequence bump, and archiving should be reserved for genuinely unhealable ones. Happy to rebase #504/#509 onto whichever convention you'd rather standardise on — I'd just like the two not to diverge silently, since the whole point of the ledger is that it agrees with itself.

No action needed on the action.yml work, which looks good independently; this is purely about sequencing.

@0xGaspar 0xGaspar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
fi

install-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."
fi

The 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:

  1. Capture the HTTP status (curl -o file -w '%{http_code}'); treat 404 as the legitimate "no sidecar" case and any other outcome as fatal.
  2. Require a checksum for any version at or above the first release that shipped sidecars, and only allow the skip below it.
  3. Add an explicit allow-unverified: false input so skipping is opt-in.

Relatedly, README.md currently states:

every download is checksum-verified against the release's .sha256 sidecar

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)

  1. JSON parsing. grep -m1 '"tag_name"' | cut -d'"' -f4 is fragile; jq is preinstalled on GitHub-hosted runners. Also the trailing || true collapses 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.
  2. Token in argv. auth=(-H "Authorization: Bearer ${INPUT_TOKEN}") puts the token in the process table. Low risk on single-tenant runners, but curl --config - reading headers from stdin avoids it entirely.
  3. Temp dir leaks on the failure pathsmktemp -d is only cleaned on success. Ephemeral runners make this cosmetic; a trap 'rm -rf "$tmp"' EXIT is a one-liner.
  4. test-action.yml runs on every push to every branch (on: push unfiltered) 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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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

@0xLeif

0xLeif commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

All four addressed in 1e00d34 — every check green, including a new job that exercises both refusal paths on a real runner.

Blocker 1. You're right, and I reproduced it before fixing it. version is now allowlisted to latest or a release tag before it is used anywhere, and the tag that comes back from the API gets the same check — both entry points converge on one rule. install-dir got the narrower treatment you suggested: .. segments and line breaks rejected, the latter because ok\nversion=fake would otherwise forge a step output.

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 --retry-all-errors from that one fetch so a 404 returns in 0.35s instead of burning the backoff. The README now describes what the script does.

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 fledge-windows-x86_64.exe; README and the job name say the same. Wiring it up properly needs .exe handling plus cygpath for $GITHUB_PATH/outputs.path — an MSYS /c/... entry isn't usable from pwsh steps — so it's an additive follow-up rather than an untested add-on here. Happy to open an issue for it.

Nits. All four. jq with the HTTP status surfaced (a rate limit is now diagnosable, which was the whole point); token moved to a stdin curl config; trap 'rm -rf "$tmp"' EXIT; on: push narrowed to main. New refuses-unsafe-install job covers the traversal version and v0.5.0 as a live sidecar-less fixture.

Recorded as CHG-0009specsync freezes the definition of an applied change, and this contradicts CHG-0008's REQ-5 and REQ-6, so it needed its own workspace. CHG-0008 keeps its original definition with evidence refreshed via an audited reopen.

@0xLeif

0xLeif commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

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 main already archived five records this way, so archiving is the standing precedent. More concretely, CHG-0007 lists .specsync/change-sequence.json in its own affected_paths — so while it stays active, every future change new re-stales it. That isn't hypothetical: creating CHG-0009 in this PR re-staled CHG-0008 through that exact path, and I had to reopen/verify/accept it again. Re-accepting CHG-0007 in place buys quiet until the next change record, then it's back.

Your digest-consistency hypothesis does hold, though, and this PR is another data point for it: reopen worked cleanly on CHG-0008 twice, no deadlock, because its stored digest still reproduces. That's worth adding to CorvidLabs/spec-sync#481 — the real fix is upstream not putting change-sequence.json in every record's delivery inputs, at which point the whole argument goes away.

@0xLeif
0xLeif requested a review from 0xGaspar August 16, 2026 23:39
0xGaspar added a commit that referenced this pull request Aug 17, 2026
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
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.

2 participants