Skip to content

Set up GitHub Actions CI for nextiq - #59

Merged
mihirpatel1510 merged 17 commits into
SatatTech:dev-v2from
mihirpatel1510:NEW-DEV-V2
Aug 20, 2026
Merged

mihirpatel1510 merged 17 commits into
SatatTech:dev-v2from
mihirpatel1510:NEW-DEV-V2

Conversation

@mihirpatel1510

Copy link
Copy Markdown
Collaborator

No description provided.

mihirpatel1510 and others added 17 commits August 19, 2026 16:36
- Node bumped 20 -> 24: Frappe's package.json requires >=24, bench init's
  yarn install step failed outright on anything older.
- Warehouse Type "Transit" workaround switched from `bench console <
  file.py` (unreliable — console is an interactive REPL, piping a file
  into it doesn't reliably execute it) to `bench execute
  module.function` (imports and calls a real function directly).
- Added a cheap pre-flight step (compileall + merge-conflict-marker grep)
  before the expensive bench setup, borrowed from frappe/payments' CI.

The Transit workaround itself is CI-only test-environment setup, not
nextiq behavior: erpnext is a required_apps dependency, so installing it
on the test site triggers ERPNext's own Company test-record creation,
which needs "Warehouse Type: Transit" to already exist. ERPNext's own
after_install fixture installer is supposed to create it but doesn't on
some version-15 point releases (frappe/erpnext#42309, #43261, #43262,
#28928) — unrelated to nextiq, just filling the gap so tests can run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.pre-commit-config.yaml (ruff, prettier, eslint) already existed and is
documented in the README, but wasn't actually being enforced before
commits, so formatting had drifted. Ran the same tools at the same
pinned versions the hooks use (ruff 0.8.1, prettier 2.7.1, eslint 8.44.0):
import sorting, ruff-format, and prettier across the app. No logic
changes — verified by diffing the AST of every touched Python file
before/after; every difference is import ordering or one of the two
real fixes below.

Two real fixes, not just formatting:
- .eslintrc was missing "nextiq" from its globals list, so eslint flagged
  the app's own frappe.provide("nextiq") namespace as undefined in 3 files.
- api.py had one real ruff error (RUF002): an ambiguous Unicode "x" in a
  docstring, not user-facing, just needed an ASCII x.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI job — bench execute's dotted-path resolution requires the first
segment to be a real installed app name (frappe.get_attr), not just a
module on PYTHONPATH, so it could never have run a standalone CI script
regardless of Frappe version. Switched ensure_default_records.py to a
plain script calling frappe.init()/connect() directly, run via
env/bin/python — the standard way to run a one-off Frappe site script.

Linters job — fixed all 35 blocking findings from Frappe's own
semgrep-rules, verified locally against the identical baseline commit
CI uses (0 findings after):
- Type hints on every whitelisted-endpoint parameter (submit_card_scan,
  scan_callback, begin_oauth, callback).
- frappe.db.get_value/set_value on the NextIQ Settings Single doctype
  replaced with get_single_value/set_single_value (not type-safe for
  Singles) across api.py, boot.py, card_scan.py, version_check.py.
- User-facing frappe.throw messages and report column labels wrapped in
  _() for translation support.
- # nosemgrep on the two allow_guest=True endpoints, each already
  documented with its own auth model (HMAC callback secret; signed
  state + PKCE) that the generic "needs manual review" flag doesn't
  capture — with a real justifying comment, not the bare rule-name-less
  `# nosemgrep: <explanation>` syntax from the first attempt, which
  semgrep silently failed to parse as a suppression at all.
- _COL_LABEL in time_saved_report.py turned from a module-level dict
  into a function — calling _() at import time caches one language's
  translation across every site/request in a multi-tenant app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
frappe.init(sites_path="sites") fixed site lookup, but a second, separate
bug remained: frappe's logger builds its log file path as "../logs",
relative to CWD — it assumes CWD is already <bench_root>/sites, same as
every other Frappe internal. bench's own CLI always chdir()s there before
running any command; this script bypassed that by running as a plain
script. Replicated it with os.chdir("sites") so frappe.init() and
everything downstream behaves exactly like a real bench command, rather
than patching each individual symptom of skipping that step.

Verified locally against the real dev bench (Frappe v16 + ERPNext):
script exits 0, log file opens/writes correctly, no side effects on the
caller's own working directory after the script exits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first fix only handled "Warehouse Type: Transit" — but a second,
different missing root record ("Customer Group: All Customer Groups")
surfaced right after, confirming this wasn't an isolated gap. Both (plus
Territory, Item Group, Supplier Group, and others in the same family)
are normally seeded together by ERPNext's own Setup Wizard, which
Frappe's test runner never runs — it inserts a bare test Company
directly, which cascades into code expecting all of that to already
exist.

Rather than patch each missing record by hand as it turns up, call the
actual function ERPNext uses to seed all of them at once:
erpnext.setup.setup_wizard.operations.install_fixtures.install(). Every
record it creates goes through insert(ignore_if_duplicate=True) with its
own savepoint/rollback, so it's safe to call regardless of what already
exists — confirmed by tracing a "NestedSetRecursionError" during local
testing back to pre-existing tree data on the (non-fresh) local dev site
used for verification, not a real problem with the underlying approach.

country="India" is required, not actually optional despite the
function's own default of None — found by running it locally, where it
crashed immediately trying to build a country-specific Territory name
from a None value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hardcoding it

nextiq's own branches (main, dev-v2, prod, ...) don't map to Frappe
version branches the way they do in apps that branch per-Frappe-version,
so there's no branch name to derive a version from the way frappe/payments
or Arus-Info do. The alternative was a hardcoded matrix array in ci.yml —
a second, driftable copy of what pyproject.toml already declares.

Split ci.yml's tests job into two: a new "prepare" job runs
compute_frappe_matrix.py, which parses pyproject.toml's own
[tool.bench.frappe-dependencies] range and outputs it as a JSON array;
"tests" now depends on that and reads its matrix from
fromJson(needs.prepare.outputs.frappe-versions) instead of a literal list.

Narrowed the declared range from >=14.0.0,<17.0.0 to >=15.0.0,<17.0.0 as
part of this: version-14 was never actually exercised by CI (would need
an older Python/Node than what current Frappe requires), so claiming
support for it while never testing it was exactly the kind of unverified
claim this whole CI effort exists to catch. pyproject.toml is now the
single place declaring what nextiq supports, and also what CI verifies —
the two can no longer quietly disagree.

Verified compute_frappe_matrix.py locally (Python 3.12, since this repo's
own Python is 3.9) against the real pyproject.toml (-> ["version-15",
"version-16"]) and a synthetic single-version range as an edge case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ix.py

frappe-security-file-traversal flags any raw open() call for manual
review, since a file path built from user/external input can be a real
traversal vector. Here it's a hardcoded literal string ("pyproject.toml"),
not a variable — nothing reaches it that could traverse anywhere.

Verified locally against the exact baseline commit CI uses (52e0eb5):
0 findings, down from 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
version-15 failed again with a different missing default record —
"Gender: Female" this time, not an ERPNext doctype at all. Same root
cause as the ERPNext fixtures, just on Frappe core's side: Gender,
Salutation, global search sync, dashboard sync, and an unsubscribe
record are normally seeded by frappe.desk.page.setup_wizard.
install_fixtures.install(), called from run_post_setup_complete()
when a real user finishes the Setup Wizard — before ERPNext's own
app-level setup-complete hook runs. The test runner never reaches
either.

Call both, in the same order the real Setup Wizard does: Frappe core's
install_fixtures.install() first, then ERPNext's. Same ignore_if_
duplicate=True safety as before.

Verified locally: Gender/Salutation records now exist after running the
script, and bench --site nextiq.test run-tests --app nextiq still exits 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
version-15 failed a third time with a different symptom: "Could not find
Party: _T-Lead-00001" — not a missing default record this time, but a
missing *test* record. Traced to erpnext/crm/doctype/opportunity/
test_records.json, which references a test Lead through a Dynamic Link
field (labeled "Party"). Frappe's test runner reliably auto-creates
dependencies for plain Link fields (Lead.territory -> Territory), but
not reliably for Dynamic Link fields, so that Lead was never created
before something needed it through the Dynamic Link.

Call frappe.tests.utils.make_test_records("Lead", commit=True) directly
— the same function the test runner itself uses — so it exists ahead of
time instead of waiting for a Dynamic Link reference to expose the gap.

Tested locally: running it against nextiq.test (a real, already-used dev
site, not a fresh one) surfaced a different failure — Lead's own test
module import triggers erpnext.tests.utils.BootStrapTestData() as a
module-level side effect, which tried to create a standard Fiscal Year
that overlaps with a real one already on that site. Root-caused via the
full traceback: this is the same class of dirty-site artifact as the
earlier NestedSetRecursionError, not a bug in this fix — a genuinely
fresh site (what version-15's CI leg actually uses) has no pre-existing
fiscal year to conflict with. Also note this import path isn't new
exposure: nextiq's real test run hits the same module via Card Scan
Log's `lead` Link field regardless: this just makes it happen earlier,
in a dedicated logged step, instead of nested inside the real test run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Last commit's Lead pre-seeding regressed both matrix legs, for two
different reasons:

- version-16: erpnext.tests.utils.BootStrapTestData (imported as a side
  effect of loading Lead's test module) tries to create a standard test
  user with a deliberately weak password. User.validate() only skips its
  password-strength check when frappe.in_test is set — our script never
  set it, even though the real bench run-tests always does before this
  point is ever reached.
- version-15: frappe.tests.utils.make_test_records doesn't exist there
  at all — it's a version-16-only package restructure (frappe/tests/utils
  went from a single file to a package). Confirmed by cloning frappe's
  actual version-15 branch: frappe.test_runner.make_test_records is the
  version-portable path — the real implementation on version-15, a
  functional deprecated wrapper forwarding to the new location on
  version-16.

Set frappe.local.flags.in_test (both versions) and frappe.in_test
(version-16 only, harmless to set unconditionally) right after connect(),
and switched the import to frappe.test_runner.make_test_records.

Verified locally: both the ImportError and the password-strength failure
are gone — confirmed by getting cleanly past both points this run. What's
left locally is the same Fiscal Year 2026-2027 conflict already
root-caused as a dirty-local-site artifact (real pre-existing data on
nextiq.test), not something a genuinely fresh CI site would hit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
make_test_records("Lead", commit=True) recursively walks Frappe's entire
test dependency graph from the given doctype, not just its immediate
needs — confirmed via a traceback showing 22+ levels of recursion. On
this codebase's actual graph that walk hit a missing mandatory field on
an auto-generated Opportunity test record (version-15) and, worse, a
doctype ("Payment Gateway") belonging to an app that isn't even installed
here ("payments") on version-16 — a hard wall, not a seedable gap.

Each attempt to fix the original _T-Lead-00001 Dynamic Link reference
made things worse, not better (password strength -> fiscal year conflict
-> missing mandatory field -> missing app entirely). Reverting back to
the state that was cleanly getting past Warehouse Type / Customer Group /
Gender, and leaving the Opportunity Dynamic Link gap as a known, accepted
limitation on version-15 rather than continuing to chase it.

Kept: frappe.local.flags.in_test / frappe.in_test, set right after
connect() — genuinely correct regardless (matches what the real test run
always has set by this point) and harmless on its own.

Verified locally: back to exit 0, only the already-explained dirty-site
NestedSetRecursionError noise, no new failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the original _T-Lead-00001 Dynamic Link gap without the
recursive-walker problems the previous attempt caused: erpnext/crm/
doctype/opportunity/test_records.json references a Lead by hardcoded
name through a Dynamic Link field ("Party"), which Frappe's test runner
doesn't reliably auto-create the way it does for plain Link fields.

Instead of frappe.test_runner.make_test_records("Lead", ...) (reverted
previously — it recursively walks Frappe's entire test dependency graph,
not just Lead's immediate needs), create exactly the two records needed
directly: Territory "_Test Territory" (plain frappe.get_doc().insert(),
autoname = field:territory_name) and Lead "_T-Lead-00001" (naming_series
= "_T-Lead-" replicates exactly what frappe/tests/utils/generators.py's
_try_create does for naming_series-based doctypes, so the counter
assigns the same name Opportunity's fixture hardcodes). Field values
confirmed against the real version-15 branch source for both doctypes.

Verified against a real, fresh version-15 bench (Docker container, not
the dirty local dev site used for earlier verification): both records
are created with the exact expected names/values, and the earlier
NestedSetRecursionError noise is gone entirely — confirmed as a dirty
local-site artifact, not a real issue, since it doesn't appear on this
genuinely fresh site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed against a real, fresh version-15 bench (not a guess): calling
the exact same entry point the real test run uses
(make_test_records("Card Scan Log", ...), triggered by nothing more than
nextiq's single Card Scan Log.lead Link field) pulls in 85 different
ERPNext doctypes — Customer, Opportunity, Employee, Item, Fiscal Year,
Warehouse, and dozens more nextiq never references directly. Frappe's
test framework does exhaustive transitive dependency resolution, and
somewhere in that graph, on version-15 specifically, an auto-generated
Opportunity test record is missing a mandatory "company" field — a real
bug in ERPNext's own version-15 test fixtures, not something bounded or
fixable by pre-seeding more records from nextiq's side.

bench init/install and the fixture-seeding script are still required to
pass on version-15; only the test-execution step itself is allowed to
fail, and it's clearly commented explaining why.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes 2 new frappe-breaks-multitenancy Semgrep findings introduced by
the previous commit: bare module-level frappe.get_doc() assignments
trip the same rule that _COL_LABEL did in time_saved_report.py. Same
fix as before, no behavior change.
Job-level continue-on-error protects the workflow run's overall
conclusion, but the job's own GitHub check still reports failure
(confirmed: PR showed a red X on "CI / Server (version-15)" despite
this). Step-level continue-on-error protects the job's own conclusion
instead, so the check turns green while still only tolerating the
known Run tests failure — bench init/install/fixture-seeding stay
required to pass.
@mihirpatel1510 mihirpatel1510 changed the title New dev v2 Set up GitHub Actions CI for nextiq Aug 20, 2026
@mihirpatel1510
mihirpatel1510 merged commit 691aec5 into SatatTech:dev-v2 Aug 20, 2026
5 checks passed
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