GitHub Actions — Patterns¶
OWNER: alex, tjitte
ALSO_USED_BY: leon, marta, iwona
LAST_VERIFIED: 2026-03-26
GE_STACK_VERSION: GitHub Actions (github-hosted runners, ubuntu-latest)
Overview¶
Workflow patterns used in GE: reusable workflows, composite actions,
matrix builds, caching strategies, secrets management, and environment
protection rules. All patterns optimised for the GE monorepo and
multi-project structure.
Reusable Workflows¶
Reusable workflows extract shared CI/CD logic into callable units.
GE prefixes these with reusable- for clarity.
Defining a Reusable Workflow¶
# .github/workflows/reusable-docker-build.yml
name: Reusable Docker Build
on:
workflow_call:
inputs:
image-name:
description: "Docker image name"
required: true
type: string
context:
description: "Build context path"
required: true
type: string
secrets:
registry-token:
required: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c816612b # v6.18.0
with:
context: ${{ inputs.context }}
tags: ${{ inputs.image-name }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
push: true
Calling a Reusable Workflow¶
jobs:
build-executor:
uses: ./.github/workflows/reusable-docker-build.yml
with:
image-name: ge-executor
context: ./ge_agent
secrets:
registry-token: ${{ secrets.REGISTRY_TOKEN }}
CHECK: reusable workflows have clear input/output contracts
CHECK: all inputs have description and type
CHECK: secrets are passed explicitly, not inherited
Composite Actions¶
Composite actions bundle multiple steps into a single action.
Use for steps shared within a single workflow (not across workflows).
# .github/actions/setup-ge/action.yml
name: "Setup GE Environment"
description: "Install dependencies and configure GE development environment"
inputs:
node-version:
description: "Node.js version"
default: "22"
runs:
using: composite
steps:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 9
- run: pnpm install --frozen-lockfile
shell: bash
- run: pnpm exec playwright install --with-deps chromium
shell: bash
if: ${{ hashFiles('playwright.config.ts') != '' }}
CHECK: composite actions live in .github/actions/{name}/action.yml
CHECK: all run steps specify shell: bash
Matrix Builds¶
Matrix builds run jobs across multiple configurations simultaneously.
GE uses matrices for multi-project testing in the monorepo.
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
project:
- admin-ui
- ge-orchestrator
- ge-executor
include:
- project: admin-ui
test-command: "pnpm test"
working-directory: "./admin-ui"
- project: ge-orchestrator
test-command: "pytest"
working-directory: "./ge-orchestrator"
- project: ge-executor
test-command: "pytest"
working-directory: "./ge_agent"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: ${{ matrix.test-command }}
working-directory: ${{ matrix.working-directory }}
CHECK: fail-fast: false — do not cancel other matrix jobs when one fails
CHECK: matrix size stays under 256 combinations (GitHub limit)
Caching¶
Node Modules (pnpm)¶
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.pnpm-store
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
Docker Layer Caching¶
- uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c816612b # v6.18.0
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Python (pip)¶
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-
CHECK: cache keys include lockfile hash for cache invalidation
CHECK: restore-keys provides fallback for partial matches
CHECK: cache size monitored — clean up if approaching repository limit
Secrets Management¶
Environment Secrets¶
GE uses GitHub Environments with secrets scoped per deployment zone:
| Environment | Purpose | Secrets |
|---|---|---|
development |
Zone 1 deploys | UpCloud dev credentials |
staging |
Zone 2 deploys | UpCloud staging credentials |
production |
Zone 3 deploys | UpCloud production credentials, bunny.net API key |
jobs:
deploy:
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploying with ${{ secrets.UPCLOUD_TOKEN }}"
CHECK: secrets are scoped to the narrowest environment
CHECK: production secrets only accessible from production environment
READ_ALSO: wiki/docs/stack/github-actions/security.md
Repository Secrets¶
For secrets needed across all environments (e.g., code coverage tokens):
CHECK: prefer environment secrets over repository secrets
CHECK: never log secrets — use ::add-mask:: if dynamic masking is needed
Environment Protection Rules¶
GE enforces protection rules on staging and production environments:
| Rule | Staging | Production |
|---|---|---|
| Required reviewers | 1 (thijmen) | 2 (rutger + arjan) |
| Wait timer | None | 5 minutes |
| Branch restriction | main only |
main only |
| Deployment branches | main, release/* |
main, release/* |
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
# Deployment steps...
CHECK: production deploys require 2 reviewers
CHECK: deployments only from main or release/* branches
CHECK: wait timer gives time to cancel accidental deploys
Workflow Dispatch¶
For manual triggers (emergency deploys, maintenance tasks):
on:
workflow_dispatch:
inputs:
zone:
description: "Deployment zone"
required: true
type: choice
options:
- staging
- production
reason:
description: "Reason for manual deploy"
required: true
type: string
CHECK: manual workflows require a reason input for audit trail
CHECK: workflow_dispatch inputs have descriptions and types
Job Dependencies¶
jobs:
lint:
runs-on: ubuntu-latest
# ...
test:
runs-on: ubuntu-latest
# ...
build:
runs-on: ubuntu-latest
needs: [lint, test]
# ...
deploy:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main'
# ...
CHECK: deploy jobs depend on build
CHECK: deploy jobs have if condition for branch restriction
CHECK: independent jobs (lint, test) run in parallel
Artifact Passing¶
jobs:
build:
steps:
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: docker-image
path: image.tar
retention-days: 1
deploy:
needs: build
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: docker-image
CHECK: artifact retention set to minimum needed (1 day for CI, 7 for releases)
CHECK: artifact names are descriptive
Cross-References¶
READ_ALSO: wiki/docs/stack/github-actions/index.md
READ_ALSO: wiki/docs/stack/github-actions/security.md
READ_ALSO: wiki/docs/stack/github-actions/pitfalls.md
READ_ALSO: wiki/docs/stack/github-actions/checklist.md