GitHub Actions
GitHub Actions needs no Stategraph-specific integration: install the CLI on the runner, export three variables, and replace terraform plan/apply with stategraph plan/apply. Plans run on pull requests; the apply runs when the change lands on your default branch.
Prerequisites
- Stategraph running and reachable from GitHub's runners (see Deployment)
- A Stategraph API key and tenant (see Setup)
- Your state imported once, with
stategraph.jsoncommitted (see Setup)
Store the API key as a repository or environment secret named STATEGRAPH_API_KEY.
Workflow
name: Stategraph
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
env:
STATEGRAPH_API_BASE: https://stategraph.example.com
STATEGRAPH_API_KEY: ${{ secrets.STATEGRAPH_API_KEY }}
STATEGRAPH_TENANT_ID: your-tenant-id
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
defaults: { run: { working-directory: infra } }
steps:
- uses: actions/checkout@v7
- uses: opentofu/setup-opentofu@v2
with: { tofu_wrapper: false }
- name: Install Stategraph CLI
run: |
curl -fsSL https://example.com/stategraph-linux-amd64.tar.gz -o /tmp/sg.tgz
sudo tar xzf /tmp/sg.tgz -C /usr/local/bin stategraph
- name: Plan
id: plan
shell: bash
continue-on-error: true
run: stategraph plan 2>&1 | tee plan.txt
- name: Comment plan on PR
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
OUTCOME: ${{ steps.plan.outcome }}
run: |
{
echo '<!-- stategraph-plan -->'
echo "#### Stategraph Plan \`${OUTCOME}\`"
echo '<details><summary>Show plan</summary>'
echo
echo '```'
cat plan.txt
echo '```'
echo '</details>'
echo
echo "*Pusher: @${GITHUB_ACTOR}, Action: \`${GITHUB_EVENT_NAME}\`*"
} > comment.md
cid=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --jq '[.[] | select(.body | startswith("<!-- stategraph-plan -->"))][0].id')
if [ -n "$cid" ] && [ "$cid" != "null" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${cid}" -F body=@comment.md > /dev/null
else
gh pr comment "$PR" --body-file comment.md > /dev/null
fi
- name: Fail the job if the plan failed
if: steps.plan.outcome == 'failure'
run: exit 1
apply:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
defaults: { run: { working-directory: infra } }
steps:
- uses: actions/checkout@v7
- uses: opentofu/setup-opentofu@v2
with: { tofu_wrapper: false }
- name: Install Stategraph CLI
run: |
curl -fsSL https://example.com/stategraph-linux-amd64.tar.gz -o /tmp/sg.tgz
sudo tar xzf /tmp/sg.tgz -C /usr/local/bin stategraph
- name: Apply
id: apply
shell: bash
continue-on-error: true
run: stategraph apply --auto-approve 2>&1 | tee apply.txt
- name: Comment apply on the merged PR
shell: bash
env:
GH_TOKEN: ${{ github.token }}
OUTCOME: ${{ steps.apply.outcome }}
run: |
pr=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" --jq '.[0].number')
if [ -z "$pr" ] || [ "$pr" = "null" ]; then
echo "no associated PR; apply output stays in the job log"
exit 0
fi
{
echo "#### Stategraph Apply \`${OUTCOME}\`"
echo '<details><summary>Show apply</summary>'
echo
echo '```'
cat apply.txt
echo '```'
echo '</details>'
} > comment.md
gh pr comment "$pr" --body-file comment.md > /dev/null
- name: Fail the job if the apply failed
if: steps.apply.outcome == 'failure'
run: exit 1
The plan lands on the pull request the way reviewers expect from Terraform CI: an outcome badge, the output in a collapsible section, and a pusher footer. Each PR gets one sticky comment, updated in place on every push — the <!-- stategraph-plan --> marker is what the workflow finds and edits, and gh ships on GitHub-hosted runners.
Failures comment too. The badge flips to failure with the error inside the collapsible section, and the final step then fails the job, so the PR shows both the red check and the reason without a trip into the Actions logs.
After the merge, the apply job posts its result — success or failure — back on the pull request it came from, resolved through the merge commit. The PR ends up carrying the full plan-review-apply record.
stategraph apply --auto-approve plans and applies in one step, so the apply always reflects the merge result — there is no stale-plan-file problem to manage between jobs.
The CLI shells out to OpenTofu (or Terraform) for planning; setup-opentofu provides the binary. Pin TF_CMD in env if the runner has both and you need to choose.
Concurrent pull requests
Two PRs touching disjoint resources plan and apply independently — Stategraph locks per resource, not per state file. When changes genuinely overlap, the second apply is rejected at commit with the conflicting transaction id; re-running the job re-plans and retries. No force-unlock, no serialized queue.
Pull-request-driven applies
Want the plan as a PR comment and applies driven by commenting on the PR instead of merging? That is Stategraph Orchestration — connect the repository and skip hand-rolled workflow YAML entirely.
Next Steps
- Velocity setup — importing state and configuring the CLI
- Orchestration — the managed PR-comment workflow
- Transactions — what an apply commits