> **A practical, in-depth reference covering architecture, security, reusable workflows, enterprise governance, GitOps integration, and the platform engineering perspective — with real-world YAML examples throughout.** --- ## Executive Summary GitHub Actions is a native CI/CD and workflow automation platform built directly into GitHub. Launched in 2018 and reaching general availability in November 2019, it has rapidly become one of the most widely adopted CI/CD systems in the world — GitHub reports that more than 10 million repositories use it actively. ### Why GitHub Actions Became Popular Three structural advantages explain its adoption curve: **Zero-friction integration.** Actions lives inside the repository. There is no separate CI server to provision, no webhook to configure, no OAuth app to authorize. A single YAML file in `.github/workflows/` is enough to start. This reduces the "time to first green build" from hours (Jenkins, TeamCity) to minutes. **The Marketplace.** GitHub Marketplace hosts more than 20,000 community and vendor-published Actions. Setting up Docker builds, Terraform plans, SLSA provenance, Slack notifications, or AWS credential exchange requires three lines of YAML, not a plugin installation and server restart. **Event-driven breadth.** GitHub Actions can react to more than 35 event types — not just push and pull requests, but issue comments, project board movements, release publications, external `repository_dispatch` calls, and scheduled cron jobs. This makes it a general automation platform, not just a test runner. ### How It Compares to Traditional CI/CD |Dimension|Jenkins / TeamCity|GitHub Actions| |---|---|---| |Infrastructure|Self-managed server|Fully managed (hosted) or self-hosted| |Configuration|Jenkinsfile / Kotlin DSL|YAML (declarative)| |Plugin ecosystem|1,800+ plugins|20,000+ Marketplace Actions| |GitHub integration|Via plugin|Native| |Secret management|Credentials plugin|Built-in, with Environment scoping| |Supply chain security|Manual|OIDC, SLSA, Sigstore built-in| |Learning curve|High|Low-to-medium| |Cost model|Infrastructure cost|Per-minute (free tier included)| The shift is architectural: traditional CI systems are servers that poll or receive webhooks from source control. GitHub Actions inverts this — the platform is the source control system, and automation is a first-class citizen of the repository. --- ## Core Concepts Understanding GitHub Actions requires internalizing a hierarchy of seven objects. ### Architecture Overview ``` GitHub Repository └── Workflow (.github/workflows/*.yml) ├── Trigger (on: push, pull_request, schedule…) └── Job (runs on a Runner) ├── Service Containers (optional) └── Step ├── uses: owner/action@version (Action from Marketplace or local) └── run: shell command ``` ### Workflows A **workflow** is an automated process defined in a YAML file stored under `.github/workflows/`. One repository can have multiple workflow files. Each file is independent — they share repository secrets and variables but do not share state unless you explicitly pass artifacts or outputs. Workflow files are version-controlled alongside application code. This means CI configuration goes through code review, has a full audit trail, and can be rolled back with `git revert`. ### Events (Triggers) An **event** is what causes a workflow to run. GitHub supports more than 35 event types: - **Repository events**: `push`, `pull_request`, `pull_request_review`, `create`, `delete`, `release`, `fork`, `star` - **Issue events**: `issues`, `issue_comment`, `label`, `milestone` - **Scheduled**: `schedule` (cron syntax, UTC) - **Manual**: `workflow_dispatch` (UI button + API + CLI) - **External**: `repository_dispatch` (HTTP POST from any system) - **Cross-workflow**: `workflow_call` (called by another workflow) - **Deployment**: `deployment`, `deployment_status` - **Security**: `security_advisory`, `code_scanning_alert` ### Jobs A **job** is a group of steps that run sequentially on the same runner. Jobs within a workflow run in parallel by default. Use `needs:` to create sequential dependencies between jobs. Jobs are isolated — each job gets a fresh runner environment. To pass data between jobs, use `outputs:` (for small strings) or `actions/upload-artifact` + `actions/download-artifact` (for files). ### Steps A **step** is a single task within a job. Steps execute in sequence and share the runner's filesystem and environment variables within the job. A step is either: - `uses:` — invokes a published or local Action - `run:` — executes a shell command ### Runners A **runner** is the compute that executes a job. GitHub provides managed runners (`ubuntu-latest`, `windows-latest`, `macos-latest`). Organizations can add **self-hosted runners** for custom hardware, private networking, or cost optimization. ### Actions An **Action** is a reusable unit of automation. Actions are packaged as: - **JavaScript/TypeScript Actions** — run directly on the runner with Node.js - **Docker Container Actions** — run in a specified container image - **Composite Actions** — a sequence of steps bundled into a single `action.yml` Actions are referenced by `owner/repo@ref`. The `ref` can be a tag (`@v4`), a branch (`@main`), or a full commit SHA (`@abc1234`). ### Marketplace The GitHub Marketplace is a directory of published Actions. Notable categories include language setup (`actions/setup-node`, `actions/setup-python`), container tools (`docker/build-push-action`), cloud credentials (`aws-actions/configure-aws-credentials`), security scanning (`aquasecurity/trivy-action`), and notification (`slackapi/slack-github-action`). ### Artifacts **Artifacts** are files produced during a workflow run that you want to retain or share between jobs. `actions/upload-artifact` saves files to GitHub's artifact storage (retained for 90 days by default). `actions/download-artifact` retrieves them in a later job. Common uses: compiled binaries, test reports, coverage files, Docker layer caches, Terraform plan files. ### Secrets **Secrets** are encrypted variables stored at repository, environment, or organization level. They are masked in logs and never exposed to workflow code as plaintext — they are injected as environment variables. Secrets are accessible via `${{ secrets.SECRET_NAME }}`. Secret scoping: - **Repository secrets** — available to all workflows in the repo - **Environment secrets** — available only when a job targets a named Environment - **Organization secrets** — shareable across repositories with policy controls ### Environments **Environments** are named deployment targets (e.g., `staging`, `production`). They enable: - **Required reviewers** — a human must approve before the job runs - **Wait timers** — enforced delay before execution - **Deployment branch rules** — only specified branches can deploy here - **Environment-scoped secrets and variables** — credentials isolated per target Environments create an auditable deployment history visible in the GitHub UI under the "Deployments" tab. --- ## Workflow Syntax Deep Dive ### Basic YAML Structure ```yaml name: CI # Display name in GitHub UI on: # Triggers push: branches: [main] pull_request: branches: [main] permissions: # Least-privilege token permissions contents: read env: # Workflow-level environment variables NODE_VERSION: '20' jobs: build: # Job ID name: Build and Test # Display name runs-on: ubuntu-latest # Runner steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - run: npm ci - run: npm test ``` ### workflow_dispatch — Manual Triggers with Inputs ```yaml on: workflow_dispatch: inputs: environment: description: 'Target environment' required: true type: choice options: [dev, staging, production] version: description: 'Image tag to deploy' required: true type: string dry_run: description: 'Skip actual deployment' type: boolean default: false jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy if: inputs.dry_run == false run: | echo "Deploying ${{ inputs.version }} to ${{ inputs.environment }}" ``` `workflow_dispatch` can also be triggered via the GitHub CLI (`gh workflow run`) or the REST API, enabling external orchestration. ### push — Branch and Path Filtering ```yaml on: push: branches: - main - 'release/**' - '!release/legacy' # negation supported tags: - 'v[0-9]+.[0-9]+.[0-9]+' paths: - 'src/**' - 'package.json' - '!**/*.md' # ignore markdown changes ``` `paths-ignore` and `paths` are mutually exclusive. Use `paths` when you want to _include_ only specific file changes. ### pull_request — PR Lifecycle Events ```yaml on: pull_request: types: - opened - synchronize # new commits pushed to the PR branch - reopened - ready_for_review branches: - main ``` > **Security note**: For PRs from forks, `pull_request` workflows run without access to secrets. Use `pull_request_target` with extreme caution — it runs in the context of the base branch and _does_ have access to secrets, creating a privilege escalation risk if you checkout and execute fork code. ### schedule — Cron Jobs ```yaml on: schedule: - cron: '0 2 * * 1-5' # 02:00 UTC, Monday–Friday - cron: '0 6 * * 0' # 06:00 UTC, Sunday (weekly full scan) ``` All times are UTC. GitHub may delay scheduled runs by up to 15 minutes during high load. Scheduled workflows are automatically disabled if the repository has no activity for 60 days. ### workflow_call — Reusable Workflow Interface ```yaml on: workflow_call: inputs: environment: type: string required: true image_tag: type: string required: true secrets: DEPLOY_TOKEN: required: true outputs: deployment_url: value: ${{ jobs.deploy.outputs.url }} ``` ### repository_dispatch — External Triggers ```yaml on: repository_dispatch: types: - deploy-triggered - integration-test-requested ``` Trigger via the GitHub API: ```bash curl -X POST \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos/ORG/REPO/dispatches \ -d '{"event_type":"deploy-triggered","client_payload":{"version":"1.4.2"}}' ``` Access the payload as `${{ github.event.client_payload.version }}`. --- ## Practical Examples ### Example 1: Basic CI Pipeline ```yaml name: CI on: push: branches: [main, develop] pull_request: branches: [main] permissions: contents: read jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Lint run: npm run lint - name: Type check run: npm run typecheck - name: Unit tests run: npm test -- --coverage --ci - name: Upload coverage uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/ retention-days: 7 ``` ### Example 2: Build and Publish Docker Image ```yaml name: Docker Build and Push on: push: tags: ['v*.*.*'] pull_request: branches: [main] permissions: contents: read packages: write id-token: write # for OIDC / Sigstore signing jobs: docker: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Docker metadata id: meta uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=ref,event=pr type=sha,prefix=sha- - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push id: build uses: docker/build-push-action@v6 with: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true # SLSA Level 3 provenance sbom: true # Software Bill of Materials - name: Sign image with Cosign if: github.event_name != 'pull_request' uses: sigstore/cosign-installer@v3 - name: Sign if: github.event_name != 'pull_request' run: | cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} ``` ### Example 3: Multi-Stage CI/CD Pipeline ```yaml name: CI/CD Pipeline on: push: branches: [main] permissions: {} jobs: lint-and-test: name: Lint & Test runs-on: ubuntu-latest permissions: contents: read outputs: test-passed: ${{ steps.test.outcome }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - run: pip install -r requirements.txt - run: ruff check . - id: test run: pytest --junitxml=results.xml - uses: actions/upload-artifact@v4 with: name: test-results path: results.xml security-scan: name: Security Scan runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - uses: actions/checkout@v4 - name: Trivy filesystem scan uses: aquasecurity/trivy-action@master with: scan-type: 'fs' format: 'sarif' output: 'trivy.sarif' - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy.sarif build: name: Build Image needs: [lint-and-test, security-scan] runs-on: ubuntu-latest permissions: contents: read packages: write outputs: image-tag: ${{ steps.meta.outputs.version }} image-digest: ${{ steps.push.outputs.digest }} steps: - uses: actions/checkout@v4 - id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: type=sha,prefix= - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: push uses: docker/build-push-action@v6 with: push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max deploy-staging: name: Deploy → Staging needs: build uses: ./.github/workflows/_deploy.yml with: environment: staging image-tag: ${{ needs.build.outputs.image-tag }} secrets: inherit integration-tests: name: Integration Tests needs: deploy-staging runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm run test:integration env: BASE_URL: https://staging.example.com deploy-production: name: Deploy → Production needs: [build, integration-tests] uses: ./.github/workflows/_deploy.yml with: environment: production image-tag: ${{ needs.build.outputs.image-tag }} secrets: inherit ``` ### Example 4: Terraform Deployment ```yaml name: Terraform on: push: branches: [main] paths: ['infra/**'] pull_request: paths: ['infra/**'] permissions: contents: read id-token: write # OIDC for AWS pull-requests: write # post plan as PR comment jobs: terraform: runs-on: ubuntu-latest defaults: run: working-directory: infra/ steps: - uses: actions/checkout@v4 - name: Configure AWS via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-terraform aws-region: eu-west-1 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: '1.9.0' terraform_wrapper: true - name: Terraform Format Check run: terraform fmt -check -recursive continue-on-error: true - name: Terraform Init run: terraform init -backend-config=backend.hcl - name: Terraform Validate run: terraform validate - name: Terraform Plan id: plan run: terraform plan -no-color -out=tfplan continue-on-error: true - name: Post Plan to PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const plan = `${{ steps.plan.outputs.stdout }}`; const body = `### Terraform Plan\n\`\`\`\n${plan}\n\`\`\``; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); - name: Terraform Apply if: github.ref == 'refs/heads/main' && steps.plan.outcome == 'success' run: terraform apply -auto-approve tfplan ``` ### Example 5: Kubernetes Deployment ```yaml name: Kubernetes Deploy on: workflow_call: inputs: environment: type: string required: true image-tag: type: string required: true jobs: deploy: runs-on: ubuntu-latest environment: name: ${{ inputs.environment }} url: https://${{ inputs.environment }}.example.com concurrency: group: deploy-${{ inputs.environment }} cancel-in-progress: false steps: - uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: eu-west-1 - name: Update kubeconfig run: | aws eks update-kubeconfig \ --name ${{ vars.EKS_CLUSTER_NAME }} \ --region eu-west-1 - name: Helm deploy run: | helm upgrade --install myapp ./chart \ --namespace ${{ inputs.environment }} \ --create-namespace \ --set image.tag=${{ inputs.image-tag }} \ --set environment=${{ inputs.environment }} \ --values ./chart/values/${{ inputs.environment }}.yaml \ --wait \ --timeout 10m \ --atomic - name: Verify rollout run: | kubectl rollout status deployment/myapp \ -n ${{ inputs.environment }} \ --timeout=5m - name: Smoke test run: | curl -f https://${{ inputs.environment }}.example.com/health || exit 1 ``` --- ## GitHub-Hosted Runners GitHub provides managed virtual machines for three operating systems. All runners include pre-installed tools (Docker, kubectl, Helm, AWS CLI, Azure CLI, Python, Node.js, Go, Java, etc.). |Runner Label|OS|vCPU|RAM|Storage|Cost Multiplier| |---|---|---|---|---|---| |`ubuntu-latest` (ubuntu-24.04)|Ubuntu 24.04|4|16 GB|14 GB SSD|1×| |`ubuntu-22.04`|Ubuntu 22.04|4|16 GB|14 GB SSD|1×| |`ubuntu-latest` (Large)|Ubuntu 24.04|16|64 GB|150 GB SSD|4×| |`windows-latest`|Windows Server 2022|4|16 GB|14 GB SSD|2×| |`macos-latest` (M1)|macOS 14|3 (Apple M1)|7 GB|14 GB SSD|5×| |`macos-13` (Intel)|macOS 13|4 (Intel)|14 GB|14 GB SSD|10×| **Free tier**: 2,000 Linux minutes per month (GitHub Free/Pro). Organization plans start at 3,000 minutes. macOS minutes are counted at 10× the rate. **Larger runners** (4–64 vCPU) are available on GitHub Team and Enterprise plans. They are useful for heavy compilation, large test suites, or parallel processing within a single job. --- ## Self-Hosted Runners ### When to Use Self-Hosted Runners Self-hosted runners make sense when: - You need access to a private network (VPC, on-premises databases) - Jobs require specialized hardware (GPU, hardware security modules, ARM devices) - You process data that must not leave your environment (regulated industries) - Volume is high enough that per-minute costs exceed infrastructure costs - You need consistent environments with pre-warmed caches ### Advantages - Full control over hardware and software - No per-minute billing — only infrastructure cost - Access to private networks without VPN or tunnel - Custom labels for routing jobs to specific machines - Pre-warmed caches (Docker layers, dependency stores) ### Disadvantages - You own availability, patching, and security hardening - Horizontal scaling requires additional tooling (Actions Runner Controller) - Ephemeral runners require more setup than persistent ones - GitHub is not responsible for the security of the runner environment ### Security Implications **Never register self-hosted runners to public repositories.** A malicious actor can open a pull request with code that executes on your runner, accessing your internal network, credentials, and any data on the filesystem. For private repositories, follow these controls: - Use ephemeral runners (register, run one job, deregister). The `--ephemeral` flag in the runner binary supports this. - Run runners in containers or VMs that are destroyed after each job. - Never give runners elevated privileges (no `root`, no `sudo`). - Isolate runner networks from production systems. - Use Actions Runner Controller (ARC) on Kubernetes for automated ephemeral runner lifecycle. ### Actions Runner Controller (ARC) ARC is the Kubernetes operator for self-hosted runners. It provisions ephemeral runner pods on demand and terminates them after each job. ```yaml # values.yaml for ARC installation githubConfigUrl: https://github.com/org/repo githubConfigSecret: arc-github-secret template: spec: containers: - name: runner image: ghcr.io/actions/actions-runner:latest resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2 memory: 4Gi ``` ### Enterprise Use Cases Large financial institutions and cloud-native companies typically run self-hosted runners in Kubernetes (ARC) with: - IRSA (IAM Roles for Service Accounts) for AWS access without static credentials - Workload Identity for GCP access - Network policies isolating runner pods - Images built from hardened base images (CIS benchmarks) - Centralised logging to SIEM (Splunk, Datadog) --- ## Security Best Practices and DevSecOps Security in GitHub Actions is a multi-layer discipline. Each layer addresses a different threat vector. ### 1. Secrets Management ```yaml # Good: inject secret as environment variable at step level - name: Deploy env: API_KEY: ${{ secrets.API_KEY }} run: deploy.sh # Bad: never interpolate secrets directly into run commands # run: curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" ... # Reason: the value appears in the workflow YAML and process list ``` For external secret stores, use dedicated Actions: - **HashiCorp Vault**: `hashicorp/vault-action` - **AWS Secrets Manager**: `aws-actions/aws-secretsmanager-get-secrets` - **Azure Key Vault**: `azure/get-keyvault-secrets` ### 2. OIDC — Keyless Cloud Authentication OIDC eliminates long-lived static credentials from secrets entirely. The workflow requests a short-lived token from GitHub's OIDC provider, exchanges it with the cloud provider, and receives a scoped, time-limited credential. ```yaml permissions: id-token: write # required for OIDC token issuance contents: read jobs: deploy: runs-on: ubuntu-latest steps: # AWS — configure the IAM role trust policy for GitHub OIDC - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole role-session-name: ${{ github.run_id }} aws-region: eu-west-1 # GCP - uses: google-github-actions/auth@v2 with: workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/github service_account: [email protected] # Azure - uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} ``` **AWS IAM Trust Policy for GitHub OIDC:** ```json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*" } } }] } ``` ### 3. Least Privilege Permissions Set `permissions: {}` at the workflow level (disabling all permissions) and grant only what each job needs: ```yaml permissions: {} # deny all by default jobs: test: permissions: contents: read # clone the repo runs-on: ubuntu-latest publish: permissions: contents: read packages: write # push to GHCR id-token: write # OIDC token pr-comment: permissions: pull-requests: write # post comment contents: read ``` ### 4. Action Pinning (Supply Chain Security) Pinning to a full commit SHA prevents a compromised tag from executing malicious code: ```yaml # Vulnerable — a tag can be moved to a different commit - uses: actions/checkout@v4 # Secure — SHA cannot be modified after the fact - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 ``` Use [Dependabot for GitHub Actions](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) to automate SHA updates: ```yaml # .github/dependabot.yml version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly ``` ### 5. Dependency Verification ```yaml # Verify checksums for downloaded tools - name: Install cosign uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 # Use SLSA verifier for build artifacts - name: Verify artifact provenance uses: slsa-framework/slsa-verifier/actions/installer@c9abffe4d2ab2ffa0b2ea9b2582b84164f390adc ``` ### 6. Security Summary Checklist |Control|Implementation| |---|---| |Short-lived credentials|OIDC for AWS, GCP, Azure| |No static secrets in code|Use `secrets:` context, never hardcode| |Action pinning|SHA pinning + Dependabot| |Least privilege|`permissions: {}` + per-job grants| |Ephemeral runners|ARC or GitHub-hosted| |Branch protection|Required status checks on `main`| |Environment approvals|Required reviewers for production| |Audit trail|GitHub audit log (org/enterprise level)| --- ## Reusable Workflows Reusable workflows are the primary mechanism for applying DRY principles and platform governance to GitHub Actions at scale. ### Defining a Reusable Workflow ```yaml # .github/workflows/_deploy.yml # Convention: prefix with _ to indicate internal/reusable on: workflow_call: inputs: environment: type: string required: true image-tag: type: string required: true dry-run: type: boolean default: false secrets: KUBE_CONFIG: required: true outputs: deployment-url: description: "URL of the deployed application" value: ${{ jobs.deploy.outputs.url }} jobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} outputs: url: ${{ steps.verify.outputs.url }} steps: - uses: actions/checkout@v4 - name: Deploy if: inputs.dry-run == false run: | echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}" - id: verify run: echo "url=https://${{ inputs.environment }}.example.com" >> $GITHUB_OUTPUT ``` ### Calling a Reusable Workflow ```yaml jobs: deploy-staging: uses: myorg/.github/.github/workflows/_deploy.yml@main with: environment: staging image-tag: ${{ needs.build.outputs.tag }} secrets: KUBE_CONFIG: ${{ secrets.KUBE_CONFIG_STAGING }} deploy-production: needs: deploy-staging uses: myorg/.github/.github/workflows/_deploy.yml@main with: environment: production image-tag: ${{ needs.build.outputs.tag }} secrets: inherit # pass all secrets from calling workflow ``` ### Platform Engineering Approach: Centralized Workflow Library Store reusable workflows in a central `myorg/.github` repository: ``` myorg/.github/ ├── .github/ │ └── workflows/ │ ├── _ci-node.yml # Node.js CI template │ ├── _ci-python.yml # Python CI template │ ├── _ci-go.yml # Go CI template │ ├── _docker-build.yml # Container build template │ ├── _deploy-k8s.yml # Kubernetes deployment template │ ├── _deploy-lambda.yml # Lambda deployment template │ ├── _security-scan.yml # Security scanning template │ └── _terraform.yml # Infrastructure template ├── actions/ │ └── setup-internal-tools/ # Composite action │ └── action.yml └── README.md ``` Application teams consume these without writing CI logic: ```yaml # In application repo jobs: ci: uses: myorg/.github/.github/workflows/_ci-node.yml@main with: node-version: '20' deploy: needs: ci uses: myorg/.github/.github/workflows/_deploy-k8s.yml@main with: environment: production chart-path: ./chart secrets: inherit ``` **Benefits of centralized governance:** - Platform team controls security, compliance, and standards in one place - A single PR to `myorg/.github` updates CI for all consuming repositories - Teams cannot bypass security scans or approval gates - Audit trail for all workflow changes --- ## Composite Actions A **composite action** bundles multiple steps into a single reusable unit, published with its own `action.yml`. ### When to Use Composite Actions vs. Reusable Workflows |Criterion|Composite Action|Reusable Workflow| |---|---|---| |Unit of composition|Step|Job (or set of jobs)| |Can contain jobs?|No|Yes| |Uses secrets?|Via inputs|Via `secrets:` interface| |Best for|Tool setup, utility steps|Complete pipeline stages| |Published to Marketplace?|Yes|No| ### Example: Composite Action for Internal Setup ```yaml # actions/setup-platform/action.yml name: 'Platform Setup' description: 'Installs internal tools and configures auth' inputs: environment: description: 'Target environment' required: true aws-region: description: 'AWS region' default: 'eu-west-1' outputs: cluster-name: description: 'EKS cluster name' value: ${{ steps.get-cluster.outputs.name }} runs: using: composite steps: - name: Install kubectl shell: bash run: | curl -LO "https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl" chmod +x kubectl && sudo mv kubectl /usr/local/bin/ - name: Install Helm shell: bash run: | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - name: Install internal CLI shell: bash run: pip install mycompany-cli==2.1.0 --break-system-packages - name: Get cluster name id: get-cluster shell: bash env: ENVIRONMENT: ${{ inputs.environment }} run: | NAME=$(mycompany-cli cluster get --env $ENVIRONMENT --output name) echo "name=$NAME" >> $GITHUB_OUTPUT ``` **Usage in a workflow:** ```yaml - name: Platform setup uses: myorg/platform-actions/setup-platform@v2 with: environment: production - name: Deploy run: helm upgrade --install myapp ./chart -n production ``` --- ## Matrix Strategies Matrix strategies run a single job configuration across a set of variable combinations, in parallel. ### Multi-Version, Multi-OS Testing ```yaml jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false # don't cancel other matrix runs on first failure max-parallel: 6 matrix: os: [ubuntu-latest, windows-latest, macos-latest] python: ['3.10', '3.11', '3.12'] include: # Add a specific combination with an extra variable - os: ubuntu-latest python: '3.13-dev' experimental: true exclude: # Skip macOS + Python 3.10 (not supported) - os: macos-latest python: '3.10' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: pip install -e ".[test]" - run: pytest continue-on-error: ${{ matrix.experimental == true }} ``` ### Dynamic Matrix from Script ```yaml jobs: generate-matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set.outputs.matrix }} steps: - uses: actions/checkout@v4 - id: set run: | # Generate matrix from changed services (monorepo pattern) SERVICES=$(git diff --name-only HEAD~1 HEAD \ | grep '^services/' \ | cut -d/ -f2 \ | sort -u \ | jq -R . | jq -sc .) echo "matrix={\"service\":$SERVICES}" >> $GITHUB_OUTPUT build: needs: generate-matrix runs-on: ubuntu-latest strategy: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v4 - run: docker build services/${{ matrix.service }} ``` --- ## Advanced Topics ### Concurrency Control Concurrency groups prevent multiple deployments to the same environment running simultaneously: ```yaml # Cancel previous runs on the same PR branch concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # For deployments: queue runs, never cancel in-progress deploy jobs: deploy: concurrency: group: deploy-production cancel-in-progress: false ``` ### Caching ```yaml - name: Cache Gradle dependencies uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | gradle-${{ runner.os }}- # Docker layer cache via GitHub Actions Cache - uses: docker/build-push-action@v6 with: cache-from: type=gha cache-to: type=gha,mode=max ``` ### Monorepo Support Efficient monorepo CI triggers only the services affected by a change: ```yaml on: push: paths: - 'services/api/**' - 'libs/shared/**' # rebuild api if shared lib changes jobs: detect-changes: runs-on: ubuntu-latest outputs: api: ${{ steps.changes.outputs.api }} web: ${{ steps.changes.outputs.web }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: changes with: filters: | api: - 'services/api/**' - 'libs/shared/**' web: - 'services/web/**' - 'libs/shared/**' build-api: needs: detect-changes if: needs.detect-changes.outputs.api == 'true' runs-on: ubuntu-latest steps: - run: echo "Building API" ``` ### Conditional Execution and Expressions ```yaml steps: # Run only on main branch push - if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: deploy.sh # Always run (even if previous steps failed) - if: always() run: cleanup.sh # Run only if a specific job succeeded - if: needs.test.result == 'success' && !cancelled() run: publish.sh # Use contains() / startsWith() / endsWith() - if: contains(github.event.pull_request.labels.*.name, 'deploy-to-staging') run: deploy-staging.sh ``` ### Dynamic Outputs and Step Summary ```yaml - name: Generate version id: version run: | TAG="v$(date +%Y.%m.%d)-${{ github.run_number }}" echo "tag=$TAG" >> $GITHUB_OUTPUT echo "sha=${GITHUB_SHA:0:8}" >> $GITHUB_OUTPUT - name: Report run: | cat >> $GITHUB_STEP_SUMMARY << 'EOF' ## Deployment Summary 🚀 | Key | Value | |-----|-------| | Version | ${{ steps.version.outputs.tag }} | | SHA | ${{ steps.version.outputs.sha }} | | Environment | production | | Status | ✅ Success | EOF ``` --- ## GitOps Integration GitHub Actions is a natural fit for GitOps workflows, where the Git repository is the single source of truth for both application code and infrastructure state. ### Argo CD Integration ```yaml # Trigger Argo CD sync after image push - name: Update image tag in GitOps repo uses: actions/checkout@v4 with: repository: myorg/gitops-config token: ${{ secrets.GITOPS_PAT }} path: gitops - name: Update Helm values run: | cd gitops yq e -i '.image.tag = "${{ inputs.image-tag }}"' \ apps/myapp/values-${{ inputs.environment }}.yaml git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -am "chore: update myapp to ${{ inputs.image-tag }}" git push # Argo CD watches the gitops repo and auto-syncs ``` Alternatively, trigger sync directly: ```yaml - name: Argo CD Sync run: | argocd app sync myapp-${{ inputs.environment }} \ --auth-token ${{ secrets.ARGOCD_TOKEN }} \ --server argocd.internal.example.com \ --grpc-web \ --timeout 300 ``` ### FluxCD Integration With FluxCD, GitHub Actions typically updates image tags in the GitOps repository, and Flux's image automation controller reconciles the cluster state: ```yaml - name: Update image policy run: | flux reconcile image repository myapp \ --kubeconfig ${{ steps.kubeconfig.outputs.path }} ``` ### Infrastructure as Code with Terraform The Terraform example in Example 4 demonstrates the complete pattern: 1. `pull_request` → plan + post comment 2. `push` to `main` → apply This "plan on PR, apply on merge" pattern is the standard GitOps approach for infrastructure. --- ## GitHub Actions for Platform Engineering Platform engineering teams use GitHub Actions as the automation backbone of their **Internal Developer Platform (IDP)**. The goal is to create **golden paths** — opinionated, pre-approved routes for common development tasks. ### Golden Paths A golden path is a templated workflow that encodes the platform team's best practices: ```yaml # .github/workflows/_golden-path-microservice.yml # Golden path for deploying a microservice to Kubernetes on: workflow_call: inputs: service-name: type: string required: true environment: type: string required: true jobs: compliance-check: name: Compliance Validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check for required files run: | for f in Dockerfile chart/Chart.yaml OWNERS.md; do [ -f "$f" ] || (echo "Missing required file: $f" && exit 1) done - name: Validate SBOM policy run: sbom-checker --policy ./policy/sbom.yaml security-gate: name: Security Gate runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: aquasecurity/trivy-action@master with: exit-code: '1' severity: 'HIGH,CRITICAL' build: needs: [compliance-check, security-gate] uses: myorg/.github/.github/workflows/_docker-build.yml@main with: service-name: ${{ inputs.service-name }} deploy: needs: build uses: myorg/.github/.github/workflows/_deploy-k8s.yml@main with: environment: ${{ inputs.environment }} secrets: inherit ``` ### Developer Self-Service Platform teams expose `workflow_dispatch` as a self-service interface: ```yaml on: workflow_dispatch: inputs: action: type: choice options: [create-namespace, rotate-secrets, run-migration, enable-feature-flag] target: type: string description: 'Service or resource name' ``` This enables developers to perform approved operations without filing tickets or waiting for an ops engineer. ### SDLC Governance GitHub Actions enforces SDLC standards through required workflows at the organization level (GitHub Enterprise feature): - All repositories must pass a central security scan before merging - Required deployment workflow enforces DORA metrics collection - Mandatory evidence generation (test results, SBOM, provenance) for DORA compliance ### Automated Compliance Evidence ```yaml - name: Generate compliance evidence run: | # DORA change failure rate tracking cat > evidence.json << EOF { "deployment_id": "${{ github.run_id }}", "service": "${{ inputs.service-name }}", "environment": "${{ inputs.environment }}", "image_tag": "${{ inputs.image-tag }}", "deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "deployed_by": "${{ github.actor }}", "workflow": "${{ github.workflow }}", "sha": "${{ github.sha }}" } EOF aws s3 cp evidence.json \ s3://compliance-evidence/deployments/${{ github.run_id }}.json ``` --- ## Enterprise-Scale GitHub Actions ### Governance Framework At enterprise scale, GitHub Actions governance operates across three layers: **Organization-level policies** (GitHub Enterprise): - Which Actions are allowed (allow-list by owner, verified creator, or specific SHA) - Whether self-hosted runners are permitted - Required workflows that run on all repositories - Default permissions for `GITHUB_TOKEN` **Repository-level controls**: - Branch protection rules (required status checks, required reviews) - Environment protection rules (required approvers, deployment branch rules) - Code owners for workflow files (`CODEOWNERS`: `.github/workflows/* @platform-team`) **Workflow-level controls**: - `permissions: {}` as default - Action pinning enforced by CI - Environment gates for production ### Compliance and Auditability The GitHub Audit Log (available at organization and enterprise level) records: - Every workflow run: who triggered it, from which branch, with what result - Secret access events - Runner registration and removal - Permission changes Export audit logs to SIEM systems: ```bash # Stream audit log to Splunk via GitHub CLI gh api /orgs/myorg/audit-log \ --paginate \ --field phrase="action:workflows.run" \ | jq . >> audit-export.jsonl ``` ### Cost Management |Strategy|Savings Potential| |---|---| |Cache dependencies aggressively|30–60% reduction in job duration| |Use `paths:` filtering to skip irrelevant jobs|40–70% fewer runs for docs/config changes| |Migrate high-volume jobs to self-hosted runners|60–80% cost reduction at scale| |Use `cancel-in-progress: true` on PRs|Eliminate wasted minutes on superseded runs| |Choose `ubuntu-latest` over Windows/macOS|2–10× cheaper per minute| |Optimize Docker builds with layer caching|40–70% faster builds| ### Required Workflows (Enterprise) GitHub Enterprise allows organization administrators to mandate that specific workflows run on all repositories and must pass before any merge: ``` Organization Settings → Actions → Required workflows → Add workflow: myorg/.github/.github/workflows/required-security-scan.yml@main → Apply to: All repositories ``` This ensures that no team can bypass security scanning, no matter what their local workflow configuration says. --- ## Real-World Enterprise Examples ### Banking — Compliance-First Deployment A major bank's deployment pipeline enforces regulatory controls: ```yaml jobs: compliance-approval: runs-on: ubuntu-latest environment: production # Requires 2 senior approvers generate-change-record: needs: compliance-approval runs-on: ubuntu-latest steps: - name: Create ServiceNow change ticket run: | curl -X POST ${{ vars.SNOW_ENDPOINT }}/api/now/table/change_request \ -H "Authorization: Bearer ${{ secrets.SNOW_TOKEN }}" \ -d '{"short_description":"Deploy ${{ inputs.service }} ${{ inputs.version }}"}' deploy-canary: needs: generate-change-record runs-on: [self-hosted, linux, eks-prod] steps: - run: kubectl set image deployment/myapp myapp=myapp:${{ inputs.version }} -n prod validate-canary: needs: deploy-canary steps: - run: | # Monitor error rate for 10 minutes sleep 600 ERROR_RATE=$(datadog-metrics query 'errors.rate{service:myapp}') [ "$ERROR_RATE" -lt "0.01" ] || (kubectl rollout undo deployment/myapp && exit 1) deploy-full: needs: validate-canary environment: production-full # Second approval gate ``` ### FinTech — Multi-Region Zero-Downtime Deploy ```yaml jobs: deploy-eu-west: uses: ./.github/workflows/_deploy-region.yml with: region: eu-west-1 environment: production deploy-us-east: needs: deploy-eu-west uses: ./.github/workflows/_deploy-region.yml with: region: us-east-1 environment: production deploy-ap-southeast: needs: deploy-us-east uses: ./.github/workflows/_deploy-region.yml with: region: ap-southeast-1 environment: production ``` ### SaaS — Feature Flag Automation ```yaml on: pull_request: types: [labeled] jobs: enable-feature-flag: if: contains(github.event.pull_request.labels.*.name, 'test-in-staging') runs-on: ubuntu-latest steps: - name: Enable feature flag in staging run: | launchdarkly-cli flag update \ --flag ${{ github.head_ref }} \ --env staging \ --on true ``` --- ## Comparison with Other CI/CD Systems |Feature|GitHub Actions|Jenkins|GitLab CI/CD|Azure DevOps|CircleCI|TeamCity|Argo Workflows| |---|---|---|---|---|---|---|---| |**Ease of use**|⭐⭐⭐⭐⭐|⭐⭐|⭐⭐⭐⭐|⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐|⭐⭐| |**Scalability**|⭐⭐⭐⭐|⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐⭐| |**Security**|⭐⭐⭐⭐⭐|⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐|⭐⭐⭐|⭐⭐⭐| |**Extensibility**|⭐⭐⭐⭐⭐|⭐⭐⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐|⭐⭐⭐⭐|⭐⭐⭐⭐| |**GitHub native**|⭐⭐⭐⭐⭐|⭐⭐|⭐⭐|⭐⭐|⭐⭐⭐|⭐⭐|N/A| |**Cost**|Free tier, then per-min|Infrastructure only|Free tier on GitLab.com|Included with M365|Per-min|Per-agent|Infrastructure only| |**Learning curve**|Low|High|Medium|Medium|Low|Medium|Very High| ### Jenkins Jenkins is the most flexible CI system and has the largest plugin ecosystem, but it requires significant operational overhead: server management, plugin updates, security patching, backup, and scaling. Its strength is extensibility for complex enterprise pipelines that have been built and tuned over years. The migration cost away from Jenkins is the primary reason organizations keep using it. ### GitLab CI/CD GitLab CI is the closest structural equivalent to GitHub Actions. If your source control is GitLab, it is the natural choice. YAML syntax is similar. The main disadvantage is that it's only compelling if you use GitLab for source control — integrating with GitHub requires extra configuration. ### Azure DevOps Pipelines Tightly integrated with Microsoft ecosystem (Azure, M365, Active Directory). Strong for .NET and Microsoft-stack shops. YAML pipelines are powerful but more complex than GitHub Actions. Best choice when Azure is the primary cloud and Microsoft enterprise licensing is in place. ### CircleCI CircleCI has a similar philosophy to GitHub Actions (hosted, per-minute) and was an early pioneer. Its orbs system predates GitHub's reusable workflows. GitHub Actions has largely caught up in functionality, and the lack of native GitHub integration puts CircleCI at a disadvantage for GitHub-hosted repos. ### TeamCity JetBrains TeamCity is popular in enterprises with existing JetBrains tooling (IntelliJ, YouTrack). It offers excellent build chain management and Kotlin DSL for pipeline-as-code. Requires self-managed server infrastructure. ### Argo Workflows Argo Workflows is a Kubernetes-native workflow engine, best suited for data pipelines, ML training jobs, and complex DAG-based workflows. It is not a code-delivery CI system by default — it complements GitHub Actions rather than replaces it. The combination of GitHub Actions (for CI, security scanning, Docker build) and Argo Workflows (for long-running ML pipelines or data ETL) is a common pattern in cloud-native organizations. --- ## Common Mistakes and Anti-Patterns ### 1. Checking Out and Executing Fork Code in `pull_request_target` ```yaml # DANGEROUS - gives secrets access to fork code on: pull_request_target: jobs: build: steps: - uses: actions/checkout@v4 # checks out fork code with base branch context - run: npm test # executes attacker-controlled code with secrets access ``` **Fix**: If you need `pull_request_target` for secrets, separate the checkout and the secret-using steps, or use `pull_request` for untrusted code. ### 2. Using Mutable Tags for Third-Party Actions ```yaml # Bad - tag can be moved to a different commit - uses: actions/checkout@v4 # Good - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 ``` ### 3. Printing Secrets in Run Steps ```yaml # Bad - leaks secret if echo is in run - run: echo "Token is ${{ secrets.API_TOKEN }}" # GitHub masks known secrets, but this is still bad practice # and derived values (base64, reversed) are not masked ``` ### 4. `permissions: write-all` as Default Many tutorials use `permissions: write-all` to avoid permission errors. This violates the principle of least privilege and grants unnecessary access to every resource. ### 5. Not Using Concurrency for Deployments Without concurrency control, two pushes to `main` in quick succession can trigger two simultaneous production deploys, causing race conditions and undefined state. ### 6. Storing Secrets in Environment Variables at Workflow Level ```yaml # Bad - secret visible to all jobs and potentially in logs env: API_KEY: ${{ secrets.API_KEY }} # Good - scope to the specific step that needs it - name: Deploy env: API_KEY: ${{ secrets.API_KEY }} run: deploy.sh ``` ### 7. Large Monolithic Workflows A single 500-line workflow file that does everything is hard to maintain and impossible to reuse. Break into reusable workflows and composite actions. ### 8. Not Caching Dependencies Every job that runs `npm install` or `pip install` without caching downloads the internet on every run. This is slow and expensive. ### 9. Ignoring `fail-fast` in Matrices The default `fail-fast: true` cancels all matrix jobs when one fails. For cross-platform testing, this prevents you from seeing which platforms passed and which failed. Set `fail-fast: false` for diagnostic clarity. --- ## Performance and Cost Optimization ### Performance **Cache everything that changes infrequently:** ```yaml - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # automatic npm cache # Manual cache for custom paths - uses: actions/cache@v4 with: path: ~/.cargo/registry key: cargo-${{ hashFiles('Cargo.lock') }} ``` **Parallelize aggressively:** - Split lint, test, and build into separate parallel jobs - Use matrix for cross-platform/cross-version runs - Run security scans in parallel with unit tests **Use `paths:` filters** to skip workflows for irrelevant changes: ```yaml on: push: paths-ignore: - 'docs/**' - '*.md' - '.github/ISSUE_TEMPLATE/**' ``` **Prefer `ubuntu-latest`**: It is 2× cheaper than Windows and 10× cheaper than macOS per minute. ### Cost Optimization |Technique|Impact| |---|---| |Cache dependencies|High — reduces job duration 30–60%| |`paths:` filtering|High — eliminates unnecessary runs entirely| |`cancel-in-progress: true` on PR branches|Medium — cancels stale runs| |Migrate bulk to self-hosted runners|Very high at scale| |Use smaller runner sizes where possible|Medium| |Set appropriate artifact `retention-days`|Low — storage cost| |Audit and remove unused workflows|Low-Medium| Estimate your monthly cost: ``` Monthly cost = (average_job_minutes × jobs_per_day × working_days × os_multiplier × price_per_minute) ``` GitHub charges $0.008/minute for Linux, $0.016/minute for Windows, $0.08/minute for macOS (as of 2025). --- ## Migration Guides ### Jenkins to GitHub Actions **Conceptual mapping:** |Jenkins|GitHub Actions| |---|---| |`Jenkinsfile`|`.github/workflows/*.yml`| |Stage|Job| |Step|Step| |Agent|Runner (`runs-on:`)| |Shared Library|Reusable workflow / Composite action| |Credentials|Secrets| |Plugin|Marketplace Action| |Pipeline input()|`workflow_dispatch` + `environment`| **Example migration:** Jenkins: ```groovy pipeline { agent { label 'linux' } stages { stage('Test') { steps { sh 'npm ci && npm test' } } stage('Deploy') { when { branch 'main' } steps { withCredentials([string(credentialsId: 'api-key', variable: 'API_KEY')]) { sh './deploy.sh' } } } } } ``` GitHub Actions equivalent: ```yaml on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npm test deploy: if: github.ref == 'refs/heads/main' needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./deploy.sh env: API_KEY: ${{ secrets.API_KEY }} ``` **Migration strategy:** 1. Inventory existing pipelines — identify unique patterns and shared libraries 2. Map shared library functions to composite actions or reusable workflows 3. Migrate lowest-risk pipelines first (feature teams, non-production) 4. Keep Jenkins running in parallel during migration 5. Establish the central workflow library before migrating many repositories 6. Automate credential migration from Jenkins to GitHub Secrets ### Azure DevOps to GitHub Actions |Azure DevOps|GitHub Actions| |---|---| |`azure-pipelines.yml`|`.github/workflows/*.yml`| |Stage|Job| |Job|Job| |Task|Step (`uses:` or `run:`)| |Variable group|Organization/repo variables| |Key Vault integration|OIDC + `azure/get-keyvault-secrets`| |Environments|Environments| |Service connections|OIDC + Federated credentials| **Key differences**: Azure DevOps has stages inside pipelines (stages → jobs → steps). GitHub Actions has only jobs → steps. Stages are modeled as jobs with `needs:` dependencies. ### GitLab CI to GitHub Actions |GitLab CI|GitHub Actions| |---|---| |`.gitlab-ci.yml`|`.github/workflows/*.yml`| |Stage|(modeled via `needs:`)| |Job|Job| |Script|`run:`| |`include:`|`uses:` (reusable workflow)| |Variables|`vars:` / `secrets:`| |Protected variables|Environment secrets| |Environments|Environments| |Runner|Runner| **Structural difference**: GitLab CI uses stages to define ordering; GitHub Actions uses explicit `needs:` dependencies. This gives GitHub Actions more flexibility (any DAG topology) but requires more explicit dependency declarations. --- ## Key Takeaways, Best Practices Checklist, and Future Trends ### Key Takeaways 1. **GitHub Actions is a platform, not just a CI runner.** Its event system, reusable workflows, environments, and OIDC integration make it suitable for the full software delivery lifecycle. 2. **Security must be designed in, not added later.** Pin Actions by SHA, use OIDC, set `permissions: {}`, scope secrets to environments. 3. **Reusable workflows are the platform engineering primitive.** Centralize pipeline logic in `myorg/.github` and let teams consume, not define, their CI/CD. 4. **OIDC is the correct credential model for cloud deployments.** Eliminate static AWS/GCP/Azure keys from secrets entirely. 5. **Self-hosted runners are not optional at regulated-industry scale.** Private network access, compliance data isolation, and cost at volume all drive self-hosted adoption. 6. **Monorepo support requires thoughtful design.** Dynamic matrices, `paths:` filters, and change detection patterns are essential. ### Recommended Architecture for 2026 ``` Platform Team owns: myorg/.github ├── .github/workflows/ │ ├── _ci-{language}.yml # Language-specific CI templates │ ├── _docker-build.yml # Container build with SLSA + Sigstore │ ├── _deploy-k8s.yml # Kubernetes deployment via Helm │ ├── _deploy-serverless.yml # Lambda / Cloud Run deployment │ ├── _terraform.yml # Infrastructure management │ ├── _security-scan.yml # Mandatory security gate │ ├── _compliance-evidence.yml # DORA / SOC2 evidence collection │ └── required-gate.yml # Org-required workflow (Enterprise) └── actions/ ├── setup-platform/ # Internal tool setup composite ├── notify-slack/ # Notification composite └── generate-evidence/ # Compliance evidence composite Application Teams consume: myservice/.github/workflows/ ├── ci.yml → uses: myorg/.github/_ci-node.yml@main └── cd.yml → uses: myorg/.github/_deploy-k8s.yml@main Infrastructure: - Actions Runner Controller (ARC) on Kubernetes - IRSA / Workload Identity for cloud credentials - GitHub Enterprise with Required Workflows - Audit log → Splunk / Datadog ``` ### Future Trends in CI/CD and GitHub Actions **AI-assisted workflows.** GitHub Copilot is already generating workflow files. The next step is AI agents that observe build failures and suggest fixes, optimize caching automatically, or generate security patches. **Mandatory SLSA compliance.** Supply chain security is moving from best practice to regulatory requirement, particularly in financial services and government sectors. GitHub's native SLSA Level 3 provenance support positions Actions well. **Merge queues and queue-based deployments.** GitHub's Merge Queue feature (GA in 2023) integrates with Actions to run pre-merge CI on batches of PRs, significantly increasing throughput for high-velocity teams. **Dagger and portable CI.** Projects like Dagger (container-native CI SDK) aim to make pipeline logic portable across CI systems. This may reduce CI lock-in but does not eliminate the need for an orchestration layer like GitHub Actions. **eBPF-based runner observability.** Deep observability of runner behavior (system calls, network connections) using eBPF will become standard for high-security environments, enabling anomaly detection and supply chain attack prevention at the runner level. **GitHub Actions as an IDP backbone.** The trend of using GitHub Actions as the automation layer for Internal Developer Platforms will accelerate, with tighter integrations into backstage.io, Port, Cortex, and similar IDP frameworks for service catalog integration and self-service provisioning. --- _GitHub Actions — CI/CD — DevOps — Platform Engineering — GitOps — Software Delivery — Developer Experience — DevSecOps — Kubernetes — OIDC — SLSA — Supply Chain Security_ --- **About this article:** Written for engineering practitioners who need both the conceptual framework and the practical YAML to implement GitHub Actions at scale. All examples are based on real-world patterns observed in banking, FinTech, SaaS, and cloud-native engineering organizations.