Skip to main content

CI/CD — autolastic-saas-platform

This document covers every GitHub Actions workflow in the platform repo, how they chain together, and how instance repos consume them via reusable workflow calls.


Pipeline Overview​

Platform repo pipeline (push to main)​

Push to main
│
▼
ci.yml
├── secrets-scan (gitleaks — blocks on leaked credentials)
├── actionlint (workflow YAML lint)
├── check-forbidden-strings
├── test (unit + integration, all packages)
├── lint (ESLint, all packages)
├── typecheck (tsc --noEmit, all packages)
├── build-shared
├── build-api / build-billing / build-media / build-smoke / build-infra / build-app (parallel)
└── synth (CDK synth with pre-seeded context)
└── ci-complete ✓ ← sentinel job; downstream workflow_run triggers watch this

ci-complete succeeds
│
▼
bump-version.yml (auto-tags HEAD with next v0.0.X)
│
▼
[vX.Y.Z tag pushed]
│
▼
publish-packages.yml (stamps versions, builds all packages, npm publish to CodeArtifact)

Instance repo pipeline (push to main)​

Push to main
│
▼
ci.yml (instance)
├── check-platform-version (all 10 files reference the same @vX.Y.Z)
├── secrets-scan (gitleaks via reusable)
├── actionlint
├── shell-tests (bats tests/upgrade-platform.bats)
└── typecheck (via reusable-install-and-typecheck-instance.yml)

CI workflow completes successfully
│
▼
deploy-infra.yml (workflow_run: "CI" → completed)
├── change detection (were any infra/ files changed?)
├── read-config (rootDomain from instance.config.ts)
├── deploy-infra (CDK deploy via reusable-deploy-infra.yml)
└── summary (exits 1 if changes were skipped due to CI failure)

Deploy Infra workflow completes successfully
│
▼
deploy-app.yml (workflow_run: "Deploy Infra (CDK)" → completed)
├── change detection (were any infra/ or app/ files changed?)
├── deploy-app (build + S3 sync + CloudFront invalidation)
├── health-check (curl loop against APP_BASE_URL)
└── summary

Deploy App workflow completes successfully
│
▼
smoke.yml (workflow_run: "Deploy App (S3 & CloudFront)" → completed)
└── smoke tests (Playwright via reusable-smoke.yml)

Platform CI — ci.yml​

Triggers: push to main, pull_request to main, workflow_dispatch, workflow_call

Key behaviours:

  • secrets-scan and actionlint are in synth.needs — a failure in either blocks the entire pipeline
  • synth requires all build/test/lint/typecheck jobs to pass before running CDK synth
  • ci-complete is a sentinel job (if: always()) that exits 1 if synth did not succeed. Downstream workflow_run triggers (bump-version.yml) watch this job's conclusion, not the individual job results
  • CDK synth uses pre-seeded cdk.context.json values (AZs, hosted zone) so it can synth without AWS credentials

Jobs:

JobWhat it does
secrets-scanRuns gitleaks against commits in the push/PR range
actionlintLints all .github/workflows/*.yml files
check-forbidden-stringsScans for hardcoded secrets patterns via scripts/ci.check-forbidden-strings.sh
testAll unit + integration tests across all packages (requires Postgres service container)
lintESLint across api, shared, billing, media
typechecktsc --noEmit across all packages
build-sharedBuilds packages/shared (prerequisite for all other builds)
build-{api,billing,media,smoke,infra,app}Parallel builds; each re-builds shared first
synthCDK synth with pre-seeded context — validates the full infrastructure graph
ci-completeSentinel: exits 1 if synth failed/was skipped; always runs

Bump Version — bump-version.yml​

Trigger: workflow_run on ci.yml → ci-complete succeeded
Concurrency: bump-version (non-cancellable)

Calls scripts/bump-version.sh to create and push the next v0.0.X tag on the commit that passed CI.

  • Idempotent: if HEAD is already tagged, the script exits 0 with no action
  • Force-move: pass an explicit version + --sha to move an existing tag to a different commit
  • Output: tagged_version is written to GITHUB_OUTPUT and used by the publish job

Publish Packages — publish-packages.yml​

Trigger: push of a v* tag
Concurrency: publish-packages (non-cancellable)
Permissions: id-token: write (OIDC → AWS)

Publishes all 7 @autolastic/saas-* packages to the private AWS CodeArtifact registry.

Steps:

  1. Validate tag format (v0.0.X) — exits 1 on malformed tags
  2. Authenticate npm against CodeArtifact via OIDC
  3. Stamp versions: node scripts/set-package-versions.js <version> updates all 7 package.json files
  4. Build packages in dependency order: shared first, then api, billing, media, infra, app, smoke in parallel (background processes with wait)
  5. Publish: npm publish --workspaces
  6. Write $GITHUB_STEP_SUMMARY with published package list

CodeArtifact registry: autolastic/npm-private in the AWS account associated with AWS_ROLE_ARN


Release — release.yml​

Trigger: workflow_dispatch
Concurrency: release (non-cancellable)

Manually triggers a full end-to-end deploy on the platform repo itself: infra → app → smoke tests. Used for platform-level infrastructure changes that need to be validated against a real deployment.


Tag Stable — tag-stable.yml​

Trigger: workflow_dispatch
Input: version — the vX.Y.Z to promote

Moves the stable tag to the specified version. Instance repos can pin to @stable instead of a specific version if they want automatic promotion to the latest tested release.


Reusable Workflow Catalog​

All reusable workflows accept workflow_call and expose typed inputs and secrets.

reusable-deploy-infra-pipeline.yml​

Full change detection → CDK deploy → summary pipeline for an instance repo's infra.

InputDefaultDescription
project_prefix—CDK stack prefix (e.g. autolastic-app)
aws_regionus-east-1AWS region
infra_pathinfraPath to infra package
platform_version—Platform version being deployed
event_name—github.event_name from caller
commit_sha—Commit SHA to report
workflow_run_conclusion—Conclusion of the triggering workflow_run
infra_file_patterns^infra/Regex for change detection

Critical: the summary job exits 1 when changes was skipped on a non-dispatch trigger, propagating CI failures downstream to block deploy-app.

reusable-deploy-app-pipeline.yml​

Full change detection → Next.js build → S3 sync → CloudFront invalidation → health check → summary pipeline.

InputDefaultDescription
project_prefix—CDK stack prefix
aws_regionus-east-1AWS region
app_pathappPath to app package
platform_version—Platform version being deployed
event_name—github.event_name from caller
commit_sha—Commit SHA to report
workflow_run_conclusion—Conclusion of triggering workflow_run
app_file_patterns^(infra|app)/Regex for change detection

Secrets: AWS_ROLE_ARN, APP_BASE_URL

reusable-deploy-infra.yml​

Bare CDK deploy step (no change detection, no pipeline wrapper). Used by reusable-rollback.yml.

reusable-deploy-app.yml​

Bare Next.js build + S3 deploy step. Used by reusable-rollback.yml.

reusable-rollback.yml​

Emergency redeployment at the currently-pinned platform version. Skips CI and smoke tests.

InputValuesDescription
scopeapp, infra, bothWhat to redeploy
platform_version—Informational — the version being rolled back to
  • scope: app — runs rollback-app only (rollback-infra is skipped)
  • scope: infra — runs rollback-infra only
  • scope: both — runs rollback-infra first, then rollback-app sequentially

reusable-smoke.yml​

Runs Playwright smoke tests against the deployed instance. Requires CDK_CONTEXT_ROOT_DOMAIN to resolve the app URL.

reusable-install-and-typecheck-instance.yml​

Authenticates npm against CodeArtifact (via OIDC), runs npm ci, then tsc --noEmit for both infra and app packages in an instance repo.

Install and typecheck are intentionally bundled in the same reusable because they share a runner — typecheck needs the node_modules that npm ci just produced, and splitting into two reusable workflows would force a fresh npm ci on a clean runner anyway. The explicit name makes it clear that AWS_ROLE_ARN is required for the CodeArtifact login step, not for any CDK operation.

Use in instance CI:

typecheck:
uses: autolastic/autolastic-saas-platform/.github/workflows/reusable-install-and-typecheck-instance.yml@v0.0.50
with:
aws_region: "us-east-1"
infra_path: "infra"
app_path: "app"
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}

Migrated from reusable-typecheck-instance.yml in v0.0.50. Callers pinned to older tags continue to work at those tags; they should migrate to the new filename on their next platform upgrade.

reusable-secrets-scan.yml​

Runs gitleaks to detect accidentally committed credentials.

Scan scope:

  • pull_request — commits on the PR branch since it diverged from base
  • push — commits included in the push
  • workflow_dispatch / workflow_call — full history

Suppressing false positives:

# Inline suppression (on the line in question):
MY_VAR="not-a-real-secret" # gitleaks:allow

# Repo-level suppression (.gitleaks.toml):
[[allowlist]]
description = "Test fixtures"
paths = ["tests/"]

reusable-upgrade-platform.yml​

Runs scripts/upgrade-platform.sh inside GitHub Actions to bump the platform version across all tracked files in an instance repo.


Secrets Scanning​

The secrets-scan job runs on every CI build in both the platform and instance repos. It uses gitleaks with the default ruleset extended by repo-specific allowlists in .gitleaks.toml.

If gitleaks finds a real secret:

  1. Rotate the exposed credential immediately
  2. Remove it from git history: git filter-repo --path <file> --invert-paths
  3. Force-push the cleaned history

False positives are suppressed via .gitleaks.toml allowlists or inline # gitleaks:allow comments. Do not disable the scan entirely.


Actionlint​

All .github/workflows/*.yml files are linted by actionlint on every CI run. It catches:

  • Invalid if: expressions
  • Wrong input types for workflow_call
  • Undefined secrets or contexts
  • Mismatched needs: references

The IDE extension may show false positives for cross-repository reusable workflow references (autolastic/autolastic-saas-platform/...@vX.Y.Z) because it cannot download remote workflow definitions at lint time. These are suppressed in .actionlint.yaml.


Version Consistency​

Platform packages follow a single monotonically-increasing version (v0.0.X). Every tag push triggers publish, so version numbers are the same as the git tag.

Bumping a new version:

# In the platform repo — tag and push; CI auto-bumps after green builds
# Or manually:
./scripts/bump-version.sh v0.0.25

# In an instance repo — update all 10 tracked files:
./scripts/upgrade-platform.sh v0.0.25

Version tracking in instance repos: upgrade-platform.sh tracks and updates exactly 10 files:

FileMethod
app/package.jsonnpm dep semver
infra/package.jsonnpm dep semver
smoke/package.jsonnpm dep semver
.github/workflows/deploy-infra.ymlglobal sed (updates both platform_version: and @v refs)
.github/workflows/deploy-app.ymlglobal sed
.github/workflows/release.ymlglobal sed
.github/workflows/rollback.ymlglobal sed
.github/workflows/smoke.yml@v-prefixed sed
.github/workflows/upgrade-platform.yml@v-prefixed sed
.github/workflows/ci.yml@v-prefixed sed

The instance CI's check-platform-version job verifies all 10 are in sync on every PR and push.