# How to execute a launch handoff packet from CI

Source: https://docs.quake.ai/docs/automation/how-to/execute-launch-handoff-from-ci
Markdown: https://docs.quake.ai/docs/automation/how-to/execute-launch-handoff-from-ci.md

---

# How to execute a launch handoff packet from CI

Run a CI job that turns your repository's `quake.yaml` into a running deployment. Call the `prepare_launch` MCP tool to get a handoff packet, build and push the image from the resolved build recipe, apply the templates in `resolved.template_plan` order, and read the runtime outputs to verify the deployment.

This page provides GitHub Actions and GitLab CI workflows. Quake AI does not host CI runners; use the provider's hosted runners or a [self-hosted runner on a Quake AI VM](/resources/deployments/deploy-ci-runner).



The handoff packet is a self-contained plan that Quake AI validates and emits for your pipeline to execute. For the manifest format, packet fields, and validation rules, see [Launch handoff](/resources/ai-assisted-development/launch) and the [`quake.yaml` reference](/resources/ai-assisted-development/quake-yaml). This page covers packet execution.



<PrerequisiteBlock>

- A `quake.yaml` at your repository root that validates cleanly. See the [`quake.yaml` reference](/resources/ai-assisted-development/quake-yaml).
- A container registry the target instance can pull from, and push credentials for it.
- An SSH keypair that you registered in your Quake AI project, and [application credentials](/docs/tools/generate-app-credentials) for the OpenStack provider.
- A CI runner with `docker`, `tofu` (or `terraform`), `jq`, and `yq` available.

</PrerequisiteBlock>

## How the packet flows through a pipeline

A pipeline run carries the manifest through four stages. The hosted MCP endpoint handles the first stage, and your CI runner handles the remaining stages.

1. **Prepare.** Convert `quake.yaml` to JSON and call the `prepare_launch` MCP tool with the manifest, an action, and the commit SHA. The response carries the handoff packet.
2. **Build.** Run the commands in `resolved.build.steps` to build the image, push it to your registry, and record the pushed reference.
3. **Apply.** Apply each entry in `resolved.template_plan` in `apply_order` (services before the runtime), binding the resolved `variables` and the runtime entry's `container_env`.
4. **Verify.** Read the runtime template outputs (`container_url`, `floating_ip`) and confirm the workload responds.

<Figure size="md" caption="A push triggers a CI job that prepares the packet against the MCP endpoint, then builds, applies, and verifies on the runner">

```d2
direction: down

dev: You {shape: person}
repo: "Repository\nquake.yaml"
ci: CI runner {
  prepare: "1. prepare_launch"
  build: "2. build + push"
  apply: "3. tofu apply"
  verify: "4. verify"
  prepare -> build -> apply -> verify
}
mcp: "MCP endpoint\ndocs.quake.ai/api/mcp"
registry: Container registry
cloud: Quake AI

dev -> repo: push
repo -> ci: trigger
ci.prepare -> mcp: manifest + action
ci.build -> registry: push image
ci.apply -> cloud: provision
```

</Figure>

## Map branches to launch actions

Pick the launch action from the git ref so feature branches preview and `main` deploys. The action selects which environment entry the packet resolves: `preview` requires a `preview` environment, and `deploy` requires a `production` environment.

| Git event | Launch action | Environment entry |
| --- | --- | --- |
| Push or pull request on a feature branch | `preview` | `preview` (for example `branch: "*"`) |
| Merge to `main` | `deploy` | `production` (`branch: main`) |

## Store CI secrets

Add these as repository or project secrets. Pass them by name into the job; do not commit values.

| Secret | Purpose |
| --- | --- |
| `REGISTRY_USERNAME`, `REGISTRY_PASSWORD` | Push credentials for your container registry |
| `OS_AUTH_URL`, `OS_APPLICATION_CREDENTIAL_ID`, `OS_APPLICATION_CREDENTIAL_SECRET` | OpenStack [application credentials](/docs/tools/generate-app-credentials) for the OpenTofu provider |
| `SSH_KEY_NAME` | Name of the keypair that you registered in your project |
| Any manifest `secrets[]` names | The application secret values the runtime reads, supplied by name |

The `prepare_launch` endpoint itself needs no authentication. The credentials above authenticate the build and apply stages, which run on your runner.

## The pipeline

The workflow below prepares the packet, builds and pushes the image, applies the containerized-app runtime, and verifies. It uses a single-template manifest (`runtime: container`, no `services[]`); the section after it covers manifests that declare backing services.




```yaml
name: Launch from quake.yaml
on:
  push:
    branches: [main]
  pull_request:

jobs:
  launch:
    runs-on: ubuntu-latest
    env:
      REGISTRY: registry.example.com/my-team
      OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
      OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
      OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
    steps:
      - uses: actions/checkout@v4

      - name: Choose the launch action
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo "ACTION=deploy" >> "$GITHUB_ENV"
          else
            echo "ACTION=preview" >> "$GITHUB_ENV"
          fi

      - name: Prepare the handoff packet
        run: |
          yq -o=json '.' quake.yaml > manifest.json
          jq -n \
            --slurpfile m manifest.json \
            --arg action "$ACTION" \
            --arg sha "$GITHUB_SHA" \
            '{jsonrpc:"2.0",id:1,method:"tools/call",
              params:{name:"prepare_launch",
                arguments:{manifest:$m[0],action:$action,source_sha:$sha}}}' \
            > rpc.json
          curl -sS https://docs.quake.ai/api/mcp \
            -H 'Content-Type: application/json' \
            -d @rpc.json \
            | jq '.result.structuredContent.content' > packet.json
          jq -e 'has("resolved")' packet.json > /dev/null \
            || { echo "prepare_launch failed:"; cat packet.json; exit 1; }

      - name: Build and push the image
        run: |
          printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" \
            | docker login "$REGISTRY" \
                --username "${{ secrets.REGISTRY_USERNAME }}" \
                --password-stdin
          IMAGE="$REGISTRY/$(jq -r '.manifest.name' packet.json):${GITHUB_SHA::12}"
          docker build -t "$IMAGE" -f "$(jq -r '.resolved.build.dockerfile // "Dockerfile"' packet.json)" .
          docker push "$IMAGE"
          echo "IMAGE=$IMAGE" >> "$GITHUB_ENV"

      - name: Apply the runtime template
        run: |
          tofu -chdir=iac/templates/containerized-app init
          tofu -chdir=iac/templates/containerized-app apply -auto-approve \
            -var "flavor_name=$(jq -r '.resolved.template_plan[] | select(.role == "runtime") | .variables.flavor_name.value' packet.json)" \
            -var "app_name=$(jq -r '.manifest.name' packet.json)" \
            -var "key_name=${{ secrets.SSH_KEY_NAME }}" \
            -var "container_image=$IMAGE"

      - name: Verify
        run: |
          tofu -chdir=iac/templates/containerized-app output -raw container_url
```




```yaml
stages: [launch]

launch:
  stage: launch
  image: docker:27
  services:
    - docker:27-dind
  variables:
    REGISTRY: registry.example.com/my-team
  before_script:
    - apk add --no-cache curl jq yq opentofu
  script:
    - |
      if [ "$CI_COMMIT_BRANCH" = "main" ]; then ACTION=deploy; else ACTION=preview; fi
      yq -o=json '.' quake.yaml > manifest.json
      jq -n --slurpfile m manifest.json --arg action "$ACTION" --arg sha "$CI_COMMIT_SHA" \
        '{jsonrpc:"2.0",id:1,method:"tools/call",
          params:{name:"prepare_launch",
            arguments:{manifest:$m[0],action:$action,source_sha:$sha}}}' > rpc.json
      curl -sS https://docs.quake.ai/api/mcp -H 'Content-Type: application/json' -d @rpc.json \
        | jq '.result.structuredContent.content' > packet.json
      jq -e 'has("resolved")' packet.json > /dev/null || { cat packet.json; exit 1; }
    - printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin
    - IMAGE="$REGISTRY/$(jq -r '.manifest.name' packet.json):${CI_COMMIT_SHORT_SHA}"
    - docker build -t "$IMAGE" -f "$(jq -r '.resolved.build.dockerfile // "Dockerfile"' packet.json)" .
    - docker push "$IMAGE"
    - tofu -chdir=iac/templates/containerized-app init
    - |
      tofu -chdir=iac/templates/containerized-app apply -auto-approve \
        -var "flavor_name=$(jq -r '.resolved.template_plan[] | select(.role == "runtime") | .variables.flavor_name.value' packet.json)" \
        -var "app_name=$(jq -r '.manifest.name' packet.json)" \
        -var "key_name=$SSH_KEY_NAME" \
        -var "container_image=$IMAGE"
    - tofu -chdir=iac/templates/containerized-app output -raw container_url
```




The build step follows the packet's build method and Dockerfile, then replaces the packet's registry placeholder with your registry and a commit-specific image tag. Fetch the template source with the [`get_template`](/resources/ai-assisted-development/ai-tools-reference#get_template) MCP tool or vendor it into the repository under `iac/templates/`, the path the packet references.

## Manifests that declare backing services

When the manifest declares `services[]` (Postgres, Redis, object storage, or a vector store), the packet lists each service as its own entry in `resolved.template_plan` with an `apply_order` lower than the runtime. Apply them in that order so the runtime can read their outputs:

1. Iterate `resolved.template_plan` sorted by `apply_order`. Apply each `service` entry first, binding the `variables` the entry lists.
2. Read each service entry's `provides` outputs (for example a Postgres `private_ip` and `database_url`).
3. Apply the `runtime` entry last. Set its `container_env` keys from the prior service outputs and from your manifest `secrets[]` values, then bind `container_image` to the image you pushed.

Connection-string passwords stay name-only in the manifest. Declare them in [`secrets`](/resources/ai-assisted-development/quake-yaml#secrets) and inject the values from your CI secret store. The packet marks values that your pipeline must supply with `value: null` and a `source` note, so a script can walk the plan and fail early when a required binding is missing.

## Verify the result

After the apply stage, read the runtime outputs and check the workload:

```bash
tofu -chdir=iac/templates/containerized-app output -raw container_url
curl -fsS "$(tofu -chdir=iac/templates/containerized-app output -raw container_url)/healthz"
```

A successful health check confirms the image runs and the instance is reachable. Record the deploy id, the resolved outputs, and the gate results in the packet's `evidence_bundle` if you keep an audit trail.



When the target instance sits on a private subnet, run the job on a [self-hosted CI runner](/resources/deployments/deploy-ci-runner) or a [self-hosted git forge with CI](/resources/deployments/deploy-forgejo-git-ci-template) inside the project. The runner reaches private addresses without exposing them to the public internet. The [Forgejo Actions specialization](/docs/automation/how-to/forgejo-actions-launch-deploy) shows the same packet flow on a self-hosted forge.



## Next steps

- [Deploy an app to Coolify from a quake.yaml manifest](/resources/deployments/deploy-quake-yaml-to-coolify): run the packet through a PaaS consumer instead of raw OpenTofu
- [Run a push-to-webhook deploy without a CI server](/docs/automation/how-to/webhook-deploy-flow): the lightweight executor for small projects
- [CI/CD pipelines](/resources/solutions/cicd-pipelines): the broader build, test, and ship pattern on Quake AI

## See also

- [Launch handoff](/resources/ai-assisted-development/launch): the manifest, the packet, and the branch workflow
- [`quake.yaml` reference](/resources/ai-assisted-development/quake-yaml): the manifest fields and resolution rules
- [How to integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration): plan and apply OpenTofu state from a pipeline
- [How to deploy an application to a Quake AI VM from CI](/docs/automation/how-to/app-cicd-vm): ship code to an existing instance over SSH
