Skip to content

Repository files navigation

Expensify Shared GitHub Actions workflows 🔄

What is the repository used for?

Expensify has multiple repositories that use the same GitHub Actions workflows. This repository centralizes and consolidates frequently used workflows to enhance security and maintain consistent standards across projects.

Usage

npmPublish.yml

Used to publish a package to npmjs, should be triggered when code is merged into the main branch. Note: Please follow these instructions to grant our bots the correct access to publish.

jobs:
  publish:
    uses: Expensify/GitHub-Actions/.github/workflows/npmPublish.yml@main
    secrets: inherit
    with:
      # Repository name with owner. For example, Expensify/eslint-config-expensify
      # Required, String, default: ${{ github.repository }}
      repository: ""

      # True if we should run npm run build for the package
      # Optional, Boolean, default: false
      should_run_build: true

cla.yml

Used to check if a user has signed the Contributor License Agreement, Should be triggered when a PR is opened or updated.

jobs:
  CLA:
    uses: Expensify/GitHub-Actions/.github/workflows/cla.yml@main
    # Required to pass along secrets for `CLA_BOTIFY_TOKEN`
    secrets: inherit

verifyPeerReview.yml

Org-level ruleset workflow that verifies pull requests have an independent employee approval — e.g. blocking cases where an employee self-approves a pull request they asked Melvin to create.

Configure it to run via an org ruleset that requires this workflow on pull_request_target events. See the Rulesets section below for caveats.

The check only reads GitHub pull request metadata via the API; it does not checkout or execute code from the pull request branch. It uses pull_request_target so the workflow YAML and scripts always run from main, and only GitHub-Actions@main is checked out.

This workflow requires a GitHub App token with read access for repository metadata, pull requests, organization members, and branch administration. It uses the Peer Review Checker app ID 3877737 and the org secret PEER_REVIEW_CHECKER_PRIVATE_KEY to generate that token.

  • If the target branch has no branch-protection rule, or has one that requires zero approving reviews, the check passes.
  • If branch protection cannot be read — missing permissions, an API error, an unknown branch, or a response the script can't interpret — the check fails rather than assuming a review count.
  • Co-authors of bot-authored commits count as authors. Each Co-authored-by email is resolved to a login from the users.noreply.github.com pattern, or otherwise by asking GitHub which user has verified that email. The check fails if an email can't be resolved either way.

secretScan.yml

Scans a repository for committed credentials using TruffleHog.

Add one caller per repository:

# .github/workflows/secret-scan.yml
on:
  push:

jobs:
  secretScan:
    uses: Expensify/GitHub-Actions/.github/workflows/secretScan.yml@main
    with:
      # Optional. Fail the job on a finding. Leave unset to warn only.
      should_fail_on_findings: false

      # Optional. File of newline-separated regexes for paths to skip.
      # Ignored when the file does not exist.
      exclude_paths_file: .github/trufflehog-exclude-paths.txt

      # Optional. Runner label.
      runner: blacksmith-2vcpu-ubuntu-2404

Scan on push, not on pull_request. Every commit in a pull request is pushed first, so push covers the same ground and also covers a branch that never opens a pull request — which is how credentials go unnoticed for years. Add a second caller on pull_request only where external forks contribute, such as App, because a fork's own push never reaches us.

The scan scope follows the triggering event:

Event Scope
push The commits the push introduced
pull_request The commits in the pull request
schedule, workflow_dispatch The full history

Any other event fails the job with an explicit error rather than scanning the wrong range. The scan reads file:///repo and cannot reach the remote, so an event only qualifies if the local clone is guaranteed to hold the commits it names. pull_request_target does not qualify, because it checks out the base repository and an external fork's head commit is absent.

Add workflow_dispatch to the caller too. push only covers commits that land after the workflow exists, so run it once from the Actions tab to scan the history that predates it. Do not add a schedule: history does not change, so repeating a full scan reports the same answer every time.

Three pushes need care, and the workflow handles each:

  • Deleting a ref introduces no commits, so it is skipped.
  • A tag push is skipped only if the tagged commit already reaches a branch. Git allows pushing a tag whose commit reaches no branch, which transfers that commit with the tag, and that is the only event that can scan it.

Known gap: more than three tags at once

GitHub does not create events for tags when more than three are pushed at once, so git push --tags with four or more tags starts no workflow run. A commit reaching no branch, carried in such a batch, therefore goes unscanned.

Run the workflow from the Actions tab to recover. A schedule would close this automatically, but today a full scan re-reports every finding already in history, so the noise costs more than the gap. Once findings create deduplicated issues, repeats collapse and a schedule becomes worth adding.

  • A force push or a new branch names no usable starting commit, so the scan falls back to the point where the branch left the default branch. Where there is no shared ancestor at all — an orphan branch, a new repository, a force push to the default branch — it scans the whole branch, because no later push covers those commits.

Eight behaviours worth knowing before you change anything:

  • The scan runs with --no-verification. Verification authenticates each candidate against its live provider, and a burst of failed authentication attempts from CI is indistinguishable from credential stuffing in CloudTrail. A consequence is that every finding is classified unverified, so do not add --results=verified — it would report nothing.

  • Warn-only mode suppresses findings, not errors. TruffleHog exits 183 for a finding and 1 for an operational error such as a failed image pull, and the workflow branches on that exit code. With should_fail_on_findings: false a 183 becomes a warning annotation, while every other non-zero exit still fails the job. A scan that never ran must not report a pass, so do not reach for continue-on-error here — it cannot tell those two exits apart.

  • The workflow runs the TruffleHog container directly rather than using trufflesecurity/trufflehog. That action hardcodes --fail and exposes no exit code, and --fail cannot be repeated, so --no-fail is rejected with flag 'fail' cannot be repeated. Reading the exit code is the only way to separate the two failure kinds above. The image is pinned by digest.

  • The workflow checks that every commit in the scan range resolves locally before it starts. TruffleHog aborts with an unhelpful operational error on a missing commit, so this turns that into a message naming the commit.

  • The checkout needs fetch-depth: 0, despite costing around 1 GB on Auth or Web-Expensify. TruffleHog resolves the merge base for --since-commit through go-git, which ignores .git/shallow and fails with unable to resolve merge base: object not found on a shallow clone at any depth. Real git merge-base handles the same clone fine, so this is a go-git limitation rather than something depth tuning can fix.

  • --fail-on-scan-errors is not optional. Without it TruffleHog logs a scan error, reads zero commits and still exits 0, so a scan that covered nothing reports a clean result. That is the same silently-green-check failure the exit-code branching exists to prevent.

  • TruffleHog needs an access key ID adjacent to a plausible secret to detect an AWS credential, so it misses keys split across separate key = value lines. Treat a clean scan as a weak signal, not proof.

  • A finding does not fail the check, by design. A credential that has been pushed is already compromised, so blocking a merge does not undo the leak. The value is detection latency, and rotation is the response.

This repository scans itself via secretScanSelf.yml, which uses a local ref so that a pull request changing secretScan.yml is checked by the version it proposes.

setup-composer-cache

Restores Composer download caches and optionally runs composer install. See setup-composer-cache/README.md for details.

- name: Setup Composer Cache
  uses: Expensify/GitHub-Actions/setup-composer-cache@main
  with:
    run_install: true
    dev: false

Rulesets

GitHub org-level rulesets can be configured to run a workflow check against pull requests in all repos in the org. This is a very powerful feature, but there are some caveats and best practices to be aware of when enabling a ruleset.

  • Supported Event Triggers are documented here. However:
    • When a workflow runs in response to a ruleset, some configs such as branches, paths, paths-ignore, that would normally be valid in a workflow are ignored.
    • The default activity types for each event will be used. This means that something like pull_request:comment will not work - the pull_request event will always be triggered for the default activity types listed in the documentation.
    • If you need to target or exclude specific branches, that can be configured in the ruleset settings.
    • If you need to target or exclude specific paths, that must be implemented manually in the workflow itself.
  • Due to a GitHub 🐛, PRs that are open when the rule is enabled will get stuck with a pending check that will never get picked up. The easiest way to fix that is to close and reopen the PR. Consider writing a script to close and reopen all open PRs across the org after the check is enabled.
  • It is less disruptive to configure the ruleset to Evaluate first, then Active once the kinks are worked out.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

29 watching

Forks

Releases

Packages

Used by

Contributors

Languages