DidWork for GitHub Actions

A pull request is where a claimed outcome becomes permanent. Your tests prove the code does what the code does. They do not prove the deploy this branch depends on is live, the migration ran, the release published, or the endpoint the change assumes is answering.

This action runs the claims in your repo's .didwork.yml against the systems that can prove them, and fails the job when a required outcome is not true. Because it fails the job, branch protection can require it — and a merge that rests on a self-report stops being possible.

Install

# .github/workflows/verify.yml
name: Verify outcomes
on: pull_request

permissions:
  contents: read
  pull-requests: write   # only for the comment

jobs:
  didwork:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: didworksh/verify-action@v1
        with:
          api-key: ${{ secrets.DIDWORK_API_KEY }}

Add DIDWORK_API_KEY as a repository secret (free key at didwork.sh/console). Without it the gate still runs, but only http.ok claims verify, nothing is stored, and there are no receipts to link.

The gate file

.didwork.yml at the repo root declares what must be true to merge:

version: 1
claims:
  - name: Production health
    type: http.ok
    expected:
      url: https://example.com/health

  - name: Release published
    type: github.release_published
    expected:
      repository: acme/app
      tag: v1.4.0

  - name: Canary
    type: http.ok
    required: false        # reported, cannot block
    expected:
      url: https://canary.example.com/health

Every claim is required unless it says required: false. A non-required claim still runs and still appears in the report — it just cannot fail the job, which is how a repo adopts a new check before trusting it with the merge button.

The file is parsed strictly. An unknown key, a malformed claim, or an empty claims: list is an error, not something quietly skipped — a gate that verifies nothing would report success on everything.

What passes

VerdictMeaningRequired claim
verifiedthe outcome is truepasses
failedthe outcome is not trueblocks
unknowncould not be establishedblocks
errorthe check could not be runblocks

unknown blocking is deliberate, and it is the decision most people want to argue with. A check that could not reach its evidence has not shown anything to be true. Letting it through would make this exactly the thing it exists to prevent: a green light backed by nothing.

Exit codes, if you are wiring it yourself: 0 the gate passed, 1 a required claim is not verified, 2 the command or the config was wrong. A broken gate exits 2 rather than 0, because "we could not check" must never read as "it passed".

The pull request comment

The job's pass or fail is what branch protection enforces. The comment is so a human can see why without opening the log: one row per claim, the verdict, and the evidence behind it — and, when the run had a key, a link to the receipt showing claim, evidence and verdict in full.

It updates itself. A branch pushed ten times has one comment showing the current truth, not ten showing its history.

Make it a required check

Settings → Branches → branch protection rule → Require status checks to pass before merging, then select the job (didwork in the example above). The check has to have run once on a branch before GitHub will offer it in that list.

Permissions

The comment is written with your workflow's GITHUB_TOKEN, under whatever scope you granted it. DidWork holds read-only credentials to the systems it verifies and has no write access to your repository at all — it can observe an outcome, never cause one. That is not a policy, it is the shape of the integration: there is nowhere for repository write access to live.

Don't want the comment? Drop pull-requests: write and set comment: false. The gate is unchanged.

Without Actions

The action is a thin wrapper over a CLI, so any CI runner works:

npx @didwork/inspect verify              # run the gate
npx @didwork/inspect verify --json       # machine-readable
npx @didwork/inspect verify --config ci/gate.yml

Set DIDWORK_API_KEY in the environment. CI runs identify themselves as the ci source, so they stay distinguishable from an agent's or a person's verifications in your log.

Troubleshooting

Exit 2, "no gate file found". The CLI looks for .didwork.yml, .didwork.yaml, then .didwork.json in the working directory. Pass --config for anywhere else, and make sure the job ran actions/checkout first.

The comment never appears. Three usual causes: the workflow has no pull-requests: write permission; the event is a push rather than a pull_request, so there is nothing to comment on; or the run came from a fork, where GitHub deliberately withholds write tokens.

Everything is unknown. The claims named systems DidWork has no read credential for. Connect them at didwork.sh/console/providers.

Verdicts appear but receipts do not link. The run is keyless — nothing was stored, so there is no receipt. Add the api-key input.

Next

Give the agent the same verdicts while it works: Claude Code, Cursor, or any MCP host.

DidWork: verification infrastructure for software outcomes