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 trustsrepo:<owner>/<repo>:*for audiencests.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¶
Run it once onboarding has finished. It makes three idempotent calls in order and stops at the first failure:
- Installs or refreshes the push webhook.
- Pushes five repository secrets and rotates the app's deploy token. The previous token stops working at once.
- 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.