Files
redwoodjs__local-ci/packages/cli/compatibility.md
2026-05-19 14:38:33 +02:00

126 lines
57 KiB
Markdown

# YAML Compatibility
Agent CI aims to run real GitHub Actions workflows locally. The table below shows current support against the [official workflow syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions).
✅ = Supported   ⚠️ = Partial   ❌ = Not supported   🟡 = Ignored (no-op)   🚫 = Not planned
## Workflow-Level Keys
| Key | Status | Notes |
| ------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | ✅ | |
| `run-name` | 🟡 | Parsed but not displayed anywhere |
| `on` (push, pull_request) | ✅ | Branch and path filters are evaluated when using `--all` |
| `on.<event>.branches` / `branches-ignore` | ✅ | Branch-name glob filters; `--all` only runs workflows whose filter matches the current branch (`main` for non-PR runs). |
| `on.<event>.paths` / `paths-ignore` | ✅ | Path-pattern filters. With `--all`, a workflow is skipped if none of the changed files match `paths:` or all of them match `paths-ignore:`. |
| `on.<event>.tags` / `tags-ignore` | ❌ | Tag-pattern filters for push events are not evaluated — `--all` treats a tagged run as an untagged push. Workflows gated only by tag patterns will run when you don't expect them to. |
| `on.<event>.types` | 🟡 | Activity-type filters (e.g. `pull_request.types: [opened, synchronize]`) are parsed but not applied — `--all` runs the workflow regardless of event sub-type. |
| `on.workflow_dispatch.inputs` | 🟡 | `workflow_dispatch` itself is not simulated, so its declared inputs are parsed but unreachable — the CLI cannot inject them. |
| `on.workflow_call.inputs.*` (`type` / `required` / `default`) | ✅ | Reusable workflows resolve caller-side `with:` values against the callee's `inputs:` declarations, honoring `default:` when a caller omits an input. |
| `on.workflow_call.outputs.*.value` | ✅ | Reusable-workflow callees can expose `outputs.<name>.value: ${{ jobs.<id>.outputs.<name> }}`, and callers read them the same way as a normal `needs.*.outputs.*`. |
| `on` (schedule, workflow_dispatch) | 🟡 | Accepted without error, but Agent CI does not simulate event triggers — workflows must be run manually |
| `on` (workflow_call) | ✅ | Local reusable workflows (`uses: ./.github/workflows/...`) are inlined into the caller's dependency graph. Remote refs are fetched from GitHub (requires `--github-token` or `AGENT_CI_GITHUB_TOKEN`). `inputs:`/`outputs:` passing and nested reusable workflows are supported |
| `on` (other events) | 🟡 | Covers every GitHub-Actions webhook event Agent CI does not simulate (`issues`, `issue_comment`, `label`, `pull_request_review`, `release`, `registry_package`, `status`, `deployment`, `deployment_status`, `fork`, `watch`, `star`, `discussion`, `milestone`, `project`, `repository_dispatch`, `check_run`, `check_suite`, `workflow_run`, etc.). The parser accepts the event name but the run is not triggered — the workflow only ever runs when invoked manually via `pnpm agent-ci run`. |
| `env` | ✅ | Workflow-level env is propagated to all steps |
| `defaults.run.shell` | ✅ | Non-bash shells (`sh`, `python`, `pwsh`) are invoked via a parse-time heredoc wrap because the runner ignores `inputs.shell` for script steps. `bash` is the default. |
| `defaults.run.working-directory` | ✅ | Passed through to the runner |
| `permissions` | 🟡 | Accepted but not enforced — the mock GITHUB_TOKEN has full access |
| `concurrency` | 🚫 | Concurrency groups are a GitHub-side queuing and cancellation mechanism. Agent CI has no persistent server to track group state across runs, so this cannot be implemented locally |
## Job-Level Keys
| Key | Status | Notes |
| ------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jobs.<id>` | ✅ | Multiple jobs in a single workflow |
| `jobs.<id>.name` | ✅ | |
| `jobs.<id>.needs` | ✅ | Jobs are sorted topologically into dependency waves |
| `jobs.<id>.if` | ✅ | `success()`, `failure()`, `always()`, `cancelled()`, `==`/`!=`, `&&`/`\|\|`, `!`, `needs.*.outputs.*`, `needs.*.result` |
| `jobs.<id>.runs-on` | ⚠️ | `ubuntu-*` / `linux` (and unspecified `self-hosted` / custom labels) run in a Linux container — the default image is minimal, see [runner-image.md](./runner-image.md) to add system tools. `macos-*` runs in a real macOS VM on Apple Silicon hosts with [tart](https://github.com/cirruslabs/tart) + `sshpass` installed; other hosts skip the job with a reason. `windows-*` is not yet supported and skips on every host. When a label declares a larger runner spec (e.g. `ubuntu-latest-8-cores`) than the local machine provides, the job is tagged `degraded` and a warning is printed before it starts — execution is not blocked, only annotated. |
| `jobs.<id>.environment` | 🟡 | Accepted but not enforced — environment protection rules are GitHub-side only. Object form (`environment.name` / `environment.url`) is also accepted and ignored. |
| `jobs.<id>.permissions` | 🟡 | Accepted but not enforced — job-level permission scopes have no effect because the mock `GITHUB_TOKEN` has full access to the local API emulation. |
| `jobs.<id>.env` | ✅ | |
| `jobs.<id>.defaults.run` | ✅ | `shell` and `working-directory` |
| `jobs.<id>.outputs` | ✅ | Resolved after each job completes and accumulated across dependency waves |
| `jobs.<id>.timeout-minutes` | ❌ | Not implemented. Agent CI's pause-on-failure model is the intended way to handle long-running steps — a hard timeout would destroy the container state that makes local debugging possible |
| `jobs.<id>.continue-on-error` | ❌ | Not implemented. Agent CI pauses on failure so you can inspect and fix the container in place; `continue-on-error` would skip past failures and discard that debugging opportunity |
| `jobs.<id>.concurrency` | 🚫 | See workflow-level `concurrency` above |
| `jobs.<id>.container` | ⚠️ | Short and long form; `image`, `env`, `ports`, `volumes` honored. Within `options:`, only `--env`/`-e` and `--label`/`-l` are forwarded to the runner container; other Docker flags (`--privileged`, `--user`, `--network`, `--cap-add`, `--workdir`, etc.) are silently ignored because they tend to clash with agent-ci's own container orchestration. |
| `jobs.<id>.container.credentials` | ❌ | Registry `username` / `password` credentials for pulling a private container image are not parsed. Private images fail to pull unless the host's docker already has credentials for the registry (e.g. `docker login` was run out-of-band). |
| `jobs.<id>.services` | ✅ | Sidecar containers with image, env, ports, and options |
| `jobs.<id>.services.*.credentials` | ❌ | Per-service registry credentials are not parsed — see `jobs.<id>.container.credentials`. Private service images fail to pull unless the host's docker has credentials out-of-band. |
| `jobs.<id>.uses` (reusable workflows) | ✅ | Local refs (`./`) are expanded inline. Remote refs are fetched from GitHub (requires `--github-token` or `AGENT_CI_GITHUB_TOKEN`; public repos may work without auth). `with:` (inputs) and `secrets:` pass-through are supported |
| `jobs.<id>.secrets` | 🚫 | Agent CI cannot access GitHub's secret storage. Secrets are resolved from a `.env.agent-ci` file at the project root or from shell environment variables as fallback. `--github-token` / `AGENT_CI_GITHUB_TOKEN` auto-populates `secrets.GITHUB_TOKEN`. All are injected as `${{ secrets.* }}` expressions |
## Strategy / Matrix
| Key | Status | Notes |
| ------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `strategy.matrix` | ✅ | Cartesian product of all array values is fully expanded |
| `strategy.matrix.include` | ❌ | Not implemented. The matrix parser only processes array-valued keys; `include` entries (which are objects) are silently dropped. Adding support would require post-processing the Cartesian product |
| `strategy.matrix.exclude` | ❌ | Not implemented — same reason as `include`. `exclude` entries are objects and are dropped by the array-only parser |
| `strategy.fail-fast` | ✅ | Setting `fail-fast: false` allows remaining matrix jobs to continue after a failure |
| `strategy.max-parallel` | ❌ | Not implemented. Parallelism is controlled by Agent CI's host-level concurrency limiter (based on CPU count), not per-workflow job limits |
## Step-Level Keys
| Key | Status | Notes |
| ------------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `steps[*].id` | ✅ | |
| `steps[*].name` | ✅ | Expression expansion in names |
| `steps[*].if` | ⚠️ | The condition is passed to the official runner binary, which evaluates it at runtime. Limitation: `steps.*.outputs.cache-hit` and similar outputs resolve to an empty string at parse time because prior steps have not yet run when the workflow is parsed |
| `steps[*].run` | ✅ | Multiline shell scripts with `${{ }}` expression expansion |
| `steps[*].uses` | ✅ | Public actions are downloaded via the GitHub API |
| `steps[*].uses` (local `./`) | ✅ | Local actions (e.g. `uses: ./.github/actions/my-action`) are resolved from the workspace |
| `steps[*].uses` (`docker://…`) | ❌ | Docker-image action references (`uses: docker://alpine:3.19`) are not resolved — agent-ci treats every `uses:` as a repository action. The step fails with an action-not-found error rather than running the image. |
| `steps[*].with` | ✅ | Expression expansion in values |
| `steps[*].env` | ✅ | Expression expansion in values |
| `steps[*].working-directory` | ✅ | |
| `steps[*].shell` | ✅ | See `defaults.run.shell` above — the same heredoc-wrap mechanism applies. |
| `steps[*].continue-on-error` | ❌ | Not implemented — see `jobs.<id>.continue-on-error` above for the reasoning |
| `steps[*].timeout-minutes` | ❌ | Not implemented — see `jobs.<id>.timeout-minutes` above for the reasoning |
## Expressions (`${{ }}`)
| Key | Status | Notes |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hashFiles(...)` | ✅ | SHA-256 of matching files; supports multiple glob patterns. Descends into dotted directories (e.g. `.github/`) when the pattern asks for them. |
| `format(...)` | ✅ | Template substitution with recursive expression expansion |
| `matrix.*` | ✅ | |
| `secrets.*` | ✅ | Resolved from `.env.agent-ci` at the project root, with shell environment variables as fallback. `--github-token` auto-populates `secrets.GITHUB_TOKEN` |
| `vars.*` | ✅ | Repository / organization / environment variables. Resolved from `--var NAME=VALUE` CLI flags or JSON supplied via `--var-file <path>` / `--var-file -`; unresolved `vars.*` references fail the run at pre-flight with a list of every missing name. |
| `env.*` | ✅ | The merged step environment (workflow-level → job-level → step-level, with step taking precedence) is available as `env.*` in `with:` inputs, `run:` scripts, `name:`, and `env:` value expressions. Not yet supported in `if:` conditions (the expression is evaluated by the runner, not the parser). Note: `env.*` references inside an `env:` block (one env var referencing another declared in the same block) are not evaluated — only the already-computed merged value is exposed. |
| `inputs.*` | ✅ | Reusable-workflow and (nominally) `workflow_dispatch` input values. Inside a called workflow, `inputs.<name>` resolves to the caller's `with:` value or the declared `default:`. |
| `runner.os` | ✅ | `Linux` for container-based jobs, `macOS` for jobs routed to the macOS VM |
| `runner.arch` | ✅ | `X64` for container-based jobs, `ARM64` for macOS VM jobs (Apple Silicon) |
| `runner.name`, `runner.temp`, `runner.tool_cache`, `runner.debug`, `runner.environment` | ❌ | Only `runner.os` and `runner.arch` are populated; the rest of the `runner.*` context resolves to an empty string. Workflows that read `runner.tool_cache` (common in tool-setup actions) will see an empty value — the actions themselves typically fall back to the `RUNNER_TOOL_CACHE` env var which the runner does set. |
| `github.sha` | ✅ | Real commit SHA from `HEAD`. When the working tree is dirty, a synthetic tree SHA is computed so that expressions that hash-seed from `github.sha` still differ between working-tree states. |
| `github.repository`, `github.repository_owner` | ✅ | Derived from the git remote origin URL (`owner/repo` and `owner`). |
| `github.ref`, `github.ref_name`, `github.head_ref`, `github.base_ref`, `github.actor`, `github.run_id`, `github.run_number`, and other `github.*` fields | ⚠️ | Returned as static defaults: `ref_name` / `head_ref` default to `main`, `base_ref` is empty, `actor` is the repo owner, `run_id` / `run_number` are `1`, `api_url` / `server_url` point at Agent CI's local API emulation. Anything not in this row or one of the ones above resolves to an empty string — that covers `action_path`, `workflow_ref`, `workflow_sha`, `triggering_actor`, `retention_days`, `secret_source`, `token`, `graphql_url`, `job`, `run_attempt`, `ref_protected`, `ref_type`, `env`, `path`, and the rest of the context documented by GitHub. |
| `github.event.*` | ⚠️ | All event payload fields return empty strings — no real webhook event is triggered locally |
| `strategy.job-total`, `strategy.job-index` | ✅ | |
| `steps.*.outputs.*` | ⚠️ | Always resolves to an empty string at parse time — the producing step has not run yet and the runner does not re-evaluate `${{ }}` inside `run:` script bodies. Use `needs.*.outputs.*` for cross-job values instead. |
| `steps.*.conclusion` / `steps.*.outcome` | ❌ | Step result states are not exposed — neither `conclusion` nor `outcome` resolves against a meaningful value. Workflows that branch on prior-step status via these context variables will always evaluate as if the step didn't produce a result. |
| `job.*` (`job.status`, `job.container.id`, `job.services.*`) | ❌ | The per-job runtime context is not populated. Expressions that read `job.status` etc. resolve to empty strings. |
| `*` object-filter operator | ❌ | Array-of-object filtering like `github.event.commits.*.author.name` is not implemented — the entire expression resolves to an empty string. |
| `needs.*.outputs.*` | ✅ | Resolved after dependency jobs complete. The needs context is built from actual job outputs and passed into subsequent job evaluation |
| Boolean/comparison operators | ✅ | `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `\|\|`, `!`, parentheses. Case-insensitive string comparison. Numeric coercion follows GitHub Actions semantics: empty string and `null` become `0`, numeric-looking strings are parsed, anything else becomes `NaN` and compares false. |
| `toJSON`, `fromJSON` | ✅ | `toJSON` emits 2-space-indented JSON matching GitHub Actions' pretty-printed output, so `hashFiles`/diff uses of `toJSON(x)` are reproducible. |
| `contains(search, item)` | ✅ | Case-insensitive substring for strings; element membership for JSON arrays (e.g. `contains(fromJSON('["a","b"]'), 'b')`). The `*` object-filter form (`contains(labels.*.name, 'bug')`) is not supported — see the `*` object-filter row. |
| `startsWith(searchString, searchValue)` | ✅ | Case-insensitive. |
| `endsWith(searchString, searchValue)` | ✅ | Case-insensitive. |
| `join` | ✅ | Joins JSON arrays with a separator (default `, `) |
| `success()`, `failure()`, `always()`, `cancelled()` | ⚠️ | `success()` / `failure()` / `always()` are evaluated against the accumulated job status in dependency waves. `cancelled()` always returns `false` locally — there is no cancellation signal for Agent CI to observe, so steps gated by `if: cancelled()` never run. |
| Type literals (`true`, `false`, `null`, numbers) | ✅ | Boolean, null, and numeric literals supported in expressions |
## GitHub API Features (DTU Mock)
| Key | Status | Notes |
| ------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Action downloads | ✅ | Action tarballs are resolved and downloaded from github.com |
| `actions/cache` | ⚠️ | Cache is stored on the local filesystem via bind-mount, giving ~0 ms round-trip on cache hits. Scoping is by cache key only — GitHub's ref-based isolation (a branch cannot read another branch's cache, default branch is a fallback) is not implemented. If two workflows on different refs share a key they share the cache. |
| `actions/checkout` | ✅ | The workspace is rsynced into the container with `clean: false` to preserve local changes |
| `actions/setup-node`, `actions/setup-python`, etc. | ✅ | Tool setup actions run natively inside the runner container |
| `actions/upload-artifact` / `download-artifact` | ✅ | Artifacts are stored on the local filesystem |
| `GITHUB_TOKEN` | ⚠️ | Not injected automatically. Pass `--github-token` or set `AGENT_CI_GITHUB_TOKEN` to make it available as `secrets.GITHUB_TOKEN`. GitHub API calls from the runner are answered locally by Agent CI's API emulation layer. OIDC ID-token issuance (`permissions.id-token: write` → federated cloud auth) is not implemented — actions that exchange the OIDC token with AWS/GCP/Vault will fail. |
| Workflow commands (`::set-output::`, `::error::`, etc.) | ✅ | Handled by the official runner binary |