Commit Graph

499 Commits

Author SHA1 Message Date
github-actions[bot] f0b538361d chore: version packages (#311)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.13.0 dtu-github-actions@0.13.0
2026-04-22 16:31:56 +02:00
Peter Pistorius ba29b6fe6d chore(changeset): downgrade AGENT_CI_DOCKER_HOST rename from major to minor
Avoid a premature 1.0.0 release; bundle this change into 0.13.0 instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:28:47 +02:00
Peter Pistorius f5f7dbd724 fix(workflow): honor step-level if: conditions (#313)
parseWorkflowSteps never extracted `if:` from the workflow, so the DTU
server fell back to `condition: "success()"` for every step. Custom
gates — `contains(...)`, `always()`, even literal `${{ false }}` — were
silently ignored and every step ran regardless. A new `parseStepIf`
helper strips the optional `${{ }}` wrapper and threads the naked
expression through to the runner's EvaluateStepIf, so step gates now
behave as they do on real GitHub Actions. Adds unit coverage for the
helper and a smoke workflow that fails loudly if any gate leaks.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:19:59 +02:00
Peter Pistorius 3212927d6b feat(cli): persist latest run result per worktree as JSON
* feat(cli): persist latest run result per worktree to a stable JSON path

Lets external consumers (tmux panes, status bars, editor integrations) read
the current branch's CI status without re-running agent-ci or scraping human
output. Written atomically after every `agent-ci run` / `run --all`, keyed by
`<branch>.<worktree-hash>.json` under `<org>/<repo>/` so two worktrees on the
same branch don't stomp each other. Includes `headSha` so consumers can detect
stale results themselves.

Closes #288

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): include steps[] and per-step log paths in run-result JSON

Each job entry in the persisted run-result JSON now carries:
- `debugLogPath` for the whole job
- `failingStepLogPath` when a step failed
- `steps[]` with name, status, and `logPath` for each step

Path fields are only emitted when the file still exists at write time,
so consumers get an honest view after passing runs clean up their logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): drop job-level failingStepLogPath from run-result JSON

The failing step's log path is already discoverable in `steps[]` via the
per-step `logPath` on the entry whose `status == "failed"`, so the job-level
field is redundant. Keep `failingStep` (the name) as a top-level convenience.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:24:01 +02:00
Peter Pistorius f0dc684ab6 chore(demo): add pnpm demo script and fix demo.sh runtime issues
Target tmux panes by captured pane ID instead of window:pane indices so
the script works regardless of the user's tmux base-index. Invoke
scripts/agent-ci-dev.sh directly instead of going through
pnpm --dir "$REPO_ROOT", so the CLI inherits DEMO_DIR as its cwd and
resolves the demo's own .github/workflows/ci.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:15:52 +02:00
Peter Pistorius 77ea148803 feat(config)!: AGENT_CI_DOCKER_HOST + load AGENT_CI_* from .env.agent-ci (#309)
DOCKER_HOST collides with the shell's Docker CLI expectation and can't
live in .env.agent-ci without leaking to every docker command. Rename
to AGENT_CI_DOCKER_HOST and hard-exit if the legacy DOCKER_HOST is set.
At bootstrap, apply AGENT_CI_*-prefixed keys from .env.agent-ci to
process.env so Docker/network config persists without shell exports.

Closes #308.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:40:14 +02:00
Peter Pistorius 37f98a6c37 test(diff-parse): expand to job + workflow scopes, vendor third-party corpus, gate via pnpm check (#306)
Closes #297. CI/scripts-only — no packages/ change, so no changeset required
per .claude/rules/changeset-required.md.

- scripts/diff-parse.ts: walks raw YAML at workflow + job scopes (in addition
  to the existing step scope) and grades every recognized key against a
  preservation predicate or a documented expected-drop. Reuses focused parsers
  (parseJobIf, parseJobOutputDefs, parseWorkflowContainer, parseWorkflowServices,
  parseMatrixDef, parseFailFast, parseJobDependencies, getWorkflowTemplate).
  defaults.run.shell preservation detects the heredoc-wrap marker emitted by
  wrapScriptForShell.
- third-party-workflows/: vendored scan-only corpus — turborepo-lint.yml,
  playwright-infra.yml, docker-validate.yml — each with attribution header.
  Picked up automatically by collectWorkflowFiles.
- package.json: parse:diff added to the parallel pnpm check pipeline so
  regressions fail standard CI.

Result: 37 workflows scanned (.github/workflows + third-party-workflows),
0 drifts. pnpm check exits 0; pnpm agent-ci-dev run --all reports 39/39.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:35:52 +02:00
github-actions[bot] ba1c14ae7b chore: version packages (#301)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.12.4 dtu-github-actions@0.12.4
2026-04-20 11:03:42 +02:00
Peter Pistorius fdec27e537 docs(compat): split the overloaded rows surfaced by the #296 audit (#305)
Each overloaded row becomes one row per documented feature (or a tighter
note when the sub-features behave identically). No behaviour changes.

  * github.*        split into sha / repository family / everything-else
  * runner.*        new row for unsupported siblings
  * contains etc.   three rows, one per function
  * success family  downgraded partial (cancelled() always false locally)
  * on (other)      note enumerates ~20 events

Closes the "overloaded row" bucket on #296.

Refs #296.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:39:28 +02:00
Peter Pistorius 5a23a5a22d docs(compat): fill in 15 previously-absent rows from the #296 spec audit (#304)
Adds rows for features GitHub Actions documents that our compat table
said nothing about. Each status was picked per code inspection of the
existing parser / runner paths — no behaviour changes.

Closes the "missing rows" bucket on #296.

Refs #296.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:38:58 +02:00
Peter Pistorius 044de23532 fix(runner): forward --env/--label from container.options into createContainer (#303)
The YAML parser extracted `jobs.<id>.container.options` into job.container
but the docker createContainer call never consumed it, so `options: --env
FOO=bar` silently produced a container without FOO.

parseContainerOptions now pulls --env/-e and --label/-l out of the options
string and merges them into the container's Env array and Labels map. All
other Docker flags in options: are still ignored by design — --privileged,
--user, --network, --cap-add, --workdir tend to clash with agent-ci's own
container orchestration, and silently dropping them is safer than a partial
honoring that breaks the runner's invariants. Documented in the
compatibility.json note for jobs.<id>.container.

Also updated two nearby notes while running through #296:

  * actions/cache — documents the lack of ref-based isolation (branches
    share cache keys locally, unlike GitHub's per-ref scoping)
  * GITHUB_TOKEN — documents that OIDC id-token issuance is not wired up,
    so permissions: id-token: write → federated cloud auth does not work

Refs #296.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:38:36 +02:00
Peter Pistorius ab410c79c9 fix(workflow): toJSON pretty-print + empty-string numeric coercion (#302)
Two small expression-engine fixes surfaced by extending smoke-expressions
with assertions for #296's "questionable claim" rows:

1. toJSON now emits 2-space-indented JSON (was compact), so hashFiles
   keys that hash a toJSON(x) match GitHub. Parses rawValue first so
   toJSON(fromJSON(x)) round-trips cleanly.

2. compareValues() no longer bails out of the numeric path when either
   side is empty. `''`, `null`, and numeric strings all coerce to
   numbers per the spec; non-numeric strings become NaN (string compare
   path, always false).

Five new assertions (contains-on-array, toJSON pretty-print, coercion,
hashFiles multi-pattern, hashFiles negation) all pass. hashFiles
multi-pattern and negation were never broken — cleared off #296 with
proof rather than a fix.

Refs #296.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:38:12 +02:00
Peter Pistorius f44620bf35 fix(workflow): hashFiles + steps.*.outputs.* — two small correctness fixes (#300)
* fix(workflow): hashFiles descends into dotted directories

findFiles was skipping any directory starting with `.`, so a pattern
like hashFiles('.github/workflows/*.yml') never matched a file and
returned the 40-zero placeholder. Skip only node_modules now.

The smoke assertion is tightened back to 64 chars (SHA-256) since the
real digest now appears.

Closes #294.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflow): resolve steps.*.outputs.* to empty at parse time

The parser was returning a sentinel so that `${{ steps.X.outputs.Y }}`
expressions would be preserved verbatim for the runner to evaluate at
runtime. The runner does not re-evaluate expressions inside run-script
bodies, so the literal `${{ }}` reached bash as "bad substitution".

Per the long-standing compatibility contract, these expressions now
resolve to an empty string at parse time. Cross-job values still work
through needs.*.outputs.*.

Closes #295.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:37:41 +02:00
Peter Pistorius 78e3e01266 fix(workflow): honor shell: for non-bash via parse-time heredoc wrap (#299)
The runner executes every run: step with bash regardless of inputs.shell,
so setting shell: sh / python / pwsh had no effect. Wrap the expanded
script at parse time — `<interpreter> <flags> <<'EOF' ... EOF` — so the
requested shell actually runs the user's code. Workflow, job, and step
scopes compose via the existing resolveStepRunDefault helper.

Adds smoke-shell-defaults.yml (removed in an earlier bug-filing cut)
covering all three scopes with a BASH_VERSION assertion.

Closes #293.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:36:30 +02:00
Peter Pistorius af76237154 tooling: add pnpm parse:diff — step-level parser drift detector (#298)
For each .github/workflows/*.yml, read the raw YAML and compare against
what parseWorkflowSteps emits, flagging any step-level key our parser
silently drops without a documented expected-drop policy.

First run over all 26 workflows: 0 drifts. Job-level and workflow-level
scans are tracked as follow-ups in #297.

Refs #297.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 08:51:39 +02:00
Peter Pistorius e2fe5761ba feat(compat): JSON source of truth + fill gaps + fix defaults.run.working-directory (#291)
* feat(compat): make compatibility.json the source of truth

Flip the compatibility matrix so packages/cli/compatibility.json is
authoritative. scripts/gen-compatibility-md.mjs renders the .md from it,
pnpm check runs a drift check, and the website imports the JSON directly
and renders inline markdown in keys/notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add compat smoke coverage for secrets, working-dir defaults, runner image

Fill three audit gaps identified against packages/cli/compatibility.json:

- smoke-secrets-env-file: secrets resolved from .env.agent-ci and from
  the shell environment fallback
- smoke-defaults-workdir: workflow- and job-level
  defaults.run.working-directory plus a step-level override
- smoke-runner-image: .github/agent-ci.Dockerfile is picked up and the
  resulting image is used at job start

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflow): propagate defaults.run.working-directory to steps

Parse defaults.run.working-directory at workflow and job level and merge
it into each run-step's inputs with step > job > workflow precedence.
Step-level working-directory already worked; the defaults blocks were
silently ignored.

Closes #290.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(compat): add proof field to compatibility.json rows

Every `supported`/`partial` row now lists the smoke workflows that
exercise it end-to-end. `pnpm compat:gen`/`compat:check` fails if a
listed path does not resolve, catching renames.

Rows with an empty `proof: []` are the audit gaps tracked in #292.

Refs #292.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dtu): attach step env per-step so it does not leak to siblings

Step-level env was being aggregated into the job-wide EnvironmentVariables
map (last-wins), so a later step's `env: FOO=x` would override an earlier
step's effective FOO at runtime. Emit step.Env as the mapped step's own
`environment` instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(compat): fill audit gaps #292 — env, expressions, local action

Three smokes covering eight of the ten gaps listed in #292:
- smoke-env-scoping: workflow + job + step `env:` precedence
- smoke-expressions: hashFiles, format, join, runner.arch, type
  literals, startsWith/endsWith
- smoke-local-action: `steps[*].uses: ./.github/actions/...`
  (verified via marker file rather than steps.<id>.outputs.*)

Proof fields updated for every row these smokes cover. hashFiles
downgraded to ⚠️ partial — agent-ci emits SHA-1, GitHub Actions emits
SHA-256 (tracked by #294).

Two gaps remain open as filed issues:
- #293 defaults.run.shell / steps[*].shell not applied
- #295 ${{ steps.<id>.outputs.<name> }} leaks literal text in run
  scripts

Refs #292.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 08:47:20 +02:00
github-actions[bot] 5a9d2d1ea5 chore: version packages (#285)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.12.3 dtu-github-actions@0.12.3
2026-04-19 15:10:12 +02:00
Peter Pistorius 2e7c84449c fix(docs+cli): point Docker Desktop users at the default-socket toggle (#253) (#283)
Docker Desktop 4.x ships with /var/run/docker.sock disabled by default,
so a fresh install hits 'agent-ci couldn't use a Docker socket' even
with Docker Desktop running. Two small fixes:

1. docs/docker-socket.md — replace the stale 'starts automatically'
   line with the actual Settings → Advanced toggle steps.
2. docker-socket.ts — when the resolver throws, detect Docker Desktop's
   user-side socket (~/.docker/run/docker.sock) and append a focused
   hint pointing at the toggle, so users don't have to read the whole
   doc to find the one paragraph that matters.

Refs #253. The underlying permission failure from #253 was already
fixed by #274 (chmod 1777 on _diag/_work); this PR addresses the
remaining onboarding paper-cut.
2026-04-19 14:57:42 +02:00
Peter Pistorius e727f313f3 ci: add manual-only macOS smoke workflow
Single job on macos-14, workflow_dispatch-only. Confirms the
GitHub-hosted macOS runner is healthy: OS info, toolchain sanity.

CI-only change — no changeset needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:05:53 +02:00
github-actions[bot] 2e61c83619 chore: version packages (#281)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.12.2 dtu-github-actions@0.12.2
2026-04-18 20:38:25 +02:00
Peter Pistorius e320288f73 fix(runner): unique sibling container names when nested (#282)
When two `agent-ci-dev` runs execute in parallel, each nested CLI
process starts in its own isolated filesystem and allocates the same
sibling container name (`agent-ci-1`). The pre-spawn
`docker rm -f <name>` then kills the other nested run's sibling.

Include the outer container's hostname in the name prefix when
`/.dockerenv` is present so sibling names stay unique across
concurrent nested runs.

Also update smoke-docker-buildx.yml to look for nested-run logs under
both `/tmp/agent-ci` (GitHub-hosted runner) and
`$GITHUB_WORKSPACE/packages/cli/.agent-ci` (nested under
agent-ci-dev), since the nested working dir differs in each case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:37:29 +02:00
Peter Pistorius 3f1c836064 fix(workflow): expand runner.os/runner.arch from job runs-on (#279) (#280)
GitHub Actions evaluates ${{ runner.os }} and ${{ runner.arch }} per-job
based on the job's runs-on label. Prior to this change we hardcoded
Linux/X64 in the expression expander regardless of runs-on, which broke
tart-backed macOS VM jobs — any step gated on `runner.os == 'macOS'`
would evaluate as false inside the VM.

Thread an optional RunnerContext through resolveExprAtom /
evaluateExprValue / expandExpressions / buildStepEnv, and derive it in
parseWorkflowSteps from `parseJobRunsOn`. Unknown/absent labels keep
the previous Linux/X64 default so existing self-hosted configurations
are unaffected. macOS is mapped to ARM64 because agent-ci's macOS
backend (tart) only runs Apple Silicon VMs.
2026-04-18 16:32:45 +02:00
github-actions[bot] b24b6b45e3 chore: version packages (#278)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.12.1 dtu-github-actions@0.12.1
2026-04-18 14:09:04 +02:00
Peter Pistorius cbf0c44bf2 Close referenced issues on release, not on version-PR merge (#277)
During pnpm run version, scripts/release-closes.mjs captures
Closes|Fixes|Resolves #N references from pending changesets into
.release-closes.json, pairs each with the PR that added the
changeset, and rewrites the keywords to Refs #N so the 'Version
Packages' PR does not close them on merge. After the release
workflow publishes, a new step closes each captured issue with
the comment 'Closes Issue #N via PR #M.'
2026-04-18 13:48:34 +02:00
Peter Pistorius 59d6c40163 fix(runner): chmod /home/runner/_diag and _work for Colima/Docker Desktop (#263) (#274)
* fix(runner): chmod /home/runner/_diag and _work for Colima/Docker Desktop (#263)

On macOS with Colima or Docker Desktop the bind-mounted _diag and _work
directories surface as root:root 0755 inside the container (host perms
don't translate through the VM mount layer), so the runner user (uid
1001) crashes with UnauthorizedAccessException when writing diag logs
and hits write failures in the workspace. Add MAYBE_SUDO chmod 1777 on
both mount points during container boot, mirroring the existing fix
for /home/runner/.cache (#234). OrbStack and native Linux Docker are
unaffected — the chmod is a no-op there.

* fix(cli): require /var/run/docker.sock, link to per-provider setup doc

Today resolveDockerSocket falls back through a list of well-known
provider sockets (~/.orbstack/..., ~/.docker/..., ~/.colima/<profile>/...)
whenever /var/run/docker.sock isn't usable. On macOS + Colima, those
provider paths are not bind-mountable through the provider's VM, and
Docker rejects the later container launch with a confusing

  error while creating mount source path: operation not supported

Stop guessing. Require /var/run/docker.sock to exist (or an explicit
DOCKER_HOST) and fail fast with a short error that links to the new
packages/cli/docs/docker-socket.md — which walks users through the
per-provider setup (Docker Desktop / OrbStack / Colima / native Linux
/ rootless).

The bind-mount invariant comment in docker-socket.ts was already
implicitly assuming this rule; this change just enforces it at the
resolver boundary instead of deferring the failure to container
launch, and gives users a doc URL with the recipe they need.
2026-04-18 13:48:23 +02:00
github-actions[bot] 6574e26013 chore: version packages (#276)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.12.0 dtu-github-actions@0.12.0
2026-04-18 13:27:15 +02:00
Peter Pistorius 12220befc4 Run macos-* jobs in real macOS VMs via tart (#275)
* feat(cli): run macos-* jobs in real macOS VMs via tart

When the host is darwin/arm64 with tart + sshpass installed, jobs
targeting runs-on: macos-* boot an ephemeral cirruslabs macOS VM,
rsync the macOS actions-runner binary in, and connect it to the
in-process DTU via the host bridge.

- New runner/macos-vm/ module (tart wrapper, image mapping, host
  capability probe, runner binary fetch/cache, semaphore, job driver).
- cli.ts routes macOS jobs to the VM executor when the host supports
  it; otherwise falls through to the existing skip-with-warning path.
- DTU generators.ts accepts runnerWorkDir/runnerOs/runnerArch so the
  seed payload reflects where the runner actually materializes _work
  (sibling of run.sh, not a caller-chosen path).
- runner-credentials.ts extracted from local-job.ts so the macOS path
  can reuse the same deterministic .runner/.credentials generation.

Concurrency capped at 2 VMs by default (AGENT_CI_MACOS_VM_CONCURRENCY).
Image override via AGENT_CI_MACOS_VM_IMAGE.

Closes #258.

* feat(cli): say *why* macOS jobs skip on incapable hosts

Before: the skip warning for macos-* jobs on a host that can't run the
VM just pointed at #258 and parroted the pre-Phase-3 line about only
running in a Linux container — which is no longer accurate now that
macOS jobs *can* run locally when the host supports it.

Now: formatUnsupportedOSWarning accepts the HostCapability result and
prints the specific reason + install hint when available — e.g.
'tart not installed' with a brew command, or 'requires Apple Silicon
(got x64)'.

* refactor(cli): deduplicate runs-on classification + warning body

- cli.ts: extract classifyJob(ej) + canRunMacosHere(kind) so the
  skip path and the executor-routing path share one source of truth
  for 'should this job run in the macOS VM?' — the two previously
  duplicated the parseJobRunsOn + classifyRunsOn + capability check.
- runs-on-compat.ts: flatten formatUnsupportedOSWarning to a single
  body array per kind, filter undefined, then uniformly indent —
  drops the optional-hint if-branch and the per-push indent literals.

Pure refactor; all 14 runs-on-compat tests still pass, warning output
is byte-identical across all 4 permutations (macOS with/without
capability, with/without hint, windows).

* docs: document macOS VM runner support

- README: add tart + sshpass to 'Optional' prerequisites and a new
  'macOS jobs' section (image table, env vars, caveats).
- compatibility.md: unstale the three entries that claimed Linux-only
  (jobs.<id>.runs-on, runner.os, runner.arch). runs-on now reads as a
  partial (⚠️) that explains linux/macos/windows separately.

Leaves apps/website/CompatibilityMatrix.tsx alone — it has the same
stale rows but is in an unrelated mid-merge state on this branch.

* chore: oxfmt packages/cli/README.md and compatibility.md
2026-04-18 12:32:35 +02:00
github-actions[bot] b20187ad60 chore: version packages (#272)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.11.0 dtu-github-actions@0.11.0
2026-04-18 08:33:35 +02:00
Peter Pistorius 9474fb5b10 Skip jobs with unsupported runs-on OS instead of running them on Linux (#273)
Jobs with runs-on: macos-* or windows-* were silently routed to the
Linux runner container and failed at the first OS-specific step.

Parse runs-on (string, array, or {group,labels} form), classify the
OS, and skip the job with a clear stderr warning when it is not Linux.
Linux jobs are unchanged. macOS support lands later via tart (#258).
2026-04-18 08:32:15 +02:00
Peter Pistorius b6b9310bcc Fix docker/setup-buildx-action permission denied on native Linux Docker (#257) (#267)
* test: smoke reproducer for #257 docker/setup-buildx-action

* chore: retrigger CI

* ci: switch smoke to pull_request so it triggers from the PR branch

* ci(smoke): dump agent-ci step logs + upload as artifact on failure

* fix: use MAYBE_SUDO to chmod bind-mounted docker socket (#257)

On native Linux Docker the socket is owned root:docker 0660. The runner
user isn't in the host's docker group, so the existing plain 'chmod 666
/var/run/docker.sock' silently failed — leaving every subsequent Docker
client call (docker/setup-buildx-action@v4, docker login, compose, buildx)
to fail with 'permission denied while trying to connect to the docker
API'. Escalating via MAYBE_SUDO matches the surrounding entrypoint ops.

* ci(smoke): assert against Verify-buildx.log, not top-level agent-ci stdout

---------

Co-authored-by: Peter Pistorius <peter@redwoodjs.com>
2026-04-17 21:29:12 +02:00
Peter Pistorius f069ecad5a Add pi harness support: /skill:agent-ci-dev + extension (#268)
* Add pi-native validate skill mirroring .claude/commands/validate.md

Pi (the coding agent harness) doesn't read Claude Code's commands/
directory, so contributors using pi didn't have access to /validate.
This adds .pi/skills/validate/SKILL.md with the same background-run +
monitor + retry workflow, invokable as /skill:validate.

Tooling-only change: no code in packages/, no changeset needed.

* Symlink AGENTS.md -> CLAUDE.md

Pi and other non-Claude agent harnesses (Codex, etc.) look for
AGENTS.md. Symlinking keeps a single source of truth in CLAUDE.md
while making the same instructions discoverable to every harness.

* Rename pi validate skill to agent-ci-dev

Matches the underlying command (pnpm agent-ci-dev) and disambiguates
from the published agent-ci skill, which wraps npx @redwoodjs/agent-ci
for downstream users. Description now explicitly calls out the
distinction.

* Generalize CLAUDE.md for AGENTS.md symlink; reference agent-ci-dev pi skill

- Retitle and add a preamble noting the file is shared via AGENTS.md
  symlink, so it's now the single source of truth for all harnesses.
- CI section now references both the Claude Code /validate command
  and the pi /skill:agent-ci-dev skill, so pi contributors follow the
  same background-monitor-retry workflow Claude Code contributors do.

* Rename Claude Code /validate to /agent-ci-dev

Aligns the Claude Code slash command name with the pi skill
(/skill:agent-ci-dev) and the underlying package command
(pnpm agent-ci-dev). Backward compat preserved via the existing
aliases: [validate] frontmatter, so /validate still resolves.

* Add pi extension: bash_background + monitor_wait tools

Pi's native bash is synchronous-only, and pi doesn't ship a Monitor
tool. Previously the agent-ci-dev skill assumed Claude Code's
run_in_background Bash + streaming Monitor, which don't exist in pi.

This adds .pi/extensions/background-shell.ts, a pi extension that
registers two tools:

  - bash_background  spawn a shell command; return taskId + outputFile
                     + pid immediately. stdout+stderr tee'd to a log.
  - monitor_wait     block until a regex matches new log output, OR
                     the task exits, OR timeoutMs elapses. Tracks
                     per-task byte offsets across calls.

The skill is rewritten to drive these tools. Zero npm dependencies at
runtime (Node built-ins only, ExtensionAPI imported type-only, JSON
Schema written inline), so no npm install step is needed after clone.

Tooling-only change, no changeset required.
2026-04-17 21:18:35 +02:00
Peter Pistorius 372a47b295 feat(cli): support env: at workflow and job level (#271)
The workflow parser only read env: declared directly on a step.
Workflow-level and job-level env: blocks were silently ignored, so
any workflow that relied on them — including the smoke-vars-preflight
smoke test that uses a job-level env with a vars.X reference — saw
empty values at runtime.

The parser now merges env: from all three levels per the GitHub
Actions spec: workflow → job → step, step-most-specific wins.
Expressions (${{ vars.X }}, ${{ secrets.X }}, etc.) are expanded on
each value with the full context, so var/secret references resolve
identically regardless of which level declares the env.

Implementation:
  * New internal buildStepEnv() helper in workflow-parser.ts that
    merges the three levels and runs expandExpressions per value.
  * Both the run: and uses: step branches now call it, dropping
    the duplicated inline Env-construction logic.
  * 5 new unit tests covering job-only, workflow-only, three-level
    precedence, vars expansion in job env, and the no-env case.

Fixes the Case 3 failure in .github/workflows/smoke-vars-preflight.yml
that #270 uncovered — Case 3 had been silently passing via env leak
from the outer runner process, not via actual var substitution.
2026-04-17 21:13:27 +02:00
Peter Pistorius a6f52d55af fix(smoke-vars-preflight): make generated test.yml retain literal vars refs (#270)
Two bugs in the smoke test, both making it fail every run since #260:

1. The header comment contained `${{ vars.FOO }}` inside backticks.
   extractVarRefs() is a text-level regex scan and doesn't understand
   YAML comments, so the outer preflight falsely required `--var FOO`
   before the workflow would even start. Rephrase the comment to just
   `vars.FOO`.

2. The heredoc that writes the inner test.yml used
   `env: API_URL: ${{ vars.API_URL }}`. GitHub Actions' template engine
   processes `${{ ... }}` in every `run:` script regardless of shell
   heredoc quoting (`<< 'WF'` doesn't protect against it), so the
   expression was being substituted at outer-run time — stripping the
   vars.X references the inner agent-ci needs to exercise preflight.
   Result: every Case 1 (no --var flags) run saw test.yml with no
   refs, preflight passed, the workflow ran with empty env, and the
   assertion against 'https://api.example.com' failed.

Fix: build the literal '${{' at shell runtime from non-triggering parts
(D='$' + "{{") so the outer template engine never sees the pattern in
source. Switch heredoc to unquoted (<<WF) so the shell variable expands,
and escape \/\ to keep them literal for the inner
job's shell.

Validated via `pnpm agent-ci-dev run --workflow smoke-vars-preflight.yml
--var API_URL=https://api.example.com --var DEPLOY_ENV=production`:
all three cases now pass (14s).

No changeset: workflow-only change.
2026-04-17 20:44:07 +02:00
Peter Pistorius 4a5c5f8cf9 fix(website): run oxfmt on home.md to fix pnpm check (#269)
#266 merged an edit to home.md that didn't match oxfmt's canonical
markdown table formatting, leaving `pnpm format:check` (and therefore
`pnpm check`) red on main. Run `pnpm exec oxfmt` to repair.

No changeset: apps/website is private.
2026-04-17 20:39:30 +02:00
Peter Pistorius b54494314c docs: document website release command (CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_ENV) (#265)
Adds a 'Releasing the Website' section to RELEASING.md covering the
exact command used to deploy agent-ci.dev:

  cd apps/website
  CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_ENV=production pnpm release

The env vars are required because the Wrangler OAuth user has access
to multiple Cloudflare accounts, and the deploy script runs
non-interactively.

No changeset: docs-only, does not touch packages/.
2026-04-17 17:51:38 +02:00
Peter Pistorius 51dbd48841 feat(website): serve markdown version of homepage via Accept negotiation (#266)
Implements the 'Markdown for Agents' agent-readiness goal for '/' without
relying on any Cloudflare dashboard feature flag.

- apps/website/src/app/pages/home.md — hand-authored markdown version of
  the homepage. Same content as home.tsx (hero, principles, architecture
  comparison, quick start, agent instructions, testimonials) but without
  the React-only decorative bits (CRT effects, sticky nav, avatars).
  Lives next to home.tsx so they stay in sync through review.

- apps/website/src/app/lib/markdown-negotiation.ts — Accept-header parser
  and route middleware factory. Returns markdown only when the client
  explicitly lists 'text/markdown' and its q-value beats text/html
  (RFC 9110 §12.5.1). Sets Vary: Accept so caches key correctly. Bare
  '*/*' is treated as browser default -> HTML, matching Cloudflare's
  'Markdown for Agents' semantics.

- apps/website/src/worker.tsx — wire the middleware into route('/') via
  the array form: [serveMarkdownIfPreferred(homeMarkdown), Home]. The
  React component renders normally unless the middleware short-circuits.

- apps/website/types/vite.d.ts — declare *?raw module so the markdown
  import type-checks.

Verified against 'npm run preview':
  Accept: text/html, ... */*;q=0.8     -> text/html
  Accept: text/markdown                 -> text/markdown; charset=utf-8
  Accept: text/markdown;q=0.9, ... q=.8 -> text/markdown
  Accept: text/html, text/markdown;q=.5 -> text/html
  Accept: */*                           -> text/html

No changeset: change is entirely under apps/website/; does not touch packages/.
2026-04-17 17:46:54 +02:00
Peter Pistorius 3b16523aea fix: handle 404 on remote workflow fetch as private-repo auth failure (#261)
* kindling: start task [skip ci]

* harness: save uncommitted work from Analyst (cycle 1) [skip ci]

* Fix misleading 401/403 error hint when GitHub token is already provided

When fetching a remote workflow from a private repo with a token supplied,
a 401 or 403 from GitHub now says the token may be invalid, expired, or
lack the required 'repo' scope — instead of repeating instructions to use
--github-token that the user already followed.

Added two tests covering the with-token 401 and 403 paths, which were
previously uncovered.

Fixes #255

* harness: save uncommitted work from Developer (cycle 2) [skip ci]

* harness: save uncommitted work from Reviewer (cycle 3) [skip ci]

* harness: pre-DONE sweep [skip ci]

* kindling: ready for CI

* harness: save uncommitted work from Analyst (cycle 1) [skip ci]

* harness: save uncommitted work from Developer (cycle 2) [skip ci]

* harness: save uncommitted work from Reviewer (cycle 3) [skip ci]

* harness: save uncommitted work from Developer (cycle 4) [skip ci]

* harness: save uncommitted work from Reviewer (cycle 5) [skip ci]

* harness: pre-DONE sweep [skip ci]

* fix: handle 404 on remote workflow fetch as private-repo auth failure

GitHub's Contents API returns HTTP 404 (not 401/403) when a caller lacks
access to a private repository — this is intentional to avoid leaking repo
existence. The previous error hint only covered 401/403, so users hitting
the most common private-repo failure modes (missing token, classic PAT
without 'repo' scope, fine-grained PAT without 'contents: read', unauthorized
SSO PAT) saw a bare "HTTP 404" with no guidance.

Unify hint generation into buildAuthHint() covering 401/403/404 × with/
without token. Hints now include explicit gh auth login instructions and
spell out the token-insufficient scenarios.

Verified end-to-end against a private repro repo (peterp/agent-ci-private):
no-token → 404 path now emits full auth guidance, invalid-token → 401 path
emits token-insufficient hint, valid-token → resolves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: smoke test for private-repo remote workflow fetch (#261)

Adds .github/workflows/smoke-remote-private-workflow.yml — the end-to-end
verification workflow used to reproduce and prove the #261 fix, now preserved
as a regression-guard smoke test.

It references a reusable workflow in a private repo
(peterp/agent-ci-private/.github/workflows/lint.yml@main). Under agent-ci this
exercises prefetchRemoteWorkflows against the GitHub Contents API with the
supplied token, covering the success path (HTTP 200 from a private repo when
a valid token is attached) that the unit tests in
packages/cli/src/workflow/remote-workflow-fetch.test.ts don't cover.

Verified locally with 'pnpm agent-ci-dev run --workflow … --github-token':

    Status:    ✓ 2 passed (2 total)
    Duration:  9s

Without a token, the same smoke surfaces the improved #261 hint chain verbatim
(HTTP 404 → 'The repository or ref was not found …' → auth instructions).

---------

Co-authored-by: kindling <kindling@harness>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:26 +02:00
Peter Pistorius b9dbddcf7b feat(website): publish robots.txt, sitemap.xml, Link headers, and agent-skills index (#264)
Address agent-readiness findings from isitagentready.com:

- apps/website/public/robots.txt — RFC 9309 crawl rules with explicit
  entries for GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web,
  anthropic-ai, Google-Extended, PerplexityBot, Applebot-Extended, CCBot,
  Meta-ExternalAgent, Bytespider, DuckAssistBot (and standard search bots),
  a Sitemap reference, and Content Signals
  (search=yes, ai-input=yes, ai-train=no).

- apps/website/src/app/pages/sitemap.ts — dynamic /sitemap.xml handler
  registered outside render(Document, ...) so it returns application/xml.
  Enumerates static routes, the copied CLI docs, and blog posts from
  content-collections with lastmod.

- apps/website/src/app/headers.ts — adds a RFC 8288 Link response header
  on all routes: sitemap, agent-skills (extension), service-doc.

- apps/website/scripts/copy-docs.mjs — sync script that copies the public
  CLI docs (README.md, compatibility.md, runner-image.md, SKILL.md) into
  apps/website/public/docs/ and regenerates
  apps/website/public/.well-known/agent-skills/index.json with fresh
  sha256 digests. Hooked into predev/prebuild so the website's static
  tree cannot drift from the CLI.

The generated public/docs/ and public/.well-known/agent-skills/ trees are
gitignored.

Verified end-to-end against 'npm run preview': all endpoints return the
expected status, content-type, and body, and the Link header appears on
both HTML and XML responses.

No changeset: this change is entirely under apps/website/ and does not
affect published packages (per .claude/rules/changeset-required.md).
2026-04-17 16:43:44 +02:00
Peter Pistorius 2bb4e572e3 Add --var flag so workflow vars work locally (#260)
* kindling: start task [skip ci]

* Add behavioral specs and tests for vars context support (SPEC-V-001 through SPEC-V-015)

Tests cover loading vars from dotenv file, env-var allowlist fallback, file-over-env
precedence, extracting ${{ vars.X }} refs from workflow YAML (whole-file and job-scoped),
validating that all referenced vars are supplied (error names missing keys and config file
path), and that workflows with no vars refs run without any config present.  Tests mirror
the existing secrets test conventions and will pass once the implementation (Track A) is
in place.

* harness: save uncommitted work from Developer+QA (cycle 1) [skip ci]

* harness: save uncommitted work from Reviewer (cycle 2) [skip ci]

* harness: save uncommitted work from Developer (cycle 3) [skip ci]

* harness: save uncommitted work from Reviewer (cycle 4) [skip ci]

* Add vars context loading, extraction, validation, and expression substitution

- loadMachineVars in config.ts reads .env.agent-ci.vars with the same
  KEY=VALUE parsing and envFallbackKeys allowlist as loadMachineSecrets
- extractVarRefs in workflow-parser.ts scans workflow YAML for
  ${{ vars.NAME }} references using the same pattern as extractSecretRefs
- validateVars throws a descriptive error listing missing var names and
  the expected file path, mirroring validateSecrets
- resolveExprAtom gains a vars? 7th param and a vars. prefix case after
  the secrets. case; all 11 internal evaluateExprValue calls for
  fromJSON/toJSON/format/contains/startsWith/endsWith/join updated
- evaluateExprValue gains vars? 7th param threaded through all 6
  recursive calls (||, &&, !, comparisons, parens) and the
  resolveExprAtom leaf at line 606
- expandExpressions and parseWorkflowSteps each gain vars? as optional
  7th param; all internal expandExpressions call sites updated

* Wire vars context into all three CLI call sites

- Import loadMachineVars, extractVarRefs, validateVars from their
  respective modules
- Single-job path: extract refs, load vars, validate, thread vars
  through expandExpressions (inputsContext expansion) and
  parseWorkflowSteps
- buildJob: extract refs, load vars, validate (no parseWorkflowSteps
  call here, consistent with how secrets are handled)
- runJob: extract refs, load vars, validate, thread vars through
  resolveInputsForJob and parseWorkflowSteps
- resolveInputsForJob: accepts vars? as 4th optional param and threads
  it through its expandExpressions call

* Add Vars section to README and minor-bump changeset

README documents .env.agent-ci.vars file and shell env fallback,
explaining that vars are non-sensitive and can be committed.

Changeset bumps both @redwoodjs/agent-ci and dtu-github-actions minor
as they are in the same fixed versioning group.

* Fix loadMachineVars test helper to write .env.agent-ci.vars

The QA-authored loadMachineVars tests used .env.agent-ci as the
fixture file name, but loadMachineVars reads from .env.agent-ci.vars
per the approved RFC. Updated the writeEnvFile helper and test
description in the loadMachineVars block to match the correct file.

* harness: save uncommitted work from Developer (cycle 5) [skip ci]

* Replace file+env vars sourcing with --var CLI flag

Address PR feedback: workflow vars (${{ vars.FOO }}) are now supplied
exclusively via the repeatable --var KEY=VALUE CLI flag. Removes the
.env.agent-ci.vars file and the shell-env fallback so workflow vars
don't overload shell env vars. Missing vars fail fast with an error
listing the --var flags needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pre-flight missing vars across all workflows before setup

Scan every workflow for ${{ vars.* }} refs at the start of the run
and fail with one combined message listing every missing var across
every file. Happens before Docker prefetch and workspace setup, so
users find out immediately instead of waiting through bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add smoke test for --var pre-flight

Covers three cases against a temp workflow referencing vars.API_URL
and vars.DEPLOY_ENV:

1. No --var flags → exit 1, error lists both vars with --var syntax
2. Partial fill → error lists only the still-missing var
3. All vars supplied → run proceeds and the job succeeds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: kindling <kindling@harness>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:50:50 +02:00
github-actions[bot] e19997e37a chore: version packages (#251)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.10.7 dtu-github-actions@0.10.7
2026-04-15 10:54:37 +02:00
Peter Pistorius e482875a95 fix: setup-node "Bad credentials" + stale-toolcache hint (#252)
* fix: setup-node "Bad credentials" + stale-toolcache hint (#171, #249)

Route setup-node's versions-manifest fetch through the DTU so it no
longer 401s against real api.github.com, and surface an actionable
hint when a stale /opt/hostedtoolcache bind mount blocks tar extract.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: unit + smoke coverage for setup-node manifest mock

Adds unit tests for rewriteSetupNodeTarball and for the cached tree/blob
route paths, plus a nested-agent-ci smoke workflow that fails if
setup-node logs "Bad credentials" (the symptom of the manifest mock
breaking and falling back to nodejs.org).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:46:11 +02:00
Simon 2114d67739 fix: permission errors in direct-container mode on Arch Linux (#248) 2026-04-15 08:26:24 +02:00
Peter Pistorius bcd5f0e356 docs: make packages/cli/README the main README (symlinked from root) (#247)
* docs: promote packages/cli/README as the main README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: symlink root README to packages/cli/README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: reframe Why section — remote CI is the gatekeeper, agent-ci is the pre-flight check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note Docker Desktop is also supported on Linux

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:56:51 +02:00
github-actions[bot] 95b748c344 chore: version packages (#246)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.10.6 dtu-github-actions@0.10.6
2026-04-13 21:57:16 +02:00
Peter Pistorius 64d654d768 fix: auto-pull runner image with progress on first run (#242) (#245)
First-time users saw a frozen spinner or "No such image" error because
the runner image pull was silent and failures were only debug-logged.

Now the CLI detects a missing image, shows download/extraction progress,
and surfaces pull errors visibly.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:55:53 +02:00
github-actions[bot] ccf77636f9 chore: version packages (#244)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.10.5 dtu-github-actions@0.10.5
2026-04-13 19:58:05 +02:00
Peter Pistorius 9f7bc6feeb fix: include default_branch in event.repository payload (#241) (#243)
Actions like dorny/paths-filter read github.event.repository.default_branch
to determine the base branch. The DTU was already passing this value in
Resources.Repositories but omitting it from the event context.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:10:18 +02:00
Peter Pistorius 2e2bd5e124 fix: always show workflows and jobs in --all mode (#240)
* fix: always show workflows and jobs in --all mode, fix duplicate matrix jobs

Two problems fixed:

1. Jobs would disappear. When running --all, jobs under a workflow
   (like install-a, install-b, install-c) would only show up while
   the workflow was running. Before it started or after it finished,
   they'd vanish. Now they always stay visible.

2. Matrix jobs showed up twice. When a workflow had matrix shards
   (like 3 copies of the same job), the display would show 5 entries
   instead of 3. This happened because each job got two different
   names — one when we reserved its spot, and a different one when
   it actually started running. The second name didn't match the
   first, so the system thought they were different jobs. Now both
   use the same name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce spinner flicker in --all mode with line-diff renderer

Replace log-update's full erase-and-rewrite approach with a line-level
diff renderer that only sends ANSI escape codes for changed lines. For a
30-line tree where only spinner characters change, this skips ~90% of
terminal I/O per frame.

Also make job cleanup non-blocking: convert fs.rmSync to fs.promises.rm
and store.save() to async fs operations so completing runners don't
freeze the event loop (and stall all other spinners).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: unblock event loop during workspace prep to keep spinners smooth

Convert initFakeGitRepo (7 sequential execSync calls) and the
git-archive path in prepareWorkspace to use promisified exec. Each
await yields to the event loop between git process spawns, letting
the render timer fire instead of freezing all spinners for ~200ms.

Also convert the chmod -R 777 call to async exec, and wrap diff-renderer
updates in DEC synchronized output markers so terminals that support it
batch the entire frame into a single repaint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: return Promise from save() and update tests to await it

The async save() was fire-and-forget, so tests checking the file
immediately after save() failed. Return the Promise so callers
(including tests) can await completion when needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:59:05 +02:00
github-actions[bot] c61f27d773 chore: version packages (#239)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@redwoodjs/agent-ci@0.10.4 dtu-github-actions@0.10.4
2026-04-13 13:22:55 +02:00
Peter Pistorius 25c1c5decd fix: Cypress install fails with EACCES on /home/runner/.cache (#238)
* test: add smoke test reproducing Cypress cache permission failure (#234)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make /home/runner/.cache world-writable so Cypress can install (#234)

The Playwright bind mount causes Docker to create /home/runner/.cache as
root, so Cypress postinstall fails with EACCES when it tries to mkdir
/home/runner/.cache/Cypress. chmod 1777 in the entrypoint fixes it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:21:47 +02:00