| name | setup-cicd-workflows |
| description | Generate Entur-standard CI/CD GitHub Actions workflows for a project. Detects language (Kotlin/Java, Go, Python) and generates all required workflow files: ci.yml, build.yml, cd.yml, pr.yml, codeql.yml, dependabot-pr.yml, terraform.yml, terraform-drift-detection.yml, and dependabot.yml. Use this skill when the user says "set up CI/CD", "create pipelines", "add GitHub Actions", "configure deployment", or needs CI/CD workflows for a new or existing Entur project. Always uses Entur reusable workflows -- never custom steps.
|
CI/CD Workflow Setup
Generate the complete set of GitHub Actions workflows for an Entur project using Entur reusable workflows. Uses the image promotion model: PRs build and push images, merges resolve and deploy the PR-built image.
Pipeline Architecture
.github/workflows/
ci.yml ← Reusable CI (lint, test, Docker build/scan/push)
build.yml ← PR: calls ci.yml
cd.yml ← Deploy: resolve PR-built image, deploy dev → tst → prd
pr.yml ← PR verification (title/body validation)
codeql.yml ← Security code scanning
dependabot-pr.yml ← CI for Dependabot PRs after human approval
terraform.yml ← Terraform lint/plan/apply (if terraform/ exists)
terraform-drift-detection.yml ← Weekly Terraform drift check (if terraform/ exists)
.github/
dependabot.yml ← Dependabot dependency update config
Step 1: Detect Project Configuration
Determine from the project files or ask the user:
| Input | How to detect | Default |
|---|
| Language | build.gradle.kts → Kotlin/Java, go.mod → Go, requirements.txt/pyproject.toml → Python | Kotlin |
| Has Helm chart | helm/ directory exists | yes |
| Has Terraform | terraform/ directory exists | yes |
| Has OpenAPI specs | specs/ directory exists | no |
| Has Artifactory deps | build.gradle.kts references entur2.jfrog.io | no |
| Multi-namespace deploy | Multiple values-kub-ent-*.yaml files in helm env | no |
| Environments | from self-service manifest or ask | [dev, tst, prd] |
| Repo name | from git remote or directory name | ask |
| Slack channel ID | ask user (optional) | omit |
Step 2: Generate .github/workflows/ci.yml
Reusable build workflow called by both build.yml (PRs) and cd.yml (workflow_dispatch without pre-built image).
name: CI
on:
workflow_call:
outputs:
image_and_tag:
description: Fully qualified image reference
value: ${{ jobs.docker-push.outputs.image_and_tag }}
jobs:
docker-lint:
uses: entur/gha-docker/.github/workflows/lint.yml@v1
Add Helm lint job (if Helm chart exists):
Helm linting runs inside ci.yml (not as a separate workflow) so charts are validated on every PR and dispatch build, not only when helm/** files change.
helm-lint:
uses: entur/gha-helm/.github/workflows/lint.yml@v1
strategy:
matrix:
environment: [{environments}]
with:
chart: helm/{repoName}
environment: ${{ matrix.environment }}
values: values-kub-ent-${{ matrix.environment }}.yaml
Add test job by language:
Replace {module} with the Gradle subproject name (e.g. app). For root-level builds without subprojects, use the repo root paths directly (e.g. build/test-results/test/*.xml).
Kotlin/Java:
test:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-24.04
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "25"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e
with:
cache-provider: basic
- name: Build and test
run: ./gradlew build
env:
ARTIFACTORY_AUTH_USER: ${{ secrets.ARTIFACTORY_AUTH_USER }}
ARTIFACTORY_AUTH_TOKEN: ${{ secrets.ARTIFACTORY_AUTH_TOKEN }}
- uses: dorny/test-reporter@v2
if: always()
with:
name: test-results
path: "{module}/build/test-results/test/*.xml"
reporter: java-junit
- uses: actions/upload-artifact@v4
with:
name: build
path: "{module}/build/distributions/{module}.tar"
retention-days: 4
Go:
test:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-24.04
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go test ./...
Python:
Use this block verbatim. Do NOT add dorny/test-reporter, upload-artifact, --junit-xml, or any Java/Kotlin steps. Those belong only in the Kotlin/Java variant above. Python is intentionally a simple pip install && pytest.
test:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-24.04
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: '.python-version'
- run: pip install -r requirements.txt && pytest
Add Docker build, scan, and push jobs:
Kotlin/Java (uses build artifact from test job):
docker:
needs: [test, docker-lint{, helm-lint}]
uses: entur/gha-docker/.github/workflows/build.yml@v1
with:
build_artifact_name: build
build_artifact_path: "{module}/build/distributions"
If Artifactory deps detected, add build secrets:
secrets:
BUILD_SECRETS: |
"ARTIFACTORY_AUTH_USER=${{ secrets.ARTIFACTORY_AUTH_USER }}"
"ARTIFACTORY_AUTH_TOKEN=${{ secrets.ARTIFACTORY_AUTH_TOKEN }}"
Go / Python (no build artifact needed):
docker:
needs: [test, docker-lint{, helm-lint}]
uses: entur/gha-docker/.github/workflows/build.yml@v1
Scan and push (all languages):
docker-scan:
needs: [docker]
uses: entur/gha-security/.github/workflows/docker-scan.yml@v2
secrets: inherit
with:
image_artifact: ${{ needs.docker.outputs.image_artifact }}
docker-push:
needs: [docker]
uses: entur/gha-docker/.github/workflows/push.yml@v1
secrets: inherit
Step 3: Generate .github/workflows/build.yml
PR build trigger. Calls the reusable ci.yml on every pull request.
name: Build
on:
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
- 'terraform/**'
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
uses: ./.github/workflows/ci.yml
secrets: inherit
Step 4: Generate .github/workflows/cd.yml
Continuous deployment using image promotion. On merge to main, resolves the image built during the PR (via git tag) and deploys through dev -> tst -> prd. Also supports manual deployment via workflow_dispatch.
name: Deploy
on:
push:
branches: [main]
paths-ignore:
- '**/*.md'
- 'terraform/**'
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment to deploy to
default: dev
options:
- dev
- tst
- prd
image:
description: Docker image tag to deploy (image_name:image_tag). Leave empty to build a new image.
required: false
type: string
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
if: github.event_name == 'workflow_dispatch' && inputs.image == ''
uses: ./.github/workflows/ci.yml
secrets: inherit
resolve-image:
if: github.event_name == 'push'
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
outputs:
image: ${{ steps.resolve.outputs.image }}
pr_number: ${{ steps.resolve.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Resolve image from merged PR git tag
id: resolve
env:
GH_TOKEN: ${{ github.token }}
run: |
REPO_NAME="${GITHUB_REPOSITORY#*/}"
PR_NUMBER=$(gh pr list --search "$GITHUB_SHA" --state merged --json number --jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
echo "::error::Could not find merged PR for commit $GITHUB_SHA"
exit 1
fi
BRANCH=$(gh pr view "$PR_NUMBER" --json headRefName --jq '.headRefName')
BRANCH=$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' | tr -d 'ÆØÅæøå')
BRANCH=${BRANCH//\//-}
BRANCH=${BRANCH//./-}
BRANCH=${BRANCH//!/-}
BRANCH=${BRANCH:0:43}
BRANCH=${BRANCH%-}
IMAGE_TAG=$(git tag -l "${BRANCH}.*" --sort=-creatordate | head -1)
if [ -z "$IMAGE_TAG" ]; then
echo "::warning::No git tag matching '${BRANCH}.*' (PR #${PR_NUMBER}) -- no image was built for this PR, skipping deploy"
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Resolved image: ${REPO_NAME}:${IMAGE_TAG} (from PR #${PR_NUMBER})"
echo "image=${REPO_NAME}:${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
Deploy jobs (for each environment):
No Helm chart? Skip this entire section. If the project has no helm/ directory,
cd.yml ALWAYS omits all deploy-* jobs and ci.yml ALWAYS omits the
helm-lint job. The Docker job's needs: must then be [test, docker-lint] only,
with no helm-lint entry. The cd workflow still keeps resolve-image: (so the
built image is tagged for downstream consumers) but has no deploy jobs at all.
Do NOT generate deploy-dev, deploy-tst, deploy-prd, helm-lint,
gha-helm references, or values-kub-ent-* paths in that case.
Generate deploy jobs for all environments. This example shows the standard three-environment setup:
deploy-dev:
needs: [build, resolve-image]
if: >-
always() && !cancelled() && !contains(needs.*.result, 'failure')
&& (github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && (inputs.environment == '' || inputs.environment == 'dev')))
&& (needs.resolve-image.outputs.image != '' || inputs.image != '' || needs.build.outputs.image_and_tag != '')
uses: entur/gha-helm/.github/workflows/deploy.yml@v1
concurrency:
group: cd-deploy-dev
cancel-in-progress: false
with:
environment: dev
image: ${{ needs.resolve-image.outputs.image || inputs.image || needs.build.outputs.image_and_tag }}
secrets: inherit
deploy-tst:
needs: [build, resolve-image, deploy-dev]
if: >-
always() && !cancelled() && !contains(needs.*.result, 'failure')
&& (github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && (inputs.environment == '' || inputs.environment == 'tst')))
&& (needs.resolve-image.outputs.image != '' || inputs.image != '' || needs.build.outputs.image_and_tag != '')
uses: entur/gha-helm/.github/workflows/deploy.yml@v1
concurrency:
group: cd-deploy-tst
cancel-in-progress: false
with:
environment: tst
image: ${{ needs.resolve-image.outputs.image || inputs.image || needs.build.outputs.image_and_tag }}
secrets: inherit
deploy-prd:
needs: [build, resolve-image, deploy-tst]
if: >-
always() && !cancelled() && !contains(needs.*.result, 'failure')
&& (github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && (inputs.environment == '' || inputs.environment == 'prd')))
&& (needs.resolve-image.outputs.image != '' || inputs.image != '' || needs.build.outputs.image_and_tag != '')
uses: entur/gha-helm/.github/workflows/deploy.yml@v1
concurrency:
group: cd-deploy-prd
cancel-in-progress: false
with:
environment: prd
image: ${{ needs.resolve-image.outputs.image || inputs.image || needs.build.outputs.image_and_tag }}
secrets: inherit
Add Slack notifications (if Slack channel ID provided):
Add slack_channel_id: {CHANNEL_ID} to each deploy job's with: block.
Multi-namespace deploy (if detected):
Replace simple deploy jobs with matrix strategy:
deploy-{env}:
needs: [build, resolve-image{, deploy-previous-env}]
if: >-
always() && !cancelled() && !contains(needs.*.result, 'failure')
&& (github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && (inputs.environment == '' || inputs.environment == '{env}')))
&& (needs.resolve-image.outputs.image != '' || inputs.image != '' || needs.build.outputs.image_and_tag != '')
strategy:
fail-fast: false
matrix:
include:
- environment: {env}
namespace: {namespace1}
release_name: {release1}
values: values-kub-ent-{env}.yaml
- environment: {env}
namespace: {namespace2}
release_name: {release2}
values: values-kub-ent-{env}-{variant}.yaml
uses: entur/gha-helm/.github/workflows/deploy.yml@v1
concurrency:
group: cd-deploy-{env}
cancel-in-progress: false
with:
environment: ${{ matrix.environment }}
chart: helm/{repoName}
namespace: ${{ matrix.namespace }}
release_name: ${{ matrix.release_name }}
image: ${{ needs.resolve-image.outputs.image || inputs.image || needs.build.outputs.image_and_tag }}
values: ${{ matrix.values }}
secrets: inherit
Skip Helm (if no helm/ directory):
Omit all deploy jobs from cd.yml. The workflow only builds and pushes the Docker image.
Step 5: Generate .github/workflows/pr.yml
PR verification using the Entur reusable PR verification workflow.
name: PR Verification
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, edited]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
verify-pr:
name: Verify PR
if: ${{ github.event_name == 'pull_request' }}
uses: entur/gha-meta/.github/workflows/verify-pr.yml@v1
Step 6: Generate .github/workflows/codeql.yml
Security code scanning with GitHub CodeQL.
name: CodeQL Analysis
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
jobs:
code-scan:
uses: entur/gha-security/.github/workflows/code-scan.yml@v2
secrets: inherit
For Java/Kotlin projects, add configuration:
with:
java_version: '25'
java_distribution: 'temurin'
use_setup_java: true
codeql_queries: 'security-extended'
Step 7: Generate .github/workflows/dependabot-pr.yml
Dependabot PRs do not receive repository secrets by default. This workflow adds an approval gate so secrets are only exposed after a human has reviewed and approved the PR.
name: on-pull_request_review-submitted
on:
pull_request_review:
types: [submitted]
permissions:
contents: read
jobs:
ci:
if: github.event.review.state == 'approved' && github.event.pull_request.user.login == 'dependabot[bot]'
uses: ./.github/workflows/ci.yml
secrets: inherit
Step 8: Generate .github/workflows/terraform.yml (if terraform/ exists)
Manages Terraform infrastructure changes across all environments. On PR: lint, plan all environments, apply dev. On merge: lint, plan tst+prd, apply tst, apply prd.
Required job names (use these exact identifiers):
terraform-lint, tf-plan-dev, tf-plan-tst, tf-plan-prd,
tf-apply-dev, tf-apply-tst, tf-apply-prd.
Required conditional: tf-plan-dev MUST be gated with if: github.event_name != 'push'
(it is PR/dispatch only). tf-apply-dev runs on PR/dispatch; tf-apply-tst and
tf-apply-prd run on push/dispatch.
Required per-job permissions: (do NOT omit, do NOT declare at workflow level).
entur/gha-terraform/.github/workflows/{plan,apply}.yml@v2 declare
contents: read, id-token: write, pull-requests: write on their inner job.
A reusable workflow cannot grant itself more than the caller provides, so the
caller MUST also declare these at the job level. lint.yml@v2 only needs
contents: read.
Two failure modes to avoid:
- Declaring
permissions: at the workflow level (or on the caller's job)
that omits any required scope -- GitHub clamps the unlisted scopes to
none and the called workflow fails with startup_failure
("Refusing to allow a GitHub App to create or update workflow ... without
pull-requests permission").
- Omitting
permissions: entirely -- works while the repo's default
GITHUB_TOKEN is write-all, breaks the moment that default is tightened.
Always keep permissions: on the calling job (as shown below) and never
add a workflow-level permissions: block to terraform.yml.
name: Terraform
on:
push:
branches: [main]
paths:
- 'terraform/**'
pull_request:
branches: [main]
paths:
- 'terraform/**'
workflow_dispatch:
jobs:
terraform-lint:
uses: entur/gha-terraform/.github/workflows/lint.yml@v2
permissions:
contents: read
concurrency:
group: terraform-lint-${{ github.ref }}
cancel-in-progress: true
tf-plan-dev:
needs: [terraform-lint]
if: github.event_name != 'push'
uses: entur/gha-terraform/.github/workflows/plan.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-plan-dev-${{ github.ref }}
cancel-in-progress: true
with:
environment: dev
tf-plan-tst:
needs: [terraform-lint]
uses: entur/gha-terraform/.github/workflows/plan.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-plan-tst-${{ github.ref }}
cancel-in-progress: true
with:
environment: tst
tf-plan-prd:
needs: [terraform-lint]
uses: entur/gha-terraform/.github/workflows/plan.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-plan-prd-${{ github.ref }}
cancel-in-progress: true
with:
environment: prd
tf-apply-dev:
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
needs: [tf-plan-dev]
uses: entur/gha-terraform/.github/workflows/apply.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-apply-dev
cancel-in-progress: false
with:
environment: dev
has_changes: ${{ needs.tf-plan-dev.outputs.has_changes }}
tf-apply-tst:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
needs: [tf-plan-tst]
uses: entur/gha-terraform/.github/workflows/apply.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-apply-tst
cancel-in-progress: false
with:
environment: tst
has_changes: ${{ needs.tf-plan-tst.outputs.has_changes }}
tf-apply-prd:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
needs: [tf-plan-prd, tf-apply-tst]
uses: entur/gha-terraform/.github/workflows/apply.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-apply-prd
cancel-in-progress: false
with:
environment: prd
has_changes: ${{ needs.tf-plan-prd.outputs.has_changes }}
Step 9: Generate .github/workflows/terraform-drift-detection.yml (if terraform/ exists)
This is a separate file from terraform.yml. When the user asks for Terraform
workflows, ALWAYS generate BOTH terraform.yml (Step 8) AND
terraform-drift-detection.yml (Step 9).
Required job names: tf-plan-dev, tf-plan-tst, tf-plan-prd, drift-check.
Required schedule: cron: '0 10 * * 4' (Thursdays at 10:00 UTC).
Required outputs on drift-check: has_drift, drifted_envs, plus a step that
calls gh issue create when drift is detected.
Weekly scheduled Terraform plan to detect infrastructure drift. Creates a GitHub issue and sends a Slack notification if drift is found.
name: Terraform Drift Detection
on:
schedule:
- cron: '0 10 * * 4'
workflow_dispatch:
jobs:
tf-plan-dev:
uses: entur/gha-terraform/.github/workflows/plan.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
with:
environment: dev
tf-plan-tst:
uses: entur/gha-terraform/.github/workflows/plan.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
with:
environment: tst
tf-plan-prd:
uses: entur/gha-terraform/.github/workflows/plan.yml@v2
permissions:
contents: read
id-token: write
pull-requests: write
with:
environment: prd
drift-check:
needs: [tf-plan-dev, tf-plan-tst, tf-plan-prd]
runs-on: ubuntu-24.04
permissions:
contents: read
issues: write
outputs:
has_drift: ${{ steps.check.outputs.has_drift }}
drifted_envs: ${{ steps.check.outputs.drifted_envs }}
issue_url: ${{ steps.check.outputs.issue_url }}
steps:
- name: Check for infrastructure drift
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEV_CHANGES: ${{ needs.tf-plan-dev.outputs.has_changes }}
TST_CHANGES: ${{ needs.tf-plan-tst.outputs.has_changes }}
PRD_CHANGES: ${{ needs.tf-plan-prd.outputs.has_changes }}
run: |
DRIFTED_ENVS=""
if [ "$DEV_CHANGES" = "true" ]; then DRIFTED_ENVS="$DRIFTED_ENVS dev"; fi
if [ "$TST_CHANGES" = "true" ]; then DRIFTED_ENVS="$DRIFTED_ENVS tst"; fi
if [ "$PRD_CHANGES" = "true" ]; then DRIFTED_ENVS="$DRIFTED_ENVS prd"; fi
if [ -n "$DRIFTED_ENVS" ]; then
echo "has_drift=true" >> "$GITHUB_OUTPUT"
echo "drifted_envs=$DRIFTED_ENVS" >> "$GITHUB_OUTPUT"
echo "Drift detected in:$DRIFTED_ENVS"
ISSUE_URL=$(gh issue create \
--title "Terraform drift detected in:$DRIFTED_ENVS" \
--body "$(cat <<BODY
## Terraform Drift Detected
The weekly scheduled Terraform plan found infrastructure drift in the following environments: **$DRIFTED_ENVS**
### Action Required
Review the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details and apply changes or update Terraform configuration to match actual infrastructure.
### Details
| Environment | Drift Detected |
|-------------|---------------|
| dev | $DEV_CHANGES |
| tst | $TST_CHANGES |
| prd | $PRD_CHANGES |
BODY
)" \
--repo "${{ github.repository }}")
echo "issue_url=$ISSUE_URL" >> "$GITHUB_OUTPUT"
else
echo "has_drift=false" >> "$GITHUB_OUTPUT"
echo "No drift detected in any environment"
fi
Add Slack notification (if Slack channel ID provided):
slack-notify:
if: needs.drift-check.outputs.has_drift == 'true'
needs: [drift-check]
uses: entur/gha-slack/.github/workflows/post.yml@v3
with:
channel_id: {CHANNEL_ID}
message: "Terraform drift detected in:${{ needs.drift-check.outputs.drifted_envs }}. Issue: ${{ needs.drift-check.outputs.issue_url }}"
blocks: >-
[{"type":"section","text":{"type":"mrkdwn","text":"Terraform drift detected in:${{ needs.drift-check.outputs.drifted_envs }}. Issue: ${{ needs.drift-check.outputs.issue_url }}"}},{"type":"divider"}]
secrets: inherit
Step 10: Generate .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directories: ["/"]
schedule:
interval: "weekly"
Add language-specific ecosystems:
Kotlin/Java -- add:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
Go -- add:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
Python -- add:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
Step 11: Generate Optional Lint Workflows
lint-api.yml (if OpenAPI specs exist):
name: lint-api
on:
pull_request:
paths: ['specs/**']
jobs:
api-lint:
uses: entur/gha-api/.github/workflows/lint.yml@v5
if: github.actor != 'dependabot[bot]'
secrets: inherit
with:
spec: specs/*.yaml
Step 12: Print Summary
List all generated files and their purpose:
Generated CI/CD workflows:
.github/workflows/ci.yml - Reusable CI (lint, test, Docker build/scan/push)
.github/workflows/build.yml - PR build trigger (calls ci.yml)
.github/workflows/cd.yml - Deploy: resolve PR-built image, deploy dev -> tst -> prd
.github/workflows/pr.yml - PR verification (title/body validation)
.github/workflows/codeql.yml - Security code scanning (CodeQL)
.github/workflows/dependabot-pr.yml - CI for Dependabot PRs after approval
.github/workflows/terraform.yml - Terraform lint/plan/apply (if applicable)
.github/workflows/terraform-drift-detection.yml - Weekly Terraform drift detection (if applicable)
.github/workflows/lint-api.yml - API spec linting (if applicable)
.github/dependabot.yml - Automated dependency updates
Critical Rules
-
ALWAYS use Entur reusable workflows for all CI/CD steps
-
Pin workflow versions to major tags: @v1, @v2
-
Use secrets: inherit for security scanning and deploy workflows
-
Image promotion model: PRs build and push images; merges ALWAYS resolve the PR-built image via git tag
-
Terraform ALWAYS runs in a separate workflow from cd.yml
-
Deploy concurrency ALWAYS uses cancel-in-progress: false to protect running deployments
-
Plan concurrency uses cancel-in-progress: true -- only the latest plan matters
-
Use has_changes output from terraform-plan to skip unnecessary applies
-
Use the conditional pattern for deploy jobs with multiple dependency paths:
if: >-
always() && !cancelled() && !contains(needs.*.result, 'failure')
-
Dependabot PRs need approval before CI runs with secrets (dependabot-pr.yml)
-
Use paths-ignore on build and cd workflows to skip Terraform-only and docs-only changes
-
Use paths triggers on Terraform workflows to only run on infrastructure changes
-
Upload test reports using dorny/test-reporter for PR check visibility (Java/Kotlin)
-
Upload build artifacts for Docker builds that need compiled output (Java/Kotlin)