Template

Advanced Security & Review Pipeline

Conditional security scanning with rules:exists, include:rules, retry on exit codes, and environment stop jobs.

Best for projects with compliance scanning, code review apps, and granular conditional includes.

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH =~ /^release\//'

stages:
  - security-scan
  - build
  - review
  - cleanup

variables:
  SCAN_FAILURE_ACTION: warn

# Include security templates only when relevant files change (GitLab 16.4+)
include:
  - local: security/sast.yml
    rules:
      - exists:
          - "**/*.js"
          - "**/*.ts"
        when: always
      - when: never
  - local: security/dependency-scan.yml
    rules:
      - exists:
          - "package.json"
          - "package-lock.json"
        when: always
      - when: never

# SAST job with retry on specific exit codes (GitLab 17.5+)
sast_scan:
  stage: security-scan
  image: registry.example.com/security/sast:latest
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure
    exit_codes:
      - 137
      - 139
  script:
    - sast-scanner --output gl-sast-report.json --severity high
  artifacts:
    reports:
      sast: gl-sast-report.json
    when: always

# Build with custom cache strategy (GitLab 18.2+)
build_app:
  stage: build
  image: node:22-alpine
  needs:
    - sast_scan
  cache:
    key: $CI_COMMIT_REF_SLUG
    paths:
      - .npm/
      - node_modules/
    strategy: mirror
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  artifacts:
    paths:
      - dist/

# Review app deployed from merge request pipeline
review_app:
  stage: review
  image: alpine:3.21
  needs:
    - build_app
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: manual
      allow_failure: false
  environment:
    name: review/$CI_MERGE_REQUEST_IID
    url: https://review-$CI_MERGE_REQUEST_IID.app.example.com
    auto_stop_in: 2 days
    on_stop: stop_review
  script:
    - echo "Deploying review app for MR !$CI_MERGE_REQUEST_IID"
    - echo "URL: https://review-$CI_MERGE_REQUEST_IID.app.example.com"

# Environment stop trigger (GitLab 18.3+)
stop_review:
  stage: cleanup
  image: alpine:3.21
  needs:
    - review_app
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: manual
  environment:
    name: review/$CI_MERGE_REQUEST_IID
    action: stop
  script:
    - echo "Tearing down review app for MR !$CI_MERGE_REQUEST_IID"

# Production deploy with variables in publish (GitLab 17.9+)
deploy_prod:
  stage: cleanup
  image: alpine:3.21
  needs:
    - build_app
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  environment:
    name: production
    url: https://app.example.com
  script:
    - echo "Deploying to production"
    - echo "Version: $CI_COMMIT_TAG"
  release:
    tag_name: $CI_COMMIT_TAG
    name: "Release $CI_COMMIT_TAG"
    description: "./CHANGELOG.md"
    assets:
      links:
        - name: "Build Artifacts"
          url: "$CI_JOB_URL/artifacts/download"