mirror of
https://github.com/redwoodjs/local-ci.git
synced 2026-09-19 07:54:29 +08:00
417bd260b6
* Support private workflow container images * Resolve private image expression contexts * feat: rename Agent CI to Local CI
57 KiB
57 KiB
YAML Compatibility
Local CI aims to run real GitHub Actions workflows locally. The table below shows current support against the official workflow syntax.
✅ = 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 Local 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 LOCAL_CI_GITHUB_TOKEN). inputs:/outputs: passing and nested reusable workflows are supported |
on (other events) |
🟡 | Covers every GitHub-Actions webhook event Local 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 local-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. Local 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 to add system tools. macos-* runs in a real macOS VM on Apple Silicon hosts with 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. Local 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. Local 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 local-ci's own container orchestration. |
jobs.<id>.container.credentials |
✅ | Registry username / password credentials are expanded from workflow contexts and passed to Docker only when a missing private image must be pulled. |
jobs.<id>.services |
✅ | Sidecar containers with image, env, ports, and options |
jobs.<id>.services.*.credentials |
✅ | Each service can provide registry username / password credentials. Expressions are expanded before Docker pulls a missing image. |
jobs.<id>.uses (reusable workflows) |
✅ | Local refs (./) are expanded inline. Remote refs are fetched from GitHub (requires --github-token or LOCAL_CI_GITHUB_TOKEN; public repos may work without auth). with: (inputs) and secrets: pass-through are supported |
jobs.<id>.secrets |
🚫 | Local CI cannot access GitHub's secret storage. Secrets are resolved from a .env.local-ci file at the project root or from shell environment variables as fallback. --github-token / LOCAL_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 Local 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 — local-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.local-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 Local 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 Local 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 LOCAL_CI_GITHUB_TOKEN to make it available as secrets.GITHUB_TOKEN. GitHub API calls from the runner are answered locally by Local 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 |