Skip to content

Commit 76d47e6

Browse files
committed
Merge branch 'main' into fix/expense-unexpected-error-after-report-rename
2 parents 3c3a597 + 43a57cf commit 76d47e6

842 files changed

Lines changed: 27732 additions & 10500 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.

.claude/skills/playwright-app-testing/SKILL.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,19 @@ When signing in to dev environment:
4141
- **Email**: Generate random Gmail address (e.g., `user+throwaway<random>@gmail.com`)
4242
- **New Account**: Press join to create account
4343
- **Existing Account**: Magic code: Always `000000`
44-
- **Onboarding**: Skip all optional steps
44+
- **Onboarding**: The `SKIP_ONBOARDING` env flag is set to `false` by default in `.env`. When `false`, onboarding screens will appear after sign-in for new accounts. Unless you are specifically asked to test onboarding, update the flag to `true` before starting the dev server so that onboarding is bypassed entirely:
45+
```bash
46+
sed -i '' 's/SKIP_ONBOARDING=false/SKIP_ONBOARDING=true/' .env
47+
```
48+
If you need to test onboarding flows, set it back to `false`:
49+
```bash
50+
sed -i '' 's/SKIP_ONBOARDING=true/SKIP_ONBOARDING=false/' .env
51+
```
52+
You can check the current value with:
53+
```bash
54+
grep SKIP_ONBOARDING .env
55+
```
56+
**Important**: After changing `SKIP_ONBOARDING` in `.env`, the web dev server must be restarted for the change to take effect.
4557

4658
## Example Usage
4759

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SECURE_NGROK_URL=https://secure-expensify-user.ngrok.io/
99
NGROK_URL=https://expensify-user.ngrok.io/
1010
USE_NGROK=false
1111
USE_WEB_PROXY=false
12+
SKIP_ONBOARDING=false
1213
USE_WDYR=false
1314
USE_REDUX_DEVTOOLS=false
1415
CAPTURE_METRICS=false

.github/ISSUE_TEMPLATE/OnboardOffboardExpertContributor.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ Which action do you wish to take for this team member (select one):
3434
### Ring0 Tasks
3535

3636
- [ ] If adding, add to the appropriate GitHub child team of [external-expert-contributors](https://github.com/orgs/Expensify/teams/external-expert-contributors/teams) (each agency must have its own child team)
37+
- [ ] If adding, create an [IdentityDot account](https://stackoverflowteams.com/c/expensify/questions/22914) for the contributor
3738
- [ ] If removing, remove from our organization [here](https://github.com/orgs/Expensify/people)
39+
- [ ] If removing, delete the contributor's [IdentityDot account](https://stackoverflowteams.com/c/expensify/questions/22914)

.github/actions/javascript/getPullRequestIncrementalChanges/getPullRequestIncrementalChanges.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as core from '@actions/core';
22
import {context} from '@actions/github';
3+
import {RequestError} from '@octokit/request-error';
34
import type {PullRequestEvent, PullRequestSynchronizeEvent} from '@octokit/webhooks-types';
45
import {getJSONInput} from '@github/libs/ActionUtils';
56
import CONST from '@github/libs/CONST';
@@ -89,7 +90,29 @@ async function run(): Promise<void> {
8990

9091
// Now we know there are local changes - get PR diff from the GitHub API to compare
9192
console.log(`🌐 Using GitHub API to validate ${localChangedFiles.size} files with local changes`);
92-
const prDiff = Git.parseDiff(await GitHubUtils.getPullRequestDiff(prNumber));
93+
94+
let prDiffString: string;
95+
try {
96+
prDiffString = await GitHubUtils.getPullRequestDiff(prNumber);
97+
} catch (error) {
98+
const isTooLarge =
99+
error instanceof RequestError &&
100+
typeof error.response?.data === 'object' &&
101+
error.response.data !== null &&
102+
'errors' in error.response.data &&
103+
((error.response.data as {errors?: Array<{code?: string}>}).errors ?? []).some((e) => e.code === 'too_large');
104+
105+
if (!isTooLarge) {
106+
throw error;
107+
}
108+
109+
core.warning(`PR #${prNumber} diff is too large for the GitHub API. Skipping incremental change detection.`);
110+
core.setOutput('CHANGED_FILES', JSON.stringify([]));
111+
core.setOutput('HAS_CHANGES', false);
112+
return;
113+
}
114+
115+
const prDiff = Git.parseDiff(prDiffString);
93116

94117
// Compare the local push diff with the PR diff and collect changed files, checking for overlapping content changes at the line level
95118
for (const prFileDiff of prDiff.files) {

.github/actions/javascript/getPullRequestIncrementalChanges/index.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11582,6 +11582,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1158211582
Object.defineProperty(exports, "__esModule", ({ value: true }));
1158311583
const core = __importStar(__nccwpck_require__(2186));
1158411584
const github_1 = __nccwpck_require__(5438);
11585+
const request_error_1 = __nccwpck_require__(537);
1158511586
const ActionUtils_1 = __nccwpck_require__(6981);
1158611587
const CONST_1 = __importDefault(__nccwpck_require__(9873));
1158711588
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
@@ -11655,7 +11656,25 @@ async function run() {
1165511656
}
1165611657
// Now we know there are local changes - get PR diff from the GitHub API to compare
1165711658
console.log(`🌐 Using GitHub API to validate ${localChangedFiles.size} files with local changes`);
11658-
const prDiff = Git_1.default.parseDiff(await GithubUtils_1.default.getPullRequestDiff(prNumber));
11659+
let prDiffString;
11660+
try {
11661+
prDiffString = await GithubUtils_1.default.getPullRequestDiff(prNumber);
11662+
}
11663+
catch (error) {
11664+
const isTooLarge = error instanceof request_error_1.RequestError &&
11665+
typeof error.response?.data === 'object' &&
11666+
error.response.data !== null &&
11667+
'errors' in error.response.data &&
11668+
(error.response.data.errors ?? []).some((e) => e.code === 'too_large');
11669+
if (!isTooLarge) {
11670+
throw error;
11671+
}
11672+
core.warning(`PR #${prNumber} diff is too large for the GitHub API. Skipping incremental change detection.`);
11673+
core.setOutput('CHANGED_FILES', JSON.stringify([]));
11674+
core.setOutput('HAS_CHANGES', false);
11675+
return;
11676+
}
11677+
const prDiff = Git_1.default.parseDiff(prDiffString);
1165911678
// Compare the local push diff with the PR diff and collect changed files, checking for overlapping content changes at the line level
1166011679
for (const prFileDiff of prDiff.files) {
1166111680
const filePath = prFileDiff.filePath;
@@ -12506,6 +12525,7 @@ class Git {
1250612525
newStart,
1250712526
newCount,
1250812527
lines: [],
12528+
contextLineCount: 0,
1250912529
};
1251012530
}
1251112531
continue;
@@ -12533,7 +12553,8 @@ class Git {
1253312553
});
1253412554
}
1253512555
else if (firstChar === ' ') {
12536-
// Context line - skip it (we only care about added/removed lines)
12556+
// Context line - count it so calculateLineNumber accounts for position advancement
12557+
currentHunk.contextLineCount++;
1253712558
continue;
1253812559
}
1253912560
else if (firstChar === '\\') {
@@ -12597,9 +12618,9 @@ class Git {
1259712618
const removedCount = hunk.lines.filter((l) => l.type === 'removed').length;
1259812619
switch (lineType) {
1259912620
case 'added':
12600-
return hunk.newStart + addedCount;
12621+
return hunk.newStart + hunk.contextLineCount + addedCount;
1260112622
case 'removed':
12602-
return hunk.oldStart + removedCount;
12623+
return hunk.oldStart + hunk.contextLineCount + removedCount;
1260312624
default:
1260412625
throw new Error(`Unknown line type: ${String(lineType)}`);
1260512626
}
@@ -12795,6 +12816,7 @@ class Git {
1279512816
newStart: 1,
1279612817
newCount: lines.length,
1279712818
lines: diffLines,
12819+
contextLineCount: 0,
1279812820
};
1279912821
const fileDiff = {
1280012822
filePath,

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11622,15 +11622,13 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1162211622
})
1162311623
.then(() => {
1162411624
console.log(`Looking through all ${combinedComments.length} comments for the reviewer checklist...`);
11625+
const maxCompletedItems = numberOfChecklistItems + 2;
11626+
const minCompletedItems = numberOfChecklistItems - 2;
1162511627
let foundReviewerChecklist = false;
1162611628
let numberOfFinishedChecklistItems = 0;
1162711629
let numberOfUnfinishedChecklistItems = 0;
1162811630
// Once we've gathered all the data, loop through each comment and look to see if it contains the reviewer checklist
1162911631
for (let i = 0; i < combinedComments.length; i++) {
11630-
// Skip all other comments if we already found the reviewer checklist
11631-
if (foundReviewerChecklist) {
11632-
break;
11633-
}
1163411632
const whitespace = /([\n\r])/gm;
1163511633
const comment = combinedComments.at(i)?.replaceAll(whitespace, '');
1163611634
console.log(`Comment ${i} starts with: ${comment?.slice(0, 20)}...`);
@@ -11640,14 +11638,16 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1164011638
foundReviewerChecklist = true;
1164111639
numberOfFinishedChecklistItems = (comment?.match(/- \[x\]/gi) ?? []).length;
1164211640
numberOfUnfinishedChecklistItems = (comment?.match(/- \[ \]/g) ?? []).length;
11641+
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
11642+
console.log('PR Reviewer checklist is complete 🎉');
11643+
return;
11644+
}
1164311645
}
1164411646
}
1164511647
if (!foundReviewerChecklist) {
1164611648
core.setFailed('No PR Reviewer Checklist was found');
1164711649
return;
1164811650
}
11649-
const maxCompletedItems = numberOfChecklistItems + 2;
11650-
const minCompletedItems = numberOfChecklistItems - 2;
1165111651
console.log(`You completed ${numberOfFinishedChecklistItems} out of ${numberOfChecklistItems} checklist items with ${numberOfUnfinishedChecklistItems} unfinished items`);
1165211652
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
1165311653
console.log('PR Reviewer checklist is complete 🎉');

.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,14 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
4343
})
4444
.then(() => {
4545
console.log(`Looking through all ${combinedComments.length} comments for the reviewer checklist...`);
46+
const maxCompletedItems = numberOfChecklistItems + 2;
47+
const minCompletedItems = numberOfChecklistItems - 2;
4648
let foundReviewerChecklist = false;
4749
let numberOfFinishedChecklistItems = 0;
4850
let numberOfUnfinishedChecklistItems = 0;
4951

5052
// Once we've gathered all the data, loop through each comment and look to see if it contains the reviewer checklist
5153
for (let i = 0; i < combinedComments.length; i++) {
52-
// Skip all other comments if we already found the reviewer checklist
53-
if (foundReviewerChecklist) {
54-
break;
55-
}
56-
5754
const whitespace = /([\n\r])/gm;
5855
const comment = combinedComments.at(i)?.replaceAll(whitespace, '');
5956

@@ -65,6 +62,11 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
6562
foundReviewerChecklist = true;
6663
numberOfFinishedChecklistItems = (comment?.match(/- \[x\]/gi) ?? []).length;
6764
numberOfUnfinishedChecklistItems = (comment?.match(/- \[ \]/g) ?? []).length;
65+
66+
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
67+
console.log('PR Reviewer checklist is complete 🎉');
68+
return;
69+
}
6870
}
6971
}
7072

@@ -73,9 +75,6 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
7375
return;
7476
}
7577

76-
const maxCompletedItems = numberOfChecklistItems + 2;
77-
const minCompletedItems = numberOfChecklistItems - 2;
78-
7978
console.log(`You completed ${numberOfFinishedChecklistItems} out of ${numberOfChecklistItems} checklist items with ${numberOfUnfinishedChecklistItems} unfinished items`);
8079

8180
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {

.github/scripts/createDocsRoutes.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,16 @@ function toTitleCase(str: string): string {
6060
}
6161

6262
/**
63-
* @param filename - The name of the file
63+
* @param filename - The name of the file (path used for href)
64+
* @param order - Optional order from front matter
65+
* @param titleOverride - Optional display title (e.g. subfolder name: "Export-Errors" -> "Export Errors")
6466
*/
65-
function getArticleObj(filename: string, order?: number): Article {
67+
function getArticleObj(filename: string, order?: number, titleOverride?: string): Article {
6668
const href = filename.replace('.md', '');
69+
const title = titleOverride ? toTitleCase(titleOverride.replaceAll('-', ' ')) : toTitleCase(href.replaceAll('-', ' '));
6770
return {
6871
href,
69-
title: toTitleCase(href.replaceAll('-', ' ')),
72+
title,
7073
order,
7174
};
7275
}
@@ -123,10 +126,40 @@ function createHubsWithArticles(hubs: string[], platformName: ValueOf<typeof pla
123126
const section = fileOrFolder;
124127
const articles: Article[] = [];
125128

126-
// Each subfolder will be a section containing articles
127-
for (const subArticle of fs.readdirSync(`${docsDir}/articles/${platformName}/${hub}/${section}`)) {
128-
const order = getOrderFromArticleFrontMatter(`${docsDir}/articles/${platformName}/${hub}/${section}/${subArticle}`);
129-
articles.push(getArticleObj(subArticle, order));
129+
// Section can contain .md files directly and/or subfolders (and nested subfolders) that contain .md files
130+
const sectionPath = `${docsDir}/articles/${platformName}/${hub}/${section}`;
131+
for (const entry of fs.readdirSync(sectionPath)) {
132+
const entryPath = `${sectionPath}/${entry}`;
133+
if (entry.endsWith('.md') && fs.statSync(entryPath).isFile()) {
134+
const order = getOrderFromArticleFrontMatter(entryPath);
135+
articles.push(getArticleObj(entry, order));
136+
continue;
137+
}
138+
if (fs.statSync(entryPath).isDirectory()) {
139+
// One level: section/SubFolder/file.md -> href "SubFolder/file", display title = "Troubleshoot SubFolder"
140+
for (const file of fs.readdirSync(entryPath)) {
141+
const filePath = `${entryPath}/${file}`;
142+
if (file.endsWith('.md') && fs.statSync(filePath).isFile()) {
143+
const order = getOrderFromArticleFrontMatter(filePath);
144+
articles.push(getArticleObj(`${entry}/${file}`, order, `Troubleshoot ${entry}`));
145+
continue;
146+
}
147+
if (fs.statSync(filePath).isDirectory()) {
148+
// Two levels: section/SubFolder/NestedFolder/file.md -> href "SubFolder/NestedFolder/file", display title = "Troubleshoot NestedFolder" (e.g. "Troubleshoot Export Errors")
149+
for (const nestedFile of fs.readdirSync(filePath)) {
150+
if (!nestedFile.endsWith('.md')) {
151+
continue;
152+
}
153+
const nestedPath = `${filePath}/${nestedFile}`;
154+
if (!fs.statSync(nestedPath).isFile()) {
155+
continue;
156+
}
157+
const order = getOrderFromArticleFrontMatter(nestedPath);
158+
articles.push(getArticleObj(`${entry}/${file}/${nestedFile}`, order, `Troubleshoot ${file}`));
159+
}
160+
}
161+
}
162+
}
130163
}
131164

132165
pushOrCreateEntry(routeHubs, hub, 'sections', {

.github/workflows/buildAndroid.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ jobs:
183183
comment-bot: false
184184
rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }}
185185
custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
186-
validate-elf-alignment: false
186+
validate-elf-alignment: ${{ inputs.variant != 'Release' && 'true' || 'false' }}
187187

188188
- name: Upload Gradle profile report
189189
if: always()
@@ -283,6 +283,10 @@ jobs:
283283
--key-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }}
284284
unzip -p Expensify.apks universal.apk > Expensify.apk
285285
286+
- name: Validate ELF alignment for Release APK
287+
if: ${{ inputs.variant == 'Release' && steps.collectArtifacts.outputs.HAS_AAB == 'true' }}
288+
run: npx rock validate-elf-alignment "Expensify.apk"
289+
286290
- name: Upload Android APK build artifact
287291
if: steps.collectArtifacts.outputs.HAS_AAB == 'true'
288292
# v6

0 commit comments

Comments
 (0)