Skip to content
Draft
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
731d00c
Add independent peer review validation workflow
AndrewGable May 13, 2026
aba7b19
Use GitHub App client ID for peer review check
AndrewGable May 13, 2026
2d2a6c9
Use GraphQL for peer review validation
AndrewGable May 15, 2026
89bebf1
Simplify peer review validation helpers
AndrewGable May 15, 2026
e72fba8
Add newline
AndrewGable May 15, 2026
f473b3b
Hard-code OS Botify app ID
AndrewGable May 15, 2026
23b2a2c
use client-id
AndrewGable May 15, 2026
61d554c
Fix OS Botify token permissions
AndrewGable May 15, 2026
0b48d74
Rename peer review workflow
AndrewGable May 15, 2026
e57f967
Rename peer review verifier
AndrewGable May 15, 2026
b6aea85
Clarify peer review failures
AndrewGable May 15, 2026
2d8ce9c
Test informational peer review checks
AndrewGable May 15, 2026
4bee49d
Undo draft
AndrewGable May 15, 2026
1d3a559
Show informational peer review failures
AndrewGable May 15, 2026
74415cd
Restore hard peer review failure
AndrewGable May 15, 2026
74efe1d
Pass peer review check before approval
AndrewGable May 18, 2026
72a99fd
Use Melvin app for peer review check
AndrewGable May 18, 2026
fec8f31
Pin Node 24 and upgrade setup-node to v6 with npm cache
roryabraham Jun 10, 2026
76b600f
Add GithubUtils wrapper with rate limiting
roryabraham Jun 10, 2026
9eafdd8
Extract peer review logic into testable modules
roryabraham Jun 10, 2026
da27d6e
Normalize co-author email parsing to match Web-Expensify chore
roryabraham Jun 10, 2026
104bf8e
Extract evaluatePeerReview pass/skip/fail decision function
roryabraham Jun 10, 2026
c690b0c
Fail peer review check when branch protection cannot be read
roryabraham Jun 10, 2026
a886588
Add unit tests for peer review verification
roryabraham Jun 10, 2026
0a8b0f8
Use Peer Review Checker bot and fix workflow triggers
roryabraham Jun 10, 2026
11c368d
Add test and typecheck CI workflows and local dev docs
roryabraham Jun 10, 2026
82015d8
Remove OSBotify guard from test and typecheck jobs
roryabraham Jun 10, 2026
0ba1286
Rename GithubUtils to GitHubAPI and standardize module exports
roryabraham Jun 10, 2026
c61abf0
Rename GitHubAPI class to GitHubAPIClient
roryabraham Jun 10, 2026
a20f5bd
Switch peer review verification to CLI-based arguments
roryabraham Jun 11, 2026
6a0d11f
Use published expensify-common 2.0.184 for CLI parsing
roryabraham Jun 18, 2026
7a72ee9
Add ESLint with eslint-config-expensify and lint CI workflow
roryabraham Jun 18, 2026
f817ca1
Upgrade actions/create-github-app-token to 3.2.0
roryabraham Jun 25, 2026
3e08588
app-id -> client-id
roryabraham Jun 25, 2026
f4bdf04
Alphabetize scripts
roryabraham Jun 25, 2026
cf5b09d
refactor: move libs to scripts/libs
roryabraham Jun 25, 2026
4236743
Add Prettier with CI check
roryabraham Jun 25, 2026
fa4e331
refactor(peer-review): drop eventContext eslint overrides
roryabraham Jun 25, 2026
d082b36
refactor(tests): drop test eslint overrides
roryabraham Jun 26, 2026
41b95cb
refactor: extract CLI wrapper to scripts/libs/CLI.ts
roryabraham Jun 26, 2026
334a6d6
chore: upgrade expensify-common to 2.0.188 for CLI subpath
roryabraham Jun 26, 2026
1537ef7
style: fix prettier formatting in eventContext.ts
roryabraham Jun 26, 2026
b3d954a
Remove unhelpful docs
roryabraham Jun 26, 2026
c3b65ea
ci: broaden workflow path filters for JS/TS extensions
roryabraham Jun 26, 2026
3b1ce0d
ci: remove redundant CI=true from workflow env
roryabraham Jun 26, 2026
af679b5
refactor: flatten peer review libs into reusable modules
roryabraham Jun 26, 2026
e243313
refactor: rename coAuthorEmails to parseCoAuthorEmails
roryabraham Jun 26, 2026
2296821
Run prettier
roryabraham Jun 26, 2026
87af15a
refactor: extract uniqueSorted into CollectionUtils
roryabraham Jun 26, 2026
38a4caf
refactor: split GitHubUtils into focused submodules
roryabraham Jun 26, 2026
4a6aa55
feat: detect GitHub App bots via isBotUser helper
roryabraham Jun 26, 2026
3630fe4
refactor: rename CLI arg to --pull-request-number
roryabraham Jun 26, 2026
e38935d
refactor: inline CLI parsing in verifyPeerReview main
roryabraham Jun 26, 2026
40831be
refactor: inline peer review failure handler in main catch
roryabraham Jun 26, 2026
a944774
refactor: extract prSlug in evaluatePeerReview
roryabraham Jun 26, 2026
ec927c2
feat: resolve co-authors from display name fallback
roryabraham Jun 26, 2026
62b825a
Fix prettier
roryabraham Jun 26, 2026
817d866
fix: throw when canonical commit author cannot be resolved
roryabraham Jun 29, 2026
7c496a8
Fix prettier
roryabraham Jun 29, 2026
3a3b3e8
fix: attribute authors from referenced PR on merge commits
roryabraham Jun 29, 2026
de60ef3
Revert "fix: attribute authors from referenced PR on merge commits"
roryabraham Jun 29, 2026
f496ec3
Merge origin/main into andrew-validate-independent-peer-review
roryabraham Jul 2, 2026
18facec
Merge origin/main into andrew-validate-independent-peer-review
roryabraham Jul 9, 2026
d7c2da9
Add OSBotify and CLABotify to known bot users
roryabraham Jul 9, 2026
6f10b59
Match employee approvers case-insensitively
roryabraham Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Lint

on:
pull_request:
types: [opened, synchronize]
paths:
- "**/*.[tj]s"
- "**/*.?[tj]s"
- "eslint.config.mjs"
- "tsconfig.json"
- "package.json"
- "package-lock.json"
- ".nvmrc"
- ".github/workflows/lint.yml"

jobs:
lint:
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout
# 6.0.3
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10

# v6.1.0
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json

- name: Install npm packages
run: npm ci

- name: Lint
run: npm run lint

- name: Tell people how to run the failing check
if: failure()
run: echo "::error::The lint check failed! To run it locally, run \`npm ci && npm run lint\` from the GitHub-Actions repo root."
4 changes: 2 additions & 2 deletions .github/workflows/npmPublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ jobs:
repository: ${{ inputs.repository }}
token: ${{ steps.generateAppToken.outputs.token }}

# actions/setup-node@v4.3.0
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e
# actions/setup-node@v6.1.0
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Prettier

on:
pull_request:
types: [opened, synchronize]
paths:
- "**/*.[tj]s"
- "**/*.?[tj]s"
- ".prettierrc.mjs"
- ".prettierignore"
- "package.json"
- "package-lock.json"
- ".nvmrc"
- ".github/workflows/prettier.yml"

jobs:
prettier:
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout
# 6.0.3
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10

# v6.1.0
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json

- name: Install npm packages
run: npm ci

- name: Verify there's no Prettier diff
run: |
npm run prettier -- --log-level error
if ! git diff --name-only --exit-code; then
echo "::error::Prettier diff detected! Run \`npm run prettier\` from the GitHub-Actions repo root and commit the changes."
exit 1
fi
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Test

on:
pull_request:
types: [opened, synchronize]
paths:
- "**/*.[tj]s"
- "**/*.?[tj]s"
- "tsconfig.json"
- "package.json"
- "package-lock.json"
- ".nvmrc"
- ".github/workflows/test.yml"

jobs:
test:
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout
# 6.0.3
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10

# v6.1.0
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json

- name: Install npm packages
run: npm ci

- name: Run unit tests
run: npm test

- name: Tell people how to run the failing check
if: failure()
run: echo "::error::The test check failed! To run it locally, run \`npm ci && npm test\` from the GitHub-Actions repo root."
39 changes: 39 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Typecheck

on:
pull_request:
types: [opened, synchronize]
paths:
- "**/*.ts"
- "**/*.[mc]ts"
- "tsconfig.json"
- "package.json"
- "package-lock.json"
- ".nvmrc"
- ".github/workflows/typecheck.yml"

jobs:
typecheck:
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout
# 6.0.3
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10

# v6.1.0
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json

- name: Install npm packages
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Tell people how to run the failing check
if: failure()
run: echo "::error::The typecheck check failed! To run it locally, run \`npm ci && npm run typecheck\` from the GitHub-Actions repo root."
6 changes: 4 additions & 2 deletions .github/workflows/validateActions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ jobs:
id: repo
uses: Expensify/GitHub-Actions/checkoutRepoAndGitHubActions@main

# v4.3.0
# v6.1.0
- name: Setup Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version-file: GitHub-Actions/.nvmrc

# Install node to get the ajv-cli
- name: Install node modules
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/verifyPeerReview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Note: This workflow is configured to run on all pull requests throughout the Expensify org, not just this repo.
# That has a few consequences:
# - We need to checkout the repo it's running on, and not just the GitHub-Actions repo
# - branch and path matching does not work in the workflow layer. From the docs: https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#supported-event-triggers
# > Any filters you specify for the supported events are ignored - for example, branches, branches-ignore, paths, types and so on. The workflow is only triggered, and is always triggered, by the default activity types of the supported events
name: Verify peer review
run-name: Verify peer review for ${{ github.repository }}#${{ github.event.pull_request.number }}

on:
pull_request:
pull_request_review:
types: [submitted, dismissed]

permissions:
contents: read
pull-requests: read

jobs:
verifyPeerReview:
name: Check independent approval
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
# v3.2.0
- name: Generate a GitHub App token
id: generateAppToken
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: "3877737"
private-key: ${{ secrets.PEER_REVIEW_CHECKER_PRIVATE_KEY }}
Comment thread
roryabraham marked this conversation as resolved.
owner: ${{ github.repository_owner }}
repositories: |
${{ github.event.repository.name }}
GitHub-Actions
permission-administration: read
permission-contents: read
permission-members: read
permission-metadata: read
permission-pull-requests: read
Comment thread
roryabraham marked this conversation as resolved.

- name: Checkout repos
id: repo
uses: Expensify/GitHub-Actions/checkoutRepoAndGitHubActions@main

# v6.1.0
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version-file: GitHub-Actions/.nvmrc
cache: npm
cache-dependency-path: GitHub-Actions/package-lock.json

- name: Install npm packages
run: npm ci
working-directory: GitHub-Actions

- name: Verify peer review
run: >-
npm run verify-peer-review --
--owner ${{ github.repository_owner }}
--repo ${{ github.event.repository.name }}
--pull-request-number ${{ github.event.pull_request.number }}
--base-ref ${{ github.event.pull_request.base.ref }}
working-directory: GitHub-Actions
env:
GITHUB_TOKEN: ${{ steps.generateAppToken.outputs.token }}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package.json
package-lock.json
*.yml
*.yaml
*.md
*.markdown
dist/
20 changes: 20 additions & 0 deletions .prettierrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {createRequire} from 'node:module';

const require = createRequire(import.meta.url);

/** @type {import('prettier').Config} */
export default {
tabWidth: 4,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: false,
arrowParens: 'always',
printWidth: 190,
singleAttributePerLine: true,
plugins: [require.resolve('@prettier/plugin-oxc'), require.resolve('@trivago/prettier-plugin-sort-imports')],
importOrderParserPlugins: ['typescript'],
importOrderImportAttributesKeyword: 'with',
importOrder: ['^[./]'],
importOrderSortSpecifiers: true,
importOrderCaseInsensitive: true,
};
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,38 @@ jobs:
secrets: inherit
```

### `verifyPeerReview.yml`

Used as an org-level ruleset workflow to block pull requests that do not have enough independent Expensify employee approvals. The check only reads GitHub pull request metadata; it does not checkout or execute code from the pull request branch.

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.

The workflow runs on `pull_request` and `pull_request_review` (`submitted`, `dismissed`) so the check re-evaluates when reviews change without a new push. Org rulesets may only trigger default `pull_request` activity types; `pull_request_review` support under rulesets depends on GitHub's ruleset event coverage.

If branch protection cannot be read due to missing permissions, the check fails. If the target branch has no branch-protection review requirement, the check skips. If branch protection cannot be queried due to a transient API error, the script defaults to requiring one independent approval.

#### Local development

This script depends on the shared `CLI` utility from `expensify-common`.

```bash
cd GitHub-Actions
nvm use
npm ci
npm test
npm run verify-peer-review -- --help
```

Smoke-test against a real pull request by passing the pull request metadata as CLI arguments and setting `GITHUB_TOKEN` (or `GH_TOKEN`) to a token with the same read permissions as the Peer Review Checker app:

```bash
GITHUB_TOKEN=... npm run verify-peer-review -- \
--owner Expensify \
--repo Auth \
--pull-request-number 12345 \
--base-ref main
```

### `setup-composer-cache`

Restores Composer download caches and optionally runs `composer install`. See [setup-composer-cache/README.md](./setup-composer-cache/README.md) for details.
Expand All @@ -60,3 +92,4 @@ GitHub [org-level rulesets](https://docs.github.com/en/enterprise-cloud@latest/r
- If you need to target or exclude specific paths, that must be implemented manually in the workflow itself.
- Due to a GitHub :bug:, 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](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#using-evaluate-mode-for-ruleset-workflows), then `Active` once the kinks are worked out.
- For `verifyPeerReview.yml`, start with a ruleset targeting only a test branch, then test the workflow from a GitHub-Actions branch, then from `main`, and only then enable it for the intended repositories and branches.
41 changes: 41 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import jestConfig from 'eslint-config-expensify/jest';
import nodeConfig from 'eslint-config-expensify/node';
import scriptsConfig from 'eslint-config-expensify/scripts';
import tsConfig from 'eslint-config-expensify/typescript';
import {defineConfig, globalIgnores} from 'eslint/config';
import rulesdir from 'eslint-plugin-rulesdir';
import {createRequire} from 'node:module';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const projectRoot = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const expensifyConfigDirectory = path.dirname(require.resolve('eslint-config-expensify'));

rulesdir.RULES_DIR = path.resolve(expensifyConfigDirectory, 'eslint-plugin-expensify');

export default defineConfig([
globalIgnores(['node_modules']),
{
plugins: {
rulesdir,
},
},
...nodeConfig,
...tsConfig,
...scriptsConfig,
...jestConfig,
{
files: ['**/*.ts'],
languageOptions: {
parserOptions: {
project: path.resolve(projectRoot, 'tsconfig.json'),
projectService: false,
},
},
rules: {
// node:test is used instead of Jest in this repo.
'jest/no-jest-import': 'off',
},
},
]);
Loading
Loading