# Action reference

> Markdown source of https://docs.magmamoose.com/tremvok/action-reference/.

<!-- sources: action.yml -->

Generated by `scripts/gen_action_reference.py`. Edit `action.yml`, not this page.
For the task-shaped version, see [Setup](https://docs.magmamoose.com/tremvok/setup/).

## Targets

`target` is the only required input and it has no default. Everything else is
optional, and every one of them is checked against the target you picked: an input
that belongs to another target is a hard error naming both, before the checkout.

| Target | What it does |
| --- | --- |
| `github-pages` | Build an MkDocs site strictly and publish it to GitHub Pages |
| `cloudflare-docs` | Build an MkDocs site strictly and publish it to Workers Static Assets |
| `s3-cloudfront` | Sync a built static site to S3, invalidate CloudFront |
| `lambda-zip` | Publish a Lambda package to S3, update the function, move an alias |
| `terragrunt` | Discover, plan and (on an approval) apply Terragrunt stacks |
| `ansible` | Run a playbook over SSH, then prove it is idempotent |
| `cloudflare-workers` | Deploy a Worker and its static assets with Wrangler |
| `azure-functions-zip` | Publish a zip to an Azure Function App, then wait for it to answer |

## Inputs

`MagmaMoose/tremvok@v2` takes 121 inputs. `target` is the only one that
is required.

| Input | Applies to | Default | Description |
| --- | --- | --- | --- |
| `target` | the selector | not set | The deployment target. One of: github-pages Build an MkDocs site strictly and publish it to GitHub Pages. s3-cloudfront Sync a built static site to S3 and invalidate CloudFront. lambda-zip Publish a Lambda package to S3, update the function, move an alias. terragrunt Discover, plan and (on an approval) apply Terragrunt stacks. ansible Run an Ansible playbook over SSH, then prove it is idempotent. cloudflare-workers Deploy a Worker and its static assets with Wrangler. cloudflare-docs Build an MkDocs site strictly and publish it to Cloudflare Workers Static Assets, behind the docs router. azure-functions-zip Publish a zip to an Azure Function App, then wait for it to answer. |
| `mode` | all | `auto` | What this run should do. One of: auto (default) push to the default branch = deploy, pull\_request = preview, workflow\_dispatch = deploy (pinned to the default branch). deploy Publish to the environment. preview Publish somewhere disposable; production is untouched. rollback Re-publish a previously published version. |
| `environment` | all | not set | Logical environment name, surfaced in notifications and the deployment record. Defaults to "production" (deploy) or "preview". |
| `working-directory` | all | `.` | Directory to run in. Paths in the other inputs are relative to it. |
| `checkout` | all | `true` | Run actions/checkout first. Set false if the caller already checked out. |
| `dry-run` | all | `false` | Resolve, plan and report without changing anything. |
| `aws-region` | `s3-cloudfront`, `lambda-zip`, `terragrunt` | not set | s3-cloudfront, lambda-zip, terragrunt: AWS region. Falls back to the AWS\_REGION environment variable. |
| `aws-role-to-assume` | `s3-cloudfront`, `lambda-zip`, `terragrunt` | not set | s3-cloudfront, lambda-zip, terragrunt: IAM role ARN to assume with this run's GitHub OIDC token. Strongly preferred over stored keys: the credential expires in an hour and the role's trust policy decides which repository and ref may use it. Requires `permissions: id-token: write`. Leave empty to use credentials an earlier step already configured. |
| `aws-role-duration-seconds` | `s3-cloudfront`, `lambda-zip`, `terragrunt` | `3600` | s3-cloudfront, lambda-zip, terragrunt: lifetime of the assumed-role session. |
| `azure-client-id` | `terragrunt`, `azure-functions-zip` | not set | terragrunt, azure-functions-zip: client id of the Entra ID app registration this run signs in as, using the run's GitHub OIDC token. The Azure counterpart of `aws-role-to-assume`, and preferred over a stored publish profile or client secret for the same reasons: the session is minted per run and expires, and the app registration's federated credential decides which repository and which ref may mint one. A publish profile is a long-lived file carrying the deployment rights of the whole site, tied to nothing. Requires `permissions: id-token: write`. Leave empty to use a session an earlier step created. On `terragrunt` this is the PROVIDER's credential and has nothing to do with the state backend's, which `terragrunt-stack-env` carries. `provider "azurerm"` with no explicit auth falls through its default chain to the Azure CLI, so a run with a perfectly good ARM\_ACCESS\_KEY still fails at plan on a runner nobody signed in. |
| `azure-tenant-id` | `terragrunt`, `azure-functions-zip` | not set | terragrunt, azure-functions-zip: Entra ID tenant the app registration lives in. Required whenever `azure-client-id` is set. |
| `azure-subscription-id` | `terragrunt`, `azure-functions-zip` | not set | terragrunt, azure-functions-zip: subscription to select after login. Required whenever `azure-client-id` is set. On terragrunt it is the CLI default a stack inherits when its own provider block names no subscription. |
| `gcp-workload-identity-provider` | `terragrunt` | not set | terragrunt: full resource name of the workload identity pool provider this run federates with, `projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>`. The Google counterpart of `aws-role-to-assume` and `azure-client-id`, and preferred over a downloaded service-account key for the same reasons: the credential is minted per run and expires, and the pool provider's attribute condition decides which repository and which ref may mint one. A service-account key is a long-lived file, valid until somebody deletes it, tied to nothing. Requires `permissions: id-token: write`. Leave empty to use credentials an earlier step created. On `terragrunt` this is the PROVIDER's credential. If your state lives in GCS the backend needs one too, and the same federated credentials serve both. |
| `gcp-service-account` | `terragrunt` | not set | terragrunt: service account to impersonate after federating, `name@project.iam.gserviceaccount.com`. Empty (default) uses the federated identity directly, which works when the IAM bindings name the pool's `principalSet` rather than a service account. Set, the pool's principalSet needs `roles/iam.workloadIdentityUser` on it. |
| `gcp-project-id` | `terragrunt` | not set | terragrunt: project exported as GOOGLE\_PROJECT and GOOGLE\_CLOUD\_PROJECT, for configurations that do not name one themselves. Optional. |
| `pages-toolchain` | `github-pages`, `cloudflare-docs` | `auto` | github-pages, cloudflare-docs: how to install MkDocs, auto (default) \| uv \| pip. `auto` picks uv when a uv.lock is present, otherwise pip against `pages-requirements`. Detection exists so a caller does not have to declare per-repo what is already visible in the repo. |
| `pages-dependency-group` | `github-pages`, `cloudflare-docs` | `docs` | github-pages, cloudflare-docs: uv dependency-group holding the docs tooling (uv toolchain only). |
| `pages-requirements` | `github-pages`, `cloudflare-docs` | `docs/requirements.txt` | github-pages, cloudflare-docs: requirements file pinning the docs build (pip toolchain only). |
| `pages-python-version` | `github-pages`, `cloudflare-docs` | `3.12` | github-pages, cloudflare-docs: Python version used to build the site. |
| `pages-strict` | `github-pages`, `cloudflare-docs` | `true` | github-pages, cloudflare-docs: build with `--strict`, so a broken internal link or a nav entry pointing at a missing file fails rather than publishing a site with holes in it. |
| `pages-site-dir` | `github-pages`, `cloudflare-docs` | `site` | github-pages, cloudflare-docs: directory the built site is written to. |
| `pages-lint` | `github-pages`, `cloudflare-docs` | `true` | github-pages, cloudflare-docs: run the repo-shape checks (README budget and section order, licence agreement, link targets, Marketplace preflight, INHERIT clobber) before building. These are the rules nothing else covers; --strict already catches broken internal links. |
| `pages-profile` | `github-pages`, `cloudflare-docs` | `auto` | github-pages, cloudflare-docs: repo profile for the shape checks, auto \| action \| service \| spec. |
| `pages-readme-budget` | `github-pages`, `cloudflare-docs` | `0` | github-pages, cloudflare-docs: override the README line budget. 0 uses the profile default. |
| `pages-markdownlint` | `github-pages`, `cloudflare-docs` | `true` | github-pages, cloudflare-docs: run markdownlint-cli2 over docs/ and README.md when a markdownlint config is present. Runs here rather than under MegaLinter because MegaLinter's `security` flavor carries no markdown linter, and MARKDOWN\_MARKDOWNLINT emits no SARIF, so it could never gate on net-new findings anyway. |
| `pages-seo` | `github-pages`, `cloudflare-docs` | `true` | github-pages, cloudflare-docs: after the build, give every page its own meta description (from its first paragraph of prose; the home page keeps `site_description`), Open Graph and Twitter tags, a JSON-LD graph (WebSite, TechArticle, BreadcrumbList) and a markdown copy at `<page>/index.md`, linked from the page and from llms.txt. Configured by `extra.seo` in mkdocs.yml (`locale`, `image`, `publisher`, `author`, `twitter`), every key optional. A tag a page already carries is never replaced or doubled, so a site with its own SEO hook keeps it. `false` skips the step. |
| `pages-agent-ready` | `github-pages`, `cloudflare-docs` | `true` | github-pages, cloudflare-docs: after the build, make the site agent-ready. Writes an Agent Skills index at `/.well-known/agent-skills/index.json` with one skill that says what the site covers and how to read and cite it; loads WebMCP tools on every page (search the docs, read a page as markdown, list and open pages) from a same-origin script that does nothing in a browser without the API; and, for a public site served at the root of its host, writes an `/auth.md` saying the docs need no credentials, never for a site behind `cloudflare-docs-require-access`. Configured by `extra.agents` in mkdocs.yml (`mcp`, `skill`, `webmcp`, `auth_md`), every key optional. Anything the site already publishes of these is left alone. On github-pages the artifact then includes dot-directories, which is where the index lives. `false` skips the step. |
| `build-git-credentials` | `github-pages`, `cloudflare-workers`, `cloudflare-docs` | not set | github-pages, cloudflare-workers, cloudflare-docs: credentials for the private git hosts the build fetches from, one `<host> <username>:<token>` per line. Empty (default) changes nothing. git.example.invalid x-access-token:&lt;a short-lived token&gt; A docs build that installs its theme with `pkg @ git+https://<host>/<org>/<repo>.git@<tag>` needs that clone authenticated, and the clone is git's own, several processes below this action. Each line becomes one `url.<credentialled>.insteadOf` rewrite carried in GIT\_CONFIG\_COUNT / GIT\_CONFIG\_KEY\_n / GIT\_CONFIG\_VALUE\_n for the rest of this job, so the requirements file keeps pinning the plain URL and stays reviewable. Never `git config --global`: a self-hosted runner is shared and long-lived, and a global rewrite would leave the token in `~/.gitconfig` for whatever runs on that machine next. The username is written out rather than assumed, because the forges disagree about it: `x-access-token` for a GitHub App token, `oauth2` for a GitLab one. The split is at the FIRST `:`, which is the safe way round — the username is the half that cannot contain one, so a token that does survives intact. Every token is masked on receipt and never echoed. A malformed line is refused by index and host, and the host is only quoted when it looks like one, because a bare token pasted as a line would otherwise be printed into an annotation as public as the repository. Blank lines and `#` comments are ignored. |
| `artifact-path` | `s3-cloudfront`, `lambda-zip`, `cloudflare-workers`, `azure-functions-zip` | not set | s3-cloudfront, lambda-zip, cloudflare-workers, azure-functions-zip: the built artifact, a directory for s3-cloudfront, a .zip for lambda-zip and azure-functions-zip. |
| `s3-bucket` | `s3-cloudfront`, `lambda-zip` | not set | s3-cloudfront, lambda-zip: the bucket. For s3-cloudfront it serves the site; for lambda-zip it holds published artifacts. |
| `s3-key-prefix` | `s3-cloudfront`, `lambda-zip` | not set | s3-cloudfront, lambda-zip: key prefix within the bucket. Previews are placed under `<s3-key-prefix>/previews/<alias>/`. |
| `s3-delete-orphans` | `s3-cloudfront` | `auto` | s3-cloudfront: pass --delete to `aws s3 sync`, removing bucket objects with no local counterpart. `auto` (default) means yes. |
| `cloudfront-distribution-id` | `s3-cloudfront` | not set | s3-cloudfront: CloudFront distribution to invalidate after the sync. Empty means no invalidation, the CDN keeps serving the old objects until its TTL expires. |
| `cloudfront-site-url` | `s3-cloudfront` | not set | s3-cloudfront: base URL the distribution serves, used to build the URL reported in notifications. |
| `lambda-function-name` | `lambda-zip` | not set | lambda-zip: the function to update. |
| `lambda-function-alias` | `lambda-zip` | `live` | lambda-zip: the alias moved to the new version in deploy mode. A preview publishes a version and does not move it. |
| `lambda-version-label` | `lambda-zip` | not set | lambda-zip: names the immutable S3 key (`<s3-key-prefix>/<lambda-version-label>.zip`). Defaults to the short commit SHA. |
| `terragrunt-root` | `terragrunt` | `terraform` | terragrunt: directory the stacks live under. |
| `terragrunt-exclude` | `terragrunt` | `modules _modules` | terragrunt: space-separated path segments that never hold a stack. `modules _modules` by default: a module has no state of its own, and guessing which stacks use it from a path is how a small module tidy-up ends up planning the whole estate. Add a stack that must only ever be applied by hand, one that rewrites a firewall wholesale, say, and the pipeline will not touch it. |
| `terragrunt-scope` | `terragrunt` | `auto` | terragrunt: `auto` (changed stacks on a PR/push, all on a schedule or dispatch), `all`, or `changed`. With `terragrunt-pull-request` set, `auto` means the stacks that pull request touches even on a manual run; `all` stays legal and means plan the whole estate, gate on that pull request's approval, comment on that pull request. |
| `terragrunt-pull-request` | `terragrunt` | not set | terragrunt: act on this pull request number instead of the one in the event payload. For a manual run that has to plan a named pull request. Its reviews are what the approval gate reads, its thread is where the plan comment goes, and its head commit is what the check run is published against, fetched from the API because a dispatch event carries no pull request. Empty (default) uses the event, which is every automatic run. Digits only. A manual run plans: dispatching a workflow is not approving a commit, so `terragrunt-apply: force` is what applies one by hand. Checking out the right tree stays yours: pass `ref: refs/pull/<n>/merge` to your own actions/checkout step and set `checkout: false` here, or the run plans whatever is on disk and says so. |
| `terragrunt-apply` | `terragrunt` | `auto` | terragrunt: `auto` (apply on an independent approval), `never` (plan only), or `force`. Under `auto` the run must also be the event that asks for an apply: a pull-request review that is an approval, or a push with `terragrunt-apply-on-merge` on. A run that only finds an approval already standing plans and reports, because that approval was given for the commit it was given for and a commit pushed after it has not been reviewed. Re-approving applies. `force` skips the approval and is restricted to `terragrunt-apply-operators`. |
| `terragrunt-apply-on-merge` | `terragrunt` | `false` | terragrunt: `true` lets a push to the default branch apply what was merged. `false` (the default) plans on a push and applies nothing, which is what this target has always done. A push event carries no pull request, so with this on the run asks the API which pull request the commit was merged from and reads that pull request's reviews: a merge that had an independent approval applies, a merge without one is reported and not applied. Off, none of that happens: no commit-to-pull-request lookup, no approval read, plan only. Separate from `terragrunt-apply` on purpose. That input answers "who may authorise an apply?"; this one answers "should a merge commit apply at all?". Stacking the two questions into one enum makes both harder to read. |
| `terragrunt-apply-operators` | `terragrunt` | not set | terragrunt: comma-separated GitHub logins allowed to force an apply by hand (`terragrunt-apply: force` on a manual run). Empty means nobody can, so the manual path fails closed. The normal path is an independent pull-request approval and needs nothing here. |
| `terragrunt-stack-env` | `terragrunt` | not set | terragrunt: environment applied per stack, one `<glob> KEY=VALUE` per line. The first matching line wins for a given KEY, so a specific pattern goes above the catch-all exactly as it would in a `case`. Blank lines and `#` comments are ignored. \*/prd/\*\|\*/prod/\* ARM\_ACCESS\_KEY=&lt;the production state key&gt; \* ARM\_ACCESS\_KEY=&lt;the shared state key&gt; For an estate whose production state lives in a separate account from the rest, which is a deliberate blast-radius boundary rather than an accident: one credential cannot reach both, and without this the only options are a job per credential class or a pipeline that fails on the first stack of the other kind. The values are secrets, so they are passed to each run with `env` rather than exported into the shell, and one stack's credential never reaches the next stack's run. A malformed line is refused by index, glob and KEY: nothing at or after the first `=` is ever printed, because the annotation it would appear in is as public as the repository. |
| `terragrunt-check-name` | `terragrunt` | `Terragrunt apply` | terragrunt: name of the check run published against the head commit. Make it a required check to enforce apply-before-merge. |
| `terragrunt-install` | `terragrunt` | `auto` | terragrunt: install pinned, checksum-verified tofu and terragrunt binaries before running, `auto` (default) installs only what is not already on PATH, `always` reinstalls, `never` uses whatever the runner provides. The download is cached per version pair, so a bump lands in a new directory and can never pick up a stale binary. |
| `terragrunt-version` | `terragrunt` | `1.0.8` | terragrunt: the Terragrunt version to install and run. |
| `terragrunt-tofu-version` | `terragrunt` | `1.12.1` | terragrunt: the OpenTofu version to install and run under Terragrunt. |
| `terragrunt-plugin-cache` | `terragrunt` | `~/.terraform-plugin-cache` | terragrunt: directory to persist provider plugins in across stacks and runs, exported as TF\_PLUGIN\_CACHE\_DIR. The workspace is wiped on each checkout, so without it every stack re-downloads every provider, the single biggest cost in a multi-stack run. Empty disables the cache. |
| `terragrunt-refresh` | `terragrunt` | `auto` | terragrunt: refresh state from the provider before planning, `auto` (default) skips the refresh on a pull request and keeps it everywhere else, `true` always refreshes, `false` never does. Config-versus-state is enough for a pull-request check; the scheduled drift run is the one that has to ask the provider. |
| `terragrunt-timeout` | `terragrunt` | `900` | terragrunt: seconds any one init, plan or apply may take before it is killed. 0 disables the timeout. A stalled provider call otherwise looks like a silent hang until the job limit. |
| `terragrunt-preflight-urls` | `terragrunt` | not set | terragrunt: URLs to probe for reachability before the first plan, one per line. Empty (default) probes nothing. Terragrunt buffers plan output to a file, so a state backend or provider API the runner cannot reach is a silent wait until `terragrunt-timeout` rather than an error. One 8-second request per URL turns that into a red step naming the endpoint. `https://&lt;your-state-backend-host&gt;/?comp=list` `https://&lt;your-provider-api-host&gt;/api/versions` The rules worth knowing before you read a result: a code of 000 (DNS, connect, TLS or timeout) fails the run; any HTTP answer passes, 401 and 403 included, because an unauthenticated probe of a credentialed endpoint is supposed to be refused and being refused proves the endpoint is there. This proves reachability, not authorisation: a 403 does not mean the credential works. Blank lines and `#` comments are ignored. Put no credentials in these URLs; they are printed into the run log. A URL carrying userinfo is refused, and the refusal names the line by its index and shows the URL with the userinfo replaced rather than echoing the line. |
| `terragrunt-credential-preflight` | `terragrunt` | `auto` | terragrunt: check that every cloud the discovered stacks name has a credential on this runner, before the first plan. `auto` (default) fails the run, `warn` annotates and plans anyway, `off` checks nothing. Exists because the credential a terragrunt run needs is not one credential. The state backend has its own (`terragrunt-stack-env`), and every `provider` block the stacks declare has another, from a different chain, that nothing in the workflow mentions. The failure that produces is the least readable in this action: state reads fine, init succeeds, and the plan dies inside a provider with `could not configure AzureCli Authorizer: … Please run 'az login'`, twenty stacks in a row, which reads as a broken runner rather than as a credential nobody wired. What it reads: the `provider "<name>"` blocks in each discovered stack and in every parent directory up to `terragrunt-root`, so a `generate` block in a shared root.hcl counts. What it knows: azurerm/azuread/azapi, aws, google/google-beta, vcd. A provider it does not recognise is passed over in silence rather than guessed at. The state backend is deliberately not checked. A backend with no credential fails in `init`, immediately, naming the backend, which is already the good failure; and an ARM\_ACCESS\_KEY opens one storage account and cannot configure a provider, so counting it either way would be wrong. What it will NOT report: a provider block that configures its own authentication (`client_id`, `client_secret`, `use_oidc`, `use_msi`, `access_key`, `credentials`, `token`, `api_token` …), because that stack answers the question itself. That is the exemption that keeps this from being a check people turn off. This proves a credential is present, never that it is valid or that it can reach the subscription, project or account the stack names — the same distinction `terragrunt-preflight-urls` draws between reachable and authorised. |
| `terragrunt-log-level` | `terragrunt` | not set | terragrunt: value for TF\_LOG when diagnosing a stuck provider call (debug, trace). Empty (default) leaves provider logging off; the log is written next to the plan and redacted before it is shown. |
| `terragrunt-extra-args` | `terragrunt` | not set | terragrunt: extra flags appended to every plan and apply, split on whitespace. |
| `ansible-playbook` | `ansible` | not set | ansible: path to the playbook to run, relative to working-directory. Required for this target. |
| `ansible-inventory` | `ansible` | not set | ansible: inventory path or comma-separated host list passed to `-i`. Required for this target. |
| `ansible-version` | `ansible` | `11.1.0` | ansible: exact version of the `ansible` PyPI distribution to install. Pinned rather than floating, because a collection that changes behaviour between releases is indistinguishable from a change you made. |
| `ansible-python-version` | `ansible` | `3.12` | ansible: Python version the control node runs on. |
| `ansible-galaxy-requirements` | `ansible` | not set | ansible: path to a galaxy requirements file installed with `ansible-galaxy install -r` before the run. Empty skips it. |
| `ansible-limit` | `ansible` | not set | ansible: value for `--limit`, restricting the run to a subset of the inventory. |
| `ansible-tags` | `ansible` | not set | ansible: value for `--tags`. |
| `ansible-skip-tags` | `ansible` | not set | ansible: value for `--skip-tags`. |
| `ansible-check` | `ansible` | `auto` | ansible: run in check mode, `auto` (default) means check mode on a pull request and a real run everywhere else, `true` forces check mode, `false` forces a real run. A pull request that silently reconfigured a fleet would be a surprising way to find out what a diff does. |
| `ansible-diff` | `ansible` | `true` | ansible: pass `--diff`, so the log shows what each changed task changed. |
| `ansible-extra-vars` | `ansible` | not set | ansible: value for `--extra-vars`. Accepts `key=value` pairs or JSON. |
| `ansible-extra-args` | `ansible` | not set | ansible: extra flags appended to the playbook run, split on whitespace. |
| `ansible-ssh-private-key` | `ansible` | not set | ansible: SSH private key used to reach the inventory. Pass a secret, never a literal. It is masked on receipt, written to a 0600 file under RUNNER\_TEMP, and removed when the step exits however it exits. |
| `ansible-ssh-user` | `ansible` | not set | ansible: value for `--user`. Leave empty to let the inventory or ansible.cfg decide. |
| `ansible-ssh-known-hosts` | `ansible` | not set | ansible: known\_hosts entries for the inventory, one per line. Empty means host-key checking is disabled for the run, which is a real downgrade, supply this for anything reachable from a network you do not control. |
| `ansible-vault-password` | `ansible` | not set | ansible: vault password. Pass a secret. Masked on receipt, written to a 0600 file under RUNNER\_TEMP, removed when the step exits. |
| `vault-addr` | `ansible` | not set | ansible: base URL of a HashiCorp Vault, e.g. `https://vault.example.com:8200`. Set it, with `vault-token`, to name secrets by Vault reference instead of passing them in. Only the fields you reference are read. |
| `vault-token` | `ansible` | not set | ansible: Vault token the references are read with. Pass a secret. Needs read on the paths you reference and nothing else. |
| `vault-namespace` | `ansible` | not set | ansible: Vault namespace (Enterprise). Empty is correct for open-source Vault. |
| `ansible-vault-passthrough` | `ansible` | `false` | ansible: hand `VAULT_ADDR`, `VAULT_TOKEN` and `VAULT_NAMESPACE` to the ansible-playbook process, so the playbook can read its own secrets from the same HashiCorp Vault rather than having them copied into a second store that quietly stops being rotated. Needs `vault-addr` and `vault-token`; without both, nothing is passed. `false` by default, and the default removes them from the playbook's environment. A token scoped to the fields this action reads becomes a token every task, role and collection in the play can use, and widening a credential's blast radius should be a decision rather than something inherited by accident. Nothing to do with ansible-vault, the file-encryption tool; that is `ansible-vault-password`. |
| `ansible-ssh-private-key-vault` | `ansible` | not set | ansible: Vault reference to the SSH private key, `<path>#<field>`, e.g. `secret/data/team/app#ssh_private_key`. The alternative to `ansible-ssh-private-key`, not a supplement: setting both fails. Use this where Vault is already the source of truth, so rotating there keeps rotating here and the key is not copied into a second store that quietly goes stale. |
| `ansible-ssh-known-hosts-vault` | `ansible` | not set | ansible: Vault reference to the known\_hosts entries, `<path>#<field>`. The alternative to `ansible-ssh-known-hosts`. |
| `ansible-vault-password-vault` | `ansible` | not set | ansible: Vault reference to the ansible-vault password, `<path>#<field>`. The alternative to `ansible-vault-password`. |
| `ansible-verify-idempotence` | `ansible` | `true` | ansible: after a successful real run, run the playbook again in check mode and fail if any task would still change. A zero exit only proves the playbook ran; this is what proves it converged. Skipped automatically when the run itself was check mode. |
| `cloudflare-api-token` | `cloudflare-workers`, `cloudflare-docs` | not set | cloudflare-workers, cloudflare-docs: API token Wrangler authenticates with. Pass a secret, never a literal. Mint it from Cloudflare's "Edit Cloudflare Workers" template rather than a hand-picked permission list, or the first deploy of a custom domain fails on a permission nobody thought to grant. |
| `cloudflare-account-id` | `cloudflare-workers`, `cloudflare-docs` | not set | cloudflare-workers, cloudflare-docs: Cloudflare account id. Pass a secret. |
| `cloudflare-config` | `cloudflare-workers`, `cloudflare-docs` | not set | cloudflare-workers, cloudflare-docs: path to the Wrangler config (wrangler.toml or wrangler.jsonc), passed as `--config`. Empty lets Wrangler find it, which is the normal case. The config owns the asset directory, routes, custom domains and 404 handling; the inputs here are overrides for the few things a workflow legitimately varies. |
| `cloudflare-worker-name` | `cloudflare-workers`, `cloudflare-docs` | not set | cloudflare-workers, cloudflare-docs: overrides the `name` in the Wrangler config, passed as `--name`. |
| `cloudflare-env` | `cloudflare-workers` | not set | cloudflare-workers: Wrangler environment, passed as `--env`. This selects a config block, which is not the same thing as the shared `environment` input: that one is a label for notifications and the deployment record. |
| `cloudflare-main` | `cloudflare-workers` | not set | cloudflare-workers: entry point for a Worker that runs code, passed as Wrangler's positional path argument. Leave empty for an assets-only Worker, which is the shape that serves static files straight from the edge with no cold start and no code in the request path. |
| `cloudflare-build-command` | `cloudflare-workers` | not set | cloudflare-workers: command run in working-directory before Wrangler, for a Worker that needs bundling. Empty skips it. |
| `cloudflare-compatibility-date` | `cloudflare-workers` | not set | cloudflare-workers: overrides the config, passed as `--compatibility-date`. |
| `cloudflare-minify` | `cloudflare-workers` | `false` | cloudflare-workers: pass `--minify`. Applies to Worker code, not to static assets. |
| `cloudflare-vars` | `cloudflare-workers` | not set | cloudflare-workers: plain (NOT secret) variables, one `KEY=VALUE` per line, each passed as `--var`. Wrangler deletes vars not present in the config unless `--keep-vars` is set, so anything set in the dashboard belongs in the config instead. |
| `cloudflare-wrangler-version` | `cloudflare-workers`, `cloudflare-docs` | `4.114.0` | cloudflare-workers, cloudflare-docs: exact Wrangler version to run. Pinned, because the tool that publishes to production is not a floating dependency. |
| `cloudflare-node-version` | `cloudflare-workers`, `cloudflare-docs` | `24` | cloudflare-workers, cloudflare-docs: Node version Wrangler runs on. Wrangler 4 declares `engines.node >= 22`, and on 20 it installs cleanly and then refuses to run, so this does not default to whatever the runner happens to ship. |
| `cloudflare-docs-host` | `cloudflare-docs` | not set | cloudflare-docs: hostname the docs router serves, e.g. `docs.magmamoose.com`. The site Worker itself has no hostname — it is reachable only through the router's service binding — so this is the address the Access check and the reported URL are about. Required when `cloudflare-docs-require-access` is on. |
| `cloudflare-docs-path` | `cloudflare-docs` | not set | cloudflare-docs: path segment this repository's site is served under on `cloudflare-docs-host`. Empty (default) uses the repository name, which is what the router dispatches on, so overriding it is for a repo whose site is mounted elsewhere. |
| `cloudflare-docs-require-access` | `cloudflare-docs` | `false` | cloudflare-docs: refuse to publish unless a Cloudflare Access application actually covers `<cloudflare-docs-host>/<cloudflare-docs-path>`. This is enforcement, not convention: "the site is behind Access" is otherwise a belief nothing checks, and the deploy is the one moment something can ask Cloudflare and refuse. Needs the `Access: Apps` READ permission on `cloudflare-api-token` — the "Edit Cloudflare Workers" template does not include it, and a 403 is treated as "could not tell", never as "nothing covers it". |
| `cloudflare-docs-index` | `cloudflare-docs` | `true` | cloudflare-docs: emit the docs corpus from the build: `llms.txt` and `llms-full.txt` written into the site before it is published, and a search index of every page for the documentation MCP servers. One pass over the markdown that was just rendered; no credentials and no network. Publishing the index is a separate opt-in, `cloudflare-docs-index-bucket`. |
| `cloudflare-docs-index-bucket` | `cloudflare-docs` | not set | cloudflare-docs: R2 bucket the index is published to, as `index/<repo>.json`. Empty (default) writes llms.txt into the site and uploads no index. Only a deploy writes it, and only after the site itself deployed: a pull request would otherwise overwrite the shared corpus with an unmerged branch. Needs R2 object write on `cloudflare-api-token`, which the "Edit Cloudflare Workers" template does not include. |
| `cloudflare-docs-capability-file` | `cloudflare-docs` | `capability.json` | cloudflare-docs: this repository's capability declaration, published to `capability/<repo>.json` in `cloudflare-docs-index-bucket`. Empty disables it; the default names a file most repositories do not have, and absent is the normal case — most repositories are not house tools. Present, it is validated against `https://mcp.magmamoose.com/schema/capability.schema.json` on every run, including a pull request, and a declaration that does not satisfy it FAILS THE RUN: the MCP reads a malformed document as private and reports it as unreadable, counted and never named, which is the one state invisible from the publishing side. `private` is republished from the repository's own visibility on the same fail-closed rule as the docs index, and `commit` and `generated` are stamped from the run. |
| `cloudflare-extra-args` | `cloudflare-workers` | not set | cloudflare-workers: extra flags appended to the Wrangler invocation, split on whitespace. |
| `cloudflare-verify-config` | `cloudflare-workers` | `true` | cloudflare-workers: before publishing, run `wrangler deploy --dry-run` and refuse to publish if Wrangler reports configuration it will not apply: an unexpected field (a misspelled `[[r2_bucket]]` is only a warning, and the Worker deploys with no bucket), or a top-level binding an `--env` deploy does not inherit. A binding is only real if Wrangler prints it. Costs one extra bundle per run. |
| `functions-app-name` | `azure-functions-zip` | not set | azure-functions-zip: the Function App to publish to. Required for this target. |
| `functions-resource-group` | `azure-functions-zip` | not set | azure-functions-zip: resource group the Function App lives in. Required for this target. |
| `functions-slot` | `azure-functions-zip` | not set | azure-functions-zip: deployment slot to publish to instead of production. Empty (default) publishes to the app itself. This is also what a preview has to use. A pull request must not publish onto the routes production serves, and a slot is Azure's only destination that does not — but a Linux Consumption plan HAS NO SLOTS. So on Consumption a preview is refused and says why, rather than quietly shipping a branch to production. On Premium or Dedicated, set this and previews go to the slot. |
| `functions-ready-attempts` | `azure-functions-zip` | `30` | azure-functions-zip: how many times to ask the app for an answer after publishing, before calling the deploy dead. Any HTTP status passes, including a 404 at the root, which is what a Function App whose only trigger is at `/api/<name>` normally returns. Only a connection failure and a 503 count as "not yet". The default is generous on purpose: a freshly created Consumption app 503s from both the site and its SCM endpoint until content is first published, so a FIRST deploy has to retry through that rather than fail on it. |
| `functions-ready-delay` | `azure-functions-zip` | `10` | azure-functions-zip: seconds between readiness attempts. With the default attempts, that is a five-minute ceiling on a cold first publish. |
| `verify-url` | all | not set | Post-deploy: the URL that must answer. Empty skips verification. Catches the deploy that uploaded but did not bind, the one failure that otherwise looks green. |
| `verify-header` | all | not set | Post-deploy: a response header that must be present on `verify-url` (e.g. content-security-policy). |
| `verify-header-match` | all | not set | Post-deploy: an extended regex the `verify-header` value must match. |
| `verify-method` | all | `GET` | Post-deploy: HTTP method used to request `verify-url`. `GET` by default. Set it where a GET cannot verify the endpoint at all. A webhook receiver binds POST and nothing else, so a GET reaches no function and the platform answers 404 — which is also what an empty package returns, leaving the check unable to tell a working deploy from a broken one. `verify-method: POST` with `verify-status: 401` asserts that the function is bound and that its signature check refuses an unsigned request, which is the thing worth knowing. The method survives a redirect: curl otherwise downgrades a redirected POST to a GET, which would quietly turn the assertion into a different one. |
| `verify-status` | all | `200` | Post-deploy: expected HTTP status from `verify-url`. |
| `verify-attempts` | all | `6` | Post-deploy: how many times to try `verify-url` before failing. |
| `verify-delay` | all | `10` | Post-deploy: seconds between verification attempts. |
| `notify` | all | `always` | `always` (default), `on-success`, or `on-failure`. Applies to the webhook sinks. |
| `pr-comment` | all | `auto` | Sticky pull-request comment. `auto` (default) means on for preview mode and for the terragrunt target. |
| `slack-webhook` | all | not set | Slack incoming-webhook URL. Empty means the sink is off. Never fails the deploy. |
| `teams-webhook` | all | not set | Microsoft Teams incoming-webhook URL. Empty means the sink is off. Never fails the deploy. |
| `api-url` | all | not set | Base URL of a Tremvok API to record this deployment with. Authenticates with a GitHub OIDC token, so the repository stores no credential. Empty means no record is kept. |
| `api-audience` | all | `tremvok` | OIDC audience the Tremvok API expects. |
| `auth-token` | all | `${{ github.token }}` | Token used for the pull-request comment and the check run. Needs `pull-requests: write` and, for the terragrunt target, `checks: write`. |
| `allow-fork-preview` | all | `false` | Let a fork pull request attempt a deploy. Off by default and almost always wrong: a fork cannot read secrets, so this only converts an honest skip into an auth error. |
| `allow-dispatch-from-any-ref` | all | `false` | Let a manual run deploy from a branch other than the default one. Off by default, publishing a topic branch to production usually is not what "Run workflow" meant. |

## Outputs

| Output | Description |
| --- | --- |
| `target` | The deployment target this run used. |
| `mode` | The resolved mode: deploy, preview or rollback. |
| `environment` | The resolved environment name. |
| `url` | URL of what was deployed, when the target produces one. |
| `deployed` | true when something was actually deployed (false for a skip). |
| `verified` | true when post-deploy verification ran and passed. |
| `skipped` | true when the run skipped (fork pull request, or no credential). |
| `skip-reason` | Why it skipped, in a sentence. |
| `record-id` | Identifier returned by the Tremvok API, when api-url is set. |
| `site-dir` | github-pages: absolute path to the built site. |
| `pages-toolchain` | github-pages: the toolchain actually used, uv or pip. |
| `version-id` | lambda-zip, cloudflare-workers, azure-functions-zip: the published version. A Lambda version number, a Worker Version ID, or the package sha256 — Azure exposes no per-deploy content hash of its own. |
| `stacks` | terragrunt: how many stacks this run discovered. |
| `plan-changes` | terragrunt: how many of those stacks planned with a diff. |
| `applied` | terragrunt: true when this run applied, false when it only planned. |
| `changed-tasks` | ansible: how many tasks reported changed on the real run. |
| `idempotent` | ansible: true when the second check-mode run found nothing left to change. |

## Required permissions

Declared by the **caller**, because a composite action cannot declare
`permissions:`. Only what the target actually uses:

### `target: github-pages`

```yaml
permissions:
  contents: read        # checkout
  pages: write          # actions/deploy-pages, in the caller's own job
  id-token: write       # actions/deploy-pages
```

### `target: s3-cloudfront`

```yaml
permissions:
  contents: read        # checkout
  id-token: write       # assume the deploy role by OIDC
  pull-requests: write  # the sticky preview comment
```

### `target: lambda-zip`

```yaml
permissions:
  contents: read        # checkout
  id-token: write       # assume the deploy role by OIDC
  pull-requests: write  # the sticky preview comment
```

### `target: terragrunt`

```yaml
permissions:
  contents: read        # checkout
  id-token: write       # assume the deploy role by OIDC
  pull-requests: write  # the rolling plan comment
  checks: write         # the check run that makes apply-before-merge enforceable
```

### `target: ansible`

```yaml
permissions:
  contents: read        # checkout
  pull-requests: write  # the sticky run comment
```

### `target: cloudflare-workers`

```yaml
permissions:
  contents: read        # checkout
  pull-requests: write  # the sticky preview comment
```

### `target: cloudflare-docs`

```yaml
permissions:
  contents: read        # checkout
  pull-requests: write  # the sticky preview comment
```

### `target: azure-functions-zip`

```yaml
permissions:
  contents: read        # checkout
  id-token: write       # sign in to Azure by OIDC
  pull-requests: write  # the sticky preview comment
```

`target: github-pages` is the one target the action cannot finish on its own:
`actions/deploy-pages` needs `pages: write` and the `github-pages` environment, and a
composite action can declare neither. The action builds and stages the artifact; the
caller's job runs `actions/deploy-pages`. Every other target completes inside the
action.
