Skip to content

feat(identity): make persons-seed CLI-only with a scheduled CronJob (#1690) - #2046

Merged
mozhaev-dev merged 11 commits into
mainfrom
feat/identity-seed-cli-1690
Jul 30, 2026
Merged

feat(identity): make persons-seed CLI-only with a scheduled CronJob (#1690)#2046
mozhaev-dev merged 11 commits into
mainfrom
feat/identity-seed-cli-1690

Conversation

@mozhaev-dev

@mozhaev-dev mozhaev-dev commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #1690.

Problem

The identity org projection (persons / account_person_map / org_chart in MariaDB) is materialized and was only rebuilt by the persons-seed at bootstrap. Nothing re-ran it, while connectors keep its input (identity.identity_inputs in ClickHouse) fresh daily — so every manager/org change ingested after the last seed run never reached the Team view (527 re-parented people invisible on the reporting Virtuozzo instance).

How it works now

The seed becomes a first-class scheduled operation of the identity-resolution service; the HTTP trigger is removed (team decision — the .NET service and its POST /v1/persons-seed are being deleted together).

CronJob (30 6 * * * UTC, or a manual Job)
  └─ identity-resolution seed [--mode link-by-email] [--force]
       1. GET_LOCK('persons-seed:<tenant>', 0)   ← per-tenant MariaDB advisory lock,
          held on a dedicated single connection for the whole run
       2. zombie sweep                            ← reclaims operations rows a killed
          Job left `queued`/`running`
       3. operations journal row                  ← author = nil UUID (SYSTEM_AUTHOR),
          request records {"trigger": "cli"}
       4. read identity_inputs → input guards     ← see below
       5. the same domain pipeline the endpoint used
          (resolve → INSERT IGNORE persons → rebuild account_person_map + org_chart,
          one transaction), bounded by a 10-minute timeout
       6. journal → completed (summary) / failed

Exit codes (diagnosable straight from the Job status): 0 ok · 1 failed · 2 another run holds the lock · 3 refused by an input guard.

Concurrency. Runs serialize on the advisory lock — it lives on the MariaDB server, so a cron Job, a manual Job, and even a second Insight instance sharing the same database all serialize through it, crash-safe by construction (the lock dies with the connection). concurrencyPolicy: Forbid on the CronJob is belt-and-braces for cron-vs-cron only. A concurrent run fails fast (exit 2) rather than queueing a stale re-run.

Input guards (both --force-overridable, both journaled as a failed operation so GET /v1/persons-seed explains why nothing was written):

What stays / what goes. POST /v1/persons-seed, the in-process queue/worker and the 503 queue-full path are gone. The read-only journal routes GET /v1/persons-seed and GET /v1/persons-seed/{id} stay as the observability window over CLI runs. The subcommand paths now install a tracing subscriber — previously migrate logged into the void.

Helm. The CronJob lives in the identity-resolution subchart (same image/config/secret wiring as the deployment; distinct pod labels so the Service never routes traffic to seed pods). Values: identityResolution.seed.{enabled, schedule} (+ subchart-only seed.tenantDefaultId that env-overrides the Secret for standalone installs). The umbrella fails the render when the seed is enabled but no tenant is configured — only on the path where the umbrella composes the config Secret itself (credentials.autoGenerate); pre-created-secret installs are the operator's source of truth.

Running it manually

Inside the cluster, from the CronJob (same image/env/config — nothing to remember):

kubectl -n <ns> create job --from=cronjob/<release>-identity-resolution-seed seed-manual-$(whoami)
kubectl -n <ns> logs -f job/seed-manual-$(whoami)
kubectl -n <ns> get job seed-manual-$(whoami)   # exit code semantics above

Watching a run / history over HTTP (admin JWT):

GET /v1/persons-seed            # list runs, ?status=&limit=
GET /v1/persons-seed/{id}       # one run: status, summary, error_message

Overriding the guards knowingly (e.g. a legitimate first seed while another tenant's data exists):

kubectl -n <ns> create job seed-force --image=<identity-resolution image> \
  -- /app/identity-resolution -c /app/config/insight.yaml seed --force

(or simplest: kubectl create job --from=cronjob/... seed-force, then edit the Job's args to append --force before creating — the CronJob itself never runs forced.)

Tests

  • Unit (67): guard decision table (empty input, wrong tenant, --force, fresh-install and steady-state pass), mode/tenant validation, pipeline fakes.
  • Contract e2e (src/ingestion/tests/e2e/identity/, both CI lanes):
    • CLI trigger cases (rust lane): completed run journaled with nil author + trigger: "cli", empty-input guard (exit 3, journaled), wrong-tenant guard (exit 3, journaled), lock busy (exit 2 against a held GET_LOCK);
    • inputs → org_chart correspondence: after a seed the open edges match identity_inputs (resolvable manager → edge, unresolvable → NULL-parent membership, top-of-tree → Path-B row);
    • the Team view shows a stale roster — manager and org changes never appear #1690 regression proper: a newer parent_email lands in the inputs → a re-run moves the open edge to the new manager and closes the old one (SCD2 history intact);
    • POST cases are gated to the dotnet lane and die with the .NET service; the rust coverage gate carries an approved SKIP for the removed POST.

Verification

  • cargo fmt / clippy -D warnings / 67 unit tests green; release binary smoke-tested (seed --help, exit codes).
  • Identity contract suite: rust lane 113 passed (three back-to-back runs — flake check), dotnet lane 108 passed; both endpoint-coverage gates rc=0.
  • helm lint both charts; render matrix: umbrella fails fast on enabled-seed-without-tenant, renders with global.tenantDefaultId / seed.tenantDefaultId / seed.enabled=false; functional-ci values render unchanged.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a scheduled persons-seed CronJob and seed job configuration (schedule, resources, history limits) with force support.
    • Added a Rust CLI seed workflow that records progress via the existing operations journal (write trigger removed; status/journal reads remain).
  • Bug Fixes
    • Helm/seed rendering now fails fast when seed is enabled without a tenant, including umbrella-chart guard behavior.
    • Improved tenant handling by supporting safe tenant inference when unconfigured (and refusing ambiguous/empty cases).
  • Tests
    • Expanded Helm contract and end-to-end suites to validate rendering, labels, locking/guard behavior, and journaling semantics.

mozhaev-dev and others added 2 commits July 30, 2026 08:07
…1690)

The identity org projection (persons / account_person_map / org_chart)
froze after the bootstrap seed: nothing re-ran the persons-seed while
connectors kept identity_inputs fresh daily, so Team-view rosters went
stale for everyone re-parented since the last run.

The seed becomes a first-class scheduled operation of the service; the
HTTP trigger is removed (team decision — the .NET service and its POST
die together):

- new `seed` subcommand runs the same domain pipeline synchronously and
  exits (0 ok / 1 failed / 2 lock busy / 3 input guard); subcommand
  paths now install a tracing subscriber (migrate logs were silent
  no-ops before)
- runs serialize on a per-tenant MariaDB GET_LOCK held on a dedicated
  single-connection session — covers cron-vs-manual overlap and
  multiple instances sharing one database, crash-safe by construction
- input guards (--force overrides, refusals journaled as failed
  operations): empty identity_inputs read (broken/misconfigured
  pipeline) and wrong-tenant run (would mint a parallel person universe
  under a wrong tenant — the #1550 failure mode)
- every run writes the operations journal (author = nil SYSTEM_AUTHOR,
  the legacy Python seed convention); zombie sweep moves to CLI start,
  so a killed Job cannot strand a `running` row
- POST /v1/persons-seed + its in-process queue/worker/503 path are
  removed; the GET journal routes stay as the observability window
- Helm: seed CronJob in the identity-resolution subchart (same
  config/secret wiring as the deployment; distinct pod labels so the
  Service never routes to seed pods), daily 06:30 UTC after overnight
  syncs; optional seed.tenantDefaultId env-overrides the Secret for
  standalone installs; the umbrella validates a tenant is present when
  it composes the config Secret itself (credentials.autoGenerate)
- e2e: POST cases gated to dotnet (die with .NET), CLI cases gated to
  rust (guards, lock, exit codes, journal contract); coverage gate
  skips the removed POST on the rust lane and drops its REQUIRED_EXTRA

Verified: 67 unit tests, clippy/fmt clean; identity contract suite
green on both lanes (rust 111 passed, dotnet 108 passed) with coverage
gates rc=0; helm lint + render matrix for both charts.

Closes #1690

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eed (#1690)

The suite verified the persons half of the seed but never built an org
chart from inputs — the roster carried no parent_email at all, so the
projection #1690 is about went unexercised end to end.

- roster grows a manager chain: a boss, a parent_email on the shared
  person, and a deliberately unresolvable parent (ghost) on the solo one
- test_seed_org_chart_matches_inputs: after a CLI seed the open edges
  match the inputs — resolvable manager → edge to the right parent,
  unresolvable → NULL-parent membership row (ADR-0010: no stub persons),
  top-of-tree → Path-B NULL-parent row; asserted over SQL on purpose
  (the read projection filters by org_chart_source_type and is covered
  by the read tests over the handcrafted fixture)
- test_seed_manager_change_reaches_org_chart: THE #1690 regression — a
  newer parent_email lands in identity_inputs, a re-run moves the open
  edge to the new manager and closes the old one (SCD2 history intact)

The manager-change cast is per-run unique (uuid-suffixed accounts):
the MariaDB persons log outlives sessions on a kept local stack (the
session seed wipes only reason='e2e-seed' rows), so re-parenting a
shared roster account poisons the next session's latest-observation
race whenever runs land seconds apart. A fresh child has no
cross-session history by construction. CI is unaffected either way
(fresh containers per lane).

Verified: three back-to-back rust-lane runs 113 passed each, dotnet
lane 108 passed, both coverage gates rc=0.

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

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a guarded identity-resolution seed CLI runner, removes server-side seed queuing, schedules the seed through Helm, adds tenant validation and advisory locking, and updates Helm and identity E2E contracts for CLI-only Rust execution.

Changes

Persons-seed lifecycle

Layer / File(s) Summary
CLI seed runner and guarded pipeline
src/backend/services/identity-resolution/src/{main.rs,seed_runner.rs,gear.rs}, src/backend/services/identity-resolution/src/{domain,infra}/...
Adds the seed CLI command, tenant inference and guards, advisory locking, operation journaling, timeout handling, and direct row-based seed orchestration.
Read-only operations journal API
src/backend/services/identity-resolution/src/api/{mod.rs,seed.rs}
Removes the HTTP POST enqueue path and in-process worker while retaining admin-gated operation retrieval and listing.
Helm CronJob and tenant validation
.github/workflows/identity-resolution-helm.yml, charts/insight/..., src/backend/services/identity-resolution/helm/...
Adds the scheduled persons-seed CronJob, tenant render guard, runtime configuration, and Helm render-contract coverage.
Implementation-aware E2E coverage
src/ingestion/tests/e2e/identity/..., src/ingestion/tests/e2e/lib/...
Adds Rust CLI triggering and capability detection, updates API coverage skips, and tests guards, journaling, org-chart synchronization, SCD2 updates, and lock contention.
Source annotation updates
src/backend/services/identity-resolution/src/infra/identity_inputs.rs, src/ingestion/tests/e2e/lib/identity_seed.py
Normalizes HOTFIX marker formatting in comments.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: priority:P0

Suggested reviewers: cyberantonz, mitasovr, aleksdotbar

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated changes like the toolbox image bump, presentation DB addition, and HOTFIX comment edits. Remove unrelated chart/image tweaks and comment-only HOTFIX edits, or split them into separate PRs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: moving persons-seed to a CLI-only scheduled CronJob.
Linked Issues check ✅ Passed The PR adds the scheduled CLI seed job, removes the HTTP/queue path, and adds guard and locking tests matching #1690.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/identity-seed-cli-1690

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.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Regenerate the connectors-ddl snapshot

This PR changes src/ingestion/**. If your change affects any
bronze / silver / gold schema, regenerate the committed DDL snapshot
and include it in this PR.

Prerequisites (details: src/ingestion/scripts/bootstrap-db/README.md):

  • docker + a fresh throwaway ClickHouse 25.7.5 (README "Local ClickHouse for testing")
  • .env from .env.bootstrap.example pointing at it; use the host LAN IP,
    reachable from both the host and connector containers
    (host.docker.internal does not resolve on the macOS host itself)
  • python3.12 or python3.11 on PATH (pinned dbt venv)
  • HubSpot + Salesforce credentials in .env — their discover calls the
    live APIs; without them, apply ../connectors-ddl/{hubspot,salesforce}.sql
    (relative to bootstrap-db/) to seed their bronze, then run the dbt step
cd src/ingestion/scripts/bootstrap-db
set -a; source pins.env; source .env; set +a
./bootstrap-db.sh connectors-config.yaml   # fresh ClickHouse 25.7.5
./dump-ddl.sh                              # writes scripts/connectors-ddl/*.sql

Commit the resulting scripts/connectors-ddl/*.sql diff. If nothing
changed, no snapshot update is needed. (Regeneration is manual for now.)

… zombie sweep (#1690)

Review found three coverage gaps; all three are now pinned.

Scheduling mechanism (the essence of #1690 — the bug WAS the absence of
a schedule, and the functional-k3s lane doesn't deploy identityResolution):
- helm/tests/test_seed_cronjob_contract.py — pure `helm template` +
  PyYAML assertions, no cluster: CronJob exists by default with the
  documented schedule and Forbid; exact seed command/args (and never
  --force — forcing is a deliberate manual act); Secret/ConfigMap equal
  the deployment's own wiring (compared against its manifest, not
  literals); seed.tenantDefaultId env-overrides the Secret and is absent
  by default; seed.enabled=false removes only the CronJob; seed pod
  labels never satisfy the Service selector; umbrella refuses an
  enabled seed with no tenant and renders with either tenant source or
  with the seed disabled
- .github/workflows/identity-resolution-helm.yml runs helm lint (both
  charts) + these tests on any subchart/umbrella change (gateway.yml
  precedent)

Exit code 1 through the real binary:
- run_seed_cli grows extra_env; the test points clickhouse_url at a
  closed port → rc 1 AND a failed journal row carrying exactly the
  generic "persons-seed failed; see job logs" (pins the no-leak rule:
  raw driver text must never reach the GET-returned error_message)

Zombie sweep:
- synthetic running operations at 2h and 5min; a seed run flips the
  stale one to failed ("aborted by pod restart") and leaves the fresh
  one alone — both sides of the 1h cutoff asserted; synthetic rows
  cleaned up

Verified: helm contract 10/10; rust lane 115 passed twice back-to-back,
dotnet lane 108 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread .github/workflows/identity-resolution-helm.yml Fixed
Comment thread .github/workflows/identity-resolution-helm.yml Fixed
@mozhaev-dev
mozhaev-dev marked this pull request as ready for review July 30, 2026 06:02
@mozhaev-dev
mozhaev-dev requested a review from a team as a code owner July 30, 2026 06:02

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/backend/services/identity-resolution/src/infra/db/seed_repo.rs (1)

81-107: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full persons table scan on every seed run — consider existence checks instead of SUM.

input_guards only needs zero-vs-non-zero for own_rows/other_rows, but this query aggregates over the entire persons table (all tenants) with no WHERE. As the table grows across tenants, this becomes an increasingly costly per-run full scan/aggregate ahead of every seed pipeline execution.

Two indexed EXISTS (or SELECT 1 ... LIMIT 1) queries on insight_tenant_id would let the optimizer short-circuit instead of scanning/aggregating the whole table.

⚡ Proposed existence-based rewrite
-    const SQL: &str = r"
-        SELECT
-            CAST(COALESCE(SUM(insight_tenant_id = ?), 0) AS SIGNED)  AS own_rows,
-            CAST(COALESCE(SUM(insight_tenant_id <> ?), 0) AS SIGNED) AS other_rows
-        FROM persons
-    ";
+    const SQL: &str = r"
+        SELECT
+            EXISTS(SELECT 1 FROM persons WHERE insight_tenant_id = ?)   AS own_rows,
+            EXISTS(SELECT 1 FROM persons WHERE insight_tenant_id <> ?)  AS other_rows
+    ";

Note: this changes own_rows/other_rows semantics from exact counts to 0/1 booleans — update the operator-facing guard message in seed_runner::input_guards accordingly if adopted.

🤖 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 `@src/backend/services/identity-resolution/src/infra/db/seed_repo.rs` around
lines 81 - 107, Update tenant_presence to use indexed existence checks for
matching and non-matching insight_tenant_id values instead of aggregating SUM
across persons, while preserving the TenantPresence zero/non-zero contract.
Adjust seed_runner::input_guards operator-facing messaging to describe
presence/existence rather than exact row counts if it exposes those values.
src/backend/services/identity-resolution/helm/values.yaml (1)

88-94: 🚀 Performance & Scalability | 🔵 Trivial

Verify resources sized for the batch seed workload, not just copied from the API service.

These request/limit numbers are identical to the HTTP service's defaults (charts/insight/values.yaml identityResolution.resources), but the seed job does a full per-tenant projection recompute against ClickHouse rather than serving light requests. For larger tenants this may be under-provisioned, and since restartPolicy: Never, an OOMKill just burns a backoffLimit retry silently.

🤖 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 `@src/backend/services/identity-resolution/helm/values.yaml` around lines 88 -
94, Update the resources under the seed workload’s values configuration to
reflect full per-tenant projection recomputation against ClickHouse rather than
copying the HTTP service defaults. Size CPU and memory requests and limits for
larger tenants, and ensure the batch job’s resource settings reduce the risk of
OOMKilled retries under its existing restart policy.
🤖 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 `@src/backend/services/identity-resolution/src/seed_runner.rs`:
- Around line 117-177: Bound all database operations performed by run_locked,
including sweep_zombies, enqueue, try_start, complete, and fail, with the same
SEED_TIMEOUT policy used by guarded_seed. Ensure a timeout returns the
appropriate SeedRunError while preserving operation journaling and failure
handling, so the tenant lock cannot be held indefinitely by any critical-section
call.

---

Nitpick comments:
In `@src/backend/services/identity-resolution/helm/values.yaml`:
- Around line 88-94: Update the resources under the seed workload’s values
configuration to reflect full per-tenant projection recomputation against
ClickHouse rather than copying the HTTP service defaults. Size CPU and memory
requests and limits for larger tenants, and ensure the batch job’s resource
settings reduce the risk of OOMKilled retries under its existing restart policy.

In `@src/backend/services/identity-resolution/src/infra/db/seed_repo.rs`:
- Around line 81-107: Update tenant_presence to use indexed existence checks for
matching and non-matching insight_tenant_id values instead of aggregating SUM
across persons, while preserving the TenantPresence zero/non-zero contract.
Adjust seed_runner::input_guards operator-facing messaging to describe
presence/existence rather than exact row counts if it exposes those values.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 24ec0d2e-9ace-4f8a-a2aa-3af5d9e06996

📥 Commits

Reviewing files that changed from the base of the PR and between 485a861 and 595cd18.

⛔ Files ignored due to path filters (1)
  • src/backend/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • .github/workflows/identity-resolution-helm.yml
  • charts/insight/templates/secrets.yaml
  • charts/insight/values.yaml
  • src/backend/services/identity-resolution/Cargo.toml
  • src/backend/services/identity-resolution/helm/templates/seed-cronjob.yaml
  • src/backend/services/identity-resolution/helm/tests/test_seed_cronjob_contract.py
  • src/backend/services/identity-resolution/helm/values.yaml
  • src/backend/services/identity-resolution/src/api/mod.rs
  • src/backend/services/identity-resolution/src/api/seed.rs
  • src/backend/services/identity-resolution/src/domain/seed_service.rs
  • src/backend/services/identity-resolution/src/gear.rs
  • src/backend/services/identity-resolution/src/infra/db/mod.rs
  • src/backend/services/identity-resolution/src/infra/db/ops_repo.rs
  • src/backend/services/identity-resolution/src/infra/db/seed_repo.rs
  • src/backend/services/identity-resolution/src/main.rs
  • src/backend/services/identity-resolution/src/seed_runner.rs
  • src/ingestion/tests/e2e/identity/test_error_contracts.py
  • src/ingestion/tests/e2e/identity/test_meta_gate.py
  • src/ingestion/tests/e2e/identity/test_persons_seed.py
  • src/ingestion/tests/e2e/lib/api_coverage.py
  • src/ingestion/tests/e2e/lib/identity.py

Comment thread src/backend/services/identity-resolution/src/seed_runner.rs
…ction, EXISTS guard probe, pinned actions (#1690)

CodeRabbit (major): SEED_TIMEOUT bounded only the read+pipeline, while the
zombie sweep and the journal writes are MariaDB calls under the same
advisory lock — a hang there would hold the lock past every next tick.
RUN_TIMEOUT (12m) now backstops the whole critical section; a run cut off
by it may leave its operations row `running`, which the next run's sweep
reclaims, and the chart's activeDeadlineSeconds (900s) stays the final
out-of-process backstop.

CodeRabbit (nitpick): tenant_presence aggregated over the whole persons
table ahead of every seed; the guard only needs zero-vs-non-zero, so it is
now two EXISTS probes that short-circuit on idx_tenant_person
(TenantPresence carries has_own/has_other booleans, guard message adjusted).

Semgrep: the new identity-resolution-helm workflow used mutable action
tags; pinned to full SHAs (same convention as semgrep.yml/trivy.yml).

Skipped (with reason): the seed Job resources nitpick — the live dev run
processed 4,780 accounts / 26,652 input rows in 12s well inside the
current requests/limits; revisit if an order-of-magnitude larger tenant
appears.

Verified: 67 unit tests, clippy -D warnings, fmt; rust-lane e2e 115 passed.

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

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"

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.

Why so old?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair catch — the action SHAs were copied from semgrep.yml and lagged behind; bumped to the current releases in ddf9490 (checkout v7.0.1, setup-python v7.0.0). python-version: 3.12 itself is intentional: it's the repo-wide standard (every workflow, the e2e runner image, and requires-python in the e2e pyproject all pin 3.12) — happy to bump repo-wide in a separate PR if we want to move.

The SHAs were copied from semgrep.yml and lagged behind (checkout
v5.0.1, setup-python v5.6.0). Pinned to the latest releases instead
(checkout v7.0.1, setup-python v7.0.0). python-version stays 3.12 —
the repo standard (every workflow, the e2e runner image, and
requires-python in the e2e pyproject all pin 3.12).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
return Err(SeedRunError::LockBusy);
}

let result = tokio::time::timeout(RUN_TIMEOUT, run_locked(&db, config, tenant, mode, force))

@cyberantonz cyberantonz Jul 30, 2026

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.

If you crash here, you will have lock held forever. I think it should be done via RAII pattern, i.e. lock held until the variable is in the scope. When you leave scope - the lock automatically removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in ff8d3afSeedLockGuard now owns the lock's session (RAII), every exit path releases it. Worth noting: GET_LOCK is session-scoped, so a crash already released it server-side — the guard formalizes that invariant.

"unsupported mode '{mode}'; only '{LINK_BY_EMAIL_MODE}' is available"
)));
}
if config.tenant_default_id.is_empty() {

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.

It is true for one installation. But at least dev has no default tenant id at all and in perfect world you need to grab all tenants from DB and run for each. Currently you will break multitenancy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on direction, but a per-tenant loop is blocked one level lower: the identity_inputs reader reads the whole table with no tenant filter (HOTFIX(#1550) — the dbt producer hashes tenant ids), so each tenant's run would ingest every other tenant's rows. Documented this at the validation site in ff8d3af (grep HOTFIX(#1550) for the full blast radius); the runner below is already per-tenant (lock, journal, writes), so the loop drops in once the producer fix lands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up in 6c68bfb: since existing config Secrets (dev included) can't be touched right now, an EMPTY tenant_default_id no longer hard-fails — the runner infers the SOLE distinct tenant from the persons log (WARN-logged); zero or several tenants still refuse with a clear message, so nothing is ever guessed ambiguously. Dev works out of the box with this.

mozhaev-dev and others added 2 commits July 30, 2026 10:19
…radius (#2046 review)

Review asked for RAII on the advisory lock and for the multi-tenant
limitation to be visible at the tenant-validation site.

- SeedLockGuard replaces the try_acquire/release pair: the guard OWNS
  the lock's dedicated single-connection session, so the lock's
  lifetime is tied to the guard's scope by construction — early return,
  future cancellation, and process crash all release it via session
  teardown (GET_LOCK is session-scoped; a stale lock was already
  impossible, the guard formalizes the invariant against future
  refactors). Happy path still issues an explicit RELEASE_LOCK for the
  fastest handover.

- The single-tenant contract is now documented AT the tenant validation
  in seed_runner::run: a true "enumerate tenants, seed each" mode is
  blocked by the HOTFIX(#1550) reader (whole-table read, no tenant
  filter — the dbt producer hashes tenant ids), and what unblocks it;
  the runner below is already per-tenant (lock name, journal, writes).

- HOTFIX(#1550) is now a uniform greppable tag across every dependent
  site (reader anchor declares it; runner, tenant_presence, e2e harness
  comments carry it) — unwinding the hotfix starts from one grep.

Verified: 67 unit tests, clippy, fmt; rust-lane e2e 115 passed on a
runner image rebuilt from this tree.

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

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

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 @.github/workflows/identity-resolution-helm.yml:
- Line 34: Update the actions/checkout step in the identity resolution workflow
to set persist-credentials to false, preventing the GITHUB_TOKEN from being
stored in local git configuration while preserving the existing checkout action
and pinned revision.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bf95f69e-006b-4a0b-8294-5f40d8c78e46

📥 Commits

Reviewing files that changed from the base of the PR and between 595cd18 and ff8d3af.

📒 Files selected for processing (8)
  • .github/workflows/identity-resolution-helm.yml
  • charts/insight/values.yaml
  • src/backend/services/identity-resolution/src/infra/db/mod.rs
  • src/backend/services/identity-resolution/src/infra/db/seed_repo.rs
  • src/backend/services/identity-resolution/src/infra/identity_inputs.rs
  • src/backend/services/identity-resolution/src/seed_runner.rs
  • src/ingestion/tests/e2e/identity/test_persons_seed.py
  • src/ingestion/tests/e2e/lib/identity_seed.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ingestion/tests/e2e/identity/test_persons_seed.py

steps:
# Pinned to full SHAs (mutable tags are repointable — supply-chain
# hardening, same as semgrep.yml / trivy.yml).
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on checkout.

This job only runs helm lint/pytest and never pushes back to the repo, so persisting the GITHUB_TOKEN in the local git config is unnecessary exposure (flagged by zizmor's artipacked check).

🔒 Proposed fix
-      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          persist-credentials: false
📝 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
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 34-34: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 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 @.github/workflows/identity-resolution-helm.yml at line 34, Update the
actions/checkout step in the identity resolution workflow to set
persist-credentials to false, preventing the GITHUB_TOKEN from being stored in
local git configuration while preserving the existing checkout action and pinned
revision.

Source: Linters/SAST tools

mozhaev-dev and others added 2 commits July 30, 2026 10:39
…figured (#2046 review)

Existing installs' pre-created config Secrets predate the seed and carry
no tenant_default_id (dev is one), and those Secrets cannot be touched
right now — as shipped, the CronJob would fail there on every tick.

The runner now resolves the tenant instead of hard-requiring it:
- a configured tenant_default_id always wins;
- an EMPTY config falls back to the SOLE distinct tenant in the persons
  log (WARN-logged) — writing under the one tenant the data already
  lives under is exactly what an operator would configure;
- zero tenants (fresh install) or several → refuse with an
  operator-facing message; guessing there would recreate the
  HOTFIX(#1550) wrong-tenant hazard.

resolve_tenant is a pure function with unit tests for every branch; the
e2e harness gained tenant=None (leaves the config empty) and an
ambiguous-refusal case over the multi-tenant fixture dataset.

Verified: 72 unit tests, clippy, fmt; rust-lane e2e 116 passed on a
runner image rebuilt from this tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mozhaev-dev
mozhaev-dev enabled auto-merge July 30, 2026 07:56

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ingestion/tests/e2e/lib/identity.py (1)

284-308: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear inherited tenant configuration when tenant=None.

_rust_env() copies the parent environment, so an inherited tenant override prevents the intended empty-config inference path from being tested.

Proposed fix
         env = self._rust_env()
-        if tenant is not None:
-            env["APP__gears__identity-resolution__config__tenant_default_id"] = tenant
+        tenant_key = "APP__gears__identity-resolution__config__tenant_default_id"
+        if tenant is None:
+            env.pop(tenant_key, None)
+        else:
+            env[tenant_key] = tenant
🤖 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 `@src/ingestion/tests/e2e/lib/identity.py` around lines 284 - 308, Update the
seed CLI environment setup in the method containing _rust_env() and the
tenant_default_id assignment so tenant=None explicitly removes
APP__gears__identity-resolution__config__tenant_default_id from env, while
preserving the existing override when tenant is provided.
🤖 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.

Outside diff comments:
In `@src/ingestion/tests/e2e/lib/identity.py`:
- Around line 284-308: Update the seed CLI environment setup in the method
containing _rust_env() and the tenant_default_id assignment so tenant=None
explicitly removes APP__gears__identity-resolution__config__tenant_default_id
from env, while preserving the existing override when tenant is provided.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 77e3239e-0c26-4f2d-b4a2-d49946d60163

📥 Commits

Reviewing files that changed from the base of the PR and between ff8d3af and e36d8e9.

📒 Files selected for processing (5)
  • charts/insight/values.yaml
  • src/backend/services/identity-resolution/src/infra/db/seed_repo.rs
  • src/backend/services/identity-resolution/src/seed_runner.rs
  • src/ingestion/tests/e2e/identity/test_persons_seed.py
  • src/ingestion/tests/e2e/lib/identity.py

@mozhaev-dev
mozhaev-dev merged commit fab0089 into main Jul 30, 2026
59 of 60 checks passed
@mozhaev-dev
mozhaev-dev deleted the feat/identity-seed-cli-1690 branch July 30, 2026 09:25
mozhaev-dev added a commit that referenced this pull request Jul 30, 2026
…ase-2026.07.1

feat(identity): make persons-seed CLI-only with a scheduled CronJob (#1690) (#2046)
cyberantonz added a commit that referenced this pull request Jul 31, 2026
)

Manual equivalent of the publish-chart job, which does not yet run on
release branches (CI gap, being fixed separately in #2097): pin the
branch-built image tags into the subchart appVersions + the toolbox ref,
and patch-bump the umbrella version so the chart carrying the
identity-resolution seed CronJob template (#2046 backport) can be
published and consumed from gitops.

- backend subcharts + toolbox: 2026.07.31.06.52-bf09d6a.release-2026.07.1
- frontend subchart:           2026.07.31.06.52-0bb785c.release-2026.07.1
- umbrella: 0.4.68 -> 0.4.70 (appVersion = max subchart appVersion)

Signed-off-by: Anton Zelenov <antonz@constructor.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

Team view shows a stale roster — manager and org changes never appear

5 participants