Skip to content

About

Shared workflows for GH Actions

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

64 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WeMove Reusable CI Workflows

A collection of reusable GitHub Actions workflows (on: workflow_call) and composite actions shared across WeMoveEU projects. Caller repositories reference these by path and version tag from their own .github/workflows.

Using these workflows

Pin to the floating major tag — the current major is @v14:

uses: WemoveEU/ci-workflows/.github/workflows/docker-build.yml@v14
uses: WemoveEU/ci-workflows/.github/actions/docker-smoke@v14

Releases follow semver with a floating major (see RELEASING.md): each release gets an immutable vMAJOR.MINOR.PATCH tag, and the floating vMAJOR tag is moved to point at it. Consumers pinned to @v14 pick up backward-compatible minor/patch fixes automatically; a breaking change cuts a new major (v15) that you opt into deliberately (Dependabot opens the PR). Always pin the floating major, never a branch.

The workflows and actions available:


docker-build.yml

Builds a Docker image and pushes it to GitHub Container Registry (ghcr.io/<name>). This is the foundational workflow that the others build on. It supports staging and production environments via its tagging strategy.

Inputs

  • dockerfile: Path to the Dockerfile used for building the image. Default Dockerfile.

  • name: Name of the image in the registry (pushed to ghcr.io/<name>). Default ${{github.repository}}.

  • dir: The root of app sources / build context. Use if your app is in a subdirectory, e.g. frontend. Default ..

  • overlay: Name of an artifact to unpack over the checked-out source before building (e.g. env files). Use a unique name based on ${{github.run_id}} to avoid races between concurrent runs. The artifact is deleted after it is unpacked.

  • prepare: Shell commands run (via bash) after unpacking the overlay, before building the image.

  • args: Docker build args (one variable, or multiline format), passed to the build-args input of docker/build-push-action.

  • production_branch: The name of the production branch. Default main.

Secrets

  • personal_token: Optional personal access token used to check out private repositories and submodules. Falls back to github.token when not provided.

Image tagging strategy

Tags are generated by docker/metadata-action from the git context:

  • feature/* branches → feature tag + the branch ref
  • Semver tags (v1.2.3) → version / major.minor / major / release tags
  • Push to production_branch → latest
  • Push to the default branch when it is not the production branch → staging

If the ref matches none of these rules (e.g. a release/* or otherwise unrecognised branch), no tag is produced. In that case the image is built but not pushed — the run still validates the Dockerfile, and a warning is emitted — rather than failing with tag is needed when pushing to registry.

Jobs

The release job runs on ubuntu-latest with contents: read, packages: write, and actions: write permissions (the last is needed because the overlay artifact is deleted via the Actions API). Its steps:

  1. Checkout Code: actions/checkout clones the repository and its submodules (fetch-depth: 20).

  2. Apply Overlay: If overlay is set, actions/download-artifact downloads it and geekyeggo/delete-artifact removes it. Create the overlay in the caller with actions/upload-artifact; its files are unpacked into the project directory. See this example.

  3. Run Prepare Script: Executes the prepare shell commands if provided.

  4. Generate Image Tag: docker/metadata-action produces tags per the strategy above.

  5. Set up Docker Buildx: docker/setup-buildx-action.

  6. Login to GitHub Container Registry: docker/login-action (authenticates to ghcr.io with GITHUB_TOKEN). Skipped when no tag matched.

  7. Build and Push: docker/build-push-action builds from dir using dockerfile and pushes the generated tags (build-only when no tag matched).

Sample usage

name: Deploy
on:
  push:
    branches:
      - main
      - "release/*"
jobs:
  release:
    uses: WemoveEU/ci-workflows/.github/workflows/docker-build.yml@v14
    with:
      production_branch: ${{vars.CI_PRODUCTION_BRANCH}}
      dockerfile: server/Dockerfile
      dir: server
      name: crm/server

deploy-strapi.yml

Orchestrates a Strapi deploy by calling docker-build.yml twice — backend first (backend/Dockerfile, image <repo>/backend), then frontend (frontend/Dockerfile, image <repo>/frontend). Between them, a wait-for-backend job polls the live backend until it is ready.

Inputs

  • backend_args: Docker build args for the backend image. Default ''.
  • frontend_args: Docker build args for the frontend image. Default ''.
  • production_branch: The name of the production branch. Default main.
  • deploy_marker: Filename in the Strapi public folder whose presence means the backend is deployed. The health check polls https://strapi.${{vars.DOMAIN}}/<deploy_marker> and only runs when this input is provided.

Secrets

  • personal_token: Optional personal access token, forwarded to docker-build.yml for private repositories and submodules.

Notes

  • The wait-for-backend job selects its GitHub Environment dynamically: production when on the vars.CI_PRODUCTION_BRANCH branch or a tag starting with v; otherwise staging.
  • Overlay names are made run-unique with ${{github.run_id}} (backend_env_<run_id>, frontend_env_<run_id>) to avoid collisions between concurrent runs.
  • Calling repositories must define the repository variables vars.CI_PRODUCTION_BRANCH and vars.DOMAIN.

notify.yml

Posts a Slack notification (green on success, red on failure) via slackapi/slack-github-action (incoming-webhook mode). Call it as a final step from a caller workflow.

Secrets

  • slack_webhook_url: Slack channel incoming-webhook URL (required).

python-build.yml

Builds a Python package with uv: checks out the repo (actions/checkout), installs uv (astral-sh/setup-uv), and runs uv build. Takes no inputs.


dependabot-auto-merge.yml

Enables auto-merge on Dependabot PRs for low-risk update types. Call it from a caller workflow triggered on: pull_request; the reusable only acts on PRs authored by dependabot[bot].

Auto-merge only completes once the target branch's required status checks pass, so this depends on a branch ruleset (require a PR + required checks) on the consuming repo. Without that ruleset gh pr merge --auto has nothing to wait for and the PR merges immediately.

Arming is retried five times, and falls back to a direct merge. That covers two real failure modes: gh pr merge --auto intermittently returns a transient GraphQL error, and auto-merge cannot be enabled at all on a PR whose required checks have already gone green — which fast CI makes common, and which otherwise leaves the PR sitting green and unmerged. The direct merge is not a bypass: branch protection still refuses it while required checks are pending or failing.

Inputs

  • merge-method: merge | squash | rebase. Default squash.

  • allowed-update-types: Comma-separated Dependabot update-types to auto-merge, matched as whole tokens (no spaces around the commas). Default version-update:semver-patch,version-update:semver-minor. Anything outside the list — majors included — is left for manual review.

Secrets

  • app-id / app-private-key: Optional GitHub App credentials used to arm the merge. Pass these whenever merging is meant to trigger something downstream. A merge armed with GITHUB_TOKEN has its push event suppressed by GitHub's recursion guard, so a deploy that runs on: push to the default branch never runs — the PR goes green and nothing ships. Omit them and the workflow falls back to GITHUB_TOKEN and warns in the run log.

    The App needs two repository permissions: Contents (read/write) and Pull requests (read/write); Metadata (read) is mandatory and comes along. It does not need Workflows (write) — that permission governs pushing workflow files through the Contents API, not merging a PR that changes them.

Sample usage

name: Dependabot auto-merge

on:
  pull_request:

permissions:
  contents: write
  pull-requests: write

jobs:
  auto-merge:
    if: github.actor == 'dependabot[bot]'
    uses: WeMoveEU/ci-workflows/.github/workflows/dependabot-auto-merge.yml@v14
    secrets:
      app-id: ${{ secrets.DEPENDABOT_POLICY_APP_ID }}
      app-private-key: ${{ secrets.DEPENDABOT_POLICY_APP_KEY }}

The caller's permissions: block must grant contents: write and pull-requests: write — a called workflow's effective GITHUB_TOKEN permissions are capped by the caller's.


docker-smoke

Composite action (.github/actions/docker-smoke) that builds the repo's Docker image, runs the container, and verifies it actually serves an HTTP request sent with the production Host header. This catches two classes of bug a plain build check cannot: image-build breaks (e.g. a base-image bump dropping a tool the Dockerfile needs), and runtime serving breaks that only appear under the real hostname (e.g. a preview server's allowedHosts rejecting an unlisted Host with 403).

Use it as a step inside the job whose name is the required status check (ci under the org ruleset) so the context stays exactly that name (a reusable workflow would produce ci / <job> and break the required-check match).

With smoke-command it also runs a deeper check against the running container, and with fail-on-log-pattern it fails on server log lines that a page can swallow while still answering 200. Whenever a step fails, the last 200 lines of the container log are printed.

Inputs

  • host: Host header to send — the production hostname the app is served under. Required.
  • port: Container port to publish and probe. Default 8080.
  • path: Request path to probe. Default /.
  • expected-status: HTTP status code the probe must return. Default 200.
  • dockerfile: Path to the Dockerfile. Default Dockerfile.
  • context: Docker build context. Default ..
  • startup-timeout: Seconds to wait for the container to start responding. Default 90.
  • build-only: When "true", only build the image and skip running/probing it. Use for apps that can't boot standalone in CI (need runtime secrets, a database, etc.). Default "false".
  • smoke-command: Shell command run with bash after the probe succeeds, from the workspace root, with SMOKE_BASE_URL=http://localhost:<port> in its environment. A non-zero exit fails the action. Default "" (skipped).
  • fail-on-log-pattern: Extended regex (grep -E). After the probe and smoke-command, the action fails if the container log matches it, e.g. "MODULE_NOT_FOUND|Cannot find module". Default "" (skipped).

Sample usage

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: WemoveEU/ci-workflows/.github/actions/docker-smoke@v14
        with:
          host: app.example.org
          port: "3000"

With a smoke script and a log check (a Next.js frontend in frontend/):

      - uses: WeMoveEU/ci-workflows/.github/actions/docker-smoke@v14
        with:
          dockerfile: frontend/Dockerfile
          context: frontend
          host: example.org
          port: "3000"
          path: /en
          smoke-command: node frontend/scripts/smoke-test.mjs "$SMOKE_BASE_URL"
          fail-on-log-pattern: "MODULE_NOT_FOUND|Cannot find module"

Releases

Version tags v1–v13 predate the current scheme and are frozen point releases. The floating-major convention (see RELEASING.md) starts at v14.

  • v14.8 — docker-smoke: optional smoke-command (runs after the probe with SMOKE_BASE_URL set; its failure fails the action) and fail-on-log-pattern (fails when the container log matches an extended regex). The last 200 lines of the container log are now printed when any step fails, not only the probe. Existing callers are unaffected.
  • v14.6 — pin-override action: a first pass re-resolves every alerted package inside the ranges the tree already declares (yarn up -R <pkg>, lockfile only, manifest untouched) before any resolution is written. This is what clears a package installed at several versions, which Dependabot cannot fix, and a fix a hand-closed Dependabot PR taught Dependabot to ignore. Measured across the fleet on 2026-09-11: 220 audit findings to 102. New bumped output; changed is true when either pass changed something.
  • v14.5 — pin-override composite action: writes Yarn resolutions for the transitive advisories Dependabot cannot fix when a parent pins the vulnerable package exactly.
  • v14.2 — dependabot-auto-merge.yml: arming is now retried (5 attempts) with a direct-merge fallback, so a transient GraphQL error no longer strands a PR and an already-green PR still merges. Consolidates logic that existed only in the inline copies in wemove.eu, youmove, wemove-charity.eu and pubstatic, which this lets them drop.
  • v14.1 — dependabot-auto-merge.yml: accepts optional app-id / app-private-key secrets so the merge is armed with a GitHub App token and fires a real push event (a GITHUB_TOKEN-armed merge does not, leaving push-triggered deploys silently unrun). Falls back to GITHUB_TOKEN when the secrets are omitted, and warns. The update-type gate is now a whole-token, fail-closed match — previously an empty update-type from fetch-metadata satisfied contains() and could auto-merge a major.
  • v14 — First release under the semver + floating-major scheme. Ships the docker-smoke composite action (build + run + production-Host-header probe, with a build-only mode).
  • v13 — Adds a reusable Dependabot auto-merge workflow and an actionlint CI gate.
  • v12 — docker-build.yml: when no tag rule matches the ref, the image is now built without pushing (with a warning) instead of failing with tag is needed when pushing to registry. Registry login is skipped in that case.
  • v11 — README rewritten as a repo overview covering all four workflows; action versions corrected.

About

Shared workflows for GH Actions

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors