Shared GitHub Actions
Cleans up after test runs by uploading screenshots and dumping system information to stdout.
Inputs:
artifact-prefix*: A string prefix to name the uploaded log file. Artifact name will be named${artifact-prefix}-logs
Example:
- name: Cleanup
uses: RoleModel/actions/test-cleanup@v3
with:
artifact-prefix: rspec-systemPosts the runtime of the longest test files. This is important because the most granular parallel_tests goes is at the file level, so when running in parallel, the entire suite can never be quicker than the slowest file.
Inputs:
test-output-path*: Path to the test output file that holds runtimestop-count: Number of slowest tests to show. Default: 10
Example:
- name: Analyze test runtimes
uses: RoleModel/actions/test-runtime-analyzer@v3
with:
test-output-path: tmp/turbo_rspec_runtime.logUse this action to maintain a staging branch with the changes from all open PRs labeled Staging. This is recommended to be used with a workflow that auto-deploys the staging branch.
If a PR cannot be merged cleanly, the action comments on the PR with conflict details and removes the Staging label.
This action assumes the repository has already been cloned with ref: staging and fetch-depth: 0 set. Commits/pushes made using the default GitHub Actions access key do not trigger other actions, so make sure to generate a personal access token or SSH key so that staging auto-deploys can work.
Inputs:
github-token: GitHub token used to list/update pull requests. Default:${{ github.token }}primary-branch: Branch used as the reset base before staged merges. Default:main
Example:
name: Maintain staging branch
on:
pull_request:
types:
- labeled
- unlabeled
- closed
- synchronize
jobs:
maintain_staging:
if: (contains(github.event.pull_request.labels[*].name, 'Staging')) || (github.event.action == 'closed' && github.event.pull_request.merged) || (github.event.label.name == 'Staging')
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
ref: staging
fetch-depth: 0
ssh-key: ${{ secrets.AUTOMATED_COMMITS_KEY }} # IMPORTANT: Use a custom key to allow triggering other workflows
- name: Rebuild staging branch
uses: RoleModel/actions/staging-auto-merge@v3.github/workflows/release.yml is a workflow_call reusable workflow that cuts a release end to end: version bump, changelog entry, version commit and tag, GitHub release, and publishing to RubyGems and/or npm with trusted publishing. Call it from a workflow with two triggers:
workflow_dispatch(the Actions tab) runs thebumpjob. It bumps the version, updatesGemfile.lockandpackage-lock.jsonif present, adds a changelog entry if there's a changelog, then commits, tags and pushes with a GitHub App token.- A
v*tag push runs thereleaseand publish jobs. That tag push comes from thebumpjob, or from someone pushing a tag by hand. A push made with the defaultGITHUB_TOKENwouldn't trigger the workflow again, which is why thebumpjob uses an App token.
# .github/workflows/release.yml
name: Release
on:
workflow_dispatch:
inputs:
bump-type:
type: choice
options: [patch, minor, major]
default: minor
changelog:
description: Changelog bullets (separate with $>). Leave blank to use the Unreleased notes or draft them from the commits.
type: string
push:
tags: ['v*']
permissions:
contents: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && 'bump' || github.ref }}
jobs:
release:
uses: RoleModel/actions/.github/workflows/release.yml@v3
with:
bump-type: ${{ inputs.bump-type }}
changelog: ${{ inputs.changelog }}
publish-gem: true
secrets: inheritbump-type:major,minor, orpatch. Default:minorchangelog: Bullets for the changelog entry, separated by$>or newlines, placed above any Unreleased notes. Blank uses just those notes, or with none, drafts the entry from the commits since the last release (see changelog-entry)changelog-file: The changelog to update. Blank, or a file that doesn't exist, skips it. Default:CHANGELOG.mdversion-file: File holding the version. Default: the singlelib/*/version.rb, elsepackage.jsonapp-client-id: Client ID of the GitHub App that pushes the version commit and tag. Default:vars.RMS_VERSION_MANAGER_CLIENT_IDpublish-gem: Publish to RubyGems. Default:falsepublish-npm: Publish to npm. Default:false
app-private-key: Private key of the GitHub App. Default:secrets.RMS_VERSION_MANAGER_CERT
The defaults are the org's Version Manager App, which must be installed on the repository, with the org secret visible to it. A reusable workflow only sees the secrets it's handed, so the default key needs secrets: inherit, which passes the caller's secrets along. A repository with its own App passes both app-client-id and app-private-key instead.
Neither registry checks its trusted publisher configuration when you save it, so a mistake shows up as a failed publish job. Fix the configuration and re-run that job; the tag and GitHub release are already in place.
- RubyGems: repository = the gem's repository, workflow filename =
release.yml, Workflow Repository Owner =RoleModel, Workflow Repository Name =actions. RubyGems checks the reusable workflow that actually publishes. - npm: repository = the package's repository, workflow filename = the calling workflow's file in that repository. npm checks the caller. The job installs Node 24, since trusted publishing needs npm 11.5.1 or later.
The composite actions behind the release workflow, for a repository that needs its own steps in the version commit (the release workflow covers lockfiles and package.json). bump-version rewrites the version in a file and outputs it. changelog-entry records the release in the changelog. push-version-tag commits every tracked change as that version, tags it, and pushes the commit and tag atomically, so a rejected branch push never leaves a published tag behind. Anything else that belongs in the version commit (another app's lockfile, generated docs) goes in steps between them.
None of them checks out the code or picks a token: the push uses whatever credentials actions/checkout persisted. Pushes made with the default GITHUB_TOKEN do not trigger other workflows (e.g. one listening for v* tags), so use a GitHub App token or deploy key when something downstream should run.
Bumps a Ruby VERSION = "x.y.z" constant, keeping its quote style, or a package.json "version" via npm version, which also updates package-lock.json. A gem with a package.json beside it gets both bumped, so the gem and its npm package release under one version. A major, minor or patch bump only applies to a plain MAJOR.MINOR.PATCH version. From a prerelease such as 2.0.0.beta2, pass the version you want instead.
Inputs:
bump*:major,minor,patch, or an explicit version (e.g.2.0.0.rc1)version-file: File holding the version. Default: the singlelib/*/version.rb, elsepackage.json
Outputs: version, previous-version
Adds ## [v1.5.0] Oct 9, 2026 and its bullets, plus a [v1.5.0]: <release URL> link if the changelog keeps reference links. An ## Unreleased section becomes the entry, keeping its notes beneath any description bullets; otherwise the entry goes above the newest one. With neither a description nor Unreleased notes, it drafts the bullets from the subjects of the commits since the previous release, folding bot commits (Dependabot) into one "Update dependencies" line. With no history to draft from, it writes a placeholder bullet to fill in later, so a release never goes unrecorded.
Inputs:
version*: The version being releaseddescription: Bullets, separated by$>or newlines, placed above any Unreleased notes. Default: just those notes, else drafted from commitsprevious-version: The version released before this one, whose tag starts the drafted commits. Check out withfetch-depth: 0so the history is therechangelog-file: Skipped if there's no such file. Default:CHANGELOG.mdtag-prefix: Default:v
Inputs:
version*: The version being releasedtag-prefix: Default:vbranch: Branch to push the version commit to. Default:${{ github.ref_name }}git-user-name/git-user-email: Commit and tag author. Default: thegithub-actions[bot]user
Example (an extra lockfile in the version commit):
steps:
# ... app token, checkout with fetch-depth: 0, setup-ruby
- uses: RoleModel/actions/bump-version@v3
id: bump
with:
bump: ${{ inputs.bump }}
- run: bundle install && (cd example_app && bundle lock)
- uses: RoleModel/actions/changelog-entry@v3
with:
version: ${{ steps.bump.outputs.version }}
previous-version: ${{ steps.bump.outputs.previous-version }}
description: ${{ inputs.changelog }}
- uses: RoleModel/actions/push-version-tag@v3
with:
version: ${{ steps.bump.outputs.version }}
git-user-name: ${{ steps.app-token.outputs.app-slug }}[bot]
git-user-email: ${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com.github/workflows/rails-ci.yml is a workflow_call reusable workflow that runs an entire Rails app's CI — linting, Brakeman, dependency audit, asset compilation/caching, and parallel_rspec — as a single job. It supersedes the composite actions below: use it for new projects, and prefer migrating an existing ci.yml to it over adding to the composite-action setup, since it schedules the independent setup steps (Ruby/gems, apt packages, Node/Yarn, asset cache, Brakeman, Rubocop, ESLint, dependency audit, Project Stats) to overlap with background:/wait:/parallel: step syntax instead of running them sequentially across multiple jobs.
apt_packages: Space-separated apt packages to install alongside setup (e.g. "libvips")brakeman_version: Brakeman gem version to run via reviewdog (omit to skip, e.g. "8.0.5")dependency_audit_command: Dependency audit command to run (omit to skip)extra_env: Additional non-secret environment variables, one KEY=VALUE per line (same format as a .env file). Use this for anything the app requires that isn't already covered by an existing input. For real secret values, use theextra_secretssecret instead —secrets.*can't be referenced here since this is awith:input, evaluated before the caller's secrets are in scope.failure-screenshot-dir: the directory where your test runner saves screenshots on failure. Default:tmp/capybarajs_lint_command: JS linting command to run (omit to skip)playwright: Install the Playwright Chromium browser (for apps using capybara-playwright-driver). Default:falsepostgres_version: Postgres Docker image version tag. Default:"17"project-stats: Runbin/rails statsand post the results to the job summary. Default:truerubocop_command: Rubocop command to run (omit to skip)runner: Runner to use. Default:blacksmith-8vcpu-ubuntu-2404timeout: Job timeout in minutes. Default:20
RAILS_MASTER_KEY: Rails master key for decrypting credentialsBUNDLE_RUBYGEMS__PKG__GITHUB__COM: Credential for the RoleModel RubyGems package registryextra_secrets: Additional secret environment variables, one KEY=VALUE per line (same shape asextra_env). Pass real secret values through here, referencing the caller's own secrets context on the right-hand side of each line.
extra_env and extra_secrets exist for the same reason (app-specific env vars the workflow doesn't already name), but land differently: extra_env is a with: input, evaluated before the caller's secrets: context is in scope, so it can only carry non-secret KEY=VALUE lines. extra_secrets is a real secrets: value, so it's the one to use for anything sensitive — reference the caller's own secrets on the right-hand side, e.g. STRIPE_PRIVATE_KEY=${{ secrets.STRIPE_PRIVATE_KEY }}.
project-stats is a built-in input (default true) that publishes its own "Project Stats" GitHub check as part of the job. Callers of rails-ci.yml get this for free and don't need the separate standalone project-stats job shown in the composite-action example below.
jobs:
ci:
uses: RoleModel/actions/.github/workflows/rails-ci.yml@v3
with:
rubocop_command: bundle exec rubocop --format github
dependency_audit_command: bin/bundler-audit && bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error
failure-screenshot-dir: tmp/screenshots
secrets: inheritjobs:
ci:
uses: RoleModel/actions/.github/workflows/rails-ci.yml@v3
with:
apt_packages: libvips
playwright: true
rubocop_command: bundle exec rubocop --format github
js_lint_command: yarn lint
dependency_audit_command: bin/bundler-audit
brakeman_version: "8.0.5"
failure-screenshot-dir: tmp/screenshots
extra_env: |
STRIPE_PUBLISHABLE_KEY=pk_test_placeholder
secrets:
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
BUNDLE_RUBYGEMS__PKG__GITHUB__COM: ${{ secrets.BUNDLE_RUBYGEMS__PKG__GITHUB__COM }}
extra_secrets: |
STRIPE_PRIVATE_KEY=${{ secrets.STRIPE_PRIVATE_KEY }}This is a combination of multiple composite actions that can be used to run your entire CI flow for a rails app using parallel_tests. Each action allow you to customize the machine, environment variables, and any custom install steps that are needed. It does require you to check out the code yourself, since some install steps might happen after that.
linting-and-non-system-tests:linting_step_required- Whether linting is required
- Optional, default is
false
linting_step_command- Command for linting (e.g., "bundle exec rubocop")
- Optional, default is
''
needs-compiled-assets- Whether to retrieve the Rails asset cache (set to false if your workflow does not depend on the compile-assets workflow)
- Optional, default is
true
system-tests:web-driver- Supported values are 'selenium' and 'playwright'
- Optional, default is
selenium
failure-screenshot-dir- the directory where your test runner saves screenshots on failure
- Optional, default is
tmp/capybara
name: CI
on:
push:
branches: [ main, master ]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}-${{ github.event.ref }}
cancel-in-progress: true
env:
CI: true
RAILS_ENV: test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
SECRET_KEY_BASE: 123 # If you are using encrypted credentials, you'll need to extract this into a GitHub secret.
# Otherwise, the only thing that matters is that the value is not nil.
jobs:
compile-assets:
name: Compile Assets
runs-on: blacksmith-4vcpu-ubuntu-2204
timeout-minutes: 5
steps:
- name: Checkout Code
uses: actions/checkout@v7
- uses: RoleModel/actions/compile-assets@v3
id: check-asset-cache
non-system-test:
name: Linting & Ruby Non-System Tests
runs-on: blacksmith-8vcpu-ubuntu-2204
timeout-minutes: 5
# If you have non-system tests that touch the browser, you may need to uncomment the following line.
# needs: compile-assets
services:
postgres:
image: postgres:17
ports:
- "5432:5432"
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
steps:
- name: Checkout Code
uses: actions/checkout@v7
- uses: RoleModel/actions/linting-and-non-system-tests@v3
with:
linting_step_required: true
linting_step_command: bundle exec rubocop --fail-level warning --display-only-fail-level-offenses --format github
needs-compiled-assets: false # Remove this input if you uncommented `needs: compile-assets` above.
system-test:
name: Ruby System Tests
runs-on: blacksmith-16vcpu-ubuntu-2204
timeout-minutes: 10
needs: compile-assets
services:
postgres:
image: postgres:17
ports:
- "5432:5432"
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
steps:
- name: Checkout Code
uses: actions/checkout@v7
# Custom install steps may be added between checkout & test run if needed. e.g.
# If your application processes ActiveStorage::Variant records you probably need the following step:
- run: |
sudo apt-get update
sudo apt-get install -y libvips
- uses: RoleModel/actions/system-tests@v3
with:
failure-screenshot-dir: tmp/screenshots # if you've configured capybara to be compatible with the tmp:clear task
web-driver: playwright # remove this input to use the default value (selenium)
# completely optional job (for Rails projects only) - outputs a stats table in your workflow's summary page
project-stats:
name: Project Stats
runs-on: blacksmith-4vcpu-ubuntu-2204
timeout-minutes: 5
steps:
- name: Checkout Code
uses: actions/checkout@v7
- name: Capture Project Stats
uses: RoleModel/actions/project-stats@v3This is using anothrNick/github-tag-action to automatically
post version tags when a commit is merged to main. Unless you include #patch or #major in your commit message,
it will automatically create a new tag a minor version higher. #none will skip tagging altogether.