
Concourse Ci
- 47 installs
- 2 repo stars
- Updated August 3, 2026
- netresearch/concourse-ci-skill
Helps with ai & agent building tasks.
About
concourse-ci is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- concourse-ci
- AI & Agent Building
- AI-coding skill
Concourse Ci by the numbers
- 47 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,551 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netresearch/concourse-ci-skill --skill concourse-ciAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | netresearch/concourse-ci-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Concourse CI Pipeline Development
Expert guidance for writing, refactoring, and optimizing Concourse CI pipelines (v8.0+).
When to Use
- Creating or modifying Concourse pipelines
- Configuring resources (git, registry-image, custom types)
- Building container images with
oci-build-task - Troubleshooting resource check failures or build issues
- Migrating from legacy patterns (docker-image, duplicate jobs)
Quick Reference
| Task | Modern (Recommended) | Legacy (Avoid) |
|---|---|---|
| Building images | oci-build-task + registry-image | docker-image resource |
| Multi-env deploys | across step modifier | Duplicate jobs per env |
| Dynamic pipelines | set_pipeline + instanced pipelines | Manual pipeline duplication |
| Notification symbols | UTF-8 characters (e.g. \u2714 for checkmark, \u274c for X) | HTML entities (e.g. ✓, ✗) |
| Resource styling | Always use icon: property | No icon |
Core Concepts
Pipelines consist of resources (external versioned artifacts), jobs (sequences of steps), and optional groups (UI organization). All execution runs in containers.
Key step types: get, put, task, set_pipeline, in_parallel, do, try, load_var. Job hooks: on_success, on_failure, on_error, on_abort, ensure. Note: on_failure (non-zero exit) differs from on_error (infrastructure crash/OOM) -- handle both. Use fly execute to test tasks locally.
See references/core-concepts.md for step types table, lifecycle hooks, and fly CLI essentials.
Critical Gotchas
1. Git tag detection after force-push -- Escape regex dots, enable clean_tags: true, separate read/write resources, force recheck with fly -t T check-resource -r pipeline/resource. See references/resources-guide.md. 2. registry_mirror format mismatch -- registry-image expects an object (host: mirror), docker-image expects a URL string. Provide separate formats in CONCOURSE_BASE_RESOURCE_TYPE_DEFAULTS. See references/resources-guide.md. 3. GitLab Container Registry JWT auth -- The JWT endpoint lives on the GitLab host, not the registry host. Discover via Www-Authenticate header. See references/resources-guide.md. 4. git push --mirror and default branch -- Target repo's default branch must exist upstream. If absent, the pre-receive hook rejects the push with "pre-receive hook declined". Set it before the first mirror push.
References
references/pipeline-syntax.md-- Complete YAML schema for pipelines, jobs, resourcesreferences/core-concepts.md-- Step types, lifecycle hooks, fly CLI essentialsreferences/resources-guide.md-- Git-resource, registry-image, docker-image migration, gotcha detailsreferences/best-practices.md-- Optimization, troubleshooting, notifications, deployment patternsreferences/resource-types-catalog.md-- Available resource types (Ansible, Terraform, etc.)
Examples
Working examples in examples/:
basic-pipeline.yml-- Build-test-deploy with versioningmodern-ci-cd.yml-- oci-build-task, across, build_log_retentionmulti-branch.yml-- Dynamic branch pipelines with set_pipelinedocker-build.yml-- Container image build and pushvars-template.yml-- Variable file organization
Validation
Use scripts/validate-pipeline.sh to check pipeline syntax before deployment.
# Checkpoints for concourse-ci skill
# Validates Concourse CI pipeline structure and best practices
version: 1
skill_id: concourse-ci
preconditions:
- type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | grep -q ."
mechanical:
# === PIPELINE STRUCTURE ===
- id: CC-01
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'jobs:'"
severity: error
desc: "Pipeline YAML must define at least one job"
- id: CC-02
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'resources:'"
severity: warning
desc: "Pipeline YAML should define resources"
- id: CC-03
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'plan:'"
severity: error
desc: "Jobs must have a plan (step sequence)"
# === MODERN PATTERNS ===
- id: CC-04
type: command
pattern: "! find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | xargs grep -q 'type: docker-image' 2>/dev/null || true"
severity: warning
desc: "Should use registry-image instead of deprecated docker-image resource type"
- id: CC-05
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'icon:'"
severity: info
desc: "Resources should have icon property for visual clarity"
# === ERROR HANDLING ===
- id: CC-06
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'on_failure:'"
severity: warning
desc: "Jobs should define on_failure hooks for error notification"
# === BUILD LOG RETENTION ===
- id: CC-07
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'build_log_retention:'"
severity: info
desc: "Jobs should configure build_log_retention to manage storage"
# === SECURITY ===
- id: CC-08
type: command
pattern: "! find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | xargs grep -qE '(password|secret|token):\\s+[\"'\\''a-zA-Z0-9]' 2>/dev/null"
severity: error
desc: "Pipeline must not contain hardcoded secrets (use var_sources or params)"
# === ENSURE HOOKS ===
- id: CC-09
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'ensure:' 2>/dev/null || true"
severity: info
desc: "Jobs should use ensure blocks for cleanup tasks"
# === SERIAL GROUPS ===
- id: CC-10
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -qE 'serial:|serial_groups:' 2>/dev/null || true"
severity: info
desc: "Jobs that should not run concurrently should use serial or serial_groups"
# === GROUPS DEFINED ===
- id: CC-11
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'groups:' 2>/dev/null || true"
severity: info
desc: "Pipeline should define groups for UI organization"
# === REGEX DOT ESCAPING IN TAG FILTER ===
- id: CC-12
type: command
pattern: "! find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | xargs grep -E 'tag_regex:.*\\[0-9\\]\\+\\.[0-9\\]' 2>/dev/null | grep -v '\\\\\\.' | head -1 | grep -q . 2>/dev/null"
severity: warning
desc: "Git resource tag_regex must escape literal dots (use \\\\. not .)"
# === ON_ERROR HOOK ===
- id: CC-13
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -q 'on_error:' 2>/dev/null || true"
severity: info
desc: "Jobs should handle on_error (container crash) separately from on_failure (exit code 1)"
# === CLEAN_TAGS FOR GIT RESOURCES ===
- id: CC-14
type: command
pattern: "! find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | xargs grep -l 'tag_regex:' 2>/dev/null | xargs grep -L 'clean_tags: true' | head -1 | grep -q . 2>/dev/null"
severity: warning
desc: "Git resources with tag_regex should enable clean_tags: true"
# === PARAMETERIZED CREDENTIALS ===
- id: CC-15
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -qE '\\(\\(' 2>/dev/null || true"
severity: warning
desc: "Pipeline should use parameterized credentials ((var)) instead of hardcoded values"
# === PARALLEL EXECUTION ===
- id: CC-16
type: command
pattern: "find . -name '*.yml' -path '*/ci/*' -o -name 'pipeline*.yml' -o -name '*-pipeline.yml' | head -1 | xargs grep -qE 'in_parallel:|across:' 2>/dev/null || true"
severity: info
desc: "Pipeline should use in_parallel or across for concurrent execution"
# === GIT PUSH MIRROR DEFAULT BRANCH ===
- id: CC-18
type: command
pattern: "! grep -rqE 'push.*--mirror' . --include='*.yml' --include='*.sh' 2>/dev/null"
severity: warning
desc: >-
Jobs using `git push --mirror` must ensure the target repo's default
branch matches a branch that exists in the upstream source. If the
target default branch is not present upstream, the pre-receive hook
rejects the mirror push with 'pre-receive hook declined'. Set the
target repo's default branch to match the upstream branch name before
the first mirror push. Learned from OPSCHEM-499.
llm_reviews:
- id: CC-20
domain: ci-quality
prompt: |
Review the Concourse CI pipeline YAML for best practices:
1. Are resources properly typed with source config?
2. Do jobs use modern step types (across, in_parallel) vs duplicate jobs?
3. Is oci-build-task used instead of docker-image for building images?
4. Are notification patterns (on_failure, on_success, ensure) used appropriately?
5. Are sensitive values parameterized (not hardcoded)?
6. Do git resources escape regex dots in tag_filter patterns?
severity: warning
desc: "Pipeline structure and modern pattern adherence"
- id: CC-21
domain: ci-quality
prompt: |
Review the Concourse CI pipeline for operational robustness:
1. Are serial/serial_groups used for jobs that shouldn't run concurrently?
2. Is build_log_retention configured to prevent unbounded storage?
3. Are resource check intervals (check_every) reasonable?
4. Do jobs use ensure blocks for cleanup tasks?
5. Are task configs inline or properly externalized in separate files?
severity: info
desc: "Pipeline operational robustness review"
- id: CC-23
domain: ci-quality
prompt: |
Find all pipeline tasks or scripts that use `git push --mirror`. For each one:
1. Is the target repository's default branch documented or known to match a branch
that exists in the upstream source? A mismatched default branch causes the
pre-receive hook to reject the entire mirror push with "pre-receive hook declined".
2. Is there a bootstrap step or README note explaining how to initialize the target
repo (set default branch) before the first mirror push runs?
Report findings with file and line references.
severity: warning
desc: "Mirror push targets must have their default branch set to match the upstream before first push"
- id: CC-22
domain: ci-quality
prompt: |
Review all YAML anchors used as shared variable groups (e.g. vault-vars-generic,
common-params, shared-env). For each one:
1. Does it contain credentials or sensitive variables that are only meaningful for one
environment (e.g. prod-only auth tokens, environment-specific API keys, or credentials
that aren't present in all deployment targets)?
2. Are there separate per-environment anchors (vault-vars-staging, vault-vars-prod)
that should carry these env-specific values instead?
3. Placing env-specific credentials in a generic/shared anchor silently passes
wrong or missing credentials to the environment that doesn't need them, which
can cause authentication conflicts or unexpected behavior.
Report any credentials or vars in shared anchors that should be scoped to a
specific environment anchor.
severity: warning
desc: "Environment-specific credentials must not be placed in shared YAML anchors"
# Basic Concourse CI Pipeline
# Build-Test-Deploy pattern with notifications
#
# Usage:
# fly -t target set-pipeline -p my-app -c basic-pipeline.yml \
# -v git_uri=https://github.com/org/repo.git \
# -v git_branch=main \
# -v registry_repo=registry.example.com/org/app
################################################
# Reusable YAML Anchors
################################################
git-source: &git-source
uri: ((git_uri))
branch: ((git_branch))
username: ((git.username))
password: ((git.token))
registry-source: ®istry-source
repository: ((registry_repo))
username: ((registry.username))
password: ((registry.password))
notify-success: ¬ify-success
put: notify
params:
text: ":white_check_mark: $BUILD_PIPELINE_NAME/$BUILD_JOB_NAME #$BUILD_NAME succeeded"
notify-failure: ¬ify-failure
put: notify
params:
text: ":x: $BUILD_PIPELINE_NAME/$BUILD_JOB_NAME #$BUILD_NAME failed"
# Build log retention policy
log-retention: &log-retention
build_log_retention:
days: 14
builds: 100
minimum_succeeded_builds: 1
################################################
# Resource Types
################################################
resource_types:
- name: slack-notification
type: registry-image
source:
repository: cfcommunity/slack-notification-resource
################################################
# Resources
################################################
resources:
# Source code
- name: source
type: git
icon: gitlab
check_every: 2m
source:
<<: *git-source
ignore_paths:
- "*.md"
- docs/**
# Release candidate image
- name: app-image-rc
type: registry-image
icon: docker
source:
<<: *registry-source
tag: rc
# Production image
- name: app-image
type: registry-image
icon: docker
source:
<<: *registry-source
tag: latest
# Version management
- name: version
type: semver
icon: tag
source:
driver: git
uri: ((git_uri))
branch: version
file: version
username: ((git.username))
password: ((git.token))
initial_version: 0.1.0
# Notifications
- name: notify
type: slack-notification
icon: slack
source:
url: ((slack.webhook_url))
################################################
# Groups
################################################
groups:
- name: all
jobs: ["*"]
- name: build
jobs: [build, test]
- name: release
jobs: [release]
- name: deploy
jobs: [deploy-staging, deploy-prod]
################################################
# Jobs
################################################
jobs:
#-----------------------------------------
# Build Job
#-----------------------------------------
- name: build
<<: *log-retention
serial: true
plan:
- in_parallel:
- get: source
trigger: true
- get: version
params: { pre: rc }
- task: compile
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
- name: version
outputs:
- name: build
caches:
- path: source/node_modules
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm ci
npm run build
cp -r dist ../build/
cp ../version/version ../build/
# Build container image using modern oci-build-task
- task: build-image
privileged: true
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
inputs:
- name: source
- name: build
outputs:
- name: image
params:
CONTEXT: source
DOCKERFILE: source/Dockerfile
caches:
- path: cache
run:
path: build
- put: app-image-rc
params:
image: image/image.tar
get_params:
skip_download: true
on_failure:
<<: *notify-failure
#-----------------------------------------
# Test Job
#-----------------------------------------
- name: test
<<: *log-retention
serial: true
plan:
- in_parallel:
- get: source
passed: [build]
trigger: true
- get: app-image-rc
passed: [build]
trigger: true
- task: unit-tests
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
caches:
- path: source/node_modules
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm ci
npm test
- task: integration-tests
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm run test:integration
on_failure:
<<: *notify-failure
#-----------------------------------------
# Release Job
#-----------------------------------------
- name: release
serial: true
plan:
- in_parallel:
- get: source
passed: [test]
- get: app-image-rc
passed: [test]
trigger: true
params:
format: oci
- get: version
params: { bump: final }
- put: app-image
params:
image: app-image-rc/image.tar
version: version/version
bump_aliases: true
- put: version
params:
file: version/version
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
#-----------------------------------------
# Deploy Staging
#-----------------------------------------
- name: deploy-staging
serial: true
plan:
- in_parallel:
- get: source
passed: [release]
- get: app-image
passed: [release]
trigger: true
- task: deploy
config:
platform: linux
image_resource:
type: registry-image
source:
repository: bitnami/kubectl
tag: latest
inputs:
- name: source
params:
KUBECONFIG_CONTENT: ((k8s.staging_kubeconfig))
NAMESPACE: staging
run:
path: /bin/bash
args:
- -exc
- |
echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
kubectl -n $NAMESPACE apply -f source/k8s/
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
#-----------------------------------------
# Deploy Production (Manual)
#-----------------------------------------
- name: deploy-prod
serial: true
plan:
- in_parallel:
- get: source
passed: [deploy-staging]
- get: app-image
passed: [deploy-staging]
# No trigger - manual deployment
- task: deploy
config:
platform: linux
image_resource:
type: registry-image
source:
repository: bitnami/kubectl
tag: latest
inputs:
- name: source
params:
KUBECONFIG_CONTENT: ((k8s.prod_kubeconfig))
NAMESPACE: production
run:
path: /bin/bash
args:
- -exc
- |
echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
kubectl -n $NAMESPACE apply -f source/k8s/
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
# Docker Image Build Pipeline
# Demonstrates container image building and pushing with Concourse
#
# Patterns covered:
# 1. oci-build-task for building images
# 2. Multi-stage builds
# 3. Semantic versioning with tags
# 4. Multi-architecture builds
#
# Usage:
# fly -t target set-pipeline -p docker-build -c docker-build.yml \
# -v git_uri=https://github.com/org/repo.git \
# -v registry_repo=registry.example.com/org/app
################################################
# Reusable YAML Anchors
################################################
git-source: &git-source
uri: ((git_uri))
branch: main
username: ((git.username))
password: ((git.token))
registry-source: ®istry-source
repository: ((registry_repo))
username: ((registry.username))
password: ((registry.password))
# Build log retention
log-retention: &log-retention
build_log_retention:
days: 14
builds: 50
minimum_succeeded_builds: 1
################################################
# Resources
################################################
resources:
# Source code
- name: source
type: git
icon: gitlab
check_every: 2m
source:
<<: *git-source
ignore_paths:
- "*.md"
- docs/**
# Base image for cache warming
- name: base-image
type: registry-image
icon: docker
check_every: 24h
source:
repository: node
tag: 20-alpine
# Release candidate image
- name: app-image-rc
type: registry-image
icon: docker
source:
<<: *registry-source
tag: rc
# Production image
- name: app-image
type: registry-image
icon: docker
source:
<<: *registry-source
# Version tracking
- name: version
type: semver
icon: tag
source:
driver: git
uri: ((git_uri))
branch: version
file: version
username: ((git.username))
password: ((git.token))
initial_version: 0.1.0
################################################
# Groups
################################################
groups:
- name: all
jobs: ["*"]
- name: build
jobs: [build-rc]
- name: release
jobs: [release-patch, release-minor, release-major]
################################################
# Jobs
################################################
jobs:
#-----------------------------------------
# Build Release Candidate
#-----------------------------------------
- name: build-rc
serial: true
plan:
- in_parallel:
- get: source
trigger: true
- get: base-image
params:
format: oci
- task: build-image
privileged: true
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
inputs:
- name: source
- name: base-image
outputs:
- name: image
params:
CONTEXT: source
DOCKERFILE: source/Dockerfile
# Build args
BUILD_ARGS_FILE: source/ci/build-args.txt
# Use base-image as cache source
IMAGE_ARG_base_image: base-image/image.tar
# BuildKit features
BUILDKIT_PROGRESS: plain
# Cache settings
CACHE: true
CACHE_TAG: cache
run:
path: build
- put: app-image-rc
params:
image: image/image.tar
get_params:
skip_download: true
#-----------------------------------------
# Release Patch Version (0.0.X)
#-----------------------------------------
- name: release-patch
serial: true
plan:
- in_parallel:
- get: source
passed: [build-rc]
- get: app-image-rc
passed: [build-rc]
params:
format: oci
- get: version
params:
bump: patch
- load_var: version-number
file: version/version
format: trim
- put: app-image
params:
image: app-image-rc/image.tar
version: version/version
bump_aliases: true
additional_tags: source/ci/tags/additional-tags
- put: version
params:
file: version/version
- put: source
params:
repository: source
only_tag: true
tag_prefix: v
tag: version/version
#-----------------------------------------
# Release Minor Version (0.X.0)
#-----------------------------------------
- name: release-minor
serial: true
plan:
- in_parallel:
- get: source
passed: [build-rc]
- get: app-image-rc
passed: [build-rc]
params:
format: oci
- get: version
params:
bump: minor
- put: app-image
params:
image: app-image-rc/image.tar
version: version/version
bump_aliases: true
- put: version
params:
file: version/version
- put: source
params:
repository: source
only_tag: true
tag_prefix: v
tag: version/version
#-----------------------------------------
# Release Major Version (X.0.0)
#-----------------------------------------
- name: release-major
serial: true
plan:
- in_parallel:
- get: source
passed: [build-rc]
- get: app-image-rc
passed: [build-rc]
params:
format: oci
- get: version
params:
bump: major
- put: app-image
params:
image: app-image-rc/image.tar
version: version/version
bump_aliases: true
- put: version
params:
file: version/version
- put: source
params:
repository: source
only_tag: true
tag_prefix: v
tag: version/version
---
# Example Dockerfile for the pipeline
# Store as Dockerfile in your repo root
# syntax=docker/dockerfile:1.4
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first (cache layer)
COPY package*.json ./
RUN npm ci --only=production
# Copy source and build
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
# Security: run as non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy built assets
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]
---
# Build args file example
# Store as ci/build-args.txt
NODE_ENV=production
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
---
# Additional tags file example
# Store as ci/tags/additional-tags
# Whitespace-separated tags to add
latest
stable
# Modern CI/CD Pipeline for Concourse v8.0+
# Demonstrates best practices: oci-build-task, across modifier, build_log_retention
#
# Usage:
# fly -t target set-pipeline -p my-app -c modern-ci-cd.yml \
# -l vars.yml
################################################
# Reusable YAML Anchors
################################################
git-source: &git-source
uri: ((git.uri))
username: ((git.username))
password: ((git.token))
registry-source: ®istry-source
repository: ((registry.repository))
username: ((registry.username))
password: ((registry.password))
# Modern notification anchor
notify-success: ¬ify-success
put: notify
params:
alert_type: success
notify-failure: ¬ify-failure
put: notify
params:
alert_type: failed
# Build log retention policy
log-retention: &log-retention
build_log_retention:
days: 14
builds: 50
minimum_succeeded_builds: 1
################################################
# Resource Types
################################################
resource_types:
# Modern Slack notifications with structured alerts
- name: slack-alert
type: registry-image
source:
repository: arbourd/concourse-slack-alert-resource
################################################
# Resources
################################################
resources:
# Source code - triggers on code changes only
- name: source
type: git
icon: gitlab
check_every: 2m
source:
<<: *git-source
branch: main
ignore_paths:
- "*.md"
- docs/**
- ci/**
# Source code for CI changes
- name: ci-config
type: git
icon: cog
check_every: 5m
source:
<<: *git-source
branch: main
paths:
- ci/**
# Container image (using modern registry-image)
- name: app-image
type: registry-image
icon: docker
source:
<<: *registry-source
# Notifications
- name: notify
type: slack-alert
icon: bell
source:
url: ((slack.webhook_url))
channel: "#ci-builds"
# Scheduled maintenance
- name: weekly
type: time
icon: clock-outline
source:
start: 3:00 AM
stop: 4:00 AM
location: UTC
days: [Sunday]
################################################
# Groups
################################################
groups:
- name: all
jobs: ["*"]
- name: build
jobs: [build-and-test]
- name: deploy
jobs: [deploy-*]
- name: maintenance
jobs: [update-dependencies]
################################################
# Jobs
################################################
jobs:
#-----------------------------------------
# Build and Test (Modern oci-build-task)
#-----------------------------------------
- name: build-and-test
<<: *log-retention
serial: true
plan:
- in_parallel:
- get: source
trigger: true
- get: ci-config
trigger: true
# Run tests first
- task: test
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
caches:
- path: source/node_modules
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm ci
npm test
# Build image using modern oci-build-task
- task: build-image
privileged: true
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
inputs:
- name: source
outputs:
- name: image
params:
CONTEXT: source
DOCKERFILE: source/Dockerfile
# Build arguments
BUILD_ARG_NODE_VERSION: "20"
BUILD_ARG_BUILD_DATE: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Multi-platform support (optional)
# IMAGE_PLATFORM: linux/amd64,linux/arm64
caches:
- path: cache
run:
path: build
# Push to registry
- put: app-image
params:
image: image/image.tar
additional_tags: source/.git/short_ref
get_params:
skip_download: true
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
#-----------------------------------------
# Multi-Environment Deploy (using across)
#-----------------------------------------
- name: deploy-all-envs
<<: *log-retention
serial: true
plan:
- in_parallel:
- get: source
passed: [build-and-test]
- get: app-image
passed: [build-and-test]
trigger: true
params:
skip_download: true
# Deploy to each environment sequentially
- task: deploy
across:
- var: env
values: [dev, staging]
max_in_flight: 1
config:
platform: linux
image_resource:
type: registry-image
source:
repository: bitnami/kubectl
tag: latest
inputs:
- name: source
params:
KUBECONFIG_CONTENT: ((k8s.((.:env))_kubeconfig))
NAMESPACE: ((.:env))
IMAGE_TAG: ((.:image_tag))
run:
path: /bin/bash
args:
- -exc
- |
echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
# Apply manifests
kubectl -n $NAMESPACE apply -f source/k8s/
# Wait for rollout
kubectl -n $NAMESPACE rollout status deployment/app --timeout=300s
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
#-----------------------------------------
# Production Deploy (Manual Gate)
#-----------------------------------------
- name: deploy-prod
<<: *log-retention
serial: true
plan:
- in_parallel:
- get: source
passed: [deploy-all-envs]
- get: app-image
passed: [deploy-all-envs]
# No trigger - manual promotion
params:
skip_download: true
- task: deploy-production
config:
platform: linux
image_resource:
type: registry-image
source:
repository: bitnami/kubectl
tag: latest
inputs:
- name: source
params:
KUBECONFIG_CONTENT: ((k8s.prod_kubeconfig))
NAMESPACE: production
run:
path: /bin/bash
args:
- -exc
- |
echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
kubectl -n $NAMESPACE apply -f source/k8s/
kubectl -n $NAMESPACE rollout status deployment/app --timeout=600s
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
#-----------------------------------------
# Scheduled Dependency Updates
#-----------------------------------------
- name: update-dependencies
<<: *log-retention
serial: true
plan:
- get: weekly
trigger: true
- get: source
- task: update
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
outputs:
- name: source-updated
params:
GIT_AUTHOR_EMAIL: ci@example.com
GIT_AUTHOR_NAME: "CI Bot"
run:
path: /bin/bash
args:
- -exc
- |
cp -r source/. source-updated/
cd source-updated
# Check for updates
npm outdated || true
# Update dependencies
npm update
# Commit if changes
if ! git diff --quiet package-lock.json; then
git config user.email "$GIT_AUTHOR_EMAIL"
git config user.name "$GIT_AUTHOR_NAME"
git add package.json package-lock.json
git commit -m "chore: update npm dependencies"
fi
- put: source
params:
repository: source-updated
rebase: true
on_failure:
<<: *notify-failure
# Multi-Branch Pipeline Management
# Dynamically creates/manages pipelines for feature branches
#
# Architecture:
# 1. branch-tracker: Monitors branches, creates/deletes pipelines
# 2. feature-pipeline.yml: Template for per-branch pipelines
#
# Usage:
# fly -t target set-pipeline -p branch-tracker -c multi-branch.yml \
# -v git_uri=https://github.com/org/repo.git
################################################
# Resource Types
################################################
resource_types:
- name: git-branches
type: registry-image
source:
repository: aoldershaw/git-branches-resource
################################################
# Resources
################################################
resources:
# Track all feature branches
- name: feature-branches
type: git-branches
icon: source-branch
check_every: 5m
source:
uri: ((git_uri))
branch_regex: "feature/.*"
private_key: ((git.private_key))
# Main repository for pipeline config
- name: ci-config
type: git
icon: gitlab
source:
uri: ((git_uri))
branch: main
paths:
- ci/**
private_key: ((git.private_key))
################################################
# Jobs
################################################
jobs:
#-----------------------------------------
# Branch Tracker - Creates pipelines for branches
#-----------------------------------------
- name: sync-branches
serial: true
plan:
- in_parallel:
- get: feature-branches
trigger: true
- get: ci-config
- load_var: branches
file: feature-branches/branches.json
# Create/update pipeline for each branch
- across:
- var: branch
values: ((.:branches))
set_pipeline: feature
file: ci-config/ci/feature-pipeline.yml
instance_vars:
branch: ((.:branch.name))
vars:
git_uri: ((git_uri))
branch_name: ((.:branch.name))
#-----------------------------------------
# Cleanup - Remove pipelines for deleted branches
#-----------------------------------------
- name: cleanup-pipelines
serial: true
plan:
- in_parallel:
- get: feature-branches
trigger: true
passed: [sync-branches]
- get: ci-config
- task: find-orphaned-pipelines
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/fly
tag: latest
inputs:
- name: feature-branches
outputs:
- name: orphaned
params:
CONCOURSE_URL: ((concourse.url))
CONCOURSE_USERNAME: ((concourse.username))
CONCOURSE_PASSWORD: ((concourse.password))
CONCOURSE_TEAM: ((concourse.team))
run:
path: /bin/bash
args:
- -exc
- |
fly -t main login \
-c "$CONCOURSE_URL" \
-u "$CONCOURSE_USERNAME" \
-p "$CONCOURSE_PASSWORD" \
-n "$CONCOURSE_TEAM"
# Get active branches
jq -r '.[].name' feature-branches/branches.json | sort > /tmp/active-branches.txt
# Get existing feature pipelines
fly -t main pipelines --json | \
jq -r '.[] | select(.name | startswith("feature:")) | .name | sub("feature:"; "")' | \
sort > /tmp/existing-pipelines.txt
# Find orphaned (pipeline exists but branch doesn't)
comm -23 /tmp/existing-pipelines.txt /tmp/active-branches.txt > orphaned/pipelines.txt
echo "Orphaned pipelines:"
cat orphaned/pipelines.txt
- task: destroy-orphaned-pipelines
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/fly
tag: latest
inputs:
- name: orphaned
params:
CONCOURSE_URL: ((concourse.url))
CONCOURSE_USERNAME: ((concourse.username))
CONCOURSE_PASSWORD: ((concourse.password))
CONCOURSE_TEAM: ((concourse.team))
run:
path: /bin/bash
args:
- -exc
- |
fly -t main login \
-c "$CONCOURSE_URL" \
-u "$CONCOURSE_USERNAME" \
-p "$CONCOURSE_PASSWORD" \
-n "$CONCOURSE_TEAM"
while read pipeline; do
if [ -n "$pipeline" ]; then
echo "Destroying pipeline: feature:$pipeline"
fly -t main destroy-pipeline -p "feature:$pipeline" -n || true
fi
done < orphaned/pipelines.txt
---
# Store this as ci/feature-pipeline.yml in your repo
# This is the template used for each feature branch
# Feature Branch Pipeline Template
# Automatically set by branch-tracker for each feature branch
################################################
# Resources
################################################
resources:
- name: source
type: git
icon: gitlab
check_every: 2m
source:
uri: ((git_uri))
branch: ((branch_name))
private_key: ((git.private_key))
################################################
# Jobs
################################################
jobs:
- name: build
plan:
- get: source
trigger: true
- task: build
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
caches:
- path: source/node_modules
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm ci
npm run build
- name: test
plan:
- get: source
passed: [build]
trigger: true
- in_parallel:
- task: unit-tests
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
caches:
- path: source/node_modules
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm ci
npm test
- task: lint
config:
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20-slim
inputs:
- name: source
caches:
- path: source/node_modules
run:
path: /bin/bash
args:
- -exc
- |
cd source
npm ci
npm run lint
- name: deploy-preview
plan:
- get: source
passed: [test]
trigger: true
- task: deploy-to-preview
config:
platform: linux
image_resource:
type: registry-image
source:
repository: alpine
tag: latest
inputs:
- name: source
params:
BRANCH: ((branch_name))
run:
path: /bin/sh
args:
- -exc
- |
# Deploy to preview environment
# Replace with actual deployment logic
PREVIEW_URL="https://${BRANCH//\//-}.preview.example.com"
echo "Deployed to: $PREVIEW_URL"
# Example vars.yml Template
# Use with: fly set-pipeline -l vars.yml
#
# This file demonstrates organizing external variables.
# Store actual secrets in a credential manager (Vault, SSM, etc.)
################################################
# Git Configuration
################################################
git:
uri: https://github.com/your-org/your-repo.git
username: ((vault:git/credentials.username))
token: ((vault:git/credentials.token))
################################################
# Container Registry
################################################
registry:
repository: registry.example.com/org/app
username: ((vault:registry/credentials.username))
password: ((vault:registry/credentials.password))
################################################
# Notification Webhooks
################################################
slack:
webhook_url: ((vault:slack/ci-notifications.webhook_url))
################################################
# Kubernetes Configurations (per environment)
################################################
k8s:
dev_kubeconfig: ((vault:k8s/dev.kubeconfig))
staging_kubeconfig: ((vault:k8s/staging.kubeconfig))
prod_kubeconfig: ((vault:k8s/prod.kubeconfig))
################################################
# Build Configuration
################################################
build:
node_version: "20"
image_platform: linux/amd64
################################################
# Notification Message Templates
################################################
# Use these with http-resource for custom webhooks
notification:
icon_success: "✅"
icon_failure: "❌"
icon_warning: "⚠️"
Concourse CI Best Practices and Troubleshooting
Optimization patterns, common pitfalls, and debugging strategies.
Pipeline Organization
Use YAML Anchors for DRY Configuration
# Top of pipeline: define reusable snippets
git-source: &git-source
username: ((gitlab.USER))
password: ((gitlab.ACCESS_TOKEN))
registry-source: ®istry-source
username: ((registry.USER))
password: ((registry.PASSWORD))
notify-failure: ¬ify-failure
put: slack
params:
text: '((SLACK_ICON_FAILURE)) $BUILD_PIPELINE_NAME/$BUILD_JOB_NAME failed'
notify-success: ¬ify-success
put: slack
params:
text: '((SLACK_ICON_SUCCESS)) $BUILD_PIPELINE_NAME/$BUILD_JOB_NAME succeeded'
# Use anchors in resources
resources:
- name: repo-main
type: git
source:
<<: *git-source
uri: https://git.example.com/org/repo.git
branch: main
- name: repo-staging
type: git
source:
<<: *git-source
uri: https://git.example.com/org/repo.git
branch: staging
# Use anchors in jobs
jobs:
- name: build
plan:
- get: repo-main
trigger: true
- task: build
file: repo-main/ci/tasks/build.yml
on_failure:
<<: *notify-failureGroup Jobs Logically
groups:
- name: all
jobs: ["*"]
- name: build
jobs:
- compile
- test
- package
- name: deploy
jobs:
- deploy-staging
- deploy-prod
- name: maintenance
jobs:
- update-dependencies
- cleanup-imagesSeparate Read and Write Resources
Avoid using the same resource for both tracking versions and pushing changes:
# BAD: Mixed read/write
resources:
- name: repo
type: git
source:
uri: https://github.com/org/repo
branch: main
tag_regex: "^v.*"
jobs:
- name: release
plan:
- get: repo
trigger: true
- task: bump-version
- put: repo # Creates version conflicts!
params:
repository: repo
tag: version/tag
# GOOD: Separate resources
resources:
- name: repo-read
type: git
source:
uri: https://github.com/org/repo
branch: main
tag_regex: "^v.*"
fetch_tags: true
clean_tags: true
- name: repo-write
type: git
source:
uri: https://github.com/org/repo
branch: main
fetch_tags: true
jobs:
- name: release
plan:
- get: repo-read
trigger: true
- task: bump-version
- put: repo-write
params:
repository: repo-read
tag: version/tag---
Git Resource Gotchas
Tag Detection After Force Push
Problem: Concourse stops detecting new tags after force-pushing a branch.
Root Causes:
1. Tags unreachable from branch: After force push, tags may point to commits no longer on the tracked branch 2. Version model conflict: Concourse's append-only version tracking conflicts with rewritten history 3. Cached tag state: Old tags cached in resource state
Diagnosis:
# Check if tag is reachable from branch
git fetch --tags origin
git branch -r --contains <tag_commit_sha> # Should show origin/main
# OR
git merge-base --is-ancestor <tag_commit_sha> origin/main # Should succeedSolutions:
# 1. Enable tag cleanup
resources:
- name: repo
type: git
source:
uri: https://github.com/org/repo
branch: main
tag_regex: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
fetch_tags: true
clean_tags: true # Critical: clears cached tags
# 2. Force resource check from specific ref
# fly -t target check-resource -r pipeline/repo --from ref:abc123Best Practice: Treat release branches and tags as immutable. Never force-push.
Regex Escaping
Problem: Unescaped dots match any character.
# BAD: . matches any character
tag_regex: "^v[0-9]+.[0-9]+.[0-9]+$" # Matches v1a2b3 too
# GOOD: Escape literal dots
tag_regex: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"Branch vs Tag Tracking
# Track branch commits
- name: repo-branch
type: git
source:
branch: main
# Track tags (no branch needed for triggering)
- name: repo-tags
type: git
source:
branch: main # Still needed for put operations
tag_regex: "^v.*"
# Tag filtering modes
tag_filter: "v*" # Bash glob (simple patterns)
tag_regex: "^v[0-9]" # Extended grep regex (complex patterns)Path Filtering Optimization
resources:
- name: app-source
type: git
source:
uri: https://github.com/org/repo
branch: main
# Only trigger on application code changes
paths:
- src/**
- lib/**
- package.json
- package-lock.json
# Ignore documentation and CI changes
ignore_paths:
- "**/*.md"
- docs/**
- ci/**
- .github/**---
Performance Optimization
Parallel Execution
# Parallel independent steps
- in_parallel:
limit: 5 # Control concurrency
fail_fast: true # Stop on first failure
steps:
- get: dependency-a
- get: dependency-b
- get: dependency-c
# Parallel tests
- in_parallel:
steps:
- task: unit-tests
- task: integration-tests
- task: e2e-testsTask Caching
# Cache dependencies between runs
platform: linux
image_resource:
type: registry-image
source:
repository: node
tag: 20
caches:
- path: source/node_modules
- path: source/.npm
run:
path: /bin/bash
args:
- -c
- |
cd source
npm ci # Uses cache if available
npm run buildShallow Clones
# For builds that don't need git history
- get: source
params:
depth: 1Resource Check Intervals
# Reduce load for stable resources
resources:
- name: base-image
type: registry-image
check_every: 24h # Daily check
source:
repository: node
tag: 20-alpine
- name: source-code
type: git
check_every: 1m # Frequent check for active development
source:
uri: https://github.com/org/repoSerial Groups
# Prevent resource contention
jobs:
- name: deploy-staging
serial_groups: [deploy]
plan:
- get: app-image
- task: deploy
- name: deploy-prod
serial_groups: [deploy]
plan:
- get: app-image
- task: deploy---
Security Best Practices
Credential Management
# Use var sources (Vault, SSM, etc.)
var_sources:
- name: vault
type: vault
config:
url: https://vault.example.com
path_prefix: /concourse/main
# Reference credentials
resources:
- name: repo
type: git
source:
username: ((vault:git.username))
password: ((vault:git.token))Minimize Privileged Tasks
# Only use privileged when absolutely necessary
- task: docker-build
privileged: true # Required for Docker-in-Docker
file: source/ci/tasks/build-image.yml
# Prefer oci-build-task over Docker-in-Docker
- task: build-image
privileged: true # Still needed but more secure
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
inputs:
- name: source
outputs:
- name: image
run:
path: buildResource Visibility
# Keep sensitive resources private
resources:
- name: credentials
type: git
public: false # Default, but be explicit
source:
uri: git@github.com:org/secrets.git
# Only expose what's necessary
- name: public-docs
type: git
public: true
source:
uri: https://github.com/org/docs.git---
Debugging Strategies
Hijack into Containers
# Hijack into a running or failed build
fly -t target hijack -j pipeline/job -b 123
# Hijack specific step
fly -t target hijack -j pipeline/job -s task-name
# List hijack targets
fly -t target hijack -j pipeline/job --listCheck Resource Versions
# List versions
fly -t target resource-versions -r pipeline/resource
# Force check
fly -t target check-resource -r pipeline/resource
# Check from specific version
fly -t target check-resource -r pipeline/resource --from ref:abc123Watch Build Logs
# Stream live logs
fly -t target watch -j pipeline/job
# Specific build
fly -t target watch -j pipeline/job -b 123Validate Pipeline
# Syntax check
fly -t target validate-pipeline -c pipeline.yml
# With variables
fly -t target validate-pipeline -c pipeline.yml -l vars.ymlDebug Task Locally
# Execute task with local inputs
fly -t target execute -c ci/tasks/build.yml \
-i source=. \
-o artifacts=./out
# Include ignored files (e.g., .gitignore'd)
fly -t target execute --include-ignored -c ci/tasks/build.yml -i source=.---
Common Patterns
Build-Test-Release with Gates
jobs:
- name: build
plan:
- get: source
trigger: true
- task: compile
file: source/ci/tasks/compile.yml
- put: artifact-rc
params:
file: build/app-*.tar.gz
- name: test
plan:
- get: artifact-rc
passed: [build]
trigger: true
- get: source
passed: [build]
- task: integration-test
file: source/ci/tasks/test.yml
- name: release
plan:
- get: artifact-rc
passed: [test]
trigger: true
- get: version
params:
bump: minor
- put: artifact-release
params:
file: artifact-rc/app-*.tar.gz
tag: version/version
- put: version
params:
file: version/versionManual Deployment Gate
- name: deploy-prod
plan:
- get: app-image
passed: [deploy-staging]
# No trigger: true - requires manual click
- task: deploy
file: source/ci/tasks/deploy.yml
params:
ENVIRONMENT: productionScheduled Jobs
resources:
- name: nightly
type: time
source:
start: 2:00 AM
stop: 3:00 AM
location: America/New_York
- name: weekday-morning
type: time
source:
start: 9:00 AM
stop: 9:30 AM
location: Europe/Berlin
days: [Monday, Tuesday, Wednesday, Thursday, Friday]
jobs:
- name: nightly-cleanup
plan:
- get: nightly
trigger: true
- task: cleanup
file: ci/tasks/cleanup.yml
- name: weekday-update
plan:
- get: weekday-morning
trigger: true
- get: source
- task: update-dependencies
file: source/ci/tasks/update.ymlMulti-Environment Deploy with across
Modern approach using the across step modifier:
jobs:
- name: deploy
plan:
- get: app-image
trigger: true
- get: source
- task: deploy
across:
- var: env
values: [dev, staging, prod]
max_in_flight: 1 # Sequential deployment
file: source/ci/tasks/deploy.yml
params:
ENVIRONMENT: ((.:env))
CONFIG: source/config/((.:env)).ymlEnvironment-Specific Resources (Traditional Pattern)
When across isn't suitable, use separate resources per environment:
# Define anchor for common settings
git-source: &git-source
uri: https://github.com/org/repo
username: ((git.username))
password: ((git.password))
resources:
- name: repo-staging
type: git
source:
<<: *git-source
branch: staging
- name: repo-prod
type: git
source:
<<: *git-source
branch: prod
- name: image-staging
type: registry-image
source:
repository: registry.example.com/org/app
tag: staging
username: ((registry.user))
password: ((registry.pass))
- name: image-prod
type: registry-image
source:
repository: registry.example.com/org/app
tag: prod
username: ((registry.user))
password: ((registry.pass))---
Notification Patterns
Modern: Dedicated Notification Resources
Use specialized resources for better formatting and features:
Slack (Recommended: arbourd/concourse-slack-alert-resource)
resource_types:
- name: slack-alert
type: registry-image
source:
repository: arbourd/concourse-slack-alert-resource
resources:
- name: notify
type: slack-alert
source:
url: ((slack.webhook_url))
channel: "#builds"
jobs:
- name: build
plan:
- get: source
trigger: true
- task: build
file: source/ci/tasks/build.yml
on_success:
put: notify
params:
alert_type: success
on_failure:
put: notify
params:
alert_type: failedMicrosoft Teams
resource_types:
- name: teams-notification
type: registry-image
source:
repository: navicore/teams-notification-resource
resources:
- name: teams
type: teams-notification
source:
url: ((teams.webhook_url))
jobs:
- name: deploy
on_failure:
put: teams
params:
text: "Deploy failed: $BUILD_PIPELINE_NAME/$BUILD_JOB_NAME"
color: "FF0000"Generic: HTTP Resource for Custom Webhooks
For Matrix, Element, Discord, or custom endpoints:
resource_types:
- name: http-resource
type: registry-image
source:
repository: jgriff/http-resource
resources:
- name: webhook
type: http-resource
source:
url: https://hooks.example.com/notify
headers:
Content-Type: application/json
Authorization: Bearer ((webhook.token))
out_only: true # No check/get operations
sensitive: true # Hide response in logs
build_metadata: [headers, body] # Enable CI variable resolution
# Usage with CI metadata variables
- put: webhook
params:
body: |
{
"pipeline": "$BUILD_PIPELINE_NAME",
"job": "$BUILD_JOB_NAME",
"build": "$BUILD_NAME",
"url": "$ATC_EXTERNAL_URL/builds/$BUILD_ID",
"status": "failed"
}Notification Anchor Pattern
DRY notification configuration:
# Top of pipeline
notify-success: ¬ify-success
put: notify
params:
alert_type: success
notify-failure: ¬ify-failure
put: notify
params:
alert_type: failed
jobs:
- name: build
plan:
- get: source
- task: build
file: source/ci/tasks/build.yml
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure
- name: deploy
plan:
- get: source
passed: [build]
- task: deploy
file: source/ci/tasks/deploy.yml
on_success:
<<: *notify-success
on_failure:
<<: *notify-failure---
Deployment Patterns
Ansible Playbook Execution
For infrastructure provisioning with Ansible:
resource_types:
- name: ansible-playbook
type: registry-image
source:
repository: troykinsella/concourse-ansible-playbook-resource
tag: latest
resources:
- name: ansible-deploy
type: ansible-playbook
source:
ssh_private_key: ((ssh.private_key))
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
SSH_USER: ((ssh.user))
jobs:
- name: provision
plan:
- get: infrastructure-repo
trigger: true
- put: ansible-deploy
params:
path: infrastructure-repo/ansible
playbook: playbooks/provision.yml
inventory: inventory/hosts
limit: production # Target host group
extra_vars:
app_version: "1.2.3"Task-Based Ansible (Alternative)
For simpler setups without the resource type:
- task: ansible-deploy
config:
platform: linux
image_resource:
type: registry-image
source:
repository: cytopia/ansible
tag: latest
inputs:
- name: source
params:
ANSIBLE_HOST_KEY_CHECKING: "false"
SSH_PRIVATE_KEY: ((ssh.private_key))
run:
path: /bin/sh
args:
- -c
- |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
cd source/ansible
ansible-playbook -i inventory/hosts playbook.ymlCross-Repository Pipeline Triggers
Trigger downstream pipelines by pushing to other repositories:
- task: trigger-downstream
config:
platform: linux
image_resource:
type: registry-image
source:
repository: alpine/git
inputs:
- name: source
params:
GIT_USER: ((git.username))
GIT_TOKEN: ((git.token))
DOWNSTREAM_REPO: https://github.com/org/downstream.git
run:
path: /bin/sh
args:
- -c
- |
VERSION=$(cat source/version)
git clone https://${GIT_USER}:${GIT_TOKEN}@${DOWNSTREAM_REPO#https://} downstream
cd downstream
echo "$VERSION" > app-version
git config user.email "ci@example.com"
git config user.name "CI Bot"
git add app-version
git commit -m "Update app version to $VERSION"
git push origin main---
Troubleshooting Checklist
Pipeline Not Triggering
1. Check resource is not paused: fly -t target unpause-resource -r pipeline/resource 2. Verify trigger: true on get step 3. Check resource versions: fly -t target resource-versions -r pipeline/resource 4. Force resource check: fly -t target check-resource -r pipeline/resource 5. Verify path filters aren't excluding changes
Build Failing Silently
1. Check ensure steps for errors masking failures 2. Review try steps that swallow failures 3. Check container limits (OOM kills) 4. Hijack and inspect logs/state
Credentials Not Working
1. Verify var source configuration 2. Check credential path/field names 3. Test credentials outside Concourse 4. Check var source connectivity from workers
Resource Check Hanging
1. Increase check_timeout 2. Check network connectivity from workers 3. Verify worker tags match resource tags 4. Check for rate limiting on external services
registry_mirror Errors
Error: json: cannot unmarshal string into Go struct field Source.source.registry_mirror of type resource.RegistryMirror
- Cause:
CONCOURSE_BASE_RESOURCE_TYPE_DEFAULTSpassesregistry_mirroras a plain string, butregistry-imageresource expects an object - Fix: Change config to
registry_mirror: { host: "hostname" }forregistry-imageentries - Note:
docker-imagestill needs the plain string format — provide both in defaults config
Error: registries must be valid RFC 3986 URI authorities: https://mirror.example.com
- Cause:
registry_mirror.hostincludes URL scheme (https://) - Fix: Strip scheme — host must be bare hostname only (e.g.,
mirror.example.com)
Error: Failed to obtain registry token (from CI tasks checking registry)
- Cause: JWT auth URL hardcoded to registry host, but GitLab Container Registry auth endpoint is on the GitLab host
- Fix: Discover auth realm dynamically from registry's
Www-Authenticateheader on/v2/ - Example:
registry.example.commay have auth atgit.example.com/jwt/auth
See resources-guide.md for detailed format reference and Ansible template patterns.
Core Concepts Reference
Step Types
| Step | Purpose |
|---|---|
get | Fetch resource version |
put | Update/push resource |
task | Execute containerized work |
set_pipeline | Dynamic pipeline config |
in_parallel | Concurrent execution |
do | Sequential execution (all steps in order) |
try | Continue on failure (wraps a step) |
load_var | Load value into a local var from file or literal |
Job Lifecycle Hooks
| Hook | Triggers When |
|---|---|
on_success | Step/job succeeds |
on_failure | Non-zero exit (task failure) |
on_error | Infrastructure error (OOM, timeout) |
on_abort | Build manually aborted |
ensure | Always runs regardless of outcome |
Important: on_failure (exit code 1) is different from on_error (container crash). Handle both.
fly CLI Essentials
fly -t target set-pipeline -p pipeline-name -c pipeline.yml -l vars.yml
fly -t target check-resource -r pipeline/resource-name
fly -t target trigger-job -j pipeline/job-name -w
fly -t target hijack -j pipeline/job-name -s step-name
fly -t target validate-pipeline -c pipeline.yml
fly -t target execute -c task.yml -i source=. # Run task locally
fly -t target execute --include-ignored -c task.yml -i source=.Concourse CI Pipeline Syntax Reference
Complete YAML schema reference for Concourse CI pipelines.
Pipeline Root Schema
# Required
jobs: [] # At least one job required
# Optional
resources: [] # External versioned artifacts
resource_types: [] # Custom resource type definitions
var_sources: [] # Variable sources (Vault, SSM, etc.)
groups: [] # UI organization
display: # Visual customization
background_image: ""
background_filter: "opacity(30%) grayscale(100%)"Resource Schema
resources:
- name: resource-name # Required: identifier
type: git # Required: resource type
source: {} # Required: type-specific config
# Optional
old_name: previous-name # Rename while preserving history
icon: gitlab # Material Design icon name
version: { ref: abc123 } # Pin specific version
check_every: 1m # Check interval (default: 1m)
check_timeout: 1h # Check timeout (default: 1h)
tags: [private-network] # Worker selection tags
public: false # Expose metadata publicly
webhook_token: secret # Webhook trigger token
expose_build_created_by: falseResource Type Schema
resource_types:
- name: slack-notification # Required: identifier
type: registry-image # Required: image source type
source: # Required: image location
repository: cfcommunity/slack-notification-resource
tag: latest
# Optional
privileged: false # Run with full capabilities
params: {} # Default get params
check_every: 1m # Version check interval
tags: [] # Worker selection tags
defaults: {} # Default source configJob Schema
jobs:
- name: job-name # Required: identifier
plan: [] # Required: steps to execute (alias: steps)
# Optional
old_name: previous-name # Rename preserving history
serial: false # Sequential execution only
serial_groups: [] # Serialize jobs sharing groups
max_in_flight: 1 # Max concurrent builds
public: false # Public build logs
disable_manual_trigger: false
interruptible: false # Allow worker shutdown
# Build log retention
build_log_retention:
days: 30 # Keep builds from last N days
builds: 100 # Keep last N builds
minimum_succeeded_builds: 1
# Lifecycle hooks
on_success: { step } # Run on success
on_failure: { step } # Run on failure
on_error: { step } # Run on error
on_abort: { step } # Run on abort
ensure: { step } # Always runStep Types
Get Step
- get: resource-name # Resource to fetch
# Optional
resource: actual-resource # Override resource name
version: latest # Version selection: latest, every, { ref: x }
passed: [job1, job2] # Only versions passing these jobs
params: {} # Resource-specific get params
trigger: false # Auto-trigger on new versions
tags: [] # Worker selection
timeout: 1h # Step timeout
attempts: 1 # Retry count
# Hooks
on_success: { step }
on_failure: { step }
on_error: { step }
on_abort: { step }
ensure: { step }Put Step
- put: resource-name # Resource to update
# Optional
resource: actual-resource # Override resource name
inputs: detect # Input artifacts: detect, all, [list]
params: {} # Resource-specific put params
get_params: {} # Implicit get params
no_get: false # Skip implicit get after put
tags: [] # Worker selection
timeout: 1h
attempts: 1
# Hooks
on_success: { step }
on_failure: { step }
on_error: { step }
on_abort: { step }
ensure: { step }Task Step
- task: task-name # Required: task identifier
# Config source (choose one)
config: { task-config } # Inline configuration
file: path/to/task.yml # File from input artifact
# Optional
image: input-name # Use input artifact as image
privileged: false # Run as root
vars: {} # Static variables for config
params: {} # Environment variables
input_mapping: # Rename inputs
task-input: get-name
output_mapping: # Rename outputs
task-output: result
tags: []
timeout: 1h
attempts: 1
container_limits:
cpu: 1024 # CPU shares
memory: 1073741824 # Memory in bytes
# Hooks
on_success: { step }
on_failure: { step }
on_error: { step }
on_abort: { step }
ensure: { step }Task Configuration Schema
platform: linux # Required: linux, darwin, windows
# Image (required for linux)
image_resource:
type: registry-image
source:
repository: alpine
tag: latest
params: {}
version: {}
# Private registry example
image_resource:
type: registry-image
source:
repository: registry.example.com/org/build-tools
username: ((registry.user))
password: ((registry.pass))
tag: "1.2.3"
# OR use docker-image type (legacy)
image_resource:
type: docker-image
source:
repository: registry.example.com/org/build-tools
username: ((registry.user))
password: ((registry.pass))
# OR use rootfs_uri for pre-uploaded images
rootfs_uri: /path/to/rootfs
inputs:
- name: input-name # Required
path: custom-path # Optional: override directory name
optional: false # Allow missing input
outputs:
- name: output-name
path: custom-path
caches:
- path: node_modules # Persistent across runs
params:
ENV_VAR: value # Environment variables
SECRET: ((vault:secret)) # From credential manager
run:
path: /bin/bash # Required: executable
args: [-c, "echo hello"] # Command arguments
dir: input-name # Working directory
user: root # Execution user
container_limits:
cpu: 1024
memory: 1073741824Set Pipeline Step
- set_pipeline: pipeline-name
file: config/pipeline.yml # Required: config file
# Optional
vars: {} # Variables to pass
var_files: [] # Variable files
instance_vars: {} # Instance pipeline variables
team: other-team # Target team (default: current)Load Var Step
- load_var: var-name
file: path/to/file # Required: file with value
# Optional
format: json # json, yaml, trim, raw
reveal: false # Show in UI (false redacts)In Parallel Step
- in_parallel:
steps: # Required: steps to parallelize
- get: resource-a
- get: resource-b
- task: parallel-work
# Optional
limit: 3 # Max concurrent steps
fail_fast: false # Abort remaining on first failureDo Step
- do: # Sequential step sequence
- get: resource
- task: step1
- task: step2Try Step
- try: # Continue regardless of outcome
task: optional-stepAcross Step Modifier
- task: deploy
across:
- var: region
values: [us-east, us-west, eu-west]
- var: env
values: [staging, prod]
max_in_flight: 1 # Serial across this dimension
file: ci/tasks/deploy.yml
vars:
region: ((.:region))
environment: ((.:env))Step Modifiers
Apply to any step:
- task: example
timeout: 30m # Step timeout
attempts: 3 # Retry on failure
tags: [specialized] # Worker selection
# Hooks run after step
on_success: { put: notify }
on_failure: { put: alert }
on_error: { put: page }
on_abort: { put: cleanup }
ensure: { task: always-run }Groups Schema
groups:
- name: group-name # Required
jobs: # Required: job references
- job-name
- deploy-* # Glob patterns
- terraform-{dev,prod} # Brace expansionVar Sources Schema
var_sources:
- name: vault
type: vault
config:
url: https://vault.example.com
path_prefix: /concourse
auth_backend: token
auth_params:
token: ((VAULT_TOKEN))
- name: ssm
type: ssm
config:
region: us-east-1
- name: secrets-manager
type: secretsmanager
config:
region: us-east-1Display Schema
display:
background_image: https://example.com/bg.png
background_filter: "blur(5px) brightness(0.5)"Variable Syntax Reference
# Basic var
((var-name))
# Var with source
((source:path))
# Var with field
((source:path.field))
# Local var (from load_var)
((.:local-var))
# Nested field access
((vault:secret/app.data.password))YAML Anchor Patterns
# Define anchor
common-config: &common
username: ((git.user))
password: ((git.pass))
# Reference anchor
resources:
- name: repo
source:
<<: *common
uri: https://github.com/org/repo
# Anchor with override
- name: other-repo
source:
<<: *common
uri: https://github.com/org/other
branch: develop # Override common settingIdentifier Rules
Pipeline, job, resource, and resource type names must:
- Start with a lowercase letter
- Contain only lowercase letters, numbers, hyphens, periods, underscores
- Cannot be purely numeric
- Cannot contain consecutive special characters
Valid: my-pipeline, build_v2, deploy.prod Invalid: My-Pipeline, 123, build--test
Concourse CI Resource Types Catalog
Comprehensive list of available resource types organized by category.
Core Resources (Bundled with Concourse)
| Resource | Repository | Description |
|---|---|---|
git | concourse/git-resource | Track commits in a Git repository branch |
registry-image | concourse/registry-image-resource | OCI/Docker images in container registries |
s3 | concourse/s3-resource | AWS S3 and compatible object storage |
time | concourse/time-resource | Trigger on time intervals |
pool | concourse/pool-resource | Manage locks and shared state via Git |
semver | concourse/semver-resource | Semantic version management |
github-release | concourse/github-release-resource | GitHub release artifacts |
---
Version Control
Git
| Resource | Repository | Use Case |
|---|---|---|
git | concourse/git-resource | Standard Git operations |
git-branches | aoldershaw/git-branches-resource | Track branch creation/deletion |
bitbucket-pr | zarplata/concourse-bitbucket-pullrequest-resource | Bitbucket pull requests |
github-pr | teliaoss/github-pr-resource | GitHub pull requests |
gitlab-mr | swisscom/gitlab-merge-request-resource | GitLab merge requests |
gerrit | google/concourse-resources/gerrit | Gerrit code review |
Example: Git Branches Resource
resource_types:
- name: git-branches
type: registry-image
source:
repository: aoldershaw/git-branches-resource
resources:
- name: feature-branches
type: git-branches
source:
uri: https://github.com/org/repo
branch_regex: "feature/.*"
private_key: ((git.private_key))---
Container Images
| Resource | Repository | Use Case |
|---|---|---|
registry-image | concourse/registry-image-resource | Modern OCI image handling |
docker-image | concourse/docker-image-resource | Legacy Docker build/push |
registry-tag | tlwr/registry-tag-resource | Track tags without pulling images |
harbor-resource | pivotalservices/concourse-harbor-resource | Harbor registry integration |
Example: Registry Tag Resource
resource_types:
- name: registry-tag
type: registry-image
source:
repository: ghcr.io/tlwr/registry-tag-resource
resources:
- name: base-image-tags
type: registry-tag
source:
repository: node
tag_regex: "^20-.*"---
Cloud Providers
AWS
| Resource | Repository | Use Case |
|---|---|---|
s3 | concourse/s3-resource | S3 object storage |
ssm | (var source) | AWS Systems Manager parameters |
secretsmanager | (var source) | AWS Secrets Manager |
ecr | Use registry-image with aws_* params | Elastic Container Registry |
cloudformation | ljfranklin/cloudformation-resource | CloudFormation stacks |
lambda | starkandwayne/lambda-resource | Lambda function deployment |
Google Cloud
| Resource | Repository | Use Case |
|---|---|---|
gcs | frodenas/gcs-resource | Google Cloud Storage |
gke | google/concourse-resources/gke | GKE cluster operations |
gcr | Use registry-image | Google Container Registry |
Azure
| Resource | Repository | Use Case |
|---|---|---|
azure-blobstore | pivotal-cf/azure-blobstore-resource | Azure Blob Storage |
acr | Use registry-image | Azure Container Registry |
---
Kubernetes & Infrastructure
| Resource | Repository | Use Case |
|---|---|---|
kubernetes | zlabjp/kubernetes-resource | kubectl operations |
helm | linkerd/helm-chart-resource | Helm chart management |
helm3 | typositoire/concourse-helm3-resource | Helm 3 deployments |
terraform | ljfranklin/terraform-resource | Terraform operations |
pulumi | ringods/pulumi-resource | Pulumi deployments |
cf | concourse/cf-resource | Cloud Foundry apps |
bosh-deployment | cloudfoundry/bosh-deployment-resource | BOSH deployments |
ansible-playbook | troykinsella/concourse-ansible-playbook-resource | Ansible deployments |
Example: Ansible Playbook Resource
resource_types:
- name: ansible-playbook
type: registry-image
source:
repository: troykinsella/concourse-ansible-playbook-resource
tag: latest
resources:
- name: deploy-playbook
type: ansible-playbook
source:
ssh_private_key: ((ssh.private_key))
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
APP_USER: ((app.user))
APP_PASSWORD: ((app.password))
jobs:
- name: deploy
plan:
- get: source
trigger: true
passed: [build]
- put: deploy-playbook
params:
path: source/ansible
playbook: playbooks/deploy.yml
inventory: inventory/hosts
limit: production # Target specific host group
tags: # Run only tagged tasks
- deploy
- configure
extra_vars:
app_version: "1.2.3"
setup_commands: # Run before playbook
- "pip install boto3"Example: Terraform Resource
resource_types:
- name: terraform
type: registry-image
source:
repository: ljfranklin/terraform-resource
resources:
- name: infrastructure
type: terraform
source:
env_name: production
backend_type: s3
backend_config:
bucket: terraform-state
key: infra/terraform.tfstate
region: us-east-1
jobs:
- name: provision
plan:
- get: infra-repo
trigger: true
- put: infrastructure
params:
terraform_source: infra-repo/terraform
vars:
instance_type: t3.medium
environment: production---
Notifications
| Resource | Repository | Use Case |
|---|---|---|
slack-notification | cfcommunity/slack-notification-resource | Slack webhooks |
slack-alert | arbourd/concourse-slack-alert-resource | Formatted Slack alerts |
teams-notification | navicore/teams-notification-resource | Microsoft Teams |
email | mdomke/concourse-email-resource | Email notifications |
http-resource | jgriff/http-resource | Generic HTTP/webhook |
Example: Slack Notification
resource_types:
- name: slack-notification
type: registry-image
source:
repository: cfcommunity/slack-notification-resource
resources:
- name: slack
type: slack-notification
source:
url: ((slack.webhook_url))
jobs:
- name: build
plan:
- get: source
trigger: true
- task: build
file: source/ci/tasks/build.yml
on_success:
put: slack
params:
text: ":white_check_mark: Build succeeded!"
channel: "#ci-notifications"
on_failure:
put: slack
params:
text: ":x: Build failed!"
channel: "#ci-notifications"---
Artifact Management
| Resource | Repository | Use Case |
|---|---|---|
artifactory | pivotalservices/artifactory-resource | JFrog Artifactory |
maven | pivotalservices/maven-resource | Maven repositories |
npm | idahobean/npm-resource | NPM packages |
pypi | cfmobile/pypi-resource | Python packages |
rubygems | troykinsella/concourse-rubygems-resource | Ruby gems |
github-release | concourse/github-release-resource | GitHub releases |
---
Databases
| Resource | Repository | Use Case |
|---|---|---|
pool | concourse/pool-resource | Database/resource locking |
postgres | (via scripts) | PostgreSQL operations |
flyway | troykinsella/concourse-flyway-resource | Flyway migrations |
---
Monitoring & Metrics
| Resource | Repository | Use Case |
|---|---|---|
datadog-event | concourse/datadog-event-resource | Datadog events |
prometheus-alertmanager | (community) | Alert management |
cogito | Pix4D/cogito | GitHub commit status |
Example: GitHub Commit Status
resource_types:
- name: cogito
type: registry-image
source:
repository: pix4d/cogito
resources:
- name: commit-status
type: cogito
check_every: never
source:
owner: org
repo: repo
access_token: ((github.token))
jobs:
- name: build
plan:
- get: source
trigger: true
- put: commit-status
params:
state: pending
context: build
- task: build
file: source/ci/tasks/build.yml
on_success:
put: commit-status
params:
state: success
context: build
on_failure:
put: commit-status
params:
state: failure
context: build---
Build Tools
| Resource | Repository | Use Case |
|---|---|---|
oci-build-task | concourse/oci-build-task | Container image building |
builder-task | concourse/builder-task | Alternative image builder |
Example: OCI Build Task
jobs:
- name: build-image
plan:
- get: source
trigger: true
- task: build
privileged: true
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
inputs:
- name: source
outputs:
- name: image
params:
CONTEXT: source
DOCKERFILE: source/Dockerfile
run:
path: build
- put: app-image
params:
image: image/image.tar---
RSS & Feeds
| Resource | Repository | Use Case |
|---|---|---|
rss | suhlig/concourse-rss-resource | RSS feed monitoring |
feed | (community) | Generic feed parsing |
---
Custom Resource Development
Create custom resources by implementing three scripts:
/opt/resource/check # Detect versions
/opt/resource/in # Fetch version
/opt/resource/out # Update/pushMinimal Custom Resource
FROM alpine:latest
RUN apk add --no-cache bash jq curl
COPY check /opt/resource/check
COPY in /opt/resource/in
COPY out /opt/resource/out
RUN chmod +x /opt/resource/*Pipeline Registration
resource_types:
- name: custom-resource
type: registry-image
source:
repository: myregistry/custom-resource
tag: latest
resources:
- name: my-custom
type: custom-resource
source:
config_option: value---
Resource Discovery
Find more resources at:
Concourse CI Resources Configuration Guide
Detailed configuration reference for commonly used Concourse CI resources.
Git Resource (concourse/git-resource)
Tracks commits in a Git repository branch or by tags.
Source Configuration
resources:
- name: source-repo
type: git
source:
# Required
uri: https://github.com/org/repo.git # Repository URL
# Authentication (choose method)
# HTTPS with username/password
username: ((git.username))
password: ((git.token))
# SSH with private key
private_key: ((git.private_key))
private_key_user: git # SSH config User
private_key_passphrase: ((passphrase)) # If key is encrypted
# Branch tracking (optional, defaults to repo default branch)
branch: main
# Tag tracking (choose one, mutually exclusive with branch for triggers)
tag_filter: "v*" # Bash glob pattern
tag_regex: "^v[0-9]+\\.[0-9]+\\.[0-9]+$" # Extended grep regex
# Path filtering (trigger only on changes to specific files)
paths:
- src/**
- lib/**
ignore_paths:
- "*.md"
- tests/**
- ci/**
# Sparse checkout (only fetch specific paths)
sparse_paths:
- src
- lib
# Tag behavior options
fetch_tags: true # Fetch all tags
clean_tags: true # Delete cached tags before fetch
tag_behaviour: match_tagged # or match_tag_ancestors
# Security
skip_ssl_verification: false
commit_verification_keys: # GPG keys for signature verification
- |
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----
# Git-crypt support
git_crypt_key: ((git-crypt-key-base64))
# Proxy configuration
https_tunnel:
proxy_host: proxy.example.com
proxy_port: 8080
proxy_user: ((proxy.user))
proxy_password: ((proxy.pass))
# Advanced options
disable_ci_skip: false # Process [ci skip] commits
version_depth: 100 # Versions returned in check
search_remote_refs: false # Search remote refs (Gerrit)
# Commit filtering
commit_filter:
exclude:
- "\\[skip ci\\]"
- "Merge pull request"
include:
- "\\[deploy\\]"
# Git config
git_config:
- name: core.autocrlf
value: input
# Submodule credentials
submodule_credentials:
- host: github.com
username: ((github.user))
password: ((github.token))Get Parameters
- get: source-repo
params:
depth: 1 # Shallow clone depth
fetch_tags: true # Override source setting
clean_tags: true # Delete tags before checkout
submodules: all # none, all, or [list]
submodule_recursive: true # Recursive submodule checkout
submodule_remote: true # Checkout for remote branch
disable_git_lfs: false # Skip LFS files
all_branches: false # Fetch all branches
# Output formatting
short_ref_format: "%s" # Printf format for short_ref
timestamp_format: iso8601 # Commit timestamp format
describe_ref_options: "--always --dirty"Put Parameters
- put: source-repo
params:
repository: modified-repo # Required: path to repo
# Branching
branch: release # Target branch (default: source)
refs_prefix: refs/heads # Reference prefix
# Tagging
tag: version/tag-file # File containing tag name
tag_prefix: "v" # Prepend to tag
only_tag: true # Push only tags, not commits
annotate: version/annotation-file # Annotated tag message
# Push behavior
force: false # Force push
rebase: false # Rebase on conflict
rebase_strategy: recursive # ort, octopus, ours, subtree
rebase_strategy_option: theirs # -X option
merge: false # Merge on conflict
returning: merged # merged or unmerged (with merge)
# Notes
notes: notes/note-file # Git notes fileMetadata Files (after get)
.git/ref # Full commit SHA
.git/short_ref # Short SHA (configurable)
.git/commit_message # Commit message
.git/author # Author name
.git/author_date # Author date
.git/committer # Committer name
.git/committer_date # Committer date
.git/branch # Branch name
.git/tags # Space-separated tags
.git/describe_ref # Git describe output
.git/metadata.json # JSON with all metadata---
Registry Image Resource (concourse/registry-image-resource)
Tracks OCI/Docker images in container registries.
Source Configuration
resources:
- name: app-image
type: registry-image
source:
# Required
repository: registry.example.com/org/app
# Tag tracking (choose one mode)
# 1. Single tag tracking
tag: latest # Default: latest
# 2. Regex-based tag tracking
tag_regex: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
created_at_sort: true # Sort by creation time
# 3. Semver auto-detection (no tag/tag_regex)
variant: alpine # Filter by suffix (1.2.3-alpine)
semver_constraint: "~1.2.x" # Semver range
pre_releases: false # Include prereleases
pre_release_prefixes: [alpha, beta, rc]
# Authentication
username: ((registry.username))
password: ((registry.password))
# AWS ECR authentication
aws_access_key_id: ((aws.key_id))
aws_secret_access_key: ((aws.secret))
aws_session_token: ((aws.token))
aws_region: us-east-1
aws_role_arn: arn:aws:iam::123:role/ecr-role
aws_role_arns: # Role chain
- arn:aws:iam::123:role/first
- arn:aws:iam::456:role/second
aws_account_id: "123456789" # For ECR
# Platform selection (multi-arch images)
platform:
architecture: amd64 # amd64, arm64, etc.
os: linux # linux, windows
# Security
insecure: false # Allow insecure registries
ca_certs: # Custom CA certificates
- |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
# Docker Content Trust
content_trust:
server: https://notary.example.com
repository_key_id: abc123
repository_key: ((notary.key))
repository_passphrase: ((notary.pass))
tls_key: ((tls.key))
tls_cert: ((tls.cert))
# Fallback registry mirror
# ⚠️ CRITICAL: registry_mirror must be an OBJECT, not a string!
# The host field must be a bare hostname (RFC 3986 authority) — NO scheme.
# See "registry_mirror Format Differences" section below.
registry_mirror:
host: mirror.example.com # ✅ Correct: hostname only
# host: https://mirror.example.com # ❌ Wrong: includes scheme
username: ((mirror.user))
password: ((mirror.pass))
debug: falseGet Parameters
- get: app-image
params:
format: rootfs # rootfs, oci, oci-layout
skip_download: false # Skip image download (optimization)
platform: # Override source platform
architecture: arm64
os: linuxOptimization: skip_download
Use skip_download: true when you only need version metadata without the image:
# Check if new version exists without downloading
- get: base-image
params:
skip_download: true
trigger: true
# Later, download only when needed
- get: base-image
passed: [check-job]
# No skip_download = full downloadPut Parameters
- put: app-image
params:
# Required: image source (choose one)
image: build-output/image.tar # OCI tarball
# OR oci-layout directory
# Tagging
version: version/version-file # Version number as tag
bump_aliases: true # Auto-tag 1.2, 1, latest
additional_tags: tags/tags-file # Whitespace-separated tags
tag_prefix: "v" # Prefix for additional_tagsOutput Files (after get)
rootfs format:
rootfs/ # Unpacked filesystem
metadata.json # Image metadata
labels.json # Image labels
repository # Repository name
tag # Tag name
digest # Image digestoci format:
image.tar # OCI tarball
labels.json
repository
tag
digest---
Time Resource (concourse/time-resource)
Triggers on time intervals.
resources:
- name: every-hour
type: time
icon: clock-outline
source:
interval: 1h # Trigger interval
- name: weekday-morning
type: time
source:
start: 9:00 AM
stop: 9:30 AM
location: America/New_York
days: [Monday, Tuesday, Wednesday, Thursday, Friday]---
S3 Resource (concourse/s3-resource)
Interacts with S3-compatible storage.
resources:
- name: artifacts
type: s3
source:
bucket: my-bucket
regexp: releases/app-(.*)\.tar\.gz # Version from filename
# OR
versioned_file: releases/app.tar.gz # S3 versioning
access_key_id: ((aws.key))
secret_access_key: ((aws.secret))
region_name: us-east-1
# Non-AWS S3-compatible
endpoint: https://minio.example.com
disable_ssl: false
# IAM role (instead of keys)
use_v2_signing: false---
Semver Resource (concourse/semver-resource)
Manages semantic versions.
resources:
- name: version
type: semver
source:
driver: git # git, s3, gcs, swift
uri: git@github.com:org/version.git
branch: main
file: version
private_key: ((git.private_key))
initial_version: 0.0.1
# Usage
- get: version
params:
bump: minor # major, minor, patch
pre: rc # Add prerelease suffix---
Pool Resource (concourse/pool-resource)
Manages locks and shared state.
resources:
- name: env-lock
type: pool
source:
uri: git@github.com:org/locks.git
branch: main
pool: environments
private_key: ((git.private_key))
# Acquire lock
- put: env-lock
params:
acquire: true
# Release lock
- put: env-lock
params:
release: env-lock---
registry_mirror Format Differences (registry-image vs docker-image)
⚠️ CRITICAL GOTCHA:registry-imageanddocker-imageexpect completely different formats forregistry_mirror. Getting this wrong causes opaque errors at check time.
registry-image (modern, pure Go binary — no Docker daemon)
registry-image:
registry_mirror:
host: registry-mirror.example.com # Object with host field
# host must be RFC 3986 URI authority = hostname only, NO schemeErrors if misconfigured:
- Passing a string instead of object:
json: cannot unmarshal string into Go struct field Source.source.registry_mirror of type resource.RegistryMirror - Including scheme in host:
registries must be valid RFC 3986 URI authorities: https://registry-mirror.example.com
docker-image (legacy, has Docker daemon internally)
docker-image:
registry_mirror: https://registry-mirror.example.com # Plain URL string with schemeDocker daemon handles the URL parsing internally, so it accepts the full URL.
CONCOURSE_BASE_RESOURCE_TYPE_DEFAULTS interaction
Concourse web nodes can inject default source params into all resource type checks via CONCOURSE_BASE_RESOURCE_TYPE_DEFAULTS. This is typically configured in /etc/concourse/resource-type-defaults.yml (Ansible-managed). The config must provide both formats:
registry-image:
registry_mirror:
host: registry-mirror.example.com # Object format for registry-image
docker-image:
registry_mirror: https://registry-mirror.example.com # String format for docker-imageNote: This is a web node setting, not a worker setting. Restart concourse-web after changes.
Ansible role template pattern (Jinja2)
When the mirror URL variable includes a scheme (e.g., https://registry-mirror.example.com), strip it for the registry-image host field:
registry-image:
registry_mirror:
host: {{ concourse_worker_registry_mirror_url | regex_replace('^https?://', '') }}
docker-image:
registry_mirror: {{ concourse_worker_registry_mirror_url }}---
GitLab Container Registry JWT Auth Discovery
⚠️ GOTCHA: The JWT auth endpoint for GitLab Container Registry is on the GitLab host, NOT the registry host.
When scripting against a GitLab Container Registry (e.g., registry.example.com), never hardcode the JWT auth URL. Discover it dynamically:
# Discover auth realm from registry's Www-Authenticate header
AUTH_HEADER=$(curl -s -o /dev/null -D - "https://${REGISTRY_URL}/v2/" \
| grep -i www-authenticate)
if [ -z "${AUTH_HEADER}" ]; then
echo "Error: Failed to get Www-Authenticate header from ${REGISTRY_URL}" >&2
exit 1
fi
REALM=$(echo "${AUTH_HEADER}" | sed -n 's/.*realm="\([^"]*\)".*/\1/p')
SERVICE=$(echo "${AUTH_HEADER}" | sed -n 's/.*service="\([^"]*\)".*/\1/p')
# Request token
TOKEN=$(curl -sf -u "${USER}:${PASSWORD}" \
"${REALM}?service=${SERVICE}&scope=repository:${REPO}:pull" \
| jq -r '.token')Example: For registry.netresearch.de, the realm is https://git.netresearch.de/jwt/auth (GitLab host), not https://registry.netresearch.de/jwt/auth.
---
Docker Image Resource (concourse/docker-image-resource)
⚠️ LEGACY: Thedocker-imageresource is deprecated. Useoci-build-task+registry-imagefor new pipelines.
Migration Guide: docker-image → oci-build-task
Before (Legacy docker-image):
resources:
- name: app-image
type: docker-image
source:
repository: registry.example.com/org/app
username: ((registry.user))
password: ((registry.pass))
jobs:
- name: build
plan:
- get: source
- put: app-image
params:
build: source
build_args:
NODE_VERSION: "20"
docker_buildkit: 1After (Modern oci-build-task + registry-image):
resources:
- name: app-image
type: registry-image
source:
repository: registry.example.com/org/app
username: ((registry.user))
password: ((registry.pass))
jobs:
- name: build
plan:
- get: source
- task: build
privileged: true
config:
platform: linux
image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
inputs:
- name: source
outputs:
- name: image
params:
CONTEXT: source
BUILD_ARG_NODE_VERSION: "20"
caches:
- path: cache
run:
path: build
- put: app-image
params:
image: image/image.tarWhy Migrate?
| Aspect | docker-image | oci-build-task |
|---|---|---|
| Maintenance | Deprecated, minimal updates | Actively maintained |
| Security | Requires Docker daemon | Uses BuildKit directly |
| Caching | Basic layer caching | Efficient BuildKit cache |
| Multi-arch | Limited support | Full IMAGE_PLATFORM support |
| Complexity | All-in-one (opaque) | Explicit build + push steps |
Legacy docker-image Reference
If migrating existing pipelines, here's the legacy syntax:
resources:
- name: app-image
type: docker-image
source:
repository: registry.example.com/org/app
username: ((registry.user))
password: ((registry.pass))
tag: latest
# Build and push
- put: app-image
params:
build: source-repo # Dockerfile context
dockerfile: source-repo/Dockerfile
tag_file: version/version # Dynamic tag
tag_as_latest: true
build_args:
BUILD_ARG: value
cache: true
cache_tag: cache
load_base: base-image # Pre-loaded base
docker_buildkit: 1 # Enable BuildKitPassing Images Between Jobs (Legacy Pattern)
When using docker-image, pass images between jobs with save/load:
# Job 1: Build and save
- get: app-image
params:
save: true # Save image layers for downstream jobs
# Job 2: Load and use
- get: app-image
passed: [build]
params:
save: true
- put: app-image
params:
load: app-image # Load from previous get
tag_file: version/tagModern alternative: Use task outputs with image.tar artifact.
---
Slack Notification Resource
Common community resource for Slack notifications.
resource_types:
- name: slack-notification
type: registry-image
source:
repository: cfcommunity/slack-notification-resource
tag: latest
resources:
- name: slack
type: slack-notification
source:
url: ((slack.webhook_url))
# Send notification
- put: slack
params:
text: "Build $BUILD_PIPELINE_NAME/$BUILD_JOB_NAME completed"
channel: "#builds"
username: Concourse CI
icon_emoji: ":concourse:"---
HTTP Resource
For webhook triggers and HTTP interactions.
resource_types:
- name: http-resource
type: registry-image
source:
repository: jgriff/http-resource
resources:
- name: webhook
type: http-resource
source:
url: https://api.example.com/webhook
method: POST
headers:
Content-Type: application/json
Authorization: Bearer ((api.token))
out_only: true # Disable implicit get
sensitive: true # Hide response
build_metadata: [headers, body] # Resolve CI vars#!/usr/bin/env bash
# Concourse Pipeline Validation Script
#
# Usage:
# validate-pipeline.sh <pipeline.yml> [vars.yml...]
#
# Features:
# - Validates YAML syntax
# - Checks for common configuration issues
# - Validates with fly if available
# - Reports potential problems
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Counters
ERRORS=0
WARNINGS=0
log_error() {
echo -e "${RED}ERROR:${NC} $1"
((ERRORS++))
}
log_warning() {
echo -e "${YELLOW}WARNING:${NC} $1"
((WARNINGS++))
}
log_success() {
echo -e "${GREEN}OK:${NC} $1"
}
log_info() {
echo -e "INFO: $1"
}
# Check dependencies
check_dependencies() {
if ! command -v yq &> /dev/null; then
log_warning "yq not found - some validations will be skipped"
log_info "Install yq: https://github.com/mikefarah/yq"
return 1
fi
return 0
}
# Validate YAML syntax
validate_yaml_syntax() {
local file="$1"
log_info "Checking YAML syntax: $file"
if command -v yq &> /dev/null; then
if yq eval '.' "$file" > /dev/null 2>&1; then
log_success "YAML syntax valid"
return 0
else
log_error "Invalid YAML syntax in $file"
yq eval '.' "$file" 2>&1 | head -5
return 1
fi
elif command -v python3 &> /dev/null; then
if python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null; then
log_success "YAML syntax valid"
return 0
else
log_error "Invalid YAML syntax in $file"
return 1
fi
else
log_warning "Cannot validate YAML syntax - install yq or python3"
return 0
fi
}
# Check for required pipeline elements
validate_pipeline_structure() {
local file="$1"
log_info "Checking pipeline structure"
if ! command -v yq &> /dev/null; then
return 0
fi
# Check for jobs (required)
local job_count
job_count=$(yq eval '.jobs | length' "$file" 2>/dev/null || echo "0")
if [[ "$job_count" -eq 0 ]]; then
log_error "Pipeline has no jobs defined"
else
log_success "Found $job_count jobs"
fi
# Check each job has a name and plan
local job_names
job_names=$(yq eval '.jobs[].name' "$file" 2>/dev/null || echo "")
for name in $job_names; do
if [[ -z "$name" || "$name" == "null" ]]; then
log_error "Job found without name"
fi
done
# Check for resources
local resource_count
resource_count=$(yq eval '.resources | length' "$file" 2>/dev/null || echo "0")
if [[ "$resource_count" -eq 0 ]]; then
log_warning "Pipeline has no resources defined"
else
log_success "Found $resource_count resources"
fi
}
# Check for common issues
validate_common_issues() {
local file="$1"
log_info "Checking for common issues"
# Check for unescaped regex dots in tag_regex
if grep -q 'tag_regex:.*\.[0-9]' "$file" 2>/dev/null; then
if ! grep -q 'tag_regex:.*\\\\.' "$file" 2>/dev/null; then
log_warning "Possible unescaped dots in tag_regex - use \\\\. for literal dots"
fi
fi
# Check for mixed read/write on same git resource with tags
if command -v yq &> /dev/null; then
local git_resources
git_resources=$(yq eval '.resources[] | select(.type == "git") | .name' "$file" 2>/dev/null || echo "")
for resource in $git_resources; do
local has_tag_regex
has_tag_regex=$(yq eval ".resources[] | select(.name == \"$resource\") | .source.tag_regex" "$file" 2>/dev/null || echo "null")
if [[ "$has_tag_regex" != "null" && -n "$has_tag_regex" ]]; then
# Check if this resource is used in both get and put
local used_in_get
local used_in_put
used_in_get=$(yq eval ".jobs[].plan[] | select(.get == \"$resource\") | .get" "$file" 2>/dev/null || echo "")
used_in_put=$(yq eval ".jobs[].plan[] | select(.put == \"$resource\") | .put" "$file" 2>/dev/null || echo "")
if [[ -n "$used_in_get" && -n "$used_in_put" ]]; then
log_warning "Resource '$resource' with tag_regex is used for both get and put - consider separating"
fi
fi
done
fi
# Check for missing trigger: true on get steps
if command -v yq &> /dev/null; then
local jobs_without_triggers
jobs_without_triggers=$(yq eval '.jobs[] | select((.plan[] | select(.get) | .trigger) != true) | .name' "$file" 2>/dev/null | head -5)
if [[ -n "$jobs_without_triggers" ]]; then
log_info "Jobs without auto-triggering gets (may be intentional): $(echo $jobs_without_triggers | tr '\n' ' ')"
fi
fi
# Check for hardcoded credentials
if grep -qiE '(password|secret|token|key):\s*[^(]' "$file" 2>/dev/null; then
if ! grep -qE '\(\(' "$file" 2>/dev/null; then
log_warning "Possible hardcoded credentials detected - use ((variables)) instead"
fi
fi
}
# Validate with fly CLI if available
validate_with_fly() {
local file="$1"
shift
local var_files=("$@")
if ! command -v fly &> /dev/null; then
log_info "fly CLI not found - skipping Concourse validation"
return 0
fi
log_info "Validating with fly CLI"
local fly_args=("-c" "$file")
for var_file in "${var_files[@]}"; do
if [[ -f "$var_file" ]]; then
fly_args+=("-l" "$var_file")
fi
done
# Try to find a logged-in target
local target
target=$(fly targets 2>/dev/null | head -1 | awk '{print $1}' || echo "")
if [[ -n "$target" ]]; then
if fly -t "$target" validate-pipeline "${fly_args[@]}" 2>&1; then
log_success "Pipeline validated successfully with fly"
else
log_error "fly validation failed"
fi
else
log_info "No fly target found - using syntax-only validation"
if fly validate-pipeline "${fly_args[@]}" 2>&1; then
log_success "Pipeline syntax validated with fly"
else
log_error "fly syntax validation failed"
fi
fi
}
# Main function
main() {
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <pipeline.yml> [vars.yml...]"
echo ""
echo "Validates a Concourse CI pipeline configuration"
exit 1
fi
local pipeline_file="$1"
shift
local var_files=("$@")
if [[ ! -f "$pipeline_file" ]]; then
log_error "Pipeline file not found: $pipeline_file"
exit 1
fi
echo "========================================="
echo "Concourse Pipeline Validator"
echo "========================================="
echo ""
check_dependencies
echo ""
validate_yaml_syntax "$pipeline_file"
echo ""
validate_pipeline_structure "$pipeline_file"
echo ""
validate_common_issues "$pipeline_file"
echo ""
validate_with_fly "$pipeline_file" "${var_files[@]}"
echo ""
echo "========================================="
echo "Summary: $ERRORS errors, $WARNINGS warnings"
echo "========================================="
if [[ $ERRORS -gt 0 ]]; then
exit 1
fi
exit 0
}
main "$@"