Same YAML, Different Everything
GitHub Actions and GitLab CI both use YAML for pipeline configuration, but the syntax, file location, and pipeline model differ. GitHub Actions: workflow files in .github/workflows/*.yml, triggered by GitHub events (push, pull_request, schedule), jobs run on GitHub-hosted or self-hosted runners, and steps use community actions (uses: actions/checkout@v4) or shell commands (run: npm test). GitLab CI: one file at .gitlab-ci.yml, triggered by Git events (push, merge request, schedules), jobs run on GitLab shared or project runners, and steps are script commands (script: [npm test]).
Without CI/CD platform rules: AI generates .github/workflows/ci.yml for a GitLab project (wrong location, wrong syntax), uses uses: actions/checkout in GitLab (GitHub Actions concept, does not exist in GitLab), writes script: arrays in a GitHub Actions file (wrong keyword — Actions uses steps with run:), or references ${{ secrets.TOKEN }} in GitLab (GitLab uses $TOKEN or CI/CD variables). The CI/CD platform determines every line of the pipeline configuration.
This article provides: the key syntax differences, the AI rules for each platform, and copy-paste CLAUDE.md templates. The rules tell the AI: this project uses GitHub Actions (workflow files in .github/workflows/, steps with uses: and run:) or GitLab CI (.gitlab-ci.yml, script arrays, stages) — preventing every cross-platform syntax error.
File Location and Pipeline Structure
GitHub Actions: workflow files in .github/workflows/ directory. Multiple workflow files (ci.yml, deploy.yml, release.yml) run independently. Each workflow: triggered by events (on: push, on: pull_request), contains jobs, each job contains steps. Jobs run in parallel by default (sequential with needs: dependency). AI rule: 'GitHub Actions: .github/workflows/*.yml. Multiple files = multiple workflows. on: for triggers. jobs: for parallel work. steps: for sequential commands within a job.'
GitLab CI: one file at .gitlab-ci.yml in the project root. One pipeline definition with stages (build, test, deploy). Jobs are assigned to stages and run in stage order (all build jobs complete before test jobs start). Within a stage: jobs run in parallel. Multiple files: include keyword to compose from templates. AI rule: 'GitLab CI: .gitlab-ci.yml at root. stages: define execution order. Jobs assigned to stages. include: for composing pipeline templates.'
The structure rule prevents: AI creating .github/workflows/ in a GitLab project (wrong location), writing stages: in a GitHub Actions file (Actions does not have stages — use needs: for job ordering), using on: triggers in GitLab (GitLab uses rules: with if conditions), or generating a single file for GitHub Actions (Actions prefers multiple focused workflow files). The pipeline model is platform-determined.
- Actions: .github/workflows/*.yml (multiple files). GitLab: .gitlab-ci.yml (one file, include for templates)
- Actions: on: push/pull_request triggers. GitLab: rules: with if conditions
- Actions: jobs run in parallel by default, needs: for ordering. GitLab: stages define order
- Actions: steps within jobs (uses: + run:). GitLab: script arrays within jobs
- AI error: .gitlab-ci.yml on GitHub = ignored. .github/workflows on GitLab = ignored
GitHub Actions: separate workflow files (ci.yml, deploy.yml, release.yml) run independently. GitLab: one .gitlab-ci.yml with stages defines the entire pipeline. Actions: multiple focused files. GitLab: one comprehensive file with include for composition.
Job and Step Syntax: steps vs script
GitHub Actions job syntax: jobs: test: runs-on: ubuntu-latest, steps: [- uses: actions/checkout@v4, - uses: actions/setup-node@v4 with: { node-version: 20 }, - run: pnpm install, - run: pnpm test]. Steps are: sequential within a job, either a community action (uses:) or a shell command (run:). Community actions: a massive marketplace (actions/checkout, actions/setup-node, actions/cache). AI rule: 'Actions: steps with uses: (community actions) and run: (shell commands). actions/checkout@v4 for code checkout. actions/setup-node@v4 for Node.js. runs-on: for runner selection.'
GitLab CI job syntax: test: stage: test, image: node:20, script: [pnpm install, pnpm test]. Steps are: script commands in an array, executed sequentially. Before script: before_script for setup commands shared across jobs. Docker image: image: specifies the container (GitLab CI is container-first). There is no equivalent to GitHub Actions community marketplace — GitLab uses Docker images and script commands. AI rule: 'GitLab: script arrays for commands. image: for Docker container. before_script: for shared setup. No uses: keyword — script commands only.'
The syntax rule prevents: AI writing uses: actions/checkout in GitLab CI (keyword does not exist — GitLab checks out code automatically), writing script: arrays in GitHub Actions (use steps: with run: instead), using image: in GitHub Actions (use container: or runs-on: with a specific OS), or relying on a community action marketplace in GitLab (use Docker images and scripts instead). The step model is fundamentally different: Actions = action + shell. GitLab = container + shell.
GitHub Actions: uses: actions/checkout@v4 — a marketplace of reusable actions for every task. GitLab: no marketplace — use Docker images (image: node:20) and script commands. AI generating uses: in GitLab: keyword does not exist. The step model is fundamentally different.
Caching and Secrets Management
GitHub Actions caching: uses: actions/cache@v4 with path and key. Cache key: hash of lockfile (hashFiles('**/pnpm-lock.yaml')). Restore: if the key matches, the cache is restored before the step. Save: automatically saved after the job completes. Cache size: 10 GB per repository. AI rule: 'Actions: actions/cache@v4 for dependency caching. Key: hashFiles for lockfile hash. Path: node_modules or .pnpm-store. Auto-save after job.'
GitLab CI caching: cache: key and paths in the job definition. cache: { key: { files: [pnpm-lock.yaml] }, paths: [node_modules/] }. Cache is: per-runner (shared across jobs on the same runner), or distributed (with S3-backed cache). Cache policy: pull-push (default), pull (read-only), or push (write-only). AI rule: 'GitLab: cache in job definition with key.files and paths. Policy: pull-push default. Distributed cache: S3 backend for multi-runner.'
Secrets: GitHub Actions uses ${{ secrets.SECRET_NAME }} syntax (secrets stored in repository or organization settings, masked in logs). GitLab CI uses $SECRET_NAME syntax (CI/CD variables set in project settings, optionally masked and protected). AI rule: 'Actions secrets: ${{ secrets.NAME }}. GitLab variables: $NAME. Both: set in project settings, masked in logs. Never hardcode secrets in YAML.'
- Actions: actions/cache@v4 action. GitLab: cache: key/paths in job YAML
- Cache key: Actions hashFiles() function. GitLab key.files array
- Secrets: Actions ${{ secrets.NAME }} vs GitLab $NAME — different interpolation syntax
- Actions secrets: repository settings. GitLab variables: project CI/CD settings
- AI error: ${{ secrets.TOKEN }} in GitLab = literal string, not interpolated. $TOKEN in Actions = shell variable, not secret
${{ secrets.TOKEN }} in GitLab: treated as a literal string, not interpolated — the pipeline runs but the secret is never injected. $TOKEN in GitHub Actions: treated as a shell variable, not a GitHub secret. Wrong secret syntax = pipeline runs with empty values. Silent failure.
Runners and Artifacts
GitHub Actions runners: runs-on: ubuntu-latest (GitHub-hosted, free for public repos, limited minutes for private). Self-hosted: runs-on: self-hosted with labels. Matrix: strategy: matrix: { node: [18, 20, 22] } runs the job for each combination. AI rule: 'Actions: runs-on for runner. ubuntu-latest for Linux. macos-latest for Mac. strategy.matrix for multi-version testing. Self-hosted with labels.'
GitLab CI runners: shared runners (GitLab-hosted, available by default), project runners (self-hosted, registered to specific projects), and group runners (shared across a group). Runner selection: tags: [docker, linux] matches runners with those tags. image: specifies the Docker image for the job. AI rule: 'GitLab: tags for runner selection. image: for Docker container. Shared runners available by default. Register self-hosted with gitlab-runner register.'
Artifacts: GitHub Actions uses actions/upload-artifact@v4 and actions/download-artifact@v4 to share files between jobs. GitLab uses artifacts: paths and dependencies or needs to share between jobs. Both support: artifact retention policies, artifact download from the UI, and inter-job data passing. AI rule: 'Actions: upload-artifact/download-artifact actions. GitLab: artifacts.paths in job definition. Both: share files between jobs, downloadable from UI.'
Ready-to-Use Rule Templates
GitHub Actions CLAUDE.md template: '# CI/CD (GitHub Actions). Workflows: .github/workflows/*.yml. Triggers: on: push/pull_request/schedule. Jobs: runs-on: ubuntu-latest. Steps: uses: (actions) and run: (shell). Checkout: actions/checkout@v4. Node: actions/setup-node@v4. Cache: actions/cache@v4 with hashFiles. Secrets: ${{ secrets.NAME }}. Matrix: strategy.matrix for multi-version. Artifacts: upload-artifact/download-artifact. Never: .gitlab-ci.yml, script: arrays, image: keyword, $VARIABLE_NAME for secrets.'
GitLab CI CLAUDE.md template: '# CI/CD (GitLab CI). Pipeline: .gitlab-ci.yml at root. Stages: stages: [build, test, deploy] define order. Jobs: stage + script arrays. Docker: image: node:20 per job. Cache: cache.key.files + cache.paths. Variables: $NAME (set in CI/CD settings). Artifacts: artifacts.paths in job. Rules: rules: with if conditions for conditional jobs. Include: include for template composition. Never: .github/workflows/, uses: keyword, actions/*, ${{ secrets.NAME }} syntax.'
The templates capture the platform boundary: GitHub Actions = .github/workflows/ + uses: + steps: + ${{ secrets.NAME }}. GitLab CI = .gitlab-ci.yml + script: + image: + $NAME. Every keyword, every file location, every secret syntax is platform-specific. Copy the template for your CI/CD platform.
Comparison Summary
Summary of GitHub Actions vs GitLab CI AI rules.
- Files: .github/workflows/*.yml vs .gitlab-ci.yml — different locations, multiple vs single file
- Steps: Actions uses: + run: vs GitLab script: arrays — actions marketplace vs Docker + shell
- Ordering: Actions needs: job dependencies vs GitLab stages: sequential stage order
- Runners: Actions runs-on: vs GitLab tags: + image: — OS-based vs container-first
- Secrets: ${{ secrets.NAME }} vs $NAME — different interpolation syntax
- Cache: actions/cache@v4 action vs cache: YAML in job definition
- Triggers: on: push/pull_request vs rules: with if conditions
- Templates: every keyword is platform-specific — mixing produces invalid YAML that fails silently