Skip to content

feat: align the python starter kit with the typescript one - #20

Merged
dannysteenman merged 6 commits into
feat/immutable-github-oidcfrom
feat/align-with-typescript-starter-kit
Aug 2, 2026
Merged

feat: align the python starter kit with the typescript one#20
dannysteenman merged 6 commits into
feat/immutable-github-oidcfrom
feat/align-with-typescript-starter-kit

Conversation

@dannysteenman

Copy link
Copy Markdown
Member

Summary

The TypeScript starter kit has been where new features land, and this kit drifted behind it. This closes the gap: same project structure, same deployment model, same CI.

Stacked on #19 and targeting that branch, so this diff shows only the realignment. It retargets to main automatically once #19 merges.

What changed

Structure

Before After
Stacks BaseStack, GitHubOIDCStack StarterStack, FoundationStack
Constructs BaseConstruct, NetworkConstruct plus GitHubActionsOidcConstruct
Aspects none permission boundary, S3 encryption, S3 public access, VPC CIDR

FoundationStack holds the account-level plumbing: the deploy role plus a CDK toolkit cleaner, which is what stops the staging bucket growing with every deployment. The OIDC role moved out of the stack into its own construct, matching the TS layout.

The constructs folder keeps the name custom_constructs. A src/constructs package would shadow the constructs pip package that every CDK module imports.

Deployments

EnvironmentConfig replaces the target_accounts dict, with test (branch deploys on) and production. List order is deployment order: cdk-deploy-production chains onto cdk-deploy-test completing, and only runs when it succeeded.

Branch deployments work like the TS kit. GIT_BRANCH_REF names stacks after the branch, so several developers share one account without collisions:

$ ENVIRONMENT=test uv run cdk ls
FoundationStack-test
StarterStack-test

$ ENVIRONMENT=test GIT_BRANCH_REF=feature/add-api uv run cdk ls
StarterStack-add-api

FoundationStack drops out of branch deployments on purpose: a feature branch has no business recreating the role its own pipeline assumes. Test-environment branch deploys and destroys use CDK express mode, and deleting the branch triggers the destroy workflow.

Tasks gain the :all / :stack split, ls, and a branch-only deploy:hotswap.

CI

Nothing ran tests in CI before, which is how the broken pytest setup in #19 went unnoticed. Now:

  • build.yml runs ruff, ty, pytest and synth on every PR, with projen's self-mutation push so a stale generated file can't merge. ruff and ty are wired into the projen test task, so they run locally too.
  • cdk-diff-pr-comment posts a diff against production on pull requests.
  • release.yml tags and publishes a GitHub release when the project version changes.
  • The CDK CLI now comes from the pinned aws-cdk-cli PyPI package instead of npm install -g aws-cdk, so cdk_cli_version actually pins something and no workflow needs Node.js set up.

Two places a literal port would have broken

BucketPublicAccessAspect cannot read its own property. jsii can't deserialize a struct set from the TypeScript side, so node.public_access_block_configuration raises ValueError: Unknown interface on any bucket built through the Bucket L2 construct. The TS aspect reads it directly. Ported as-is, this crashes synthesis on the most common case: a bucket that correctly blocks public access. The aspect catches it and leaves those buckets alone; the case it exists to catch, a bucket with no configuration, reads cleanly as None and is still corrected. test_aspects.py covers both.

GitHub ignores a branches filter on delete events. The TS destroy workflow emits one, and projen's Python DeleteOptions rejects the argument outright. The exclusions moved into the job condition, generated from the same BRANCH_EXCLUSIONS list:

if: github.event_name == 'workflow_dispatch' || (github.event.ref_type == 'branch' && github.event_name == 'delete' && github.event.ref != 'main' && !startsWith(github.event.ref, 'hotfix/') && ...)

Breaking changes

Both hit anyone who already deployed this kit.

  1. The deploy role is renamed from GitHubDeployRole to GitHubActionsServiceRole, matching the TS kit and GITHUB_DEPLOY_ROLE. Redeploy FoundationStack before the workflows can assume it.
  2. Environments changed from dev + test to test + production. The dev:* tasks and cdk-deploy-dev workflow are gone.

The trust policy also gains an environment: segment, so a deploy job must declare a matching GitHub environment. The generated workflows already do.

Where it still differs from the TypeScript kit

  • Release: projen's release component derives the next version from conventional commits and has no Python equivalent. This uses the version in .projenrc.py as the trigger instead: bump it, merge, and the workflow tags and publishes with generated notes.
  • Dependency upgrades: Dependabot, already configured with auto-approve and auto-merge, rather than projen's upgrade-main. Adding both would open competing PRs.
  • VPC CIDR aspect: stdlib ipaddress instead of the netmask package, so no new dependency.

Diagram

flowchart TD
  PR["Pull request"] --> Build["build.yml: ruff, ty, pytest, synth"]
  PR --> Diff["cdk-diff-pr-comment"]
  Push["Push to main"] --> Test["cdk-deploy-test"]
  Test -->|"workflow_run success"| Prod["cdk-deploy-production"]
  Push --> Release["release.yml: tag on version change"]
  Branch["Push a feature branch"] --> BranchDeploy["cdk-deploy-test-branch (express mode)"]
  Delete["Delete the branch"] --> Destroy["cdk-destroy-test-branch"]
  BranchDeploy --> Stacks["StarterStack-<branch>"]
  Test --> Shared["FoundationStack-test + StarterStack-test"]
Loading

Validation

  • uv run projen build: 62 tests pass, ruff and ty clean
  • cdk ls verified for both the shared and branch cases, output above
  • New tests: test_aspects.py (14), test_env_helper.py (21), test_foundation_stack.py rewritten for the new stack
  • Docs updated: root README structure and branch-deploy section, src/stacks/README.md, src/custom_constructs/README.md, new src/aspects/README.md

Brings the Python kit up to the feature set and structure of
aws-cdk-starter-kit, which has been the one getting the new features.

Structure:
- src/aspects: permission boundary, S3 encryption, S3 public access and
  RFC 1918 VPC CIDR aspects, with a README.
- src/stacks: FoundationStack (deploy role + CDK toolkit cleaner) and
  StarterStack replace GitHubOIDCStack and BaseStack.
- src/custom_constructs: the OIDC role moves out of the stack into
  GitHubActionsOidcConstruct. The folder keeps its name because a
  src/constructs package would shadow the constructs pip package.

Deployments:
- EnvironmentConfig replaces the target_accounts dict. Environments are
  test (branch deploys on) and production, deployed in list order.
- Branch deployments: GIT_BRANCH_REF names stacks after the branch,
  express mode on ephemeral test stacks, and a destroy workflow on
  branch deletion. FoundationStack is skipped for branch deploys.
- Tasks gain :all/:stack variants plus ls and branch-only hotswap.

CI:
- build.yml runs ruff, ty, pytest and synth on every PR, with projen's
  self-mutation push. Nothing ran tests in CI before.
- cdk-diff-pr-comment posts a diff on pull requests.
- release.yml tags and publishes when the project version changes.
- The CDK CLI comes from the pinned aws-cdk-cli PyPI package instead of
  npm install -g aws-cdk, so the cdk_cli_version pin is real.

Two places where a literal port would have broken:
- BucketPublicAccessAspect catches the ValueError jsii raises when
  reading a struct set from the TypeScript side, which every bucket
  built through the Bucket L2 construct hits.
- The destroy workflow filters branches in the job condition, because
  GitHub ignores a branches filter on delete events.

BREAKING CHANGE: the deploy role is now named GitHubActionsServiceRole
rather than GitHubDeployRole, and the dev:* tasks and cdk-deploy-dev
workflow are replaced by test:* and production:*.
@dannysteenman dannysteenman added the enhancement New feature or request label Aug 2, 2026
@dannysteenman
dannysteenman requested a review from axonstone August 2, 2026 07:27
Reuse and altitude reviewers both flagged the deploy role name being
retyped in .projenrc.py while the construct kept its own default. Two
sides must agree on it: FoundationStack creates the role and the
generated workflows put its ARN in role-to-assume. env_helper now owns
DEFAULT_GITHUB_DEPLOY_ROLE_NAME and both import it. DEFAULT_ENVIRONMENT
gets the same treatment.

That required env_helper to stop importing projen at runtime, since the
CDK app imports it for create_env_resource_name. The annotation moves
under TYPE_CHECKING, so synthesis no longer drags the build tool in.

Also from the reviews:
- .projenrc.py builds the task env dict once instead of repeating four
  of five keys for the branch variant.
- TRAILING_HYPHENS and TRAILING_NON_ALPHANUMERIC collapse into one
  TRAILING_SEPARATORS pattern; every string they trim is drawn from the
  same alphabet, so they did the same job.
- _create_cdk_deployment_workflow drops chained_on_previous_environment,
  which only duplicated whether triggers carries workflow_run, and the
  unreachable -1 fallback on the environment index.
- BucketPublicAccessAspect flattens its nested if/else into guard
  clauses matching the rest of the function.
- app.py tags branch deploys with extract_cleaned_branch_name directly
  rather than calling create_env_resource_name("b") for its suffix
  logic. The tag is now "add-api" rather than "b-add-api".

Generated workflows are byte-identical, confirming the cicd_helper
changes are behavior-preserving.
Nothing merges a dependency bump on its own any more. auto_approve_options
and the auto-merge override are gone, which removes .github/workflows/
auto-approve.yml, and the "auto-approve" label goes with them since it no
longer triggers anything. Dependabot still opens grouped weekly PRs; they
now wait for a human.

Adds the uv equivalent of the TypeScript kit's pnpm minimumReleaseAge:
tool.uv.exclude-newer = "7 days" makes resolution skip anything published
in the last week, so a compromised or broken release has time to be caught
and yanked. uv takes a rolling duration, so there is no timestamp to
maintain. Only `uv lock` resolves; the deploy workflows run
`uv sync --frozen` and are unaffected.

The versions this file pins needed exempting. exclude-newer applies to
explicit pins too, so pinning a CDK release on its publication day made
resolution fail outright rather than just holding back an upgrade
(aws-cdk-lib==2.263.0 hit exactly this). tool.uv.exclude-newer-package
sets "0 days" for each, and PINNED_PACKAGES now feeds both that and the
Dependabot ignore list, which previously named aws-cdk rather than the
aws-cdk-cli package this project actually depends on and missed pytest.
Merges the revert of "chore: upgrade deps" from the parent branch and
removes tool.uv.exclude-newer-package, which only existed because
aws-cdk-lib 2.263.0 was a day old and the 7-day gate refused to resolve
it. Every pinned version now predates the gate on its own:

  aws-cdk-lib  2.254.0   80 days
  aws-cdk-cli  2.1130.0  23 days
  projen       0.99.62   75 days
  pytest       9.0.3    116 days

aws-cdk-cli is the one exception to the revert. The upgrade had moved it
from 2.1117.0, but `cdk deploy --express` does not exist in that release,
so reverting it fully would have left the branch-deploy tasks passing a
flag the CLI rejects. 2.1130.0 is what the TypeScript kit pins, supports
--express, and clears the age gate.

PINNED_PACKAGES stays, since Dependabot still needs to ignore these four.
Its comment now records the constraint: keep them on releases older than
MINIMUM_RELEASE_AGE_DAYS, or `uv lock` fails outright rather than merely
declining the upgrade.
projen 0.99.62 -> 0.101.20 and pytest 9.0.3 -> 9.1.1. Both are the newest
releases at least MINIMUM_RELEASE_AGE_DAYS old, so `uv lock` resolves them
without any exclude-newer-package exemption. aws-cdk-lib stays on 2.254.0.

  aws-cdk-lib  2.254.0    80 days
  aws-cdk-cli  2.1130.0   23 days
  projen       0.101.20    8 days
  pytest       9.1.1      43 days

pytest never needed reverting: 9.1.1 is 43 days old and always cleared the
gate. Only aws-cdk-lib 2.263.0 ever tripped it.

Regenerating under 0.101.20 drops the auto-generated step `id:` fields that
0.101.23 emits. Cosmetic only; every id referenced by an output or a step
condition is set explicitly and is unchanged.
The feature list sold the kit instead of describing it ("Production-ready",
"Seamless Security", "enterprise-ready", "Ship-ready"). Every bullet now
names what the thing actually does, so a reader can tell whether they want
it. The Project Structure section opened with four paragraphs of throat
clearing before the tree; it's two sentences now, and they say why a single
stack stops working rather than that modularity is good.

Fixes three things that were wrong rather than just wordy:

- Step 3 told you to `npm install -g aws-cdk`. The CDK CLI comes from the
  aws-cdk-cli PyPI package in the lockfile now, so `uv sync` covers it.
  Removing that step renumbered the rest, and step 6 pointed at the wrong
  one for account setup.
- `cdk bootstrap` in step 7 assumed a global install that no longer exists,
  so it's `uv run cdk bootstrap`.
- Step 7 ended in a stray double bracket.

Leaves the TIP-LIST block alone, since that's synced across repos.
@dannysteenman
dannysteenman merged commit 69a773c into feat/immutable-github-oidc Aug 2, 2026
4 of 5 checks passed
@dannysteenman
dannysteenman deleted the feat/align-with-typescript-starter-kit branch August 2, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant