How to deploy from a Quake.yaml manifest with Forgejo Actions
How to deploy from a Quake.yaml manifest with Forgejo Actions
Run a Forgejo Actions or Gitea Actions workflow that turns a push into a deployment through a quake.yaml launch handoff packet. The workflow runs on a forge and runner you operate, then deploys to your Quake AI project. It adapts the generic CI executor for a self-hosted forge.
Prerequisites
- A self-hosted Forgejo or Gitea forge with a registered Actions runner.
- A repository on that forge with a valid
quake.yamlat its root. Review the launch handoff contract and thequake.yamlreference. - A container registry the target instance can pull from, and push credentials.
- An SSH keypair in your Quake AI project and application credentials for the OpenStack provider.
- The runner image (or host) provides
docker,tofu,jq, andyq.
Use the self-hosted runner#
A Forgejo Actions runner uses GitHub Actions workflow syntax. Adapt the generic CI workflow for your runner:
- Place the runner inside or alongside your Quake AI project when the workflow needs access to private addresses.
- Store deployment credentials in the forge's secret store.
Store secrets in the forge#
In the Forgejo (or Gitea) web UI, open the repository, go to Settings > Actions > Secrets, and add:
| 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 for the provider |
SSH_KEY_NAME | Name of the keypair registered in your project |
Names listed in manifest secrets[] | The application secret values the runtime reads |
The workflow reads these values as ${{ secrets.NAME }} so you can keep credentials out of the repository.
Add the workflow#
Commit the workflow to .forgejo/workflows/launch.yml (use .gitea/workflows/launch.yml on Gitea). The runner picks the launch action from the branch: main deploys to production, other branches preview.
name: Launch from quake.yaml
on:
push:
branches: [main]
pull_request:
jobs:
launch:
runs-on: self-hosted
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: Validate and prepare the handoff packet
run: |
yq -o=json '.' quake.yaml > manifest.json
# Validate first; fail the run on schema or resolution errors.
jq -n --slurpfile m manifest.json \
'{jsonrpc:"2.0",id:1,method:"tools/call",
params:{name:"validate_launch_manifest",arguments:{manifest:$m[0]}}}' \
> validate.json
curl -sS https://docs.quake.ai/api/mcp -H 'Content-Type: application/json' -d @validate.json \
| jq -e '.result.structuredContent.content.valid == true' > /dev/null \
|| { echo "manifest validation failed"; exit 1; }
# Prepare the packet for the chosen action.
jq -n --slurpfile m manifest.json --arg action "$ACTION" --arg sha "$GITHUB_SHA" \
'{jsonrpc:"2.0",id:2,method:"tools/call",
params:{name:"prepare_launch",
arguments:{manifest:$m[0],action:$action,source_sha:$sha}}}' \
> prepare.json
curl -sS https://docs.quake.ai/api/mcp -H 'Content-Type: application/json' -d @prepare.json \
| jq '.result.structuredContent.content' > packet.json
- name: Build and push the image
run: |
docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USERNAME }}" -p "${{ secrets.REGISTRY_PASSWORD }}"
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.flavor' 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: |
URL="$(tofu -chdir=iac/templates/containerized-app output -raw container_url)"
curl -fsS "$URL/healthz"This is the IaC consumer path: it builds the image, then applies resolved.template_plan with OpenTofu. When the manifest declares services[], apply each service entry before the runtime and wire its outputs into the runtime container_env, as the generic CI executor describes.
Use the Coolify consumer instead#
If you run Coolify as your deploy platform, swap the build and apply steps for a single step that runs the Coolify consumer against the packet. The consumer maps the handoff packet to Coolify API calls, so Coolify builds the image, provisions the declared services, and deploys:
- name: Deploy through the Coolify consumer
env:
COOLIFY_BASE_URL: ${{ secrets.COOLIFY_BASE_URL }}
COOLIFY_API_TOKEN: ${{ secrets.COOLIFY_API_TOKEN }}
COOLIFY_PROJECT_UUID: ${{ secrets.COOLIFY_PROJECT_UUID }}
COOLIFY_SERVER_UUID: ${{ secrets.COOLIFY_SERVER_UUID }}
run: |
python consumers/coolify/coolify_launch_consumer.py \
--packet packet.json \
--git-repository "${{ github.server_url }}/${{ github.repository }}.git"The consumer reads the Coolify base URL, API token, project UUID, and server UUID from those environment variables. It takes the git repository URL as a flag. Pick one consumer per repository. The IaC path creates OpenTofu-managed instances. The Coolify path provides a git-push deployment platform. Both consumers read the same packet and use the same manifest.
Verify the result#
The workflow's verify step reads the runtime output and checks the health endpoint. In the forge UI, open Actions, select the run, and confirm each step succeeded. For the IaC path, read the outputs from the runner:
tofu -chdir=iac/templates/containerized-app output -raw container_urlFor the Coolify path, open the application in the Coolify dashboard and confirm it shows as running.
Tear down a preview#
Remove a preview environment when its branch merges or closes. For the IaC path, destroy the resources the workflow applied:
tofu -chdir=iac/templates/containerized-app destroy -auto-approveFor the Coolify path, delete the preview application and its managed databases in the Coolify dashboard. The manifest's preview environment can carry a ttl_hours hint; teardown scheduling is your responsibility on a self-hosted forge.
Next steps#
- Execute a launch handoff packet from CI: the generic GitHub Actions and GitLab CI version this page specializes
- Deploy an app to Coolify from a quake.yaml manifest: the full Coolify consumer walkthrough
- Deploy a self-hosted git forge and CI with OpenTofu: provision the forge and runner this page runs on
See also#
- Launch handoff: the manifest, the packet, and the branch workflow
quake.yamlreference: the manifest fields and resolution rules- CI/CD pipelines: the broader build, test, and ship pattern on Quake AI
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 01.09.2026