Reusable GitHub Actions workflows for MPI Media projects.
This repository contains reusable GitHub Actions workflows used across all MPI Media Rails applications.
Full CI pipeline for Rails applications including:
- Ruby security scanning (Brakeman, Bundler Audit)
- JavaScript security scanning (Importmap Audit or Yarn Audit)
- Linting (RuboCop)
- Test suite (RSpec with PostgreSQL)
- Optional Elasticsearch support
Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
elasticsearch |
boolean | false |
Enable Elasticsearch service for tests |
libvips |
boolean | false |
Install libvips for image processing |
security_scan |
boolean | true |
Run security scans (Brakeman, bundler-audit, JS audit) |
lint |
boolean | true |
Run RuboCop linting |
importmap |
boolean | true |
Run importmap audit for projects using importmap-rails |
jsbundling |
boolean | false |
Run yarn npm audit for projects using jsbundling-rails (esbuild/webpack) |
rspec_options |
string | '' |
Additional options passed to the rspec command |
When elasticsearch: true, the workflow:
- Reads the Elasticsearch version from
.tool-versions - Starts Elasticsearch before running tests
- Sets
ELASTICSEARCH_URLenvironment variable
Automated Ruby gem updates (apps schedule it weekly, Monday mornings):
- Runs the centralized
scripts/update-gemsagainst the calling app's checkout - Honors the app Gemfile's release-age cooldown (
source "https://rubygems.org", cooldown: N) — Bundler's resolver never selects versions younger than N days - Flow: unpin exact pins →
bundle update --all→ repin from the resolved lockfile (gems with a#comment in the Gemfile are never touched) - Creates a PR listing updated and skipped gems
Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
dry_run |
boolean | false |
Compute and print the would-be update PR without creating a branch, commit, or PR |
Automated Node.js package updates (apps schedule it weekly, Monday mornings):
- Runs the centralized
scripts/update-packagesagainst the calling app's checkout - Honors Yarn's
npmMinimalAgeGate(.yarnrc.yml) —yarn up <pkg>resolves to the newest gate-compliant release, never to a fresher one - Creates a PR listing updated packages
Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
dry_run |
boolean | false |
Compute and print the would-be update PR without creating a branch, commit, or PR |
Runs on every push to this repository:
shellcheckoverscripts/actionlintover.github/workflows/- Fixture tests for the Gemfile pin-editing library (
ruby scripts/test/gemfile_edit_test.rb)
The update workflows do not rely on per-app bin/ scripts. Each reusable workflow
checks out this repository at the exact SHA the calling app pinned in uses:
(via job.workflow_repository / job.workflow_sha — the documented pattern for
reusable workflows referencing their own source) and runs:
scripts/update-gems— orchestrates unpin → resolve → repin (seescripts/lib/gemfile_edit.rb)scripts/update-packages— per-packageyarn up <name> --exactunder the age gate
Pinning an app to a workflow SHA therefore pins the script logic too. Bumping the pin is the only way an app picks up new update behavior.
The cooldowns delay fixes as well as attacks. When a security advisory demands a release younger than the cooldown window, a human applies it — automation never bypasses the gate:
- Ruby: on a branch, run
bundle update <gem> --cooldown 0, and note the CVE in the PR description. - JavaScript: temporarily add the package to
npmPreapprovedPackagesin.yarnrc.yml(the entry is visible in the PR diff), runyarn up <pkg> --exact, and remove the entry after merge. - Brand-new packages:
yarn addof a package whose every version is younger than the gate fails with an explicit quarantine error (Yarn ≥ 4.13). Use the samenpmPreapprovedPackagesoverride.
Migration index checking:
- Runs on PRs that modify migrations
- Ensures all foreign keys have indexes
Deployment via Kamal:
- Deploys Rails applications using Kamal
- Supports staging and production environments
- Uses Tailscale for secure network connectivity to deployment targets
- Optionally pings Tailscale hosts to verify connectivity before deploying
- Configures SSH to automatically accept new Tailscale host keys
- Uses 1Password CLI for secrets management
- Configures Docker buildx with GitHub Actions cache
Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
environment |
string | (required) | Target environment (staging or production) |
tailscale_hosts |
string | "" |
Comma-separated Tailscale host addresses to ping for connectivity verification (e.g. "host1,host2") |
For projects without Elasticsearch:
# .github/workflows/ci.yml
name: CI
on:
push:
branches:
- "**"
jobs:
ci:
uses: mpimedia/mpi-application-workflows/.github/workflows/ci-rails.yml@main
secrets: inheritFor projects that need Elasticsearch:
# .github/workflows/ci.yml
name: CI
on:
push:
branches:
- "**"
jobs:
ci:
uses: mpimedia/mpi-application-workflows/.github/workflows/ci-rails.yml@main
with:
elasticsearch: true
secrets: inheritNote: The Elasticsearch version is read from your project's .tool-versions file:
elasticsearch 9.2.4
For projects using esbuild or webpack via jsbundling-rails:
# .github/workflows/ci.yml
name: CI
on:
push:
branches:
- "**"
jobs:
ci:
uses: mpimedia/mpi-application-workflows/.github/workflows/ci-rails.yml@main
with:
importmap: false
jsbundling: true
secrets: inherit# .github/workflows/update-gems.yml
name: Update Gems
on:
schedule:
- cron: '0 12 * * 1' # Mondays 06:00 CST (UTC-6)
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (compute and print updates; no branch/PR)'
required: false
default: false
type: boolean
jobs:
update:
uses: mpimedia/mpi-application-workflows/.github/workflows/update-gems.yml@<sha> # pin to known-good SHA
with:
dry_run: ${{ inputs.dry_run || false }}
secrets: inherit# .github/workflows/update-packages.yml
name: Update Packages
on:
schedule:
- cron: '5 12 * * 1' # Mondays 06:05 CST (UTC-6)
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (compute and print updates; no branch/PR)'
required: false
default: false
type: boolean
jobs:
update:
uses: mpimedia/mpi-application-workflows/.github/workflows/update-packages.yml@<sha> # pin to known-good SHA
with:
dry_run: ${{ inputs.dry_run || false }}
secrets: inherit# .github/workflows/check-indexes.yml
name: Check Indexes
on:
push:
branches: [ "main" ]
paths:
- 'db/migrate/**.rb'
pull_request:
branches: [ "main" ]
paths:
- 'db/migrate/**.rb'
jobs:
check:
uses: mpimedia/mpi-application-workflows/.github/workflows/check-indexes.yml@main# .github/workflows/deploy.yml
name: Deploy
concurrency:
group: deploy-${{ github.event.inputs.environment }}
cancel-in-progress: false
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
jobs:
deploy:
uses: mpimedia/mpi-application-workflows/.github/workflows/deploy-kamal.yml@main
with:
environment: ${{ inputs.environment }}
tailscale_hosts: "web-server,worker-server"
secrets: inheritThe deploy workflow uses Tailscale's GitHub Actions OIDC integration to establish secure connectivity to deployment targets. Consumer repos need the following configuration before using the deploy workflow.
| Secret | Description |
|---|---|
TS_OAUTH_CLIENT_ID |
Tailscale OAuth client ID for the GitHub Actions OIDC integration |
TS_AUDIENCE |
Tailscale OIDC audience value, used to scope the identity token to your tailnet |
OP_SERVICE_ACCOUNT_DEPLOYMENT_TOKEN |
1Password service account token for secrets management during deploy |
The deploy workflow requires id-token: write so GitHub can issue an OIDC token for Tailscale authentication. Since secrets: inherit does not propagate permissions, the consumer workflow must set this permission itself:
# .github/workflows/deploy.yml
jobs:
deploy:
uses: mpimedia/mpi-application-workflows/.github/workflows/deploy-kamal.yml@main
with:
environment: ${{ inputs.environment }}
permissions:
contents: read
packages: write
id-token: write
secrets: inherit- In the Tailscale admin console, create a new OAuth client.
- Under the OAuth client settings, add a GitHub Actions OIDC provider.
- Configure the provider to trust tokens from your consumer repository (e.g.,
mpimedia/optimus). - Set the audience value — this becomes the
TS_AUDIENCEsecret. - Copy the OAuth client ID — this becomes the
TS_OAUTH_CLIENT_IDsecret. - Add both values as repository or organization secrets in GitHub.
- Tag the OAuth client with
tag:ci(or the tag matching your Tailscale ACLs) so the ephemeral node gets the correct network access.
For stability, you can pin to a specific commit or tag:
uses: mpimedia/mpi-application-workflows/.github/workflows/ci-rails.yml@v1.0.0To update workflows:
- Clone this repository
- Create a feature branch
- Make changes and test
- Create a PR for review
- After merge, workflows auto-update in projects using
@main
Internal use only - MPI Media