Skip to content

CI/CD ​

Deploy from CI with short-lived, keyless credentials via GitHub OIDC. YOLO provisions a GitHub Actions OIDC trust and a tightly-scoped deployer role; your workflow assumes that role at runtime — nothing to store in your repo.

Declare the ref each environment deploys from ​

Each environment declares the git ref it deploys from. That single setting drives the deployer role's OIDC trust. It defaults to the main branch, so the common case needs no configuration at all:

ManifestOIDC sub scopeTypical use
branch: main (default)…:ref:refs/heads/mainpush to a branch — e.g. staging
tag: 'v*' (true = any tag)…:ref:refs/tags/v*tag push — e.g. production

Staging-on-develop, production-on-tag:

yaml
environments:
  staging:
    branch: develop           # deploys on push to develop
  production:
    tag: 'v*'                 # only a v* tag can assume the prod role

repository is inferred from your git origin (or GITHUB_REPOSITORY in CI). Set repository: org/repo per environment only to override it — for a monorepo or fork.

What yolo sync provisions for CI ​

When a GitHub repository is detected, yolo sync sets up the OIDC trust across the scopes:

  • sync:account provisions the account's GitHub Actions OIDC identity provider (token.actions.githubusercontent.com) — an account-level singleton shared by every app.
  • sync:environment provisions the env-shared yolo-{env}-observer policy — read-only access scoped to exactly the services YOLO provisions (not AWS's everything). It's the inspection surface the pre-deploy in-sync check reads, shared by every app's deployer role (and reusable by an operator/admin role).
  • sync:app provisions the deployer role yolo-{env}-{app}-deployer, whose primary trust lets only the environment's repo + ref assume it from CI (keyless OIDC); a second trust statement permits same-account assumption, so a developer running yolo deploy / build / run locally mints exactly this role's deploy policy on top of their own identity (the Deployer tier — capped to deploy, never their broader profile). It carries a permission policy scoped to exactly what yolo deploy writes (ECR push, ECS register/update, iam:PassRole on the task + execution roles, S3 env/asset access, Route 53 record changes, and — when the app uses the shared Valkey cache — reading the cluster endpoint to bake REDIS_HOST). It also attaches the app's yolo-{env}-{app}-observer policy, so the deployer inherits the read surface the in-sync check needs — env-level resources plus this app's — without any new direct grant, and never the blast radius of AWS-managed ReadOnlyAccess. That's the per-app observer: the same unscopeable env-wide describes (AWS won't scope those), but with log content fenced to this app's log group, so a deploy grant can't read another app's logs (the gate reads the task log group's tags to plan drift, but never its content). Object reads are scoped to non-secret config (the env manifest + app claim files) plus this app's own environment-side .env (env/.env.{app}, carrying its YOLO-minted Typesense key, which the build merges in), so the deploy role still can't read the env-shared .env (the cluster admin key) or any other app's secrets.

A plain yolo sync <env> does all of it. Re-run it whenever you change the branch/tag/repository for an environment.

The consumer workflow ​

Request the OIDC token and assume the deployer role — no stored secrets:

yaml
permissions:
  id-token: write
  contents: read

steps:
  - uses: actions/checkout@v4

  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::<account-id>:role/yolo-production-myapp-deployer
      aws-region: <region>

  - run: vendor/bin/yolo deploy production --no-progress

In CI, YOLO defers to the AWS SDK's default credential chain, so the assumed-role credentials are picked up automatically — no YOLO_PRODUCTION_AWS_PROFILE needed. Because the workflow has already assumed the deployer role via OIDC, the run is already capped to the Deployer tier: YOLO recognises it's running as exactly that role and proceeds as-is, rather than redundantly re-assuming the role it already is (a self-assume the role's own policy doesn't grant). Locally it's the reverse — you authenticate as your own identity, so YOLO mints the deployer role on top of it. Either way the run is capped to the tier.

Strongest production gate

Pair tag: 'v*' with a GitHub protected-tag ruleset (only maintainers may cut v* tags). The AWS trust then just confirms "a tag push from this repo", and GitHub enforces who can trigger it.

yolo deploy refuses to run against drift ​

Before it builds, yolo deploy runs a full sync --check (account → environment → app) and aborts if anything has drifted from the manifest. A deploy only rolls a new task-definition revision onto the existing infrastructure — it never reconciles it — so this stops a deploy landing on a stale target group, a changed task role, an un-provisioned listener, or a shared foundation (VPC/ALB/OIDC) that no longer matches yolo.yml. It also fires sync's claim gate, so an app that claims an env service the environment doesn't offer (e.g. typesense) is refused with a precise message. The check plans only (never writes), runs before the build so a drift fails fast without burning one, and prints the full diff.

What happens on drift depends on the permission tier the deploy runs under, because reconciling drift is an admin-tier act — it writes the shared foundation (IAM, ALB, CloudFront, autoscaling) the deployer tier deliberately can't touch:

  • By default — and always in CI — yolo deploy runs under the least-privilege deployer role, which can't write the drift away. So the gate refuses with Refusing to deploy — <env> has drifted from its declared state and exits non-zero. The fix: ask someone with admin to run yolo sync <env>, then redeploy — or rerun with --admin if you hold admin yourself.
  • With --admin the deploy mints the admin tier up front (MFA-prompted, exactly like sync), so it holds the writes to reconcile inline: it runs the real yolo sync <env> (you approve its plan at sync's own confirm gate), re-checks once that it converged, then continues straight into the build in the same run — no second command needed. If the environment is still drifted after the reconcile, the deploy aborts rather than looping. --admin is local/interactive only — CI always stays on the least-privilege deployer role and never self-heals.

Shared-foundation rather than app-only is deliberate: a deploy is the natural — and for most setups the only — moment drift is checked, so the gate covers the env-level foundation the app sits on (VPC/ALB/RDS/…) plus this app's own slice — never a sibling app's. This is why the deployer role attaches the per-app yolo-{env}-{app}-observer policy: it lets the check read the whole stack — env-wide describes (which AWS can't scope finer) plus this app's reads, with log content fenced to this app — without granting any write. No extra workflow step is needed — the gate is part of yolo deploy itself.

Exit codeMeaning
0In sync — the build and rollout proceed.
non-zeroDrift detected (deploy refused), or the check itself errored — bad credentials, an AWS API failure, an invalid manifest, or a claimed service the environment doesn't offer.

The reliable fix is always the same: run yolo sync <env> (admin-tier, MFA-gated) to reconcile, then deploy again — or fold both into one local run with yolo deploy <env> --admin. The same check is available standalone as yolo sync <env> --check if you ever want to probe for drift without deploying — but you don't need to wire anything up; the gate is part of every deploy.

Other auth methods ​

The default credential chain means both auth methods work with no extra config:

  • OIDC (above) — recommended.
  • AWS IAM Identity Center (SSO).

Released under the MIT License.