Skip to content

gitlab-cicd

Type Skill
Plugin awl-cicd · v0.0.22
Invoke /awl-cicd:gitlab-cicd
Tools Read, Write, Edit, Bash, Glob, Grep
Source plugins/awl-cicd/skills/gitlab-cicd/SKILL.md

This skill should be used when the user asks to “set up a pipeline”, “configure CI/CD”, “create .gitlab-ci.yml”, “add Docker build”, “deploy to kubernetes”, “speed up CI”, “optimize pipeline”, “CI is slow”, “pipeline takes too long”, “add claude review”, “set up iOS CI”, “set up Android CI”, or mentions “stages”, “gitlab-ci”, “ArgoCD”, “Trivy”, “gitlab runners”, “runner tags”, “pipeline caching”, “tart”, “buildkit”. Configures GitLab CI/CD pipelines with AWL reusable components (Docker/BuildKit/NPM/Composer/.NET builds, Node/PHP/.NET/Android/iOS testing, vulnerability scanning, SBOM tracking, ArgoCD deployments, Claude Code jobs) and tunes pipeline performance.

Trigger phrases: set up a pipeline · configure CI/CD · create .gitlab-ci.yml · add Docker build · deploy to kubernetes · speed up CI · optimize pipeline · CI is slow · pipeline takes too long · add claude review · set up iOS CI · set up Android CI · stages · gitlab-ci · ArgoCD · Trivy · gitlab runners · runner tags · pipeline caching · tart · buildkit

All templates are pre-deployed as CI/CD components in devops/ci-cd-templates. Never copy template YAML into a project pipeline.

Always use include: component: syntax:

include:
- component: $CI_SERVER_FQDN/devops/ci-cd-templates/TEMPLATE_NAME@main
inputs:
key: value
  • Example files (references/examples/) are the patterns to follow when generating .gitlab-ci.yml.
  • Template config files (references/templates/) are reference only, to understand inputs, variables, and behavior. Never inline them.
  • When a template provides hidden jobs (.docker-build-simple, .deploy-autodeploy-app, .claude-code-review), customize with extends:. Don’t redefine the job.
  • Inputs resolve at include time. Set them on the include: entry, not on the job that extends the hidden job. Per-job overrides go into variables:.
  • Read a reference project before inventing a pattern. Every AWL convention exists in a live repo: bafu/becasuisse-cms (Payload + PowerSync on Natron), awl-ecosystem/my-awl (Node), ensemble/mono-repo (.NET). Fetch a file with glab api "projects/<group>%2F<project>/repository/files/<path>/raw?ref=main".

Determine the stack: Node.js, .NET, PHP/Laravel, Statamic, Payload CMS, Migros Next.js, Android/KMP, or iOS. For opinionated setups use the application templates (app-payload, app-statamic, migros-web) or the mobile templates (android, ios-fastlane, ios-lint).

  1. Read the relevant example from references/examples/.
  2. Consult references/templates/ for the inputs of each component.
  3. Assemble include: components.

Define stages: matching the templates used. Top-level stages does not merge across includes; the last component’s list wins, so declare the full list in the project. Set GIT_DEPTH: 1. Add project-specific variables:.

  • Override hidden jobs with extends: plus variables:.
  • Add project-specific jobs (e2e tests, custom scripts).
  • Give every job that reads environment-scoped CI variables an environment: block. A job without one only sees variables scoped to *. For non-deploy jobs use environment: { name: staging, action: verify }, which binds the scope without creating a deployment.
  • Keep production deploys manual or on a protected production branch.

Add the workflow: and default: blocks from the Quick start. They handle auto-cancel, no duplicate runs, and retry on infra failure. See references/performance.md for the rationale.

  • Run glab ci lint on the project pipeline. It fails on component sources ($[[ inputs ]]), so lint the consuming .gitlab-ci.yml, not a template.
  • Run the project linter locally (Prettier, SwiftFormat, ESLint) before every push. Lint failures in CI after a push were the most common self-inflicted red pipeline.
  • Never push to main directly. main auto-deploys to staging. Use /mr.
  • After changing .gitlab-ci.yml, push before judging the next pipeline run. A run started before the push proves nothing.
variables:
GIT_DEPTH: 1
GIT_LFS_SKIP_SMUDGE: "1"
workflow:
auto_cancel:
on_new_commit: interruptible
rules:
# Skip branch pipelines when an MR is open. Avoids running the same commit twice.
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_TAG
default:
interruptible: true
retry:
max: 2
when:
- runner_system_failure
- api_failure
- scheduler_failure
- data_integrity_failure
- unmet_prerequisites
stages:
- .pre
- test
- build
- deploy
include:
- component: $CI_SERVER_FQDN/devops/ci-cd-templates/version@main
- component: $CI_SERVER_FQDN/devops/ci-cd-templates/test-node@main
- component: $CI_SERVER_FQDN/devops/ci-cd-templates/deploy-default-branches@main

Mark release-path jobs interruptible: false (release, tag, deploy, version-bump) so they can’t be cancelled mid-flight.

Most projects pin @main. Template changes then land in every pipeline immediately, and a previously green .gitlab-ci.yml can become invalid overnight. When a pipeline fails with yaml invalid after no project change, check the template repo’s recent commits first. Tags exist (v0.0.25 from June 2026) but lag behind main; ask DevOps before pinning a tag.

Tag Executor Pull caching Use for
web-test Kubernetes IfNotPresent Default for Node/web/lightweight jobs
docker Docker forces always (re-pulls) Only when web-test is incompatible
tart macOS VM (Tart) fresh VM per job, no shared cache iOS builds, ios-fastlane, ios-lint
android-dev Android shell persistent Gradle cache Android builds, android template
mac-dev macOS shell n/a Legacy macOS builds
docker-dind Docker-in-Docker n/a Only if build-buildkit is not an option

Prefer web-test over docker. The docker runner forces pull_policy: always, which re-pulls every image on every job (30–75 s overhead). Full details in references/performance.md.

Kubernetes runners request 200m CPU / 128Mi and cap at 2000m / 2000Mi. Raise per job with KUBERNETES_CPU_REQUEST, KUBERNETES_CPU_LIMIT, KUBERNETES_MEMORY_REQUEST, KUBERNETES_MEMORY_LIMIT in the job’s variables:.

The tart tag is served by a single macOS runner. Jobs stuck with stuck_pending_no_matching_runners or dying after 30 minutes in Cloning a new VM... mean the runner is offline. That is an infrastructure issue, not a pipeline issue. Don’t change tags to work around it; report it in Slack #tools-claudecode or to DevOps.

registry.appswithlove.net/devops/docker-images/* are AWL-maintained, pre-warmed on runners, and include the @awl/* npm registry plus pnpm:

Image Tags Notes
node 22-alpine, 24-alpine Only alpine variants, no full Debian node:*
playwright v1.58.x-noble, v1.60.0-noble MS Playwright + browsers
laravel php8.5-dev etc. Laravel CLI + Composer auth
dotnet various .NET SDK + NuGet auth
local-awl-dev latest Local HTTPS proxy for *.local.awl.dev

Use them when the project needs @awl/* packages or pnpm. Docker Hub pulls are fine on web-test.

Template Purpose
version Semantic versioning with release-it
build-buildkit Docker build via in-cluster BuildKit, no dind. Preferred for new projects
build-docker-simple Docker build on the docker runner with custom tags
build-docker Legacy develop/release branch builds
build-docker-multiarch Buildx linux/amd64,linux/arm64 with registry cache
build-npm NPM artifact builds
build-composer PHP Composer dependency installer
build-dotnet .NET application builder
build-dotnet-migration Entity Framework Core migrations bundler
publish-npm NPM package publishing to GitLab registry
npm-scripts Matrix of npm scripts (lint, test, …) as separate jobs
Template Purpose
test-node Node.js testing with auto-detected package manager
test-dotnet .NET build and test
test-php PHP/Laravel testing with composer
test-trivy Container image vulnerability scanning
test-renovate Renovate configuration validator
Template Purpose
depcheck-node Node dependency vulnerability check using audit-ci
depcheck-composer PHP Composer vulnerability audit
depcheck-dotnet .NET dependency vulnerability scanning
Template Purpose
deploy-default-branches Build + auto-deploy main/testing/production to Natron via ArgoCD. Default for web apps
deploy-service Deploy a preset side service (redis, meilisearch) next to the app
deploy-autodeploy-app Base job behind the two above, for custom deploy jobs
deploy-grafana-dashboard Provision Grafana dashboards from JSON in the repo
deploy-argocd-project ArgoCD project creation via GitOps
deploy-dma-docker-compose-stack DMA compose stack deployments
deploy Legacy trigger-based downstream deployments

Deployment details live in the deploying-to-kubernetes and natron-deployment skills.

Template Purpose
android Gradle test/build/Updraft/Play Store, optional SBOM, coverage, CI signing
ios-fastlane fastlane test/build/TestFlight on Tart runners, optional Tuist cache
ios-lint SwiftFormat + SwiftLint gate without fastlane
Template Purpose
track-vulntracker Syft scan + upload to AWL VulnTracker
track-vulntracker-upload Upload an SBOM another job already produced

GitLab’s own Jobs/Secret-Detection.gitlab-ci.yml is included automatically by deploy-default-branches and android.

Template Purpose
git-auto-commit Git automation with direct commit or MR modes
fetch-secrets Read CI variables from another project via job token
claude-code Base job: Claude Code with credentials, extend for custom jobs
claude-code-review Post a Claude review comment on every MR push
claude-code-autocommit Run a prompt, commit, open an MR
claude-code-manual Same, triggered on demand via CLAUDE_PROMPT pipeline variable
Template Purpose
app-payload Payload CMS testing (pnpm-based)
app-statamic Statamic CMS testing (PHP + optional Node)
migros-web Migros Next.js frontend testing

steps/ holds GitLab CI/CD steps with bundled files (for example a shared Dockerfile). Available: build-sveltekit-docker, track-dependencies. Usage in references/templates/utilities.md.

Set in GitLab CI/CD settings. Instance-level variables need no project setup.

Variable Scope Purpose
GITOPS_TRIGGER_TOKEN instance Trigger the GitOps repo (autodeploy)
VULNTRACKER_API_KEY instance VulnTracker upload
CLAUDE_CODE_PERSONAL_TOKEN instance Claude Code jobs (PAT of the claude user)
TRIGGER_TOKEN project Legacy deploy template
NUGET_SOURCE/USER/PASSWORD project .NET private feeds
TUIST_TOKEN project Tuist remote cache (masked, unprotected, scope *)

GitLab variable constraints that bite: masked values need 8+ characters and a single line (base64-encode .p8 keys and keystores; keep short flags like true visible), hidden variables can’t be read by External Secrets, and a value containing $ needs “Expand variable reference” turned off.

Error Cause Fix
yaml invalid on an unchanged pipeline Template on @main changed Check devops/ci-cd-templates commits; adapt to the new contract
on_stop job ... have different environment name Deploy job overrides environment.name, stop job doesn’t Use the same name on both or drop the override
Job can’t see a CI variable Variable scoped to an environment, job has no environment: Add environment: { name: <env>, action: verify }
glab api .../projects/<id> returns 404 No Maintainer access, not a missing project Ask for membership; GitLab hides projects instead of returning 403
glab ci lint fails on a template file Components with $[[ inputs ]] aren’t lintable Lint the consuming .gitlab-ci.yml instead
stuck_pending_no_matching_runners on tart jobs macOS runner offline Infra issue; report it, don’t change tags
Postgres service starts for every matrix entry services: applies per job, not per matrix cell Include the component twice with different job_name
Permission denied (publickey) on push from Claude 1Password SSH agent locked, or port 22 instead of 22222 Unlock 1Password; SSH URL is ssh://git@gitlab.appswithlove.net:22222/
dubious ownership in Claude jobs Image runs as claude, checkout owned by root Fixed in claude-code base job; update the include
401 API key is invalid in Claude jobs OAuth token stored as ANTHROPIC_API_KEY Fixed in claude-code base job; token is re-exported as CLAUDE_CODE_OAUTH_TOKEN
Docker login fails Missing registry vars Check CI_REGISTRY_* variables
Version job skipped Wrong branch Only runs on main/release
Private npm 401/404 Missing auth Pass CI_JOB_TOKEN via BUILD_ARG
Trivy scan timeout Large image / slow DB Increase job timeout, check DB cache
Image pull takes 30–75 s per job tags: [docker] forces pull_policy: always Switch to tags: [web-test]
Stale pipelines after force-push Missing workflow: auto_cancel Add auto_cancel: on_new_commit: interruptible + default: interruptible: true
Same commit builds twice on MR push Branch + MR pipelines both triggered Add workflow: rules: to skip branch-when-MR-exists
Stacked MRs rerun every pipeline Squash merge rewrites SHAs, next MR rebases Disable squash in project settings (Settings → Merge requests)
node_modules artifact upload dominates pipeline Install stage passing 40k files via artifacts Drop install stage; cache .npm; npm ci --prefer-offline per job
  1. Read a reference project before writing pipeline config from memory.
  2. Use standard stages: test, build, deploy. Declare all stages in the project.
  3. Store secrets in GitLab CI/CD variables. Never copy a secret from one project into another; use fetch-secrets or an instance variable.
  4. Mark secrets masked and protected. Unmasked MATCH_PASSWORD or keystore variables leak into job logs.
  5. Deploy production only from a protected production branch or a manual job.
  6. Set GIT_DEPTH: 1 (and GIT_LFS_SKIP_SMUDGE: '1' if LFS isn’t needed).
  7. Prefer tags: [web-test] over tags: [docker] for Node/web jobs.
  8. Add workflow: auto_cancel and default: interruptible: true.
  9. Never upload node_modules as job artifacts. Cache .npm instead.
  10. Keep .dockerignore in sync with what the build imports. Tests run on a full checkout and won’t notice a file the Docker build can’t see.
  • Build templates: version, build-buildkit, build-docker, build-docker-simple, build-docker-multiarch, build-composer, build-dotnet, build-dotnet-migration, build-npm, publish-npm, npm-scripts
  • Test templates: test-node, test-dotnet, test-php, test-trivy, test-renovate
  • Dependency checks: depcheck-node, depcheck-composer, depcheck-dotnet
  • Deploy templates: deploy-default-branches, deploy-service, deploy-autodeploy-app, deploy-grafana-dashboard, deploy-argocd-project, deploy-dma-docker-compose-stack, deploy
  • Mobile templates: android, ios-fastlane, ios-lint
  • Tracking and security: track-vulntracker, track-vulntracker-upload
  • Utilities: git-auto-commit, fetch-secrets, claude-code, claude-code-review, claude-code-autocommit, claude-code-manual, CI/CD steps
  • Application templates: app-payload, app-statamic, migros-web
  • Node.js apps: web app, SvelteKit, full-stack, minimal, NPM publish, Migros
  • .NET apps: API, EF migrations
  • PHP apps: Laravel, Statamic, Payload CMS
  • Deployment: mono-repo, DMA compose, ArgoCD, branch-specific
  • Security: Trivy scanning, SBOM tracking
  • Utilities: Claude Code review, git auto-commit