Skip to content

[No QA] Make -changed scripts check the working tree, not just committed changes - #99764

Merged
roryabraham merged 9 commits into
Expensify:mainfrom
software-mansion-labs:korytko/99113-lint-changed-working-tree
Sep 1, 2026
Merged

roryabraham merged 9 commits into
Expensify:mainfrom
software-mansion-labs:korytko/99113-lint-changed-working-tree

Conversation

@JakubKorytko

Copy link
Copy Markdown
Member

Explanation of Change

The -changed scripts (lint-changed, check-changed, knip-changed, spell-changed) are meant to check your in-progress work, but some of them missed uncommitted or new files, or compared against your local main (which is stale if you haven't pulled recently). Now they all diff against a freshly-fetched origin/main and check your actual working tree, including files you haven't committed yet.

Fixed Issues

$ #99113
PROPOSAL: Slack

Tests

  1. Make an uncommitted (unstaged) edit to a .ts/.tsx file with an obvious lint violation (e.g. an unused variable), and create a new untracked .ts file with a lint violation, without committing either.
  2. Run npm run lint-changed.
  3. Verify that both the uncommitted edit and the new untracked file are linted and the violations are reported (previously only committed changes were checked).
  4. Add a new untracked .tsx file containing a React component/hook.
  5. Run npm run react-compiler-compliance-check check-changed.
  6. Verify that the untracked file is included in the check output (previously untracked files were skipped).
  7. Make an uncommitted edit that introduces a misspelled word in a tracked file, and create a new untracked file that also contains a misspelled word.
  8. Run npm run spell-changed with no arguments.
  9. Verify both files are checked and the misspellings are reported (previously this command did nothing without an explicit file list).
  10. On a branch, make your local main branch stale relative to origin/main (e.g. git checkout main && git reset --hard main@{1} to move it back one pull, or simply don't pull if it's already behind), then switch back to your branch.
  11. Run npm run knip-changed.
  12. Verify the "Running knip on merge base ()" line prints a SHA matching git merge-base origin/main HEAD (fresh), not git merge-base main HEAD (stale) - the script fetches origin/main itself, so it should use the up-to-date remote branch regardless of your local main's state.

Offline tests

N/A

QA Steps

N/A

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@JakubKorytko

Copy link
Copy Markdown
Member Author

@codex review

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@JakubKorytko

Copy link
Copy Markdown
Member Author

@MelvinBot review

@JakubKorytko

Copy link
Copy Markdown
Member Author

@codex review

@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 254 files with indirect coverage changes

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

Copy link
Copy Markdown
Contributor

Review — 👍 LGTM, no blocking issues

Solid, well-factored change. The three -changed scripts now share a single definition of "base" (get_merge_base_with_main) and "changed set" (get_changed_files), so they finally agree on what they check, and they now cover staged/unstaged/untracked work against a freshly-fetched origin/main — exactly what the description promises. I confirmed the change is CI-safe (see below). Only a couple of minor, non-blocking notes.

What I verified

CI is not affected by the working-tree / untracked behavior change:

  • lint / knip: CI runs the full npm run lint / knip, not the -changed scripts (lint.yml uses npm run lint). lintChanged.sh/knip-changed.sh are local-only, so diffing against the working tree can't leak into CI.
  • cspell: cspell.yml calls npm run spell-changed -- with an explicit file list, which hits the new args branch in spellChanged.shcspell --no-must-find-files "$@", identical to the old command. Backward-compatible. ✅
  • react-compiler: getChangedFilesWithStatus(..., true) is a no-op in CI — getChangedFilesWithStatus returns via the GitHub-API path when IS_CI, so the untracked flag only matters locally. ✅

Bash correctness spot-checks (all good):

  • local x is declared on its own line before the command-substitution assignment in get_merge_base_with_main, avoiding the classic local x=$(...) exit-status masking under set -e. Nicely done.
  • get_changed_files guards ${#path_specs[@]} -gt 0 before expanding "${path_specs[@]}", so it won't trip set -u on an empty array (bash 3.2/macOS). ✅
  • The printf '%s\n%s' … | grep -v '^$' || true correctly collapses the empty-diff / empty-untracked cases without a stray leading/trailing blank line. ✅
  • git fetch … >&2 keeps stdout clean so the merge-base SHA is the only thing captured by $(…). ✅
Minor, non-blocking
  1. error/info write to stdout, not stderr (shellUtils.sh:32-38). Harmless today because error inside get_merge_base_with_main is only reached on the return 1 path (the caller exits and discards the captured value). But since this helper's whole job is to be captured via $(…), redirecting these log helpers to >&2 would make it more robust against a future caller that logs on a value-producing path. Optional.

  2. Dotfile filter is root-level only (spellChanged.sh:24). grep -v '^\.' excludes .github/… etc. but not a nested dotpath like src/foo/.bar, whereas npm run spell's **/* glob skips dotfiles at every level. It does match cspell.yml's identical filter exactly, so local↔CI behavior is consistent — just noting the nuance, not a change request.

  3. Doc nit (out of scope): CLAUDE.md's "cspell needs an explicit file list" note for spell-changed is now stale, since the no-arg branch discovers changed files. Worth a follow-up touch-up.

(No runtime/UI testing applicable — this is local dev tooling with no App-behavior surface. The test plan is CLI-only; nothing a browser run would exercise.)


view run

@JakubKorytko

Copy link
Copy Markdown
Member Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Re-review of 2b4d390 — 👍 Ship it

The follow-up commit addresses my earlier notes and the changes are safe. No new issues.

  • Log helpers → stderr (shellUtils.sh:32-38): error/info now write to >&2. I checked the other ~19 scripts that source shellUtils.sh — none rely on these landing on stdout. In fact is-hybrid-app.sh:7 already had a manual error '…' >&2 workaround precisely because it echoes true/false to stdout for capture — so this default now matches existing intent (the explicit >&2 there is redundant but harmless). ✅
  • Doc note fixed (CLAUDE.md:74): the stale "cspell needs an explicit file list" guidance now reflects that spell-changed discovers changed files itself. ✅

The one remaining item from before — the root-level-only grep -v '^\.' dotfile filter in spellChanged.sh — was intentionally left as-is, which is correct since it matches cspell.yml's filter exactly. No action needed there.

(No runtime/UI testing applicable — local dev tooling with no App-behavior surface.)


view run

@JakubKorytko
JakubKorytko marked this pull request as ready for review August 28, 2026 14:48
@JakubKorytko
JakubKorytko requested a review from a team as a code owner August 28, 2026 14:48
@melvin-bot
melvin-bot Bot requested a review from situchan August 28, 2026 14:48
@melvin-bot

melvin-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

@situchan Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team August 28, 2026 14:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b4d390cce

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/spellChanged.sh Outdated
Comment thread scripts/shellUtils.sh Outdated
@situchan

situchan commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari
before.mov
after.mov

Comment thread scripts/shellUtils.sh Outdated
# what "changed" means.
# Usage: get_merge_base_with_main
get_merge_base_with_main() {
git fetch origin main --no-tags >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint-changed now silently ignores a failed origin/main fetch and exits 0

Before, git fetch origin main --no-tags was a top-level command under set -eu, so a failed fetch killed the script.
Now it lives inside get_merge_base_with_main, called as MERGE_BASE_SHA_HASH="$(get_merge_base_with_main)" || exit 1

The || exit 1 puts the call in a context where errexit is ignored, and per the shell rule, that suppression applies to the entire function body, on every bash version.
So the failing git fetch is stepped over and the merge base is computed from whatever stale origin/main the clone happens to have.

Confirmed with a stub git that fails only on fetch:

OLD:  Fetching origin/main → fatal: could not resolve host → exit 128
NEW:  Fetching origin/main → fatal: could not resolve host → "No lintable files changed" → exit 0

So when github connection fails, you now lint against a stale base and get a green result where you used to get a loud failure

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, fixed - explicit exit-status check on git fetch now, no longer relying on set -e inside the || exit 1 context. Also dropped that now-redundant || exit 1

async function checkChangedFiles(remote: string, verbose: boolean, checkOxc: OxcChecker): Promise<boolean> {
const mainBaseCommitHash = await Git.getMainBranchCommitHash(remote);
const changedFiles = await Git.getChangedFilesWithStatus(mainBaseCommitHash);
const changedFiles = await Git.getChangedFilesWithStatus(mainBaseCommitHash, undefined, true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react-compiler-compliance-check check-changed now reads every untracked file in the repo, in full

shouldIncludeUntrackedFiles=true reaches getUntrackedFiles(undefined) (no path filter) so all untracked non-ignored files, and createFileDiffsForUntrackedFiles does fs.readFileSync(path, 'utf8') on each and allocates one DiffLine object per line.
The .ts/.tsx filter only runs afterwards, at line 253.
.gitignore has no rule for *.mov, *.mp4, *.png, *.zip or heap snapshots, so a screen recording or profile parked in the tree makes the check allocate hundreds of MB for files it immediately discards.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, threaded an extensions filter into Git.diff so untracked files get extension-filtered before being read

Comment thread scripts/spellChanged.sh Outdated

if [[ -n "$ALL_CHANGED_FILES" ]]; then
# shellcheck disable=SC2086 # For multiple files in variable
exec "${TOP}/node_modules/.bin/cspell" --color --no-must-find-files $ALL_CHANGED_FILES

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spell-changed silently checks nothing for paths with spaces

CSPELL ARGS (4):
  [my]  [new]  [file.md]  [plainfile.md]
  !! MISSING: my / new / file.md
exit=0

$ALL_CHANGED_FILES is unquoted, and --no-must-find-files turns the resulting nonexistent fragments into a clean exit 0.
Same failure for non-ASCII paths, which git emits C-quoted.
The repo already has 56 tracked paths with spaces.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed and verified locally with a space-bearing and non-ASCII filename - core.quotepath=false plus array-based reads, both come through as single args now

Comment thread scripts/shellUtils.sh Outdated
Comment on lines +152 to +171
get_changed_files() {
local base_sha="$1"
shift
local path_specs=("$@")

local diff_output
if [[ ${#path_specs[@]} -gt 0 ]]; then
diff_output="$(git diff --diff-filter=AMR --name-only "$base_sha" -- "${path_specs[@]}")"
else
diff_output="$(git diff --diff-filter=AMR --name-only "$base_sha")"
fi

local untracked_output
if [[ ${#path_specs[@]} -gt 0 ]]; then
untracked_output="$(git ls-files --others --exclude-standard -- "${path_specs[@]}")"
else
untracked_output="$(git ls-files --others --exclude-standard)"
fi

printf '%s\n%s' "$diff_output" "$untracked_output" | grep -v '^$' || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

${#path_specs[@]} -gt 0 branch is written out twice, and the two outputs are stitched with printf '%s\n%s' | grep -v '^$' || true to strip the blank line the stitching itself creates.
Neither is needed.
"$@" is exempt from set -u when empty (unlike a named array: that's the only reason path_specs exists), and git diff … -- / git ls-files … -- with an empty pathspec means "everything", so one form covers both cases.
And each git command emits either complete lines or nothing at all, so there are no blank lines to filter:

get_changed_files() {
  local base_sha="$1"
  shift
  git diff --diff-filter=AMR --name-only "$base_sha" -- "$@"
  git ls-files --others --exclude-standard -- "$@"
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took your rewrite, kept || return 1 on the diff call so a diff failure isn't masked by ls-files succeeding

Comment thread scripts/spellChanged.sh
exec "${TOP}/node_modules/.bin/cspell" --color --no-must-find-files $ALL_CHANGED_FILES
else
info "No changed files to spell check"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has the same structure with lintChanged.sh:11-26.
The PR extracted the first two steps into helpers but left the last two duplicated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got your point but leaving duplicated - the two tails exec different tools with different args, a shared helper would just be an if/else around two one-liners

Comment thread scripts/shellUtils.sh
Comment on lines +137 to +143
local merge_base_sha_hash
merge_base_sha_hash="$(git merge-base origin/main HEAD)"

if [[ -z "$merge_base_sha_hash" ]] || ! [[ "$merge_base_sha_hash" =~ ^[a-fA-F0-9]{40}$ ]]; then
error "git merge-base returned unexpected output: $merge_base_sha_hash"
return 1
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merge-base validation checks the output instead of the exit status

The -z arm is dead: the empty string already fails the 40-hex regex, and both arms produce the same message.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropped the dead -z arm

Comment thread scripts/spellChanged.sh Outdated
Comment on lines +21 to +22
# Excludes dotfiles and files under dot-directories (e.g. .github/) to match this script's prior behavior
ALL_CHANGED_FILES="$(get_changed_files "$MERGE_BASE_SHA_HASH" | grep -v '^\.' || true)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inline comment explains the wrong "why"
There was no prior behavior, with no args the old cspell --no-must-find-files checked nothing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh yep, reworded to the actual reason (dotfiles aren't meant to be spell-checked)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b4d390cce

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/shellUtils.sh Outdated
@MelvinBot

Copy link
Copy Markdown
Contributor

Corrected review of 2b4d390⚠️ two real bugs, walking back my earlier "ship it"

I have to retract my previous LGTM. Re-checking against situchan's and Codex's inline comments, I independently confirmed two correctness issues (one a regression) plus some quality items. Findings, most severe first:

1. [Regression] lint-changed silently lints a stale base when git fetch fails.
lintChanged.sh:12 calls the helper as MERGE_BASE_SHA_HASH="$(get_merge_base_with_main)" || exit 1. The || exit 1 puts the call in a tested context, which suppresses errexit for the entire function body — so the failing git fetch origin main at shellUtils.sh:135 is stepped over, git merge-base succeeds against whatever stale origin/main the clone has, and you get a green lint. The old code ran git fetch as a top-level command under set -eu, so it failed loudly — this is a regression. situchan's stub-git repro (old → exit 128, new → exit 0) matches the shell semantics. Nuance: this is specific to lint-changed; knip-changed/spell-changed call the helper without || exit, so set -e still aborts them on a failed fetch (Codex's "all three callers" overstates it). Fix: check git fetch's exit status inside get_merge_base_with_main (e.g. if ! git fetch …; then error …; return 1; fi) so it's robust in every calling context.

2. [Correctness] spell-changed silently skips paths with spaces / non-ASCII.
Unquoted $ALL_CHANGED_FILES at spellChanged.sh:27 word-splits on whitespace; with --no-must-find-files, the resulting nonexistent fragments give a clean exit 0, so an edited space-bearing path (the repo has many under assets//docs/assets/) is checked as nothing. The same unquoted expansion is at lintChanged.sh:23 (eslint would likely error there rather than pass, but it's still wrong). Fix: collect into a bash array (or NUL-delimited) and pass quoted.

3. [Efficiency] react-compiler-compliance-check check-changed reads every untracked file in full before filtering.
react-compiler-compliance-check.ts:251 passes shouldIncludeUntrackedFiles=true with no path filter → getUntrackedFiles(undefined) returns all untracked files, and createFileDiffsForUntrackedFiles does fs.readFileSync(…, 'utf8') + a DiffLine per line on each (Git.ts:594) — the .ts/.tsx filter only runs afterward. An untracked .mov/.png/.zip/heap-snapshot parked in the tree gets fully read then discarded. Correct results, but wasteful. Fix: thread the extension pathspec through to the untracked-file discovery.

Quality items (non-blocking, but worth folding in)
  • get_changed_files is over-built (shellUtils.sh:152-172): the two ${#path_specs[@]} -gt 0 branches and the printf … | grep -v '^$' stitching aren't needed — "$@" is set -u-safe when empty and an empty pathspec after -- means "everything"; the git commands emit whole lines or nothing. situchan's ~4-line version is equivalent (apply the Fix spaces #2 quoting fix on top).
  • Dead validation arm (shellUtils.sh:140): the -z check is unreachable (an empty string already fails the 40-hex regex) and both arms print the same message. More to the point, it validates the output instead of git merge-base's exit status — same root as Some initial fixes and code style updates #1.
  • Wrong rationale + leftover duplication (spellChanged.sh:21): "to match this script's prior behavior" is inaccurate — the old no-arg cspell --no-must-find-files checked nothing. And spellChanged.sh still duplicates lintChanged.sh's last two steps even though the first two were extracted into helpers.

I own that #1 and #2 slipped past my earlier passes — situchan and Codex are right on those.

(No runtime/UI testing applicable — local dev tooling with no App-behavior surface.)


view run

@MelvinBot

Copy link
Copy Markdown
Contributor

Regression scan — no breaking regressions found ✅

I couldn't find any regression that would break CI or existing usage. The behavior changes are the intended ones (check the working tree + untracked files, baseline against a fresh origin/main merge-base). A few intentional behavior shifts and two minor watch-items are worth calling out for QA.

CI is safe (verified):

  • lint / knip: CI runs the full npm run lint / knip, not the -changed scripts — so diffing against the working tree can't leak into CI.
  • cspell: cspell.yml passes an explicit file list, which hits the args branch in spellChanged.sh:14cspell --no-must-find-files "$@", byte-identical to the old command.
  • react-compiler: getChangedFilesWithStatus(…, true) is a no-op in CI — getChangedFilesWithStatus returns via the GitHub-API path when IS_CI, so the untracked flag only matters locally.
Intentional behavior changes (not regressions, but flag for QA)
  1. knip-changed / spell-changed now require network. get_merge_base_with_main does git fetch origin main on every run. knip-changed previously worked fully offline against local main; it will now fail offline. This is the intended tradeoff (consistency with lint-changed, freshness), just the one genuine behavior regression — offline dev.

  2. spell-changed with no args changed from a no-op to discovery. The old spell-changed (cspell --no-must-find-files with no globs) checked nothing without an explicit file list; it now discovers changed files. Strict improvement, and CLAUDE.md was updated to match. Args-mode is unchanged.

  3. knip-changed baseline moved from local main tip → merge-base(origin/main, HEAD). More correct for "delta introduced by my branch," but the reported knip delta can differ from before if your branch is behind main.

Minor watch-items
  • Untracked React files flow correctly. createFileDiffsForUntrackedFiles tags them diffType: 'added', and checkChangedFiles treats 'added' as "must compile, no main baseline" and reads content from disk — so untracked files are handled exactly like newly-committed adds. No regression.
  • No-arg spell-changed now spell-checks all changed file extensions (not just code), relying on cspell.json ignore rules — the same rule set npm run spell uses, so parity. Watch for noise only if a changed generated/data file isn't covered by ignorePaths.
  • Lost the friendly "git diff failed" message. Old lintChanged.sh wrapped git diff to print a contextual error; get_changed_files doesn't, so a git diff failure now just aborts via set -e without the message. The merge-base validation message is preserved. Cosmetic.
  • Filenames with spaces break the unquoted $ALL_CHANGED_FILES expansion — but this is unchanged from the old scripts (which also expanded $GIT_DIFF_OUTPUT unquoted), so not a regression.

(No runtime/UI testing applicable — this is local dev tooling with no App-behavior surface, so there's nothing a browser run could exercise. The regression risk is entirely in the build/dev scripts, which I audited above.)


view run

@JakubKorytko

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85ea9aa4e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/lintChanged.sh Outdated
@JakubKorytko

Copy link
Copy Markdown
Member Author

@codex review

@situchan

Copy link
Copy Markdown
Contributor

@MelvinBot review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 85ea9aa4e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

Copy link
Copy Markdown
Contributor

Review of 85ea9aa — one blocker, three minor notes

The round of fixes landed cleanly: I re-checked each earlier finding against the current code and they are all genuinely resolved (details at the bottom). But the new commit introduced one break of its own, and I disagree with Codex's diagnosis of the other new comment.


🔴 1. mapfile breaks lint-changed on macOS system bash

scripts/lintChanged.sh:20 uses mapfile, which does not exist in bash 3.2 — the /bin/bash this script's shebang resolves to on macOS. Under set -eu the script dies with mapfile: command not found (exit 127), and it dies only when there are lintable changes, i.e. in exactly the case the script exists for.

This is the one place in scripts/ that uses mapfile, and the repo already ships the fix: read_lines_into_array is documented as "a bash 3 polyfill for readarray" and is used by pod-install.sh:79 and verifyPodfile.sh:67 — both macOS-facing scripts. spellChanged.sh in this very PR already avoids mapfile with a while read loop; lintChanged.sh just needs to match it.

In fairness: bash 3 is not universally enforced — generateCertificatePins.sh:83 already uses declare -A. But that's a rarely-run script, whereas lint-changed is the documented step 1 of the post-edit checklist in CLAUDE.md. Worth fixing.

While you're in there, also sanity-check ${#ALL_CHANGED_FILES[@]} on an empty array under set -u (lintChanged.sh:24, spellChanged.sh:33) — older bash treats empty arrays as unset. I could not test 3.2 here, so treat that as "verify", not "confirmed".


🟡 2. Codex's Git.ts:145 comment is right about the symptom, wrong about the cause

Codex says the new extension filter drops C-quoted non-ASCII untracked files. The file is dropped — but removing the filter would not fix it, so don't change line 145 on the strength of that comment.

getUntrackedFiles runs plain git ls-files --others --exclude-standard with no -z and no core.quotepath=false, so git hands back "src/caf\303\251.tsx". Even with the filter gone, createFileDiffsForUntrackedFiles then fails its fs.existsSync guard on that quoted path and continues. The real fix is -z (or -c core.quotepath=false) in getUntrackedFiles.

Non-blocking: the repo currently has zero non-ASCII tracked paths (I checked). Worth noting only because the shell half of this PR did fix quoting via core.quotepath=false at shellUtils.sh:162-163 — the TS half is now the inconsistent one.


🟡 3. spell-changed got untracked files but no extension filter

react-compiler-compliance-check was fixed by threading FILE_EXTENSIONS through, but spellChanged.sh:21 hands cspell every changed and untracked path with no filter. cspell.json sets useGitignore: true, so ignored files are safe — but per your own point on the react-compiler thread, .gitignore has no rule for *.mov / *.mp4 / *.png / heap snapshots (I confirmed: the only match is *.moved-aside). So a screen recording parked in your tree now gets handed to cspell. Same class of problem, smaller blast radius. Your call whether it's worth an extension allowlist.


🟡 4. get_changed_files mixes root-relative and cwd-relative paths

Inside get_changed_files, git diff --name-only prints repo-root-relative paths, but git ls-files prints cwd-relative paths and only lists files under the cwd. Verified in this repo:

$ git -C scripts ls-files -- '*.sh'
android-repackage-app-bundle-and-sign.sh        # no scripts/ prefix

$ git -C scripts ls-files --full-name -- '*.sh'
scripts/android-repackage-app-bundle-and-sign.sh

Harmless today because npm always runs scripts from the package root — but this is now a shared helper, and someone will call it from elsewhere. Adding --full-name to the ls-files call makes it position-independent.


🟡 5. No tests for the new Git parameter

untrackedFileExtensions is a new public param on Git.diff / Git.getChangedFilesWithStatus, and tests/tooling/Git.test.ts already has a describe('diff with shouldIncludeUntrackedFiles') block with a mocked execSync right where the case would go. The PR touches no test files. Cheap to add.


Earlier findings — all verified fixed in 85ea9aa
  • Silent stale base on fetch failure (mine + situchan + Codex): fixed correctly, and in the right way. get_merge_base_with_main now tests git fetch's exit status explicitly, and lintChanged.sh:12 dropped the || exit 1 — so the assignment is no longer in a tested context and set -e fires normally. Both halves were needed. ✅
  • Paths with spaces silently skipped in spell-changed: fixed via core.quotepath=false plus array-based reads. Residual nit: core.quotepath only governs non-ASCII — paths containing ", \, tab or newline are still C-quoted by git. No such paths exist in the repo. ✅
  • react-compiler-compliance-check reading every untracked file in full: fixed at the right layer — the extension filter runs at Git.ts:144-146, before createFileDiffsForUntrackedFiles does the readFileSync. ✅
  • || true masking git failures: gone; get_changed_files now return 1s on the diff, and ls-files is the last command so its status is the function's. ✅
  • Dead -z arm in merge-base validation: removed. ✅
  • Duplicated tail in spellChanged.sh vs lintChanged.sh: author declined, and I agree — a shared helper here would be an if/else wrapping two different exec lines. Not worth it.
  • Wrong "why" in the dotfile comment: reworded correctly. Minor: [[ "$file" != .* ]] only excludes paths whose first character is a dot, so src/.hidden/x.md still gets checked. That matches CI's grep -v '^\.' exactly, so the parity is intentional and fine.

CI remains safe, re-verified on this commit: cspell.yml passes an explicit file list via xargs, which hits the args branch at spellChanged.sh:13-15 and skips all discovery. lint and knip run their full variants in CI, not the -changed scripts. And getChangedFilesWithStatus takes the IS_CI GitHub-API branch at Git.ts:510, so the new untracked handling is local-only by construction.

Why I did not run device/browser testing

This PR changes build tooling only — shell scripts, package.json script wiring, a cspell word, and a Git utility used by CLI checks. There is no runtime or UI surface in the app to exercise, so an app test run would produce no signal. Verification here was done by reading the scripts and probing git's actual behavior in the checked-out repo.


view run

@JakubKorytko

Copy link
Copy Markdown
Member Author

@MelvinBot I addressed your & codex points, please re-review

@MelvinBot

Copy link
Copy Markdown
Contributor

Re-review of 412511b — 👍 all four addressed, ship it

Every point I raised is fixed correctly, and I checked each one against the code rather than taking the reply at face value. Nothing new. One optional leftover noted at the bottom.

1. mapfile → bash 3 ✅lintChanged.sh:20-22 now uses the same while IFS= read -r loop as spellChanged.sh. Correct as written: the <<< here-string guarantees a trailing newline, so the last line is always read — that's why you don't need read_lines_into_array's || [ -n "$line" ] guard here. -r and the empty IFS also preserve backslashes and leading/trailing spaces in paths, matching what mapfile -t did.

Following up on my own "verify this" note about ${#ARR[@]} on an empty array under set -u: I couldn't run bash 3.2 to test it, but the code is safe by construction either way — the bare "${ALL_CHANGED_FILES[@]}" expansion at lintChanged.sh:27 is the form the pre-4.4 rule affects, and it's guarded by the length check, so it's unreachable when empty. No change needed.

2. -z in getUntrackedFilesGit.ts:548-556 fixes it at the right layer (the source of the quoting, not the filter downstream). The split('\0').filter(length > 0) correctly drops the trailing empty element from git's NUL terminator, and the !untrackedOutput guard is safe because the execSync wrapper at Git.ts:30-39 force-sets encoding: 'utf8', so you're always testing a string, never a Buffer.

I also checked the bundled copy, since this file ships into a GitHub Action: getPullRequestIncrementalChanges/index.js:26493 has the -z version, and grep finds no stale non--z copy anywhere in .github/actions/ or scripts/. In sync.

3. --full-nameshellUtils.sh:163 now emits root-relative paths from ls-files, matching git diff's output format.

4. Tests ✅ — the two new cases in Git.test.ts are the right ones, and the extension-filter test asserting mockReadFileSync was not called with screenshot.png pins the actual thing situchan reported (the wasteful read), not just the output shape. Nice. The \n\0 updates across the existing mocks are consistent with the -z change.

CI agrees: Bun tests, ESLint check, lint (ShellCheck), verify, typecheck and spellcheck all pass on 412511b.

One optional leftover — not blocking
  • spell-changed still has no extension filter (my point 3 last round). You didn't take it, which is a defensible call — I flagged it as "your call" and useGitignore: true covers most of it. Leaving it here only so it's a recorded decision, not an oversight.
  • --full-name fixes the format half of my point, not the scope half. git ls-files is still scoped to the cwd regardless of --full-name — I confirmed this in the repo: run from scripts/, git ls-files --full-name returns 0 .github/ paths while the same command from the root returns 196. So with no pathspec, git diff would still cover the whole repo while ls-files covers only cwd-downward. Completely unreachable in practice — npm always runs scripts from the package root — so I'd leave it. If you ever want it airtight, running both git calls with -C "$(git rev-parse --show-toplevel)" closes it.
Why I did not run device/browser testing

This PR changes build tooling only — shell scripts, package.json wiring, a cspell word, a Git utility and its tests. There is no runtime or UI surface in the app to exercise, so an app test run would produce no signal. Verification here was reading the code and probing git's real behavior in the checked-out branch.


view run

@JakubKorytko
JakubKorytko requested a review from situchan August 31, 2026 13:53
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 412511b65e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

Copy link
Copy Markdown
Contributor

Regression scan of 412511b — 1 real defect, 2 minor, CI paths clean

No product regressions are possible here: this PR touches only dev tooling (scripts/, one built action bundle, CLAUDE.md, cspell.json) and zero NewDot runtime code, so there is nothing to exercise in the app itself. I reviewed the tooling behavior instead.

1. spell-changed hands every untracked file to cspell, with no type filter

scripts/spellChanged.sh:21 calls get_changed_files with no pathspec, so scripts/shellUtils.sh:163 returns every untracked, non-ignored file in the tree — screenshots, screen recordings, heap dumps, downloaded archives. The only filter applied is the leading-dot skip, and cspell.json's ignorePaths has no binary globs, so all of it reaches cspell at scripts/spellChanged.sh:34.

This is the exact problem the same PR fixed on the TypeScript side: untrackedFileExtensions was added at scripts/utils/Git.ts:144-145 to avoid "reading unrelated untracked files (media, snapshots, etc.) in full". The shell path never got the equivalent guard.

Fix: pass a pathspec list, the way scripts/lintChanged.sh:17 already does.

2. Only half the log helpers moved to stderr

scripts/shellUtils.sh:28-42: error and info now write to >&2, but success and title still write to stdout. Two consequences across the ~19 scripts that source this file — output can interleave out of order once redirected or piped (stdout is block-buffered, stderr is not), and any caller using 2>/dev/null now loses error output entirely. scripts/shellCheck.sh shows it: info banner on stderr, closing success on stdout. Move all four, or none.

3. The dotfile skip is narrower than its comment claims

scripts/spellChanged.sh:26 uses [[ "$file" != .* ]], which only skips paths starting with .. Nested dot-directories such as docs/.vitepress/config.ts are still checked. This matches CI's grep -v '^\.', so the two agree — only the inline comment ("files under dot-directories") overstates it.

What I checked and found clean
  • CI can't fall into discovery mode. .github/workflows/cspell.yml:40-41 guards on count != '0' and always passes an explicit file list, so the arg branch at spellChanged.sh:13 short-circuits before any git fetch. react-compiler-compliance.yml sets CI: true, so getChangedFilesWithStatus takes the GitHub-API branch at Git.ts:509-527 and ignores the new arguments entirely.
  • Built action bundles are in sync. getPullRequestIncrementalChanges/index.js is the only bundle under .github/actions/ embedding Git.ts — I grepped for the old ls-files --others --exclude-standard string and found no stale copies.
  • No stale \n test mocks. Every createMockExecSync untracked fixture in tests/tooling/Git.test.ts was converted to \0 (lines 1203, 1285, 1311, 1327, 1339, 1352, 1364). A missed one would have silently collapsed into a single bogus path.
  • Arg-mode stays offline. The $# -gt 0 check precedes get_merge_base_with_main, so npm run spell-changed -- <files> still needs no network. Bare mode does now fetch — worth knowing given CLAUDE.md recommends the bare form.
  • Earlier review comments are stale. The unquoted $ALL_CHANGED_FILES split, the swallowed git fetch failure, the dead -z arm in the merge-base check, and the react-compiler untracked-read are all fixed at this HEAD; they describe pre-412511b commits.
  • Minor, informational: knip-changed.sh now bases on the merge-base rather than main. That's the right delta semantics, but the merge-base is by construction older than main, so the symlinked-node_modules assumption gets weaker on long-lived branches — the deleted "if main has drifted, pull first" note used to warn about that.
  • No stdout captures at risk. Nothing in the repo captures $(scripts/...) from a script whose value comes from info/error; is-hybrid-app.sh echoes its value directly and already redirected its error manually.

view run

@JakubKorytko

Copy link
Copy Markdown
Member Author

@MelvinBot take a second look please

@MelvinBot

Copy link
Copy Markdown
Contributor

Second look at 77e9e47 — both fixes work; one comment now says the opposite of the code

1. The dotfile comment is backwards

scripts/spellChanged.sh:25 now claims .github/workflows/foo.yml "still passes through". It doesn't — it's skipped.

In [[ "$file" != .* ]] at line 27 the right side is a bash pattern matched against the whole string, and [[ ]] does no pathname expansion, so * crosses /. Every path beginning with . matches, including everything under a top-level dot-directory. The behavior is right — it mirrors CI's grep -v '^\.' in cspell.yml:35 — but the comment is the kind of wrong that invites someone to "fix" working code. My earlier note was narrower than you read it: only nested dot-directories pass through.

Suggested wording:

Excludes any path starting with . (dotfiles and top-level dot-directories like .github/), matching the CI filter. A nested dot-directory, e.g. docs/.vitepress/config.ts, is still checked.

2. Binary exclusion works — verified — but the denylist has real gaps

The exclude-only pathspec is valid for both git commands in get_changed_files, which I checked because CI never exercises this path: cspell.yml always passes an explicit file list, so it takes the $# -gt 0 branch at spellChanged.sh:13 and never reaches line 22.

  • git ls-files → 12365 total, 588 matching *.png, 11777 with ':!*.png'. Exact.
  • The full 13-argument list exits 0 on both git diff --diff-filter=AMR --name-only <sha> -- and git ls-files --full-name --others --exclude-standard --.

Uncovered types that exist as real files in this tree: .otf (20 tracked), .woff2 (17), .mp3 (8), .ttf (5), .jar (2), .keystore (1) — plus .webm, .tgz, .hprof, .avif, .bmp. A denylist can't close this. If you want it airtight, invert to a positive pathspec of spell-checkable extensions, the way lintChanged.sh:17 already does. Not a blocker — png/jpg/mp4/mov cover what people actually leave lying around.

Minor: ':!*.svg' skips SVGs locally while CI still checks them (829 tracked, and the API-derived list has no svg filter). None carry <title>/<desc> prose, so the exposure is just a typo'd id/class failing CI after a clean local run.

3. All four log helpers on stderr — safe, consumers checked

shellUtils.sh:28-42: success/title joined error/info, so the interleaving hazard is gone and the convention is now consistent — progress on stderr, data on stdout.

I checked every caller rather than assuming: import-cloudflare-certs-into-jdk.sh, select-device.sh (sourced, not captured), setup-mapbox-sdk-walkthrough.sh, shellCheck.sh, run-build.sh, check-elf-alignment.sh, setup-newdot-web-emulators.sh, .github/scripts/createHelpRedirects.sh, enforceVideoFormats.sh, verifyPodfile.sh. All are human-facing progress; none feed a captured stdout, and the workflow ones invoke via run: so stderr still lands in the log.


Only #1 is worth a commit, and it's a comment. CI is green on 77e9e47 apart from Check independent approval. Still zero runtime code in this PR, so there are no product regressions to test.


view run

@situchan

situchan commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Test summary — Tests 1-12 ✅

Tests 1-3 — lint-changed picks up uncommitted + untracked files

Appended an unused variable to src/libs/Url.ts (unstaged edit) and created a new untracked src/libs/TestUntrackedLintFile.ts. npm run lint-changed processed both (plus a pre-existing uncommitted edit in src/libs/Permissions.ts):

src/libs/TestUntrackedLintFile.ts  2:7  error  'anotherUnusedVariable' is assigned a value but never used
src/libs/Url.ts                  178:7  error  'unusedVariableForLintTest' is assigned a value but never used
✖ 3 problems (3 errors, 0 warnings)

✅ Both the uncommitted edit and the untracked file are linted and reported.

Tests 4-6 — react-compiler-compliance-check check-changed includes untracked files

Added untracked src/components/TestUntrackedCompilerComponent.tsx containing a component and a hook. The run reported Checking 5 changed React files with both compilers (Babel + OXC), and --verbose confirmed the untracked file was included:

✅ OK  src/components/TestUntrackedCompilerComponent.tsx (babel=compiled, oxc=compiled)

✅ The untracked file is included in the check output.

Tests 7-9 — spell-changed with no arguments

Introduced a misspelling in tracked src/libs/Url.ts (uncommitted) and in a new untracked src/libs/testUntrackedSpelling.ts, then ran npm run spell-changed with no arguments:

src/libs/testUntrackedSpelling.ts:1:43 - Unknown word (mispelling) fix: (misspelling)
src/libs/testUntrackedSpelling.ts:1:59 - Unknown word (recieve) fix: (receive)
src/libs/Url.ts:177:36 - Unknown word (mispeled)
CSpell: Files checked: 13, Issues found: 3 in 2 files.

✅ The script discovers changed files itself and reports misspellings in both files.

Tests 10-12 — knip-changed uses a fresh origin/main, not stale local main

Local main was already 4 commits behind origin/main, but that alone didn't move the merge base, so I made it genuinely stale by pointing main at an older commit (used git branch -f rather than checkout + reset --hard, to avoid touching a dirty working tree):

SHA
git merge-base main HEAD (stale) 12a92bd39f31ddabf5a425552930cbb055536555
git merge-base origin/main HEAD (fresh) 89f3ba3461442cb9dde5539ce53f4c3f6916ae01

npm run knip-changed printed:

Running knip on merge base (89f3ba3461442cb9dde5539ce53f4c3f6916ae01)...

✅ The printed SHA matches the fresh origin/main merge base, not the stale local main one — the script's own fetch of origin/main wins regardless of the local branch state. The full run completed cleanly:

Knip comparison:
  main : 128
  PR   : 128 (delta +0)
PR introduces no new knip findings.

@situchan

situchan commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

AI review result:

1. The newest commit replaced a correct comment with a false one

spellChanged.sh:25 now reads:

# … does not exclude files nested under dot-directories (e.g. .github/workflows/foo.yml still passes through)

It does exclude them. .* is a glob over the whole path, so anything whose first character is a dot is dropped:

DROPPED: .github/workflows/f.yml
DROPPED: .eslintrc.js
KEPT:    src/a.ts

At 412511b the comment said "Excludes dotfiles and files under dot-directories (e.g. .github/)" — which was right, and which was signed off on. 77e9e47 rewrote it into the opposite claim, apparently by over-applying the separate note about src/.hidden/x.md (a dot segment mid-path, which genuinely does pass). The code is correct and matches cspell.yml:35; only the comment needs reverting.

2. :!*.svg opens a gap between spell-changed and the CI cspell job

The new exclusion list at spellChanged.sh:22 is justified as "binary/media", but .svg is text and cspell reads it. Probed against the repo's real config:

zz-probe.svg:1:48 - Unknown word (Recieve) fix: (Receive)
zz-probe.svg:1:60 - Unknown word (paymnt)
exit=1

cspell.yml sends every changed file, so a typo in an SVG <title>/<text> now passes locally and fails in CI — the exact gap the no-arg mode exists to close. .pdf is the same shape, lower stakes. Drop .svg at minimum.

(I also confirmed the untracked-media worry that prompted this list is already handled: cspell ignores a Mobile-Expensify directory argument and returns in under a second, and an end-to-end run of the script in this repo reports Files checked: 0 with a dirty submodule.)

3. clear_last_line was left on stdout when the four log helpers moved to stderr

77e9e47 routed success, error, info and title to >&2, but shellUtils.sh:45 still writes its cursor-up/erase escape (\033[1A\033[K) to stdout. The two are a matched pair — pod-install.sh:99-107 prints a progress line with info and erases it with clear_last_line, and there's a bare echo -e "\n" between them on stdout too. On a plain tty it still looks right because both streams land on the same terminal, but the pairing is now broken by construction: npm run pod-install > install.log puts a stream of raw ANSI escapes in the log while the messages it was meant to erase go to the console.

Worth noting the widening itself: only error and info are reachable from the captured helper, so success and title gained nothing. I re-checked every shellUtils.sh consumer — the two whose stdout is actually captured (is-hybrid-app.sh, compute-patches-hash.sh) use neither — so nothing breaks today. Either revert those two, or send clear_last_line to stderr with them.

4. The polyfill recommended for the mapfile fix evals its input

Both review comments pointed at read_lines_into_array as the bash-3 fix. It builds the array with eval "$array_name+=(\"$line\")", which executes substitutions embedded in the line:

input: pwn$(echo INJECTED >&2).ts   →  INJECTED
input: lit`echo BACKTICK >&2`.ts    →  BACKTICK

Hand-rolling the while IFS= read -r loop instead was the right call for git-derived filenames — but that reasoning isn't recorded anywhere, so the next person will follow the same advice. It also matters beyond this PR: the polyfill is live at pod-install.sh:79 and verifyPodfile.sh:67. Worth hardening it (printf -v / declare -n, or read into a temp then assign) and leaving a one-line note in shellUtils.sh saying why the -changed scripts don't use it.

5. --full-name fixed only half the cwd-relative problem

The earlier point got ls-files position-independent, but git diff --name-only on the line above goes cwd-relative when the user has diff.relative=true configured:

default:              sub/a.ts
diff.relative=true:   a.ts
ls-files --full-name: sub/new.ts

So get_changed_files can still return two path bases at once, in exactly the from-a-subdirectory scenario --full-name was added for. git -c is already on both lines — add -c diff.relative=false at shellUtils.sh:162.

6. One of the three new cspell words is dead weight

Probing the tokens against the current config: quotepath and heapsnapshot are genuinely flagged, so those entries are load-bearing. webp is not — cspell already knows it, so the "Webp" addition at cspell.json:443 does nothing. For the other two, an inline cspell:ignore in the two shell files would scope them to where they're needed instead of widening the repo-wide dictionary for tokens that only appear in a pathspec list.

@situchan

situchan commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Please address applicable items in #99764 (comment) and #99764 (comment)

@melvin-bot
melvin-bot Bot requested a review from roryabraham September 1, 2026 11:15
@JakubKorytko

Copy link
Copy Markdown
Member Author

fixed all except webp, checked this one and it's not actually dead - tested with cspell's built-in dictionaries and "webp" is unknown without that entry. Left it in. quotepath/heapsnapshot moving to inline ignore is a style call, left as-is for now.

@roryabraham roryabraham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Some of these scripts might become faster and more readable if re-written in Bun, but that's out of scope for now.

@roryabraham
roryabraham merged commit 3f9c3e4 into Expensify:main Sep 1, 2026
42 of 43 checks passed
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🚧 roryabraham has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

OSBotify commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/roryabraham in version: 9.4.66-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@OSBotify

OSBotify commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/roryabraham in version: 9.4.68-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 cancelled 🔪

@OSBotify

OSBotify commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/francoisl in version: 9.4.68-1 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants