GitHub Actions Overview
inforge is a toolkit, not a set of opinionated pipelines. It ships one GitHub Action — a
composite action that installs the inforge CLI — and you own your workflows. Your workflow injects
your secrets as environment variables and runs inforge <command>; inforge never enumerates a fixed
list of provider secrets, so it stays decoupled from which clouds you use.
The install action
- uses: wardnet/inforge@v1
Downloads the inforge binary and runs inforge plugins install. Pin the CLI version with the
version input:
- uses: wardnet/inforge@v1
with:
version: v1.6.0 # default: latest release
:::warning @v1 pins the action, not the CLI
wardnet/inforge@v1 pins the action to the rolling v1 tag; on its own it installs the latest
inforge release, which can move under you. For reproducible runs, also pin the CLI with the
version: input (e.g. version: v1.6.0). The two are independent: the action ref controls the
install glue, version: controls the binary it downloads.
:::
That is the whole toolkit-provided surface. Everything else is a normal run: step calling the CLI.
How secrets reach inforge
inforge resolves ${ENV_VAR} references in your regions.yaml / variables.yaml, and the env:<VAR>
secret sources on your services, from the process environment. So you decide the vocabulary: set whatever
environment variables your config references, from whatever secrets you keep next to your infra
definition. inforge does not know — and does not need to know — that a secret is named
CLOUDFLARE_API_TOKEN.
jobs:
deploy:
runs-on: ubuntu-latest
env: # your names, your secrets
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
# ...only what your regions.yaml / secrets reference
Add a provider → add one env: line. Drop one → delete a line. The toolkit never changes.
Starter workflows
Copy these into your infrastructure repo and adjust the env: block to your providers.
name: PR Checks
on:
pull_request:
paths: ["resources/**", "inforge*.yaml"]
permissions:
contents: read
pull-requests: write
jobs:
preview:
runs-on: ubuntu-latest
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: wardnet/inforge@v1
with:
version: v1.6.0 # pin the CLI for reproducible runs
- run: inforge validate prd
- run: inforge preview prd --report report.md
- if: github.event_name == 'pull_request'
run: gh pr comment "${{ github.event.pull_request.number }}" --body-file report.md
env:
GH_TOKEN: ${{ github.token }}
name: Deploy
on:
push:
branches: [main]
paths: ["resources/**", "inforge*.yaml"]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
INFORGE_SECRETS_KEY: ${{ secrets.INFORGE_SECRETS_KEY }}
INFORGE_DEPLOY_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: wardnet/inforge@v1
with:
version: v1.6.0
- run: inforge deploy prd --yes
INFORGE_SECRETS_KEY is only needed when the environment uses
vault: secret sources — it is the age master identity printed by
inforge secret init. The PR validate/preview workflow does not need it: previews
substitute a placeholder for encrypted values.
Scheduled drift reconcile (optional)
inforge matrix prints the environments whose resources changed between two git refs, so a scheduled
job can re-deploy only what drifted. Run it on a cron and feed the result into a matrix:
name: Reconcile
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
matrix:
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.m.outputs.environments }}
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: wardnet/inforge@v1
with:
version: v1.6.0
- id: m
run: echo "environments=$(inforge matrix --base main --head HEAD)" >> "$GITHUB_OUTPUT"
deploy:
needs: matrix
if: needs.matrix.outputs.environments != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
include: ${{ fromJson(needs.matrix.outputs.environments) }}
env: # your providers' secrets
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
INFORGE_SECRETS_KEY: ${{ secrets.INFORGE_SECRETS_KEY }}
INFORGE_DEPLOY_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: wardnet/inforge@v1
with:
version: v1.6.0
- run: inforge deploy "${{ matrix.environment }}" --yes
Service release (optional)
A service repo builds its own artifact, then uses inforge to push it to the release store and roll it out. This lives in the service repo, not the infra repo:
name: Release
on:
push:
branches: [main]
permissions:
contents: read
concurrency: # one release per service+env at a time
group: inforge-release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
INFORGE_DEPLOY_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
# ...your build steps produce the artifact under ./deployments...
- uses: wardnet/inforge@v1
with:
version: v1.6.0
- run: inforge releases push prd --service api-server --sha "$GITHUB_SHA"
- run: inforge releases deploy prd --service api-server --sha "$GITHUB_SHA"
inforge releases deploy resolves the host, folder, and systemd unit from the infra Pulumi stack at
runtime — nothing about the deploy target needs to be committed to the service repo.
inforge deploy/preview always write a markdown run report to --report <path> (or a temp file,
whose path they print) and, when $GITHUB_STEP_SUMMARY is set, append it to the job summary
automatically. Posting it as a PR comment is a one-liner (gh pr comment --body-file) you own — the
CLI never calls the GitHub API itself.
Notes
- The environment is the first positional argument (
inforge deploy prd) — it is the Pulumi stack name. You do not need aninforge.<env>.yamljust to name it; if the file is absent, the run uses no extra stack config. ${ENV_VAR}references that are unset fail the run loudly — only set the ones your config uses.- Backend credentials (
AWS_*,CLOUDFLARE_ACCOUNT_ID) are needed only for anr2/s3state backend; afile/git-branchbackend needs none.
Migrating to 1.6
1.6 makes inforge a provider-agnostic toolkit: it stops shipping reusable workflows that enumerated a fixed set of provider secrets. Three things change for consumers.
Reusable workflows → your own workflow + the action. Replace every
uses: wardnet/inforge/.github/workflows/<name>.yml@v1 caller with a normal job that installs the CLI
via wardnet/inforge@v1 and runs inforge <command> directly. Start from the starter
workflows above — the deploy, preview, reconcile, and service-release
reusable workflows all map onto one of them.
secrets: → job env:. The reusable workflows took a secrets: block and mapped it to env vars
for you. Now you set the env: block yourself, naming only the variables your config references. A
provider you don't use is simply a line you don't add.
gha:NAME → env:NAME. The gha: secrets-DSL source is gone. Secrets are now declared
inline on a service (there is no separate secrets/*.yaml resource);
rewrite each source: gha:CLOUDFLARE_API_TOKEN as a CF_TOKEN: env:CLOUDFLARE_API_TOKEN entry in the
service's environment.yaml. The value still comes from the process environment, so the matching env: line
in your workflow is what supplies it; an unset or empty value fails the run rather than writing an empty
secret. App secrets that should live in git move to vault: instead.