CI deploys with GitHub OIDC
blogwright deploys from CI without any stored AWS credentials. Set githubRepo in your config and bootstrap provisions an IAM role that GitHub Actions assumes via OIDC federation: the workflow exchanges its short-lived GitHub token for temporary AWS credentials, runs blogwright deploy, and nothing long-lived ever exists to leak or rotate.
Enabling the deploy role
Section titled “Enabling the deploy role”Add your repository slug to the environment’s config:
{ "region": "us-east-1", "siteName": "example", "githubRepo": "owner/repo", // enables the GitHub OIDC deploy role at bootstrap}Then run (or re-run) bootstrap for each environment CI should deploy:
blogwright bootstrap # creates production-example-ghblogwright bootstrap staging # creates staging-example-ghBootstrap creates two things:
- The account-wide OIDC identity provider for
token.actions.githubusercontent.com(audiencests.amazonaws.com), created only if it doesn’t already exist. - A per-environment deploy role named
<env>-<siteName>-gh- e.g.production-example-gh- trusted only for tokens from your repository.
Adding githubRepo to an already-bootstrapped environment works the same way: re-run blogwright bootstrap <env> and the reconcile creates the missing role. Re-running bootstrap also reapplies the trust policy and permissions, so changing githubRepo (a repo rename or transfer) is a config edit plus a bootstrap.
Trust conditions
Section titled “Trust conditions”The role’s trust policy pins the token’s audience to sts.amazonaws.com and matches the OIDC subject claim per environment, mirroring how each one is meant to deploy:
| Environment | Subject claim | Meaning |
|---|---|---|
production |
repo:owner/repo:environment:production |
Only jobs running in the production GitHub environment |
Any other (e.g. staging) |
repo:owner/repo:ref:refs/heads/main |
Only workflow runs on the main branch |
| Preview stack | repo:owner/repo:* |
Any ref - PRs deploy and destroy previews |
Scoping production to a GitHub environment rather than a branch lets you gate deploys behind environment protection rules - required reviewers, wait timers - in the repository settings. The workflow job must declare environment: production, or the token’s subject won’t match and AssumeRoleWithWebIdentity fails.
What the role can do
Section titled “What the role can do”The role is scoped to one environment’s resources, not the account:
- Read, write, and delete objects in the environment’s S3 bucket (source zips, the built site, staged releases, state, and the environment lock). In a static environment it may also read object tags under
releases/*and write them undersite/*- exactly whatpromoteneeds to carry a staged release’s tags intosite/, and nothing wider. The preview role gets no tagging grant. - Run and manage builder MicroVMs, and rebuild the builder image when the agent bundle changed - so build-agent updates ship through CI without a separate
bootstrap. - Read the MicroVM build logs, and pass the environment’s build and exec roles.
- Create CloudFront invalidations on the environment’s distribution (previews are never cached, so the preview role omits this).
- When
headersis configured, read and update the environment’s own response headers policy (cloudfront:GetResponseHeadersPolicyandcloudfront:UpdateResponseHeadersPolicyon that one policy’s ARN), so adeployorpromotekeeps the headers in step with the config. It cannot attach or detach the policy - that changes the distribution, which stays abootstrapaction. The grant names the policy id that bootstrap records, so it appears after the firstbootstrapwithheadersset.deploymakes these calls only oncebootstraphas recorded the policy as attached; it tells an unattached policy from state alone, so the role has no distribution read. The preview role never gets it. - In an SSR environment, publish revisions of the environment’s own Web Function: create revisions and read or list that function’s revisions and endpoints (never account-wide), invoke its endpoint for the deploy preflight and readiness probe, pass its execution role, and read artifact object versions (
s3:GetObjectVersion) sorollbackcan reproduce a pinned revision. It cannot create or delete the function, change its endpoint, or touch the distribution’s configuration -bootstrap, including a--switch-modebootstrap, stays an operator action. The grants are added by the same bootstrap that provisions the function, since the endpoint grant names the endpoint ARN it records. - When standard.site publishing is configured and
blogwright pds bootstrap <env>has been run once for the environment, read and write the OAuth secret in Secrets Manager - the post-deploy sync rotates the session on every run. That grant is no longer part of the role’s<env>-deploypolicy: it is a separate inline policy namedblogwright-pds, owned and reconciled by the pds plugin. The preview role never gets it - its subject claim accepts any ref, while the secret is shared across environments - so preview deploys of a standard.site do not rotate the session.
When upgrading from the additive PDS migration release, run blogwright pds bootstrap <env> before the next blogwright bootstrap <env> in every configured non-preview environment (for example, production or staging). Site bootstrap rewrites the entire <env>-deploy policy, removing the old secret statement. If this step was missed, run blogwright pds bootstrap <env> to restore secret access; the next deploy can sync to the PDS again. The site deploy itself still succeeds while sync is unavailable.
Finding the role ARN
Section titled “Finding the role ARN”The name is deterministic, so the ARN is too:
arn:aws:iam::<accountId>:role/<env>-<siteName>-ghblogwright status <env> also prints each resource’s recorded outputs, including the deploy role’s ARN.
Profiles and the account pin in CI
Section titled “Profiles and the account pin in CI”aws-actions/configure-aws-credentials exports AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN into the job. Exported credentials take precedence over the config’s aws.profile, so a profile you committed for local use is ignored in CI - the command logs ignoring aws.profile "<name>": environment credentials are set - and nothing breaks for want of a ~/.aws/config on the runner. (A self-hosted runner on an EC2 instance role exports nothing, so there export the instance credentials first - see troubleshooting.)
The aws.accountId pin still applies. If the role CI assumed belongs to another account, the deploy stops before it reads any state. The identity line each command prints goes to stderr in CI, so the job log always records which account the deploy ran against.
Example: staging on push to main
Section titled “Example: staging on push to main”The workflow needs id-token: write to mint an OIDC token, and contents: read to check out the repo. aws-actions/configure-aws-credentials assumes the role and exports the temporary credentials into the environment, where blogwright’s ambient provider chain picks them up.
name: Deploy staging
on: push: branches: [main]
permissions: id-token: write contents: read
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/staging-example-gh aws-region: us-east-1 - run: pnpm exec blogwright deploy stagingExample: production on release
Section titled “Example: production on release”Identical shape, with two changes: the job runs in the production GitHub environment (the trust condition requires it), and the trigger is a release rather than a push. The trust policy only checks the environment claim - the trigger is yours to choose - but releasing production deliberately, gated by environment protection rules, is the intended flow.
name: Deploy production
on: release: types: [published]
permissions: id-token: write contents: read
jobs: deploy: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/production-example-gh aws-region: us-east-1 - run: pnpm exec blogwright deployTo gate the release instead - build when the release is drafted, promote the reviewed bytes when it is published - run blogwright stage in one job and blogwright promote <hash> in a later one (both in the production environment); see Release gating. promote checks the staged bytes against their artifact manifest before it copies anything.
The preview stack’s role
Section titled “The preview stack’s role”blogwright preview bootstrap creates its own role, preview-<siteName>-gh, trusted for any ref in the repository so a PR workflow can deploy a preview on open/update and destroy it on close. Its access is scoped to the preview stack’s own bucket, and it has no CloudFront invalidation permission - previews are served uncached. See PR previews for the preview workflow.