Skip to content

Commit 52aa1a8

Browse files
committed
Merge branch 'main' into add-ai-rule
2 parents 1d72e19 + 856c5b5 commit 52aa1a8

330 files changed

Lines changed: 5423 additions & 1720 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/javascript/isAuthorizedContributor/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ inputs:
2020
outputs:
2121
IS_AUTHORIZED:
2222
description: "'true' if the contributor is authorized, 'false' otherwise"
23+
IS_INTERNAL:
24+
description: "'true' if the actor is an internal Expensify engineer (engineering org team member), 'false' otherwise"
2325
runs:
2426
using: 'node24'
2527
main: './index.js'

.github/actions/javascript/isAuthorizedContributor/index.js

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11582,13 +11582,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1158211582
Object.defineProperty(exports, "__esModule", ({ value: true }));
1158311583
exports.isAuthorizedContributor = isAuthorizedContributor;
1158411584
exports.isContributorPlusMember = isContributorPlusMember;
11585+
exports.isInternalExpensifyEngineer = isInternalExpensifyEngineer;
1158511586
const core = __importStar(__nccwpck_require__(2186));
1158611587
const github = __importStar(__nccwpck_require__(5438));
11587-
const request_error_1 = __nccwpck_require__(537);
1158811588
const CONST_1 = __importDefault(__nccwpck_require__(9873));
1158911589
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
11590+
const isTeamMember_1 = __importDefault(__nccwpck_require__(1077));
1159011591
const AUTHORIZED_ASSOCIATIONS = new Set(['MEMBER', 'OWNER', 'CONTRIBUTOR', 'COLLABORATOR']);
1159111592
const CONTRIBUTOR_PLUS_TEAM_SLUG = 'contributor-plus';
11593+
// Internal Expensify engineers belong to this team. We can't rely on author_association, which only reports MEMBER for publicly visible org members.
11594+
const ENGINEERING_TEAM_SLUG = 'engineering';
1159211595
const ISSUE_URL_PATTERN = /https:\/\/github\.com\/(Expensify\/[^/]+)\/issues\/(\d+)/g;
1159311596
function parseExpensifyLink(match) {
1159411597
const repoFullName = match[1];
@@ -11610,25 +11613,16 @@ function logVerificationError(repoFullName, number, error) {
1161011613
}
1161111614
async function isContributorPlusMember(username, orgToken) {
1161211615
GithubUtils_1.default.initOctokitWithToken(orgToken);
11613-
try {
11614-
await GithubUtils_1.default.octokit.teams.getMembershipForUserInOrg({
11615-
org: CONST_1.default.GITHUB_OWNER,
11616-
// eslint-disable-next-line @typescript-eslint/naming-convention
11617-
team_slug: CONTRIBUTOR_PLUS_TEAM_SLUG,
11618-
username,
11619-
});
11620-
console.log(`${username} is a Contributor+ member. Authorized.`);
11621-
return true;
11622-
}
11623-
catch (error) {
11624-
if (error instanceof request_error_1.RequestError && error.status === 404) {
11625-
console.log(`${username} is not a Contributor+ member.`);
11626-
return false;
11627-
}
11628-
const message = error instanceof Error ? error.message : String(error);
11629-
core.warning(`Could not verify Contributor+ membership for ${username}. Assuming they are not a Contributor+: ${message}`);
11630-
return false;
11631-
}
11616+
const isMember = await (0, isTeamMember_1.default)(GithubUtils_1.default.octokit, CONST_1.default.GITHUB_OWNER, CONTRIBUTOR_PLUS_TEAM_SLUG, username);
11617+
console.log(isMember ? `${username} is a Contributor+ member. Authorized.` : `${username} is not a Contributor+ member.`);
11618+
return isMember;
11619+
}
11620+
/**
11621+
* Returns whether a user is an internal Expensify engineer (member of the engineering org team).
11622+
*/
11623+
async function isInternalExpensifyEngineer(username, orgToken) {
11624+
GithubUtils_1.default.initOctokitWithToken(orgToken);
11625+
return (0, isTeamMember_1.default)(GithubUtils_1.default.octokit, CONST_1.default.GITHUB_OWNER, ENGINEERING_TEAM_SLUG, username);
1163211626
}
1163311627
async function isAuthorizedViaLinkedIssues(prBody, actor) {
1163411628
for (const match of prBody.matchAll(ISSUE_URL_PATTERN)) {
@@ -11699,6 +11693,8 @@ async function run() {
1169911693
orgToken,
1170011694
});
1170111695
core.setOutput('IS_AUTHORIZED', isAuthorized);
11696+
const isInternal = await isInternalExpensifyEngineer(actor, orgToken);
11697+
core.setOutput('IS_INTERNAL', isInternal);
1170211698
}
1170311699
if (require.main === require.cache[eval('__filename')]) {
1170411700
run().catch((error) => {
@@ -12239,6 +12235,75 @@ class GithubUtils {
1223912235
exports["default"] = GithubUtils;
1224012236

1224112237

12238+
/***/ }),
12239+
12240+
/***/ 1077:
12241+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
12242+
12243+
"use strict";
12244+
12245+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12246+
if (k2 === undefined) k2 = k;
12247+
var desc = Object.getOwnPropertyDescriptor(m, k);
12248+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12249+
desc = { enumerable: true, get: function() { return m[k]; } };
12250+
}
12251+
Object.defineProperty(o, k2, desc);
12252+
}) : (function(o, m, k, k2) {
12253+
if (k2 === undefined) k2 = k;
12254+
o[k2] = m[k];
12255+
}));
12256+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
12257+
Object.defineProperty(o, "default", { enumerable: true, value: v });
12258+
}) : function(o, v) {
12259+
o["default"] = v;
12260+
});
12261+
var __importStar = (this && this.__importStar) || (function () {
12262+
var ownKeys = function(o) {
12263+
ownKeys = Object.getOwnPropertyNames || function (o) {
12264+
var ar = [];
12265+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
12266+
return ar;
12267+
};
12268+
return ownKeys(o);
12269+
};
12270+
return function (mod) {
12271+
if (mod && mod.__esModule) return mod;
12272+
var result = {};
12273+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
12274+
__setModuleDefault(result, mod);
12275+
return result;
12276+
};
12277+
})();
12278+
Object.defineProperty(exports, "__esModule", ({ value: true }));
12279+
/* eslint-disable @typescript-eslint/naming-convention */
12280+
const core = __importStar(__nccwpck_require__(2186));
12281+
const request_error_1 = __nccwpck_require__(537);
12282+
/**
12283+
* Whether a user is a member of the given org team.
12284+
* The octokit must be authenticated with a token that has read:org scope, otherwise concealed (private) members are reported as non-members.
12285+
*/
12286+
async function isTeamMember(octokit, org, teamSlug, username) {
12287+
try {
12288+
await octokit.teams.getMembershipForUserInOrg({
12289+
org,
12290+
team_slug: teamSlug,
12291+
username,
12292+
});
12293+
return true;
12294+
}
12295+
catch (error) {
12296+
if (error instanceof request_error_1.RequestError && error.status === 404) {
12297+
return false;
12298+
}
12299+
const message = error instanceof Error ? error.message : String(error);
12300+
core.warning(`Could not verify ${teamSlug} membership for ${username}. Assuming they are not a member: ${message}`);
12301+
return false;
12302+
}
12303+
}
12304+
exports["default"] = isTeamMember;
12305+
12306+
1224212307
/***/ }),
1224312308

1224412309
/***/ 9491:

.github/actions/javascript/isAuthorizedContributor/isAuthorizedContributor.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import * as core from '@actions/core';
22
import * as github from '@actions/github';
3-
import {RequestError} from '@octokit/request-error';
43
import CONST from '@github/libs/CONST';
54
import GithubUtils from '@github/libs/GithubUtils';
5+
import isTeamMember from '@github/libs/isTeamMember';
66

77
const AUTHORIZED_ASSOCIATIONS = new Set(['MEMBER', 'OWNER', 'CONTRIBUTOR', 'COLLABORATOR']);
88
const CONTRIBUTOR_PLUS_TEAM_SLUG = 'contributor-plus';
99

10+
// Internal Expensify engineers belong to this team. We can't rely on author_association, which only reports MEMBER for publicly visible org members.
11+
const ENGINEERING_TEAM_SLUG = 'engineering';
12+
1013
const ISSUE_URL_PATTERN = /https:\/\/github\.com\/(Expensify\/[^/]+)\/issues\/(\d+)/g;
1114

1215
type IsAuthorizedContributorParams = {
@@ -49,25 +52,17 @@ function logVerificationError(repoFullName: string, number: number, error: unkno
4952

5053
async function isContributorPlusMember(username: string, orgToken: string): Promise<boolean> {
5154
GithubUtils.initOctokitWithToken(orgToken);
55+
const isMember = await isTeamMember(GithubUtils.octokit, CONST.GITHUB_OWNER, CONTRIBUTOR_PLUS_TEAM_SLUG, username);
56+
console.log(isMember ? `${username} is a Contributor+ member. Authorized.` : `${username} is not a Contributor+ member.`);
57+
return isMember;
58+
}
5259

53-
try {
54-
await GithubUtils.octokit.teams.getMembershipForUserInOrg({
55-
org: CONST.GITHUB_OWNER,
56-
// eslint-disable-next-line @typescript-eslint/naming-convention
57-
team_slug: CONTRIBUTOR_PLUS_TEAM_SLUG,
58-
username,
59-
});
60-
console.log(`${username} is a Contributor+ member. Authorized.`);
61-
return true;
62-
} catch (error: unknown) {
63-
if (error instanceof RequestError && error.status === 404) {
64-
console.log(`${username} is not a Contributor+ member.`);
65-
return false;
66-
}
67-
const message = error instanceof Error ? error.message : String(error);
68-
core.warning(`Could not verify Contributor+ membership for ${username}. Assuming they are not a Contributor+: ${message}`);
69-
return false;
70-
}
60+
/**
61+
* Returns whether a user is an internal Expensify engineer (member of the engineering org team).
62+
*/
63+
async function isInternalExpensifyEngineer(username: string, orgToken: string): Promise<boolean> {
64+
GithubUtils.initOctokitWithToken(orgToken);
65+
return isTeamMember(GithubUtils.octokit, CONST.GITHUB_OWNER, ENGINEERING_TEAM_SLUG, username);
7166
}
7267

7368
async function isAuthorizedViaLinkedIssues(prBody: string, actor: string): Promise<boolean> {
@@ -155,6 +150,9 @@ async function run(): Promise<void> {
155150
});
156151

157152
core.setOutput('IS_AUTHORIZED', isAuthorized);
153+
154+
const isInternal = await isInternalExpensifyEngineer(actor, orgToken);
155+
core.setOutput('IS_INTERNAL', isInternal);
158156
}
159157

160158
if (require.main === module) {
@@ -164,5 +162,5 @@ if (require.main === module) {
164162
});
165163
}
166164

167-
export {isAuthorizedContributor, isContributorPlusMember};
165+
export {isAuthorizedContributor, isContributorPlusMember, isInternalExpensifyEngineer};
168166
export default run;

.github/actions/javascript/reviewerChecklist/action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ inputs:
44
GITHUB_TOKEN:
55
description: Auth token for New Expensify Github
66
required: true
7+
OS_BOTIFY_TOKEN:
8+
description: OSBotify token for org team membership (read:org). Absent on fork-triggered runs, which don't receive org secrets.
9+
required: false
710
runs:
811
using: 'node24'
912
main: './index.js'

.github/actions/javascript/reviewerChecklist/index.js

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11583,13 +11583,15 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
1158311583
const core = __importStar(__nccwpck_require__(2186));
1158411584
const github = __importStar(__nccwpck_require__(5438));
1158511585
const https_1 = __importDefault(__nccwpck_require__(5687));
11586+
const CONST_1 = __importDefault(__nccwpck_require__(9873));
1158611587
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
11588+
const isTeamMember_1 = __importDefault(__nccwpck_require__(1077));
1158711589
const pathToReviewerChecklist = 'https://raw.githubusercontent.com/Expensify/App/main/contributingGuides/REVIEWER_CHECKLIST.md';
1158811590
const reviewerChecklistContains = '# Reviewer Checklist';
1158911591
const issue = github.context.payload.issue?.number ?? github.context.payload.pull_request?.number ?? -1;
1159011592
const combinedComments = [];
11591-
// Org members and owners are internal Expensify engineers; external contributors (including C+) are not.
11592-
const INTERNAL_EXPENSIFY_ASSOCIATIONS = new Set(['MEMBER', 'OWNER']);
11593+
// Internal Expensify engineers belong to this team. We can't rely on author_association, which only reports MEMBER for publicly visible org members.
11594+
const ENGINEERING_TEAM_SLUG = 'engineering';
1159311595
// A reviewer's standing is their latest review in one of these states; plain "commented" reviews don't change it.
1159411596
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
1159511597
function getNumberOfItemsFromReviewerChecklist() {
@@ -11664,7 +11666,11 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1166411666
// An approval from an internal Expensify engineer means we've decided this PR doesn't need a C+ checklist, so let the check pass.
1166511667
// This workflow re-runs on every pull_request_review event, so we scan the whole review history: once an internal approval
1166611668
// stands, a later "commented" or "changes requested" review from anyone must not re-require the checklist.
11667-
async function hasStandingInternalApproval() {
11669+
async function hasStandingInternalApproval(orgToken) {
11670+
// Fork-triggered runs don't receive org secrets, so we can't verify engineering-team membership and must fall back to requiring the checklist.
11671+
if (!orgToken) {
11672+
return false;
11673+
}
1166811674
const { owner, repo } = github.context.repo;
1166911675
const reviews = await GithubUtils_1.default.paginate(GithubUtils_1.default.octokit.pulls.listReviews, {
1167011676
owner,
@@ -11674,15 +11680,20 @@ async function hasStandingInternalApproval() {
1167411680
// eslint-disable-next-line @typescript-eslint/naming-convention
1167511681
per_page: 100,
1167611682
});
11683+
const decisiveReviews = reviews.filter((review) => !!review.user?.login && DECISIVE_REVIEW_STATES.has(review.state ?? ''));
11684+
// Resolve internal status from engineering-team membership using a read:org token, since concealed members aren't reflected in author_association.
11685+
const orgOctokit = github.getOctokit(orgToken);
11686+
const reviewerLogins = [...new Set(decisiveReviews.map((review) => review.user?.login ?? ''))];
11687+
const membershipResults = await Promise.all(reviewerLogins.map((login) => (0, isTeamMember_1.default)(orgOctokit.rest, CONST_1.default.GITHUB_OWNER, ENGINEERING_TEAM_SLUG, login)));
11688+
const internalReviewerLogins = new Set(reviewerLogins.filter((_, index) => membershipResults.at(index)));
1167711689
// GitHub treats a reviewer's latest decisive review as their standing, so keep only that per internal engineer.
1167811690
const latestStateByInternalReviewer = new Map();
11679-
for (const review of reviews) {
11680-
const login = review.user?.login;
11681-
const state = review.state ?? '';
11682-
if (!login || !INTERNAL_EXPENSIFY_ASSOCIATIONS.has(review.author_association ?? '') || !DECISIVE_REVIEW_STATES.has(state)) {
11691+
for (const review of decisiveReviews) {
11692+
const login = review.user?.login ?? '';
11693+
if (!internalReviewerLogins.has(login)) {
1168311694
continue;
1168411695
}
11685-
latestStateByInternalReviewer.set(login, state);
11696+
latestStateByInternalReviewer.set(login, review.state ?? '');
1168611697
}
1168711698
for (const state of latestStateByInternalReviewer.values()) {
1168811699
if (state === 'APPROVED') {
@@ -11691,7 +11702,7 @@ async function hasStandingInternalApproval() {
1169111702
}
1169211703
return false;
1169311704
}
11694-
hasStandingInternalApproval()
11705+
hasStandingInternalApproval(core.getInput('OS_BOTIFY_TOKEN'))
1169511706
.then((isApproved) => {
1169611707
if (isApproved) {
1169711708
console.log('PR has a standing approval from an internal Expensify engineer, so the reviewer checklist is not required 🎉');
@@ -12235,6 +12246,75 @@ class GithubUtils {
1223512246
exports["default"] = GithubUtils;
1223612247

1223712248

12249+
/***/ }),
12250+
12251+
/***/ 1077:
12252+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
12253+
12254+
"use strict";
12255+
12256+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12257+
if (k2 === undefined) k2 = k;
12258+
var desc = Object.getOwnPropertyDescriptor(m, k);
12259+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12260+
desc = { enumerable: true, get: function() { return m[k]; } };
12261+
}
12262+
Object.defineProperty(o, k2, desc);
12263+
}) : (function(o, m, k, k2) {
12264+
if (k2 === undefined) k2 = k;
12265+
o[k2] = m[k];
12266+
}));
12267+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
12268+
Object.defineProperty(o, "default", { enumerable: true, value: v });
12269+
}) : function(o, v) {
12270+
o["default"] = v;
12271+
});
12272+
var __importStar = (this && this.__importStar) || (function () {
12273+
var ownKeys = function(o) {
12274+
ownKeys = Object.getOwnPropertyNames || function (o) {
12275+
var ar = [];
12276+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
12277+
return ar;
12278+
};
12279+
return ownKeys(o);
12280+
};
12281+
return function (mod) {
12282+
if (mod && mod.__esModule) return mod;
12283+
var result = {};
12284+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
12285+
__setModuleDefault(result, mod);
12286+
return result;
12287+
};
12288+
})();
12289+
Object.defineProperty(exports, "__esModule", ({ value: true }));
12290+
/* eslint-disable @typescript-eslint/naming-convention */
12291+
const core = __importStar(__nccwpck_require__(2186));
12292+
const request_error_1 = __nccwpck_require__(537);
12293+
/**
12294+
* Whether a user is a member of the given org team.
12295+
* The octokit must be authenticated with a token that has read:org scope, otherwise concealed (private) members are reported as non-members.
12296+
*/
12297+
async function isTeamMember(octokit, org, teamSlug, username) {
12298+
try {
12299+
await octokit.teams.getMembershipForUserInOrg({
12300+
org,
12301+
team_slug: teamSlug,
12302+
username,
12303+
});
12304+
return true;
12305+
}
12306+
catch (error) {
12307+
if (error instanceof request_error_1.RequestError && error.status === 404) {
12308+
return false;
12309+
}
12310+
const message = error instanceof Error ? error.message : String(error);
12311+
core.warning(`Could not verify ${teamSlug} membership for ${username}. Assuming they are not a member: ${message}`);
12312+
return false;
12313+
}
12314+
}
12315+
exports["default"] = isTeamMember;
12316+
12317+
1223812318
/***/ }),
1223912319

1224012320
/***/ 9491:

0 commit comments

Comments
 (0)