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
+224
@@ -0,0 +1,224 @@
|
||||
---
|
||||
# The GitLab port of .github/workflows/. Both are maintained; see
|
||||
# docs/ci-gitlab.md for what differs and why.
|
||||
#
|
||||
# Two ways in, and only two:
|
||||
# - a human presses play on a per-stack button (all manual, at the bottom)
|
||||
# - another project's pipeline asks for a stack by name (deploy:triggered)
|
||||
#
|
||||
# Nothing deploys on a plain push.
|
||||
|
||||
# Branch pipelines only. Any job carrying `rules: - when: manual` with no `if:`
|
||||
# matches every pipeline source -- and that alone is enough to make GitLab build
|
||||
# a *merge request* pipeline whenever an MR is open. A merge request pipeline
|
||||
# includes only jobs whose rules match merge_request_event, so it drops every job
|
||||
# that has no `rules:` at all. The result looks exactly like the deploy buttons
|
||||
# having vanished, while the branch pipeline beside it has them all.
|
||||
#
|
||||
# Nothing here needs a merge request pipeline: the lint gate and the manual
|
||||
# buttons both belong to a branch. Skipping them also stops one commit building
|
||||
# two pipelines. With no MR pipeline the MR page shows the branch pipeline for
|
||||
# its head commit, so the widget is not empty.
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: never
|
||||
- when: always
|
||||
|
||||
stages:
|
||||
- test
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
# A runner's build directory is world-writable, and Ansible refuses to read an
|
||||
# ansible.cfg out of a world-writable directory -- it says so and carries on
|
||||
# with no roles_path and no inventory, which surfaces much later as "the role
|
||||
# 'compose_stack' was not found". Naming the file explicitly opts out of that
|
||||
# discovery rule. Paths inside it then resolve relative to the file, so this
|
||||
# works from any working directory.
|
||||
ANSIBLE_CONFIG: $CI_PROJECT_DIR/ansible/ansible.cfg
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
image: python:3.12-slim
|
||||
# No `changes:` filter on purpose. Watching ansible/ and the lint configs means
|
||||
# a commit touching only this file skips the job entirely -- including the
|
||||
# commits fixing the lint job itself, so it silently proves nothing. The job
|
||||
# takes under a minute.
|
||||
script:
|
||||
- apt-get update -qq && apt-get install -qq -y --no-install-recommends git
|
||||
# scripts/check.sh is the single source of the version pins, and runs the
|
||||
# same three checks a contributor runs locally with `make check`.
|
||||
- ./scripts/check.sh --print-specs | xargs -d '\n' pip install --quiet
|
||||
- RUNNER=installed ./scripts/check.sh
|
||||
|
||||
# Shared setup: toolchain and the deploy key. Carries no rules of its own, so it
|
||||
# can never schedule itself.
|
||||
.ansible:
|
||||
stage: deploy
|
||||
# Every job that touches a host inherits this through `extends`. Remove it, or
|
||||
# change it to your own runner's tag, if your hosts are reachable from shared
|
||||
# runners. Do not add a tag before a runner carries it -- a tag no runner has
|
||||
# leaves jobs pending forever rather than failing.
|
||||
tags:
|
||||
- deploy-runner
|
||||
image: python:3.12-slim
|
||||
variables:
|
||||
SSH_KEY_BACKEND: ci
|
||||
ANSIBLE_FORCE_COLOR: '1'
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -qq -y --no-install-recommends openssh-client curl rsync
|
||||
- ./scripts/check.sh --print-specs | head -1 | xargs -d '\n' pip install --quiet
|
||||
- ansible-galaxy install -r ansible/requirements.yml
|
||||
# Two lines, not `export SSH_KEY_PATH="$(...)"`. export is a special builtin,
|
||||
# so the compound command reports *its* status and swallows the script's --
|
||||
# under `set -e` a loader failure (an unprotected ref, so no key variable)
|
||||
# goes unnoticed and the job dies much later with a per-host "Permission
|
||||
# denied (publickey)" instead of the loader's own message.
|
||||
- SSH_KEY_PATH="$(./scripts/load-ssh-key.sh)"
|
||||
- export SSH_KEY_PATH
|
||||
|
||||
# Can the runner reach the hosts with the configured key? Changes nothing, so it
|
||||
# is the right first thing to run after setting up the CI/CD variables.
|
||||
ansible:ping:
|
||||
extends: .ansible
|
||||
rules:
|
||||
- when: manual
|
||||
script:
|
||||
- cd ansible && ansible all -m ansible.builtin.ping
|
||||
|
||||
# Runs one stack's playbook. STACK reaches a shell command and, for triggered
|
||||
# pipelines, arrives from outside this project, so it is validated first.
|
||||
.ansible-stack:
|
||||
extends: .ansible
|
||||
script:
|
||||
# A per-stack button pins STACK_FIXED; the generic and triggered jobs take
|
||||
# STACK as a pipeline variable. These cannot share one name: a pipeline
|
||||
# variable outranks a job's YAML `variables:` in GitLab, so with both called
|
||||
# STACK, pressing deploy:webapp inside a pipeline triggered with
|
||||
# STACK=static-site would deploy static-site -- and with PULL=always could
|
||||
# run webapp's backup and migration against the wrong stack.
|
||||
- STACK="${STACK_FIXED:-${STACK:-}}"
|
||||
- case "$STACK" in ''|*[!a-z0-9_-]*) echo "refusing STACK='$STACK'"; exit 1;; esac
|
||||
# PULL reaches the same command line, unquoted, from the same untrusted place
|
||||
# as STACK, so it gets the same treatment: an unchecked value can smuggle in
|
||||
# extra `-e` overrides (stack_dest, ansible_host) and redirect a deploy.
|
||||
- case "$PULL" in ''|always|missing|never|policy) ;; *) echo "refusing PULL='$PULL'"; exit 1;; esac
|
||||
- test -f "ansible/playbooks/${STACK}.yml" || { echo "no playbook for '$STACK'"; exit 1; }
|
||||
- cd ansible && ansible-playbook "playbooks/${STACK}.yml" ${ANSIBLE_EXTRA_ARGS} ${PULL:+-e pull=$PULL}
|
||||
|
||||
# Run a pipeline with STACK set (Build > Pipelines > Run pipeline, add a
|
||||
# variable) and these two appear.
|
||||
check:stack:
|
||||
extends: .ansible-stack
|
||||
variables:
|
||||
ANSIBLE_EXTRA_ARGS: --check --diff
|
||||
rules:
|
||||
- if: '$STACK'
|
||||
when: manual
|
||||
|
||||
deploy:stack:
|
||||
extends: .ansible-stack
|
||||
rules:
|
||||
- if: '$STACK'
|
||||
when: manual
|
||||
|
||||
# Deploy on request from another project's pipeline. This job exists only in
|
||||
# pipelines that were triggered, so it can never fire on a push here.
|
||||
# See docs/triggering-deploys.md.
|
||||
deploy:triggered:
|
||||
extends: .ansible-stack
|
||||
rules:
|
||||
# trigger token or API call
|
||||
- if: '$CI_PIPELINE_SOURCE == "trigger" && $STACK'
|
||||
# `trigger:` keyword in an upstream project (multi-project pipeline)
|
||||
- if: '$CI_PIPELINE_SOURCE == "pipeline" && $STACK'
|
||||
|
||||
.ansible-manual:
|
||||
extends: .ansible-stack
|
||||
when: manual
|
||||
# A bare `when: manual` defaults allow_failure to true, unlike `when: manual`
|
||||
# inside `rules:`. Without this a failed deploy leaves the pipeline green and
|
||||
# an upstream using `strategy: depend` never learns the deploy broke.
|
||||
allow_failure: false
|
||||
|
||||
# One visible button per stack. Written out rather than generated with
|
||||
# parallel:matrix: a matrix collapses into a single expandable node in the
|
||||
# pipeline graph, which hides every play button one click deeper. Named jobs each
|
||||
# get their own button.
|
||||
#
|
||||
# check:<stack> --check --diff, reports what would change, touches nothing
|
||||
# deploy:<stack> does it
|
||||
#
|
||||
# To update rather than deploy, run the pipeline with PULL=always and press the
|
||||
# same deploy button. That also enables the work gated behind an update run --
|
||||
# webapp's backup and migration.
|
||||
#
|
||||
# Order mirrors ansible/playbooks/site.yml. Add a pair when a playbook lands.
|
||||
.check:
|
||||
extends: .ansible-manual
|
||||
variables:
|
||||
ANSIBLE_EXTRA_ARGS: --check --diff
|
||||
|
||||
.deploy:
|
||||
extends: .ansible-manual
|
||||
|
||||
check:reverse-proxy:
|
||||
extends: .check
|
||||
variables:
|
||||
STACK_FIXED: reverse-proxy
|
||||
|
||||
deploy:reverse-proxy:
|
||||
extends: .deploy
|
||||
variables:
|
||||
STACK_FIXED: reverse-proxy
|
||||
|
||||
check:banner:
|
||||
extends: .check
|
||||
variables:
|
||||
STACK_FIXED: banner
|
||||
|
||||
deploy:banner:
|
||||
extends: .deploy
|
||||
variables:
|
||||
STACK_FIXED: banner
|
||||
|
||||
check:metrics:
|
||||
extends: .check
|
||||
variables:
|
||||
STACK_FIXED: metrics
|
||||
|
||||
deploy:metrics:
|
||||
extends: .deploy
|
||||
variables:
|
||||
STACK_FIXED: metrics
|
||||
|
||||
check:static-site:
|
||||
extends: .check
|
||||
variables:
|
||||
STACK_FIXED: static-site
|
||||
|
||||
deploy:static-site:
|
||||
extends: .deploy
|
||||
variables:
|
||||
STACK_FIXED: static-site
|
||||
|
||||
check:webapp:
|
||||
extends: .check
|
||||
variables:
|
||||
STACK_FIXED: webapp
|
||||
|
||||
deploy:webapp:
|
||||
extends: .deploy
|
||||
variables:
|
||||
STACK_FIXED: webapp
|
||||
|
||||
check:webapp-staging:
|
||||
extends: .check
|
||||
variables:
|
||||
STACK_FIXED: webapp-staging
|
||||
|
||||
deploy:webapp-staging:
|
||||
extends: .deploy
|
||||
variables:
|
||||
STACK_FIXED: webapp-staging
|
||||
Reference in New Issue
Block a user