Skip to content

fix(deploy): make target-state generation fail loudly instead of silently wiping entries - #2148

Draft
0xDEnYO wants to merge 3 commits into
mainfrom
claude/target-state-generator-guards
Draft

fix(deploy): make target-state generation fail loudly instead of silently wiping entries#2148
0xDEnYO wants to merge 3 commits into
mainfrom
claude/target-state-generator-guards

Conversation

@0xDEnYO

@0xDEnYO 0xDEnYO commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Which Linear task belongs to this PR?

No ticket yet — Linear was unavailable in the session that produced this. Needs an EXSC ID before merge.

Context: fell out of investigating why the nightly Diamond Health Check reported 68/71 networks failing. #2120 fixed the check-side false failures; this fixes one of the two process gaps deferred from that work.

Why did I implement it this way?

What was broken

parseTargetStateGoogleSpreadsheet (scriptMaster.sh use case 10) regenerates
script/deploy/_targetState.json from a Google Sheet. It deleted all existing entries for
the environment before it had parsed anything
, then repopulated from the parse. So any
parse that produced nothing deleted target state and still printed
Processing completed successfully! and exited 0.

Three independent ways to hit that, all observed in one afternoon of use:

  1. Run it under zsh. The function uses bash's read -a, which zsh does not have. Every
    row fails with read:19: bad option: -a, the arrays stay empty — and the run still
    reports success. This collapsed 1995 facet entries to 28. The #!/bin/bash shebang
    protects ./script/scriptMaster.sh but not zsh script/scriptMaster.sh and not sourcing
    the helpers into an interactive zsh, which is how it was hit.
  2. A network missing from the export. The CSV export (/export?exportFormat=csv, no
    gid) only covers the sheet's first tab. The wipe covered all production entries, so
    anything not on that tab was silently deleted. This wiped Tron's 20 entries to 0 because
    Tron had no row. Any future non-EVM chain kept on another tab hits the same thing.
  3. A failed download. curl -L … 2>/dev/null with no status check happily wrote a 404
    HTML page into the CSV, which then parsed as an empty sheet.

Why this matters even though it's "just a config file": _targetState.json drives deploy
tooling, and the health check derives its non-core facet and periphery lists from it
(healthCheckInvariants.ts periphery-registered, healthCheck.ts:136). A network wiped
from target state therefore checks fewer contracts and reports green — the failure
mode makes the alarm quieter, not louder. CI only validates that the file is syntactically
valid JSON (jsonChecker.yml:96).

The approach

Parse and validate first, then mutate. Moving the removal to after the parse is the
structural fix — it turns every "parse produced nothing" failure from data loss into a no-op,
including failure modes nobody has thought of yet. The specific guards on top are cheap:

  • Refuse under non-bash — explicit $BASH_VERSION check with the fix in the message.
    Considered rewriting the read -a calls to be POSIX-portable instead, but this function is
    one of many bash-only ones in a 6k-line bash library; making just this one portable would
    imply a guarantee the file doesn't keep.
  • curl -fsSL with an explicit failure branch, replacing the silenced call. Note this
    only covers hard HTTP failures — a sheet that isn't shared redirects to a login page and
    returns 200 with HTML, which -f does not catch; the empty-parse guard below is what
    actually covers that case.
  • Refuse on an empty parse — 0 contracts or 0 networks is always a broken sheet/download/
    shell, never a legitimate "target state is empty now".
  • Refuse when a network with existing entries has no row in the export, listing the
    networks, with ALLOW_TARGET_STATE_NETWORK_REMOVAL=true to override deliberately. This is
    the direct fix for the single-tab export deleting Tron. Chose a refuse-with-override over
    auto-preserving the missing networks, because silently keeping stale entries for a chain
    someone deliberately removed from the sheet is its own failure mode — the operator should
    say which they meant.
  • Casing hint on the "could not find src FILE path" warning. getContractFilePath uses
    find -name, which is case-sensitive regardless of the filesystem, so a sheet cell reading
    NearIntentsFacet misses NEARIntentsFacet.sol. The warning now does a case-insensitive
    lookup and says "no src file named NearIntentsFacet.sol, but NEARIntentsFacet.sol
    exists — fix the spelling in the Google sheet"
    .

Deliberately not in this PR, to keep it to one problem:

  • The gid parameter for multi-tab exports — needs a decision on which tabs are authoritative.
  • A validator for sheet contents vs the repo (contract name has no src/**/*.sol; version
    absent from src; version would downgrade what's deployed). There are 14 live entries in
    _targetState.json today naming a version older than what is actually deployed
    , and
    nothing detects them — but that wants a tested TypeScript script wired into CI, not more
    bash. Separate ticket.
  • The exit-inside-$(…)-subshell idiom in getContractFilePath that swallows its own hard
    error — touches every caller in the file.

Verification

No automated test — parseTargetStateGoogleSpreadsheet has none today and testing it needs a
stubbed curl plus a Google Sheet fixture, which is most of the work of the separate
validator ticket. Verified manually with a stubbed curl against a copy of the real
_targetState.json:

Scenario Before After
Run under zsh "Processing completed successfully!", entries collapsed rc=1, refuses, file untouched
Export missing 69 of 70 networks 70 → 1 network, exit 0 rc=1, lists all 69, file untouched (70 preserved)
Same + ALLOW_TARGET_STATE_NETWORK_REMOVAL=true 70 → 1 70 → 1 (unchanged, as intended)
CSV with no Blue = Periphery header row (incl. an HTML login page) wiped, exit 0 rc=1, refuses, file untouched
_targetState.json unreadable by jq n/a (guard is new) rc=1, refuses, file untouched
Any guard trips, via scriptMaster use case 10 n/a checkFailure → script exits 1

The curl failure branch was exercised only for hard HTTP failures (stubbed non-zero exit),
not for the 302→200-HTML "sheet not shared" case — that path is covered by the empty-parse
guard, verified by feeding the parse loop a real HTML login-page body.

bash -n script/helperFunctions.sh clean. No .sol or .ts touched, so no forge test /
bun test:ts run. The happy path is unchanged — same parse, same merge, same output.

One gotcha worth knowing repo-wide, found while writing the guard: --arg ENV does not work
in jq.
$ENV is a jq builtin holding the environment object and shadows the argument, so
.value[$ENV] yields nothing instead of erroring usefully — the guard silently passed until I
renamed it to --arg TARGET_ENV. There's a comment on the line.

Checklist before requesting a review

Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)

  • I have checked that any arbitrary calls to external contracts are validated and or restricted
  • I have checked that any privileged calls (i.e. storage modifications) are validated and or restricted
  • I have ensured that any new contracts have had AT A MINIMUM 1 preliminary audit conducted on by <company/auditor>

…ntly wiping entries

parseTargetStateGoogleSpreadsheet removed all existing entries for the
environment before it had parsed anything, so any parse that produced nothing
deleted target state and still exited 0. Parse and validate first, then remove.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Target state regeneration

Layer / File(s) Summary
Spreadsheet input validation
script/helperFunctions.sh
Spreadsheet downloads fail fast, non-Bash execution is rejected, and parsed output is validated before processing.
Target-state mutation safety and diagnostics
.env.example, script/helperFunctions.sh
Parsed content is validated before target-state removal, network deletion is configuration-controlled, and contract filename warnings use case-insensitive lookup.
Script flow failure propagation
script/scriptMaster.sh
Both spreadsheet update paths now pass parser failures to checkFailure.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main change: making target-state generation fail loudly instead of wiping entries silently.
Description check ✅ Passed The description follows the template well and includes the task context, rationale, checklist items, and reviewer checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/target-state-generator-guards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The new guards returned 1 into callers that ignored it, so a refused run printed
an error and the menu carried on as if it had succeeded. Also stop a jq failure
in the missing-networks check from reading as "no networks would be lost".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@0xDEnYO

0xDEnYO commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Review-gate pass (local trial). Nothing escalated — no findings hit the always-escalate classes (no storage/selector/access-control/event/bridge-math surface; this PR is bash tooling only). Six findings auto-fixed in 052791a and the body correction above.

Two lower-confidence items left for human judgment, not acted on:

  • script/helperFunctions.sh — the three pre-existing config guards at the top of parseTargetStateGoogleSpreadsheet (missing MAX_CONCURRENT_JOBS, missing spreadsheet ID, bad ENVIRONMENT) still use exit 1, while every guard this PR adds uses return 1. The file-wide idiom is overwhelmingly return (179 vs 19), and the new call-site checkFailure makes return propagate correctly, so I left the old exits alone rather than widen the diff. Worth a follow-up sweep.
  • script/helperFunctions.sh — the empty-NETWORK_LINES guard is narrower than its comment suggests: it can only fire when contracts parse but network rows do not (e.g. no mainnet row). In the zsh and HTML-error-page cases the empty-CONTRACTS_ARRAY guard returns first. Not defective, just not the primary net.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
script/helperFunctions.sh (1)

1788-1788: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use mktemp instead of a fixed CSV filename.

CSV_FILE_PATH="newTest.csv" is a hardcoded, debug-sounding name written to the working directory; the function already uses mktemp -d for TEMP_DIR later on. Reusing that pattern here avoids leaving a stray file behind on abnormal exits and avoids collisions if this function is ever invoked twice concurrently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/helperFunctions.sh` at line 1788, Replace the fixed CSV_FILE_PATH
assignment with a unique temporary-file path created via mktemp, consistent with
the existing TEMP_DIR setup. Ensure the CSV path is used by the surrounding
function and remains isolated for concurrent invocations without writing a
debug-named file to the working directory.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@script/helperFunctions.sh`:
- Line 1789: Quote the CSV_FILE_PATH variable in the curl output argument within
the surrounding command, changing the unquoted -o value to preserve paths
containing spaces or glob characters while leaving the existing URL and
error-handling behavior unchanged.
- Around line 1789-1793: Update the curl invocation in the spreadsheet download
error path to include both connection and overall request timeouts, while
preserving the existing URL, output path, and failure handling through the error
message, cleanup, and return statements.
- Line 1897: Update the loop around EXISTING_NETWORKS to iterate with a while
IFS= read -r pattern, preserving each jq output entry verbatim instead of
relying on unquoted word splitting. Keep the existing loop body and
network-processing behavior unchanged.

---

Nitpick comments:
In `@script/helperFunctions.sh`:
- Line 1788: Replace the fixed CSV_FILE_PATH assignment with a unique
temporary-file path created via mktemp, consistent with the existing TEMP_DIR
setup. Ensure the CSV path is used by the surrounding function and remains
isolated for concurrent invocations without writing a debug-named file to the
working directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d83c4a2-cb36-405d-a996-ef82c3a91a6d

📥 Commits

Reviewing files that changed from the base of the PR and between 5e02a5f and 052791a.

📒 Files selected for processing (3)
  • .env.example
  • script/helperFunctions.sh
  • script/scriptMaster.sh

Comment thread script/helperFunctions.sh Outdated
Comment thread script/helperFunctions.sh Outdated
Comment thread script/helperFunctions.sh Outdated
…e sheet download

getCurrentContractVersion prints its errors to stdout, so the command
substitution in processNetworkLine captured ANSI error text on failure and
CURRENT_VERSION was never empty -- the missing-version warning (including the
case-mismatch hint added on this branch) could never fire. Check the exit code
and blank the variable so it does; verified against the real sheet export that
a misspelled cell now produces the spelling hint.

Also: distinguish "file exists but has no @Custom:version" from a true case
mismatch; name the requested network in the empty-rows error when a specific
network was asked for; quote the curl output path and add connect/transfer
timeouts; iterate jq output with a read loop instead of unquoted word
splitting; correct the jq $ENV comment (an object index errors out rather than
silently matching nothing).

Full-run output is byte-identical to the pre-fix generator on the current
production sheet, twice over.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@script/helperFunctions.sh`:
- Around line 2038-2042: Update the latest-version handling around
CURRENT_VERSION and addContractVersionToNetworkJSON to reject an empty or
unresolved lookup before writing target state, returning a worker failure.
Ensure the parent network-processing flow captures and aggregates that worker
failure before merging results, so parsing cannot continue with an invalid
version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ac14765e-5022-4f72-bf53-2885edc21dd3

📥 Commits

Reviewing files that changed from the base of the PR and between 052791a and 32f62b6.

📒 Files selected for processing (1)
  • script/helperFunctions.sh

Comment thread script/helperFunctions.sh
Comment on lines +2038 to +2042
# getCurrentContractVersion prints its error messages to stdout, so on failure the
# command substitution captures error text instead of leaving the variable empty --
# blank it explicitly or the emptiness check below can never fire.
local CURRENT_VERSION
CURRENT_VERSION=$(getCurrentContractVersion "$CONTRACT") || CURRENT_VERSION=""

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject unresolved latest versions before writing target state.

Line 2042 now clears CURRENT_VERSION on lookup failure, but the later latest branch still passes that empty value to addContractVersionToNetworkJSON at Line 2085. A missing or mis-cased contract can therefore generate an empty version while the parser continues. Fail the network before writing it, and ensure the parent aggregates that worker failure before merging.

Proposed guard
 CURRENT_VERSION=$(getCurrentContractVersion "$CONTRACT") || CURRENT_VERSION=""
+if [[ "$CELL_VALUE" == "latest" && -z "$CURRENT_VERSION" ]]; then
+  error "[$NETWORK] cannot resolve latest version for contract $CONTRACT"
+  return 1
+fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# getCurrentContractVersion prints its error messages to stdout, so on failure the
# command substitution captures error text instead of leaving the variable empty --
# blank it explicitly or the emptiness check below can never fire.
local CURRENT_VERSION
CURRENT_VERSION=$(getCurrentContractVersion "$CONTRACT") || CURRENT_VERSION=""
# getCurrentContractVersion prints its error messages to stdout, so on failure the
# command substitution captures error text instead of leaving the variable empty --
# blank it explicitly or the emptiness check below can never fire.
local CURRENT_VERSION
CURRENT_VERSION=$(getCurrentContractVersion "$CONTRACT") || CURRENT_VERSION=""
if [[ "$CELL_VALUE" == "latest" && -z "$CURRENT_VERSION" ]]; then
error "[$NETWORK] cannot resolve latest version for contract $CONTRACT"
return 1
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/helperFunctions.sh` around lines 2038 - 2042, Update the
latest-version handling around CURRENT_VERSION and
addContractVersionToNetworkJSON to reject an empty or unresolved lookup before
writing target state, returning a worker failure. Ensure the parent
network-processing flow captures and aggregates that worker failure before
merging results, so parsing cannot continue with an invalid version.

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.

1 participant