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]>
117 lines
5.4 KiB
Markdown
117 lines
5.4 KiB
Markdown
# 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).
|