Skip to content

Build a monorepo app in CI

This guide covers an app whose manifest lives in a subdirectory, such as apps/web/astrolift.toml, with ci_pushed build mode: your CI builds and pushes the image, and Astrolift AI rolls out the tag. It assumes GitHub Actions and an AWS cluster with ECR.

Register with the right build context

The build context and Dockerfile path are app settings. Registering one manifest from a monorepo does not set them from the manifest's location, so pass both:

astro app register --project-id <project-guid> \
  --source-repo owner/monorepo \
  --file apps/web/astrolift.toml \
  --manifest-path apps/web/astrolift.toml \
  --build-mode ci_pushed \
  --build-context apps/web \
  --dockerfile-path Dockerfile

--file is the local copy the CLI reads the [app] slug from; --manifest-path is where the platform finds it in the repo.

--build-context is a repository path. --dockerfile-path is read relative to the build context and must stay inside it, so the line above builds apps/web/Dockerfile with apps/web as the context. The --help text describes both flags as platform_build only, but the managed CI workflow reads them too.

Without --build-context, the context is the repository root and the build runs docker build -f Dockerfile . from the root. astrolift-cli #159 tracks defaulting the context to the manifest's directory when --manifest-path names a subdirectory. Passing the flag is correct either way.

A container's build_context and dockerfile_path in the manifest are read relative to the manifest's directory, and apply only while the app-level value is still the default. "." is the default, so a container build_context = "." does not select apps/web; use the register flag.

Fix an app that is already registered

No astro app command changes the build context or Dockerfile after registration. Set them with the updateApp mutation, using the app's ID:

astro api graphql \
  -q 'mutation($input: UpdateAppInput!) { updateApp(input: $input) { ok errors { message } data { slug buildContext dockerfilePath } } }' \
  --var 'input={"id":"<app-guid>","buildContext":"apps/web","dockerfilePath":"Dockerfile"}'

Then sync the workflow again, as described at the end of the managed workflow.

What registration creates

For a ci_pushed app on a cluster with an image registry, onboarding creates:

  • An ECR repository named <org-slug>/<app-slug>.
  • For a GitHub source, an IAM role GitHub Actions can assume through OIDC, named astrolift-<org-slug>-<app-slug>-ecr-push. Characters IAM does not accept become -, and a name over 63 characters is cut and given an 8-character hash suffix. The role trusts repo:<owner>/<repo>:* for audience sts.amazonaws.com, and may push only to the app's repository. The AWS account needs the GitHub OIDC provider (token.actions.githubusercontent.com) registered once.

Onboarding runs after astro app register returns. astro app show --app <slug> reports provisioning status. Registration also tries to wire CI on its own (workflow, webhook, secrets); a workflow step that ran before the registry existed fails and is recorded on the app, which astro ci setup repairs.

The managed workflow

astro ci setup my-app

Run it once onboarding has finished. It makes three idempotent calls in order and stops at the first failure:

  1. Installs or refreshes the push webhook.
  2. Pushes five repository secrets and rotates the app's deploy token. The previous token stops working at once.
  3. Commits the workflow to the deploy branch, or opens a pull request when the branch is protected.

--skip-webhook, --skip-secrets and --skip-workflow run a subset.

On GitHub, every secret name ends with the app's GUID, without dashes and in upper case, so several apps can share one repository. astro ci setup prints the exact names. The GUID is the ID that astro app register printed.

Secret Value
ASTROLIFT_PUSH_ROLE_ARN_<GUID> The push role's ARN
ASTROLIFT_ECR_URI_<GUID> The ECR repository URI
ASTROLIFT_APP_SLUG_<GUID> The app slug
ASTROLIFT_API_URL_<GUID> The Astrolift AI API URL
ASTROLIFT_DEPLOY_TOKEN_<GUID> A deploy token with the app.deploy scope

GitLab CI variables use the same names without the suffix.

The workflow is written to .github/workflows/astrolift-app-<guid>.yml, with the GUID in lower case and without dashes. It:

  • runs on a push to the deploy branch and on manual dispatch, with no path filter, so any commit to the branch deploys the app;
  • assumes the push role through OIDC and logs in to ECR;
  • skips the build when the commit's tag is already in ECR, because the repository's tags are immutable;
  • builds with the app's Dockerfile and build context and pushes <ecr-uri>:<commit-sha>;
  • calls POST /api/cli/v1/apps/<slug>/deploy/ with the deploy token and fails the run on any response other than 2xx.

The role ARN, ECR URI, region, Dockerfile and context are written into the file as literals. Only the deploy token is read from a secret. Changing the build context later means syncing the workflow again from the app's CI settings, or running astro ci setup my-app --skip-webhook --skip-secrets.

A hand-written workflow

Write your own when you need a path filter, a different builder or extra steps. Run astro ci setup my-app --skip-workflow to get the webhook and the secrets without the managed file.

name: deploy web

on:
  push:
    branches: [main]
    paths: ["apps/web/**"]
  workflow_dispatch: {}

permissions:
  id-token: write
  contents: read

concurrency:
  group: astrolift-web
  cancel-in-progress: true

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    env:
      ECR_URI: ${{ secrets.ASTROLIFT_ECR_URI_0123456789ABCDEF0123456789ABCDEF }}
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.ASTROLIFT_PUSH_ROLE_ARN_0123456789ABCDEF0123456789ABCDEF }}
          aws-region: us-east-1
          role-session-name: astrolift-${{ github.run_id }}

      - uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push
        run: |
          image="$ECR_URI:${{ github.sha }}"
          docker build -f apps/web/Dockerfile -t "$image" apps/web
          docker push "$image"

      - name: Notify Astrolift AI
        env:
          API_URL: ${{ secrets.ASTROLIFT_API_URL_0123456789ABCDEF0123456789ABCDEF }}
          APP_SLUG: ${{ secrets.ASTROLIFT_APP_SLUG_0123456789ABCDEF0123456789ABCDEF }}
          TOKEN: ${{ secrets.ASTROLIFT_DEPLOY_TOKEN_0123456789ABCDEF0123456789ABCDEF }}
        run: |
          code=$(curl -sS -o response.json -w '%{http_code}' -X POST \
            "$API_URL/api/cli/v1/apps/$APP_SLUG/deploy/" \
            -H "Authorization: Bearer $TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"image_tags\":{\"*\":\"${{ github.sha }}\"},\"commit_sha\":\"${{ github.sha }}\",\"branch\":\"${{ github.ref_name }}\",\"trigger_kind\":\"ci\"}")
          cat response.json; echo
          case "$code" in 2*) ;; *) echo "deploy notification failed (HTTP $code)"; exit 1 ;; esac

Replace the suffix with your app's GUID and aws-region with the region in the ECR URI. "*" tags every workload with the commit SHA; the server expands it against the current manifest. A re-run on a commit that is already in ECR fails at docker push, because tags are immutable; add a aws ecr describe-images check, as the managed workflow does, if you re-run jobs.

If the managed workflow was already committed, keep your workflow in that file rather than deleting it. A managed file that disappears is pushed back by the platform's periodic CI sweep. A managed file edited in place is reported as drift and never overwritten.

App secrets

Literal secrets set with astro app secrets create stay on the platform and are never committed by CI setup or by Push to repo. See Set up an application.