The landing zone includes two generated GitHub Actions workflows. The deployment workflow runs both phases on every push to main: the organization phase to your management account, then the landing-zone phase to your landing zone account. The pull request workflow validates and diffs the same two phases and posts the combined diff as a comment. Both authenticate through GitHub's OIDC provider, so no long-lived AWS credentials are stored in GitHub Secrets.
How the deployment pipeline works
The workflow file lives at .github/workflows/cdk-deploy-landing-zone.yml. It is generated by projen from .projenrc.ts, so edit .projenrc.ts to change it, not the workflow file directly.
The pipeline runs the two phases against two different accounts in one run:
- It assumes
GitHubActionsServiceRolein your management account, runspnpm run organization:validate, then runspnpm run organization:deploy, which deploysLandingZoneOrganizationStack. - It re-configures credentials to assume
GitHubActionsServiceRolein your landing zone account, runspnpm run landingzone:validate, then runspnpm run landingzone:deploy:all, which deploysLandingZoneFoundationStackandLandingZoneAccountProvisioningStack.
Each phase validates after it configures its own credentials, so the CloudFormation pre-deployment checks run against the account that phase actually targets. A validation failure fails the job immediately, before CloudFormation starts provisioning.
Both roles are named GitHubActionsServiceRole, one per account, and both trust only your repository. Dependencies are installed once with pnpm run setup before the organization phase and reused for the landing-zone phase.
The pull request diff workflow
.github/workflows/cdk-diff-pr-comment.yml runs on every pull request against main. It assumes the same two roles in sequence, validates and diffs each phase against its target account, then posts one combined comment on the pull request so reviewers see the infrastructure impact next to the code change.
A few behaviors worth knowing:
- Nested stack diffs are filtered out of the comment. StackSet stacks generate a lot of nested-stack noise that repeats what the parent stack already shows.
- The comment is updated, never duplicated. Push again and the existing comment is rewritten in place, so the thread stays readable. If a later push removes every change, the existing comment is rewritten to
No Changes!rather than left showing a stale diff. - A failed diff fails the check and says which phase failed, with the captured command output in the workflow run and a link to it in the comment.
- It only runs for branches in your own repository. Pull requests from forks are skipped, because a fork cannot be trusted with a role that can read your organization.
- In-progress runs are cancelled when you push again to the same pull request. Unlike a deploy, a diff is safe to interrupt, so the comment reflects your latest commit rather than queueing behind stale runs.
Both workflows run in the landingzone GitHub environment, which they declare and the trust policy requires. The environment name is fixed, so the workflows and the roles cannot drift apart.
The GitHubActionsServiceRole
The landing zone sets this up for you. The management-account role is created by GitHubActionsOidcConstruct inside LandingZoneOrganizationStack; the matching landing-zone-account role is bootstrapped by the same organization phase when it prepares the landing zone account. On the first local deployment of the organization stack, it:
- Registers
token.actions.githubusercontent.comas an OIDC identity provider in each account - Creates a
GitHubActionsServiceRolewithAdministratorAccessin the management account and a deploy role in the landing zone account - Scopes each role's trust policy to your repository, resolved automatically from your git remote
- Pins that repository by GitHub's immutable numeric owner and repository IDs alongside its name, so a repository that later takes over your old name cannot assume the role
- Restricts the trust to workflows running in the
landingzoneGitHub environment - Sets a short
maxSessionDurationso each pipeline run receives temporary credentials that expire automatically, with nothing long-lived stored anywhere
This is a one-time setup. After the first pnpm run organization:deploy completes locally and you have set landingZoneAccountId and re-run projen, the pipeline is authorized to run both phases without any manual IAM configuration.
Where the repository identity comes from
Synthesis needs your repository's numeric owner and repository IDs to build the trust policy. Inside GitHub Actions they arrive automatically, so the pipeline never needs a token. On your own machine, the GitHub CLI reads them, which is why local synthesis and deployment require it:
gh auth login
gh auth status
Without an authenticated gh, synthesis fails rather than falling back to a weaker trust policy.
After a rename or transfer
The numeric IDs survive a rename, but the subject GitHub sends also carries the repository's current name, and the trust policy still holds the old one. Rename or transfer your repository and the pipeline stops being able to assume the role until you re-run pnpm run organization:deploy from the renamed repository, which rewrites the trust policy with the new name.
The role ARNs embedded in the workflow follow the pattern:
arn:aws:iam::<managementAccountId>:role/GitHubActionsServiceRole
arn:aws:iam::<landingZoneAccountId>:role/GitHubActionsServiceRole
These values are baked into the workflow by projen when you run pnpm exec projen. If either account ID changes, re-run projen and the workflow updates automatically.
CodeArtifact access
The workflow runs pnpm run setup before deploying. This script authenticates to the private CodeArtifact registry that hosts @towardsthecloud/cdk-landing-zone-constructs and @towardsthecloud/cdk-landing-zone-foundation, then runs pnpm install --frozen-lockfile. The authentication works because:
- The workflow assumes
GitHubActionsServiceRolein your management account via OIDC - Towards the Cloud whitelists your management account on the CodeArtifact repository during initial setup
- The whitelisted account can fetch an auth token from CodeArtifact without further configuration
No separate secret is needed for package installation. The installed node_modules carry over to the landing-zone phase, so setup runs once.
Trusting additional repositories
By default, only the repository resolved from your git remote can assume GitHubActionsServiceRole. Leave it that way unless you deploy the same landing zone from more than one repository.
If you do need a second repository (for example, a separate infrastructure repo) to deploy the landing zone, add it to GitHubActionsOidcConstruct in src/stacks/landing-zone-organization-stack.ts via the additionalRepositories prop. That construct instantiation lives in a file you own:
new GitHubActionsOidcConstruct(this, 'GitHubActionsOidc', {
additionalRepositories: [{ name: 'my-other-repository', id: '123456789' }],
});
Each entry names a repository under the same GitHub owner and carries its own immutable numeric ID, which you check in rather than have the construct look up. Find one with:
gh api repos/OWNER/NAME --jq .id
Two constraints follow from the same rules the primary repository lives under. Keep each name current, because renaming a listed repository breaks its access until you update the entry and redeploy. And the owner ID comes from the repository being synthesized, so a reference stays valid only while both repositories share an owner.
Every additional repository is trusted only for workflows targeting the landingzone environment, exactly like the primary one.
Regenerating the workflows with projen
Both workflow files are managed by projen. Editing them directly works but those changes are overwritten the next time someone runs pnpm exec projen. Instead, make changes through .projenrc.ts:
- To change the management or landing zone account ID or primary region: update
landing-zone-settings.tsand then runpnpm exec projen. Projen reads the settings at synthesis time and embeds the updated account IDs and region into both workflows and theorganization:*andlandingzone:*npm tasks. - To change the Node.js version: update the
nodeVersionconstant in.projenrc.tsand runpnpm exec projen. Both thesetup-nodestep and the.nvmrcfile update together. - To add a workflow step: extend the CDK workflow helpers in
src/bin/cicd-helper.tsor add a projen workflow step in.projenrc.ts.
After any .projenrc.ts change, commit the updated .projenrc.ts along with the regenerated .github/workflows/cdk-deploy-landing-zone.yml and .github/workflows/cdk-diff-pr-comment.yml.
Manual dispatch
The deployment workflow supports workflow_dispatch, so you can trigger a deploy from the GitHub Actions UI without pushing a commit. This is useful for forcing a re-deploy after a settings change that doesn't touch any tracked file, or for recovering from a partially failed pipeline run. The pull request workflow runs on pull requests only.
Concurrency
The deployment workflow sets concurrency.cancel-in-progress: false for the cdk-deploy-landing-zone group. A second push while a deploy is in progress queues rather than cancels the running deploy. CDK deployments are not safe to cancel mid-run (CloudFormation can be left in an UPDATE_IN_PROGRESS state), so this is intentional.
The pull request workflow takes the opposite setting: its concurrency group is per pull request and cancels in progress, because a diff has no side effects and only the newest commit's result is worth reading.