BlastRadius Private beta

Five minutes, and one of them is reading. The rest is one step in a workflow you already have.

Install the app, paste one step into the workflow that already plans, open a pull request. Plus the .blastradius.yml reference, what every refusal means, and the questions people ask before they wire this into CI.

Quickstart

You need a repository that already runs terraform plan in GitHub Actions. Everything below happens in that repository.

01

Install the app

BlastRadius is in private beta, so there is no public install button yet. Email security@blastradiusapp.com and you will get an install link, the API URL to paste, and a reply from a person.

02

Issue an upload token

Sign in at /app on that API URL, pick the repository, and issue a token. It is shown once — we keep only a hash of it — so save it straight into the repository as a secret called BLASTRADIUS_TOKEN.

03

Add one step

In the same job as the plan step and after it — the Action reads the planfile off that job's disk, so a separate job would not have it. Two lines of permissions and one uses.

04

Open a pull request

One comment appears on it and one check run appears on the commit, both rewritten in place on every push.

The whole workflow file

If you already have one that plans, you need the permissions block and the last step. The api-url here is a placeholder: it resolves nowhere, so a paste before you have the real one fails loudly rather than quietly posting somewhere.

name: terraform
on: pull_request

permissions:
  contents: read
  id-token: write

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: hashicorp/setup-terraform@v3

      - run: terraform init
      - run: terraform plan -out=tfplan

      - uses: blastradiusapp/action@v1
        with:
          planfile: tfplan
          token: ${{ secrets.BLASTRADIUS_TOKEN }}
          api-url: https://api.blastradius.example

If your terraform lives in a subdirectory, add working-directory: beside planfile: and give the planfile's path relative to it. You only need a workspace label when there is more than one root — see below.

id-token: write is not optional, and it is not for our convenience. It is how an upload proves which workflow run made it. Without it, holding your repository secret would be enough to post a passing check on somebody else's pull request — so nothing is uploaded without one, and the job writes an error annotation saying exactly that. It grants no access to your repository; it lets the job ask GitHub who it is.

Nothing here can fail your build. Not a wrong token, not an unreadable plan, not our API having a bad afternoon. The Action sets no exit code on any path. The only thing that can block a merge is the check run, and only a CRITICAL sets that to failing.

What you should see

  • One comment on the pull request. Findings worst first, each naming the resource and the attributes it read. It is edited in place on every push — never a second comment, never a thread.
  • One check run on the head commit, called BlastRadius. That is the name to put in a branch protection rule. It fails only on a CRITICAL; a HIGH reports without blocking, and everything else passes.
  • A grey check that says "Not reviewed". Something stopped the review and the reason is in the check's title. Every one of them is under Troubleshooting below. A grey check passes a required status check, so it does not hold your merge — but it is not a pass either, and it says so.
  • Nothing at all. The job ran and uploaded nothing. That is nearly always the trigger or the permission — see the first two entries under Troubleshooting.

Several terraform roots

A repository that plans more than one root uploads each one separately and still gets one comment. Give each root a workspace label so their findings stay apart inside it, and so a root that found nothing can say so.

- uses: blastradiusapp/action@v1
  with:
    planfile: tfplan
    working-directory: infra/prod
    workspace: infra/prod
    token: ${{ secrets.BLASTRADIUS_TOKEN }}
    api-url: https://api.blastradius.example

Findings are ranked by severity across the whole pull request rather than grouped per root, because a third root's CRITICAL under a first root's INFO is the one ordering a reviewer must never be given. The label is what each finding carries, and there is an index at the top of the comment with a line per root.

Only the roots a push touched need to plan. A root whose last upload was for an earlier commit stays in the comment and says which commit it was planned against.

Configuring the reviewer

Optional, and none of it lives in your workflow file. Put a .blastradius.yml in your repository root — the .blastradius.yaml spelling works too.

It is read at the tip of your repository's default branch, not from the pull request. That is a repository setting, so changing it takes admin — and it means a policy change takes effect once it is merged rather than on the pull request proposing it. Otherwise a pull request could silence the finding it is about, or pick a base branch carrying no policy at all.

# .blastradius.yml, in your repository root
version: 1

severity:
  iam-diff: INFO

ignore:
  - resource: "module.legacy.*"
    reason: decommissioned this quarter
  - rule: security-groups
    resource: "module.edge.*"

workflows:
  - .github/workflows/terraform.yml

workspaces:
  infra/prod:
    severity:
      iam-diff: CRITICAL

version

Optional, and the only value today is 1. Leaving it out is fine; writing a number we do not know is an error, which is the half that will matter the day there is a 2.

severity

A rung per rule, in either direction. This is your merge button: the check run reads the severity this file produced, so raising a rule to CRITICAL blocks a merge on it and lowering one to INFO stops it blocking. We keep our own rung beside yours internally, so that telemetry about which rules matter is not one loud repository.

ignore

Stay quiet about something: by rule, by resource-address glob, or both, with an optional reason. An ignored finding is hidden, not deleted — it still appears in the comment under "Filtered as noise" with your reason beside it, so whoever inherits this repository can see what was decided rather than what was removed.

workflows

The workflow files allowed to report on this repository. This is the only key here that is a control rather than a preference: an upload proves which workflow run made it, and this is where you say which of your workflows that may be. Leave it out and any workflow in the repository may report, which is how every repository starts.

workspaces

The same two preferences, scoped to one terraform root of a monorepo. The key is the workspace label from your workflow, matched exactly rather than as a glob. A root's severities win for the rules it names; its ignores are added to the repository-wide ones rather than replacing them.

A key we do not know is an error, on purpose. A misspelled severties: that validated and did nothing would be us not doing what you asked and not saying so. If the file has a mistake in it, the review still happens under the default policy and the comment says which line to look at — a broken preference never costs you the review. The one exception is workflows: that is a control, so if we cannot read the file we fall back to what we last knew about whether this repository pins, rather than to "anything may report".

Action inputs

Six, and three of them are required.

planfile

required

The file terraform plan -out wrote, relative to working-directory. The Action runs terraform show -json on it itself, so the JSON never has to touch your runner's disk.

token

required

This repository's upload token. Keep it in a repository secret.

api-url

required

Where to upload to. There is no production host to default to yet, so this is required and the value in the snippets above is a placeholder.

workspace

optional

A label for this terraform root, for repositories that plan several. Optional, and only meaningful if you have more than one.

working-directory

optional

The root to run terraform show in. Defaults to the repository root, and must be initialized.

terraform-cli

optional

The binary to run. Defaults to terraform; use tofu for OpenTofu.

Troubleshooting

Every way this ends without a review, and what to do about each. The first two are the common ones on day one.

  • No comment, no check run, and a green job. Almost always the trigger. The workflow has to run on pull_request: a push run has no pull request to comment on and is skipped with a note. pull_request_target is refused outright, because a plan made there runs the pull request's own configuration in a job holding your secrets, and GitHub's token for such a run names no pull request for an upload to be bound to.
  • An error annotation about a missing ID token. The job was not granted id-token: write. Add it to the job that runs the action, or to the top of the workflow file if it has one job. Nothing is uploaded without it.
  • An annotation saying the ID token did not verify. Three things have to be true: the repository is on github.com, the run was triggered by pull_request, and the action is the current release. If all three are and it still refuses, get in touch — an enterprise-scoped OIDC issuer is the case we know about and do not yet accept.
  • "This upload came from a workflow this repository does not accept reviews from". Your workflows list does not name the file this job ran from — and the message says which file that was. The common surprise is a reusable workflow: the claim names the reusable file rather than the one that called it, so pin the file with the steps in it.
  • A fork's pull request is never reviewed. By default GitHub gives a fork's run neither your secrets nor id-token: write, so nothing can be uploaded and the step says so loudly. If that annotation is noise on a public repository, guard the step with a condition on github.event.pull_request.head.repo.full_name. Nothing fails either way.
  • The check never reports and a protected branch waits. A verdict only lands on the commit GitHub calls this pull request's head. If a push landed while a plan was uploading, that upload is stale and is refused — the run for the new push reports a minute later. If it stays stuck, the branch protection rule may be naming something other than BlastRadius.
  • Uploads start being refused for a while. There is a per-installation rate limit, and a monorepo replanning every root on every push is what it is sized against. Being refused by it is a 429 and a note in the job summary; nothing is lost except that push's review.

Grey checks, and what each one means

These are the titles a refusal writes on the check run. Each says whose problem it is, which is the part a grey tick cannot.

Not reviewed — no plan to read

The planfile was not there, was not readable, or terraform show printed something that is not a plan. Check that the planfile input names the file terraform plan -out actually wrote, and that working-directory is the initialized root. The workflow log has terraform's own output above ours.

Not reviewed — terraform did not finish planning

Terraform itself reported that the plan did not finish. Ours is not the error to chase: the plan step's output is. A plan that errored cannot be applied either, so nothing was lost by not reviewing it, and the next push that plans cleanly is reviewed.

Not reviewed — this Terraform is too old to redact safely

The plan came from a Terraform older than 0.15, which does not record which values are sensitive. Redaction runs in your runner off those marks, so we would have had nothing to work from and refused to upload it rather than upload it unredacted. Upgrading Terraform is the whole fix.

Not reviewed — this plan is too large to redact safely

The plan was too large for the redaction pass to finish, so it never left your runner. Do not read this as a clean review — it is the opposite. Plan each root separately if you can, and get in touch either way, because this is a limit we would rather raise than have you work around.

Not reviewed — this plan is over the size we can review

The upload is over 10 MB. Plan each terraform root separately and give each one its own workspace label; they still merge into one comment.

Not reviewed — BlastRadius could not review this push

Ours rather than yours. There is nothing to fix on your side and the next push tries again. If it keeps happening, tell us — the check run is grey rather than red precisely so this does not block you meanwhile.

Questions people ask before wiring this in

  • Does it ever fail my build? No. The Action sets no exit code on any path, including the ones where we are broken. A CRITICAL finding fails the check run, which is a merge gate rather than a build failure, and it is the only thing that does.
  • Does the plan leave my runner unredacted? No. Everything Terraform marked sensitive, plus a provider secret-attribute denylist, plus every other copy of those values in the document, becomes [REDACTED] before the upload. Then the plan is parsed down to the subset we read, which drops provider configs, provisioners, variable defaults and prior state outright.
  • Do you store my plan? No, and there is no column it would fit in. What is kept is the findings — the resources a rule named and the attribute values it quoted — for 30 days. Uninstalling deletes everything we hold for you immediately.
  • Does it read my state file? Never. It reads the planfile you already made and the event payload GitHub hands every workflow. The only thing BlastRadius fetches from your repository is .blastradius.yml, and it fetches that from GitHub rather than from your runner.
  • Does the language model see my plan? No. It is handed findings the rules already produced, and it never picks a severity. Turn it off and you get the same findings with our own sentences under them.
  • Does OpenTofu work? Yes. Set terraform-cli: tofu. Anything whose show -json emits the same document works; the floor is the Terraform 0.15 plan format, because older plans carry no sensitivity marks for redaction to read.
  • I disagree with a severity. Change it, in severity. It is your merge button and both directions are yours. If you find yourself lowering the same rule everywhere, tell us — a rule a lot of repositories move is a rule whose rung we chose wrong, and that is the most useful thing you can send us.
  • Can I see it on a repository that has no AWS in it? You can install it, and it will tell you the truth: the rules are AWS in v1, so a plan with nothing AWS in it gets "Not reviewed — no rules for this stack" and a grey check rather than a green tick. A clean bill of health we did not earn is the one output this product will not produce.