The audit tells you what each pipeline depends on. The matrix turns that into a
score and an order. You do not migrate the scariest pipeline first to "get it out
of the way" — you migrate the easy ones first to build a repeatable pattern, warm your
Docker-executor runners, and earn the team's trust, so that when you reach the hard ones
you already know your images, your services:, your cache config, and your rollback drill.
The score maps directly to the signals audit-script.sh finds, so the tool and this
rubric agree by construction.
The rubric
Easy — the job is already close to container-native. Pure build/test/lint, tools that
can be baked into an image, no host state, no host secrets, no privilege. The only work is
picking the right image: and confirming parity.
- Audit signals: none of the risk signals, or at most host-installed tools you can
bake into
ci/baseor a specialized image.
Moderate — the job leans on the host, but every dependency has a well-known Docker-world replacement. Nothing here requires a privilege or architecture decision; it's mechanical translation you'll repeat across many pipelines.
- Audit signals:
pkg-install(job-timeapt-get/pip/apk),host-path(absolute paths /$HOME),ssh-keys,secrets-file,cron. - Typical work: move tools into an image; move secrets and keys into masked/protected CI
variables (file-type where a file is expected); convert job-to-job hand-offs to
artifacts:/cache:; convert host cron to a scheduled pipeline.
Hard — the job needs a real decision: elevated privilege, the Docker daemon, or host
services. These aren't harder to type, they're harder to get right and safe, and they
often force a design choice (docker-in-docker vs. buildah vs. mounted socket; a services:
sidecar vs. an external managed service). Migrate these last, one at a time, with the
rollback tag ready.
- Audit signals:
docker-socket,docker-build,systemctl,sudo. - Typical work: choose an image-build strategy
(
../04-pipeline-migration/dind-vs-kaniko-vs-socket.md— never kaniko, it's archived); replace host daemons with GitLabservices:; removesudoby fixing the image and running as the base image's non-root user; decide, deliberately, whether any job truly needs privileged mode.
Scoring rule: the highest-severity signal present wins. One docker.sock
reference makes a pipeline hard even if everything else is clean — because that one line is
the part that will hurt you.
Worked example matrix
Six realistic pipelines from an infrastructure-heavy estate, scored the way the audit scores them:
| Pipeline | Signals found | Score | Est. effort | Blocker / notes |
|---|---|---|---|---|
docs-site/.gitlab-ci.yml | none (node build + lint) | Easy | ~1 hr | Point jobs at node:20-slim, confirm artifact parity. Perfect first migration — do this one to prove the tag-based cutover. |
terraform-modules/.gitlab-ci.yml | host tools (terraform, tflint) on host PATH | Easy | ~2 hr | No host state; just needs a ci/terraform image. Pin provider cache via cache:. |
python-api/.gitlab-ci.yml | pip install at job time; $HOME/.cache/pip reused | Moderate | ~3 hr | Bake deps into an image or use cache: keyed on requirements.txt. No secrets, low risk. |
deploy-staging/.gitlab-ci.yml | ssh -i ~/.ssh/deploy_key, known_hosts, .env sourced | Moderate | ~1 day | Move key + .env to masked/protected file-type CI variables; seed known_hosts explicitly; load via ssh-agent in before_script. Test against a staging host first. |
image-factory/.gitlab-ci.yml | docker build, docker push, /var/run/docker.sock mounted | Hard | ~2–3 days | The big one. Pick docker-in-docker or buildah (see 04-…/dind-vs-kaniko-vs-socket.md); set registry auth per 03-…/registry-auth-guide.md. Do this after the runners and images are proven. |
db-migrate/.gitlab-ci.yml | systemctl start postgresql; sudo; talks to localhost:5432 | Hard | ~2–3 days | No host daemon in a container — replace with a services: [postgres:16] sidecar reached by hostname; drop sudo by fixing the image user. May need a schema-load rework. Migrate last. |
Tally for this estate: 2 easy, 2 moderate, 2 hard — a healthy shape. Most real estates
skew heavier toward moderate; if most of your pipelines score hard, that's a signal the
shell runners were doing a lot of undocumented host work, and you should budget accordingly
and lean hard on 06-gotchas/.
Ordering the rollout
Work strictly outward from easy:
- One easy pipeline, end to end. Prove the whole loop: Docker-executor runner tag, image pull, cache/artifacts, and — critically — the tag-flip rollback. You're validating the process, not the pipeline.
- The rest of the easy tier. Reuse the exact pattern. This is where you shake out locale/tz/PATH surprises cheaply, on jobs that can't cause real damage.
- Moderate tier, grouped by replacement type. Do all the "secrets to CI variables" pipelines together, then all the "cache rework" pipelines together — batching the same mechanical change is faster and less error-prone than context-switching.
- Hard tier, one at a time, never in parallel. Each hard pipeline gets its own change, its own parity check against the last shell run (timing, artifacts, permissions, exit codes), and its rollback tag kept live until you've seen it succeed on real traffic. If it breaks, flip the tag back and regroup — that reversibility is the entire reason for the tag-based strategy in the README.
Re-score after each tier. Migrating the easy and moderate pipelines often reveals that
a "hard" one shares an image or a services: block you've already built — which quietly
demotes its effort. The matrix is a living document; update the score and notes as you
learn, and let it drive the sequence rather than gut feel.
Feed this straight from audit-script.sh: its SUMMARY already prints each file with a
score and its signals, sorted hard-first. Paste that into a table like the one above, add
an effort estimate and a blocker note per row, and you have your migration backlog.