Add the GitLab CI configuration
The same two things as the GitHub workflows, in GitLab's shape: one lint job, and a per-stack check/deploy button pair. Branch pipelines only, since a bare `when: manual` would otherwise make GitLab build a merge request pipeline that drops every job without rules — which looks exactly like the deploy buttons having vanished. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a0e56c96b7
commit
1130f2f07e
@@ -0,0 +1,116 @@
|
||||
# CI on GitLab
|
||||
|
||||
[`.gitlab-ci.yml`](../.gitlab-ci.yml) does the same two things as the GitHub
|
||||
workflows: a lint gate on every push, and manual per-stack deploys.
|
||||
|
||||
If you are only using one platform, delete the others — there is also a Gitea
|
||||
port in [`.gitea/workflows/`](../.gitea/workflows), documented in
|
||||
[ci-gitea.md](ci-gitea.md). All three are kept here because the translation
|
||||
between them is a large part of what this repository is demonstrating.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. CI/CD variables
|
||||
|
||||
Settings → CI/CD → Variables. Mark every one **Protected**.
|
||||
|
||||
| Variable | Value | Notes |
|
||||
| --- | --- | --- |
|
||||
| `SSH_KEY_BACKEND` | `ci` | Already set as a job default; override only to use `vault` |
|
||||
| `SSH_PRIVATE_KEY_B64` | the deploy key, base64, one line | **Masked** |
|
||||
| `SSH_PRIVATE_KEY` | the raw key | Alternative: type **File**, cannot be masked |
|
||||
| `VAULT_ADDR` / `VAULT_TOKEN` | a vault address and token | Only for the `vault` backend |
|
||||
|
||||
**Protected matters more than it looks.** A protected variable exists only on
|
||||
protected branches and tags. A pipeline on an ordinary feature branch gets an
|
||||
empty value and the job fails with the loader's message — the variable is not
|
||||
missing, it is invisible to that pipeline. This is the single most common
|
||||
first-run failure.
|
||||
|
||||
See [secrets.md](secrets.md) for how to encode the key.
|
||||
|
||||
### 2. A runner that can reach the hosts
|
||||
|
||||
Every job that touches a host is tagged `deploy-runner` through the `.ansible`
|
||||
template. Either tag your own runner accordingly, or change the tag, or remove
|
||||
the `tags:` block if shared runners can reach your hosts.
|
||||
|
||||
Do not add a tag before a runner carries it — a tag no runner has leaves jobs
|
||||
pending forever rather than failing.
|
||||
|
||||
The `lint` job is deliberately untagged. It needs no host access, so it keeps
|
||||
using shared runners.
|
||||
|
||||
## Deploying
|
||||
|
||||
Every stack has two buttons at the bottom of the pipeline graph:
|
||||
|
||||
- `check:<stack>` — runs `--check --diff`, reports what would change, touches
|
||||
nothing
|
||||
- `deploy:<stack>` — does it
|
||||
|
||||
To **update** rather than deploy, run the pipeline with `PULL=always` (Build →
|
||||
Pipelines → Run pipeline, add a variable) and press the same deploy button. That
|
||||
also enables the work gated behind an update run — `webapp`'s backup and
|
||||
migration.
|
||||
|
||||
`ansible:ping` is the right first thing to run after setting the variables up. It
|
||||
should print `SUCCESS => {"ping": "pong"}` for every host, and changes nothing.
|
||||
|
||||
### Testing it on a branch
|
||||
|
||||
Two things have to be true before any deploy job can work:
|
||||
|
||||
1. **GitLab needs the branch.** A pipeline only exists for a ref it has.
|
||||
`git push -u origin <your-branch>`.
|
||||
2. **The branch must be protected**, or the deploy credentials are not there.
|
||||
Add it under Settings → Repository → Protected branches, or a `feature/*`
|
||||
wildcard while testing.
|
||||
|
||||
## Notes on the file
|
||||
|
||||
**Branch pipelines only.** The `workflow:` block at the top refuses merge request
|
||||
pipelines, and the reason is not obvious. Any job carrying `rules: - when:
|
||||
manual` with no `if:` matches *every* pipeline source, including
|
||||
`merge_request_event` — and that alone makes GitLab build a merge request
|
||||
pipeline whenever an MR is open. An MR pipeline includes only jobs whose rules
|
||||
match `merge_request_event`, so it drops every job that has no `rules:` at all:
|
||||
the lint gate and all the per-stack buttons.
|
||||
|
||||
The result looks exactly like the deploy buttons having vanished, while the
|
||||
branch pipeline next to it has them all. Nothing here needs an MR pipeline, so
|
||||
the cleanest fix is to not build one.
|
||||
|
||||
**`STACK_FIXED`, not `STACK`.** The per-stack buttons pin the stack with
|
||||
`STACK_FIXED`; the generic and triggered jobs read `STACK`. These cannot share a
|
||||
name: a pipeline variable outranks a job's YAML `variables:` in GitLab, so with
|
||||
both called `STACK`, pressing `deploy:webapp` inside a pipeline that was
|
||||
triggered with `STACK=static-site` would deploy `static-site` — and with
|
||||
`PULL=always` could run `webapp`'s backup and migration against the wrong stack.
|
||||
|
||||
**Named jobs, not `parallel:matrix`.** A matrix collapses into a single
|
||||
expandable node in the pipeline graph, which buries every play button one click
|
||||
deeper. Written-out jobs each get their own button. It is more lines and a better
|
||||
interface.
|
||||
|
||||
**`allow_failure: false` on `.ansible-manual`.** A bare `when: manual` defaults
|
||||
`allow_failure` to **true**, unlike `when: manual` inside `rules:`. Without the
|
||||
explicit override a failed deploy leaves the pipeline green, and an upstream
|
||||
using `strategy: depend` never learns the deploy broke.
|
||||
|
||||
## Differences from the GitHub workflows
|
||||
|
||||
| | GitLab | GitHub Actions |
|
||||
| --- | --- | --- |
|
||||
| Per-stack manual run | A play button per job, generated from `extends` | One `workflow_dispatch` form with a `stack` dropdown |
|
||||
| Restricting who can deploy | Protected branches + protected variables | Environment protection rules (required reviewers) |
|
||||
| Runner selection | `tags:` | `runs-on:` with a label |
|
||||
| Cross-project trigger | `trigger:` keyword or trigger token | `repository_dispatch` |
|
||||
| Caller waits for the result | `strategy: depend` | Not supported by `repository_dispatch` |
|
||||
| Secret scanning | Secret Detection template | gitleaks image in `lint.yml` |
|
||||
|
||||
The GitHub version needs its stack list maintained by hand because a `choice`
|
||||
input cannot be populated dynamically; the GitLab version needs its job pairs
|
||||
maintained by hand because there is no way to generate a button per playbook
|
||||
either. Both are step 4 of
|
||||
[adding a stack](../skills/adding-a-stack/SKILL.md).
|
||||
Reference in New Issue
Block a user