Add the documentation and the agent instructions
README for humans, AGENTS.md for agents and contributors, and the docs that outlive any one platform: architecture, secrets, connectivity, triggering a deploy from another repository. CLAUDE.md, GEMINI.md and .claude/skills/ are pointers rather than copies, so every agent and every human reads the same text. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
971034e630
commit
e1f152ec3e
@@ -0,0 +1,195 @@
|
||||
# Triggering a deploy from another repository
|
||||
|
||||
The case: an application repository builds a new image, pushes it, and then asks
|
||||
*this* repository to redeploy the stack that runs it.
|
||||
|
||||
```
|
||||
application repo config repo
|
||||
────────────────────── ──────────────────────────
|
||||
build → push :latest ─── trigger ───► deploy
|
||||
stack=webapp └─ ansible-playbook
|
||||
pull=always playbooks/webapp.yml
|
||||
-e pull=always
|
||||
```
|
||||
|
||||
`pull=always` is not optional here. The image tag has not changed — only its
|
||||
contents have — and Compose is perfectly happy with the image it already has. A
|
||||
trigger that forgets it produces a deploy that does nothing and reports success.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
`deploy.yml` accepts a `repository_dispatch` event of type `deploy`.
|
||||
|
||||
### Caller
|
||||
|
||||
```yaml
|
||||
- name: Redeploy the stack
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CONFIG_REPO_TOKEN }}
|
||||
run: |
|
||||
gh api repos/OWNER/config/dispatches \
|
||||
--field event_type=deploy \
|
||||
--field 'client_payload[stack]=webapp' \
|
||||
--field 'client_payload[pull]=always'
|
||||
```
|
||||
|
||||
`CONFIG_REPO_TOKEN` is a fine-grained personal access token, or a GitHub App
|
||||
installation token, with **Contents: read and write** on the config repository.
|
||||
The default `GITHUB_TOKEN` cannot dispatch to another repository.
|
||||
|
||||
### What happens downstream
|
||||
|
||||
`deploy.yml` validates the payload before doing anything:
|
||||
|
||||
1. rejects `stack` unless it matches `[a-z0-9_-]+`, and `pull` unless it is one
|
||||
of `policy`, `always`, `missing`, `never`,
|
||||
2. fails if `ansible/playbooks/<stack>.yml` does not exist,
|
||||
3. materialises the SSH key with `scripts/load-ssh-key.sh`,
|
||||
4. runs the playbook.
|
||||
|
||||
The values arrive from another repository and end up on a command line, so they
|
||||
are treated as hostile. See "Why untrusted input is validated" in
|
||||
[ci-github-actions.md](ci-github-actions.md).
|
||||
|
||||
### The caller cannot wait
|
||||
|
||||
A `repository_dispatch` returns as soon as GitHub accepts the event. The response
|
||||
contains no run id, so the caller cannot poll for a result either. If the build
|
||||
pipeline must go red when the deploy fails, you have two options:
|
||||
|
||||
- **Put the deploy in the same repository** as the build and use `workflow_call`,
|
||||
which does propagate failure.
|
||||
- **Poll.** After dispatching, list workflow runs for `deploy.yml` created after
|
||||
your dispatch timestamp and wait on the newest. This is fiddly and racy; only
|
||||
do it if you genuinely need the coupling.
|
||||
|
||||
Most of the time you do not. A deploy that fails is visible in the config
|
||||
repository's Actions tab and can notify on failure there.
|
||||
|
||||
## GitLab CI
|
||||
|
||||
`deploy:triggered` exists only in pipelines that were triggered from outside, so
|
||||
it can never fire on a push here.
|
||||
|
||||
### Multi-project trigger (preferred)
|
||||
|
||||
In the upstream project:
|
||||
|
||||
```yaml
|
||||
deploy-config:
|
||||
stage: deploy
|
||||
trigger:
|
||||
project: your-group/config
|
||||
branch: main # must be a protected branch — see below
|
||||
strategy: depend # upstream waits and mirrors the result
|
||||
variables:
|
||||
STACK: webapp
|
||||
PULL: always
|
||||
```
|
||||
|
||||
`strategy: depend` is what makes the build pipeline report the *deployment's*
|
||||
result. Drop it and the trigger job goes green the moment the request is
|
||||
accepted, whether or not the deploy worked.
|
||||
|
||||
This authenticates with `CI_JOB_TOKEN`, so **the config project must allowlist
|
||||
the upstream project** under Settings → CI/CD → Job token permissions. Recent
|
||||
GitLab versions deny cross-project job-token access by default, and the failure
|
||||
is a **404** rather than a permissions error — which is the most common cause of
|
||||
"the trigger does nothing".
|
||||
|
||||
### Trigger token
|
||||
|
||||
For callers that are not GitLab CI — a webhook, a cron box, a person with
|
||||
`curl`. Create the token under Settings → CI/CD → Pipeline trigger tokens.
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--form token="$DEPLOY_TRIGGER_TOKEN" \
|
||||
--form ref=main \
|
||||
--form "variables[STACK]=webapp" \
|
||||
--form "variables[PULL]=always" \
|
||||
"https://gitlab.example.com/api/v4/projects/your-group%2Fconfig/trigger/pipeline"
|
||||
```
|
||||
|
||||
The project path must be URL-encoded (`%2F` for the slash), or use the numeric
|
||||
project ID. The response is the created pipeline as JSON; `.web_url` takes you
|
||||
straight to it.
|
||||
|
||||
### One-time setup
|
||||
|
||||
1. **Allowlist the caller** under Job token permissions, or the trigger 404s.
|
||||
2. **Protect the target branch**, and mark the deploy variables Protected.
|
||||
Protected variables only exist on protected refs; trigger a feature branch and
|
||||
the key comes back empty.
|
||||
3. **Give the upstream project Developer access**, or the job token is refused
|
||||
regardless of the allowlist.
|
||||
|
||||
## Gitea Actions
|
||||
|
||||
Gitea has no `repository_dispatch`. The way in is the **workflow dispatch API**,
|
||||
which fills in the same form a person would use under Actions → deploy → Run
|
||||
workflow. It needs Gitea 1.24 or newer.
|
||||
|
||||
### Caller
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: token $CONFIG_REPO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"ref": "main",
|
||||
"inputs": {"stack": "webapp", "mode": "deploy", "pull": "always"}
|
||||
}' \
|
||||
https://gitea.example.com/api/v1/repos/OWNER/config/actions/workflows/deploy.yml/dispatches
|
||||
```
|
||||
|
||||
Three things are easy to get wrong here:
|
||||
|
||||
- **`ref` is required**, and it is the ref the playbooks are read from as well as
|
||||
the one the run is attributed to. Point it at your default branch.
|
||||
- **`mode` falls back to `check`.** Omit it and the trigger runs
|
||||
`--check --diff`, reports what it would have done, and goes green having
|
||||
deployed nothing. This is the Gitea equivalent of forgetting `pull=always`, and
|
||||
it fails just as quietly. (The workflow applies that fallback itself: an
|
||||
input's declared `default:` belongs to the dispatch form, not to the API.)
|
||||
- **The job's own `GITEA_TOKEN` cannot do this.** It is scoped to the repository
|
||||
the job runs in. `CONFIG_REPO_TOKEN` is an access token belonging to a user
|
||||
with write access to the config repository — Settings → Applications → Access
|
||||
tokens, with the `write:repository` scope — stored as a secret in the calling
|
||||
repository.
|
||||
|
||||
The workflow name in the path is the file name, `deploy.yml`.
|
||||
|
||||
### The caller can wait, if it wants to
|
||||
|
||||
Add `?return_run_details=true` and the response carries the run's id and URL, so
|
||||
a caller can poll `/api/v1/repos/OWNER/config/actions/runs/<id>` until it
|
||||
finishes and fail its own pipeline on the result. That is more than GitHub's
|
||||
`repository_dispatch` offers, and more than most callers need: a deploy that
|
||||
fails is visible in the config repository's Actions tab and can notify from
|
||||
there.
|
||||
|
||||
### What happens downstream
|
||||
|
||||
Exactly what happens on GitHub — the same validation is in
|
||||
`.gitea/workflows/deploy.yml`, and it matters more here. A `type: choice` input
|
||||
constrains the *form* and not the API, so a dispatch call may put any string in
|
||||
`stack`.
|
||||
|
||||
## Before you enable this
|
||||
|
||||
**A trigger is a deploy.** Anyone holding the token, and any repository on the
|
||||
allowlist, can deploy any stack in this repository. The validation guard stops
|
||||
them running *arbitrary commands*; it does not stop them deploying a stack they
|
||||
had no business touching. Scope the allowlist deliberately, and rotate the token
|
||||
when someone leaves.
|
||||
|
||||
**Confirm the playbook exists first.** The downstream job fails the pipeline by
|
||||
design when it does not, which is correct but makes for a confusing first
|
||||
experience if you enable the trigger before converting the stack.
|
||||
|
||||
## Checking it worked
|
||||
|
||||
The Ansible recap at the end of the job log is the real answer. `changed=0` means
|
||||
the images had not moved — which, for a trigger that fired after a successful
|
||||
build, almost always means `pull=always` was not passed.
|
||||
Reference in New Issue
Block a user