Skip to main content
Runs sponson plan, sponson apply or sponson apply --destroy from release.plan.yaml and comments the result on the pull request. One action, one command, three triggers. Pin the action to a commit. A commit SHA is the only reference nobody can move under you. sponson init --setup writes the workflow with <commit-sha> filled in: the commit the installed version’s release tag (sponson@<version>) points at. To pin by hand, read that commit with git ls-remote https://github.com/MishaBear94/Sponson 'refs/tags/sponson@<version>^{}' (uses: cannot name the tag itself: GitHub splits a reference at its @).

Inputs

Outputs: result (the CLI’s JSON envelope), status (complete / partial / failed / stale for apply, ok / error for plan), exit-code, skipped. The action exits with the CLI’s exit code after posting the comment: 0 complete or partial, 1 failed, 2 usage / plan invalid / environment unknown / not approved / a manual step waits for a person, 3 another apply holds the lock (or took it over). Provider credentials are the providers’ own tokens, passed as environment variables to every step that runs the action, destroy included: a destroy without tokens cannot delete anything. Sponson has no token of its own.

When the action skips

For deployment_status, the scope is the open pull request associated with the deployed commit. When there is none (the PR was merged or closed before the build finished, or the branch never had a PR) the action skips: it sets skipped: true, prints a notice, and runs nothing. It never falls back to a closed PR, which would re-create a preview that destroy already removed. A deployment to Production never drives a preview run.

Permissions

Example workflow

The same action handles every moment of a preview environment’s life, and production on push to main. apply is idempotent and resumable: on pull_request it creates the database branch and injects variables, stops with status partial at the deploy barrier, and the deployment_status run of the preview deployment finishes the lines that needed the preview URL.
PROD_* are environment secrets of the production environment, so only the approved job can read them: production provider tokens, and every value a production line references with { secret: "env://NAME" }. Use separate tokens for preview and production when the providers allow it. On a push the action passes no scope flags; the CLI reads the GitHub context (GITHUB_REF_NAME, GITHUB_SHA) and a push to the default branch is scope main. Without an approver the CLI refuses with ENV_NOT_APPROVED and writes nothing.

Plan only on pull requests

The PR comment

One comment per environment, marked with <!-- sponson:<env> --> and updated in place on every run. It is written for the reviewer: a headline that says what this pull request’s preview has (on a plan: what it already has, and what applying would add), then one line per plan line with a link to the provider’s console where Sponson knows one, the preview URL as a link, and what a waiting line waits for. Hidden values are never shown: resources by name, inputs as references (DATABASE_URL ← db.connection_string), labelled by what they are in the plan: a { secret } reference as (secret), another value that is never shown (a connection string from { from }) as (value hidden), a literal as itself (MODE = preview: it is in the repository already). An unchanged value keeps its reference, on a plan and after an apply alike. A refused or failed line says what happened and then the fix, once (sponson apply --reconcile, --approved-by <who>, sponson apply --confirm <line>): a resource changed outside Sponson says so next to its name, and nowhere else. A line held back only because a line it depends on was refused is counted apart and has no fix of its own. The examples below are real comments: the PR comment suite (scenarios/journeys/secrets/pr-comment.test.ts) renders them from --json output against the fake cloud, and pnpm docs:gen copies its snapshots here, so they cannot go stale. The plan is the README’s (a Neon branch, two Vercel variables, a Clerk callback). While the preview deploy has not finished (apply on opened; the deployment_status run finishes the callback):
After the deploy, the same comment:
Someone edited DATABASE_URL in the Vercel console (plan; an apply refuses the same line the same way):
A line waits on a manual step (the job stays red, exit 2, until a person confirms it):
A production run without an approver, refused before anything is touched:
When the pull request closes (destroy):
Kept lists what existed before Sponson and was adopted: destroy forgets it and never deletes it. Left for you lists what a person has to finish: a delete the provider refused (with the error and the fix), something re-created outside Sponson under the same name, or the undo of a manual step. A manual step that nobody has done yet is listed under the lines with its instructions, whole, and the command a person runs once they did it (sponson apply --confirm <line>); the job stays red (exit 2) until then. A resource Sponson created that someone edited since the last apply is deleted on destroy too (the pull request is closing), and the comment’s warnings name it. The receipt records it under the line’s notes.changedOutside. When the pull request closes with the DATABASE_URL edit above still there:
Console links: Neon links the branch, Vercel the project’s Environment Variables page, Clerk the dashboard, and a recipe line its op’s console_url when the recipe declares one. They come from receipt.lines[id].consoleUrl in the JSON result. Vercel’s link goes through its documented dashboard link, which asks the person for the team and project: Vercel’s dashboard addresses a project by team slug and project name, and documents no page URL by the project id the plan holds. Clerk documents no dashboard URL deeper than its root. How the comment is made: the action runs the CLI with --json (that JSON is the result output), then renders the comment from it with Sponson’s own renderer (render-comment.mjs, the function behind sponson plan|apply --format markdown), so the comment and the CLI never disagree. Everything that comes from a provider (error messages, labels) is HTML-escaped and kept on its line, so an HTML error page or a stray backtick cannot open a tag or hide the rest of the comment. A comment that would exceed GitHub’s 65,536 characters keeps its title and headline and leaves out the lines and sections that do not fit, saying so; the run log and the result output have everything. With a version older than the renderer, the action posts a one-line summary instead. The same markdown outside this action: sponson apply --format markdown > comment.md (GitLab, CircleCI, Bitbucket: integrations), or renderMarkdown(envelope) from the sponson package on a --json result you already have.