Skip to content

chore(ci): catch frontend format, semgrep and baseline drops in preflight - #110891

Open
rnegron wants to merge 6 commits into
raul/flox-semgrepfrom
raul/preflight-shift-left
Open

rnegron wants to merge 6 commits into
raul/flox-semgrepfrom
raul/preflight-shift-left

Conversation

@rnegron

@rnegron rnegron commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Problem

  • A push still reaches CI with failures a laptop can find in seconds: unformatted frontend files, new devex semgrep findings, and visual baselines dropped from frontend/snapshots.yml.
  • A dropped baseline is the costly one. It failed every merge queue batch until fix(visual-review): restore 8 storybook baselines dropped by a stale merge #109926 restored the entries.
  • A PR that carries one cross-cutting file claims every merge queue lane, and the author gets no signal before the queue.

Changes

hogli ci:preflight gains four checks. Each runs only when the diff touches its files.

Check Result What it catches
frontend-format blocks, --fix repairs oxfmt drift in changed files
semgrep-devex advisory findings the branch introduced, with the semgrep on PATH
snapshot-baselines advisory baseline entries removed for stories the diff did not change
merge-queue-lane warns, skipped in the pre-push hook files that make the PR claim every lane, when a split would at least halve it
  • semgrep-devex scans head copies in a temp directory, then merge-base copies of the flagged files. Semgrep's --baseline-commit runs git reset --hard on a clean checkout, so the check avoids it.
  • semgrep-devex skips until semgrep is on PATH. chore(devex): add semgrep to the flox environment #110944 adds it to the flox environment, one version behind the CI pin, which is why the check advises and does not block.
  • snapshot-baselines does not block, because a branch can remove the entries of a story that an earlier PR deleted.
  • A check that cannot run reports skipped and never blocks.
  • Mechanical: DiffCheck gains a run callable for checks that read the diff, and Status moved to the new preflight_checks.py.

How did you test this code?

Test rationale: test_preflight_checks.py covers the decision of each check: a grandfathered or moved finding must pass, a baseline removal passes only when its own story changed, and a lane warning needs a split that narrows. test_ci_preflight.py gains one case: only a failed check blocks a push, a crashing one skips, and a warn-only one stays out of the hook. No existing test covered these.

  • Each check ran against a seeded violation in a worktree and reported it. A violation under a tests/ directory was ignored, which matches the CI directory scan.
  • A strict run on this branch took about 10 seconds.
  • Without semgrep on PATH the check reported skipped. With a 1.172.0 binary on PATH it reported the seeded finding as an advisory.
  • Not run: the repo-wide mypy pass.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

The running-ci-preflight skill describes the three new non-obvious checks.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Opus 5.5 (claude-opus-5-5)

  • Skills: /writing-tests, /writing-code-comments, /writing-skills, /writing-pr-descriptions.
  • Review: the built-in /code-review at high effort ran twice and /simplify once, in place of the CodeRabbit CLI, at the assignee's standing preference. Fixed: explicit file targets bypassed semgrep's default ignores, renamed files lost their baseline, strict mode read uncommitted files, a crashing check blocked the push, the lane warning blamed files without proof, an incomplete scan read as clean, and the snapshot parser missed explicit-key entries. Not fixed: the semgrep scan directories and the oxfmt globs are copied from the CI workflow and package.json.
  • Bot review: fixed the Greptile findings on explicit baseline keys, unrelated story edits hiding removals, and desktop warnings, and the CodeRabbit finding on semgrep's errors array. Declined two Greptile findings. A change to a devex rule file is not scanned locally, because that needs the whole-tree scan CI runs. Strict mode formats working-tree copies, which is how every file-taking preflight check already works.
  • The snapshot check started as blocking and became advisory after review.
  • Left out: the repo invariant tests. The suite took over 11 minutes locally, which is too slow for a pre-push hook.
  • Duplicate search for open preflight PRs found none that overlap.

@rnegron rnegron self-assigned this Oct 2, 2026
@trunk-io

trunk-io Bot commented Oct 2, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

🚨 Trunk lane — universal lane

This PR is assigned to the universal lane. It cannot merge in parallel with other PRs, so it can take longer to merge. Ask dev-ex if you think this is wrong.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

⚠️ Comment density — 6% of added code lines are comments (31 of 546)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
tools/hogli-commands/hogli_commands/preflight_checks.py 24 262
tools/hogli-commands/hogli_commands/ci_preflight.py 6 71
tools/hogli-commands/hogli_commands/tests/test_ci_preflight.py 1 44

This check does not block merging. It updates on every push and clears when the share drops.

@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[High risk] Adds new preflight checks that run custom code on every push.

The PR appears safe to merge based on this review; no new actionable finding remains.

Reviews (2) · Last reviewed commit: "chore(ci): run preflight semgrep from pa..."

Comment thread tools/hogli-commands/hogli_commands/preflight_checks.py Outdated
Comment thread tools/hogli-commands/hogli_commands/preflight_checks.py Outdated
Comment thread tools/hogli-commands/hogli_commands/preflight_checks.py Outdated
Comment thread tools/hogli-commands/hogli_commands/preflight_checks.py Outdated
Comment on lines +246 to +251
# Mirrors lint-staged's `format:js`, which agents bypass via --no-verify.
verify=["pnpm", "exec", "oxfmt", "--check", "--no-error-on-unmatched-pattern"],
fix=["pnpm", "exec", "oxfmt", "--no-error-on-unmatched-pattern"],
requires=("node",),
takes_files=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.

P1 Uncommitted formatting blocks pushes. In --strict mode, preflight selects committed changed paths, but this file-taking check runs oxfmt on their working-tree contents. An uncommitted formatting edit can block a push even though that edit is not being pushed. Check committed copies in strict mode, or make working-tree formatting advisory there.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/hogli-commands/hogli_commands/ci_preflight.py
Line: 246-251

Comment:
**Uncommitted formatting blocks pushes.** In `--strict` mode, preflight selects committed changed paths, but this file-taking check runs oxfmt on their working-tree contents. An uncommitted formatting edit can block a push even though that edit is not being pushed. Check committed copies in strict mode, or make working-tree formatting advisory there.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: b776426e-5017-4539-a136-00759cdb8aaf

📥 Commits

Reviewing files that changed from the base of the PR and between 6c29787 and e740e67.

📒 Files selected for processing (3)
  • .agents/skills/running-ci-preflight/SKILL.md
  • tools/hogli-commands/hogli_commands/preflight_checks.py
  • tools/hogli-commands/hogli_commands/tests/test_preflight_checks.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The CI preflight runner adds an Oxfmt check for selected frontend files and runs registered diff-based checks for snapshot baseline removals, newly introduced Semgrep findings, and merge-queue lane selection. The checks compare changes against a merge base and report pass, fail, warning, advisory, or skipped outcomes. Tests cover check behavior and runner handling.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to e740e

A removed baseline for an unchanged story can go unflagged and cause a merge-queue failure. Correct the snapshot check before merging unless that risk is explicitly accepted.

Security Architecture Review

Security architecture risk: 🔵 Low · up to e740e

The change adds local developer checks without demonstrating new production access or weaker CI enforcement. Incomplete scans are identified as skipped rather than clean. Remaining uncertainty concerns complete security coverage and cleanup after forced process termination.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated new execution scope is the invoking developer's checkout and local subprocess authority. Temporary scan directories isolate inputs but do not constitute a credential or privilege sandbox: Semgrep inherits the process environment.

Trust Boundaries and Controls

  • observed — Branch contents are treated as scan input rather than callable selectors. The runner invokes statically registered functions, and Semgrep receives an argument list without shell interpolation. Semgrep metrics and version checks are disabled for these scans.
  • observed — The inspected CI workflow independently enforces ERROR rules and newly introduced WARNING findings. Its blocking ERROR pass already excludes the desktop tree, matching the local exclusion. Local advisory or skipped results do not replace those CI steps.

Resilience and Maintainability Implications

  • inferred — Per-invocation scan trees structurally contain ordinary scan failures and isolate repeated runs. Status recording distinguishes incomplete scans from completed clean scans, although neither skipped scans nor advisories block a local push. Runtime interruption and concurrency behavior remain unverified.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the required Problem, Changes, testing rationale, release status, notifications, docs, and agent context sections. It clearly explains the user impact, behavior, test coverage…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 76b88204-9a99-4818-9138-4ae228ea9f87

📥 Commits

Reviewing files that changed from the base of the PR and between 393a909 and 27083f8.

📒 Files selected for processing (5)
  • .agents/skills/running-ci-preflight/SKILL.md
  • tools/hogli-commands/hogli_commands/ci_preflight.py
  • tools/hogli-commands/hogli_commands/preflight_checks.py
  • tools/hogli-commands/hogli_commands/tests/test_ci_preflight.py
  • tools/hogli-commands/hogli_commands/tests/test_preflight_checks.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

Comment on lines +143 to +184
def _semgrep_findings(semgrep: list[str], root: Path) -> dict[Finding, list[int]] | None:
"""Findings under *root*, each with the lines it starts on. None when the scan did not run.

The target is the directory and not the files in it. Semgrep applies its default
ignore list (``tests/``, ``node_modules/``, minified files) to a directory it walks,
which is how CI scans, and skips that list for a file named on the command line.
"""
try:
result = subprocess.run(
[
*semgrep,
"--config",
str(REPO_ROOT / SEMGREP_RULES),
"--severity=WARNING",
"--severity=ERROR",
"--metrics=off",
"--quiet",
"--json",
".",
],
cwd=root,
env={**os.environ, "SEMGREP_ENABLE_VERSION_CHECK": "false"},
capture_output=True,
text=True,
timeout=_SEMGREP_TIMEOUT_SECONDS,
)
findings: dict[Finding, list[int]] = {}
sources: dict[str, list[str]] = {}
for item in json.loads(result.stdout)["results"]:
path = item["path"]
if path not in sources:
sources[path] = (root / path).read_text(errors="replace").splitlines()
start, end = item["start"]["line"], item["end"]["line"]
matched = "\n".join(line.strip() for line in sources[path][start - 1 : end])
# The id prefix encodes the rule file's path relative to the working directory.
# The last segment is the rule's own id.
rule = item["check_id"].rsplit(".", 1)[-1]
findings.setdefault((rule, path, matched), []).append(start)
except (OSError, subprocess.TimeoutExpired, ValueError, KeyError, TypeError):
# A missing uvx download, a crash and a timeout all end here, because none of them prints a report.
return None
return findings

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🌐 Web query:

semgrep CLI exit codes json errors field

💡 Result:

`semgrep scan --json` puts execution errors in the top-level `errors` array; it is separate from `results`, which contains findings. Each error has required `code`, `level`, and `type` fields. Optional fields include `message`, `path`, `rule_id`, `long_msg`, and `short_msg`. The schema describes `code` as the exit code associated with that error type—not necessarily the process’s overall exit status. ([github.com](https://github.com/semgrep/semgrep-interfaces/blob/main/semgrep_output_v1.jsonschema?utm_source=openai))

For the process status, check the CLI’s actual exit code; don’t infer it from `errors` alone. The schema doesn’t establish a universal mapping between individual JSON errors and the overall CLI exit status, and behavior may depend on CLI version and flags. ([github.com](https://github.com/semgrep/semgrep-interfaces/blob/main/semgrep_output_v1.jsonschema?utm_source=openai))

Citations:

- 1: https://github.com/semgrep/semgrep-interfaces/blob/main/semgrep_output_v1.jsonschema?utm_source=openai
- 2: https://github.com/semgrep/semgrep-interfaces/blob/main/semgrep_output_v1.jsonschema?utm_source=openai

Treat Semgrep execution errors as an unavailable scan.

_semgrep_findings reads only results. A Semgrep process can return JSON with an empty results array and execution errors in the top-level errors array. The caller can then treat the scan as clean and miss a new finding.

Check both the process status and report["errors"]. Preserve the valid Semgrep finding exit code for the configured command.

Suggested fix
-        findings: dict[Finding, list[int]] = {}
+        if result.returncode not in (0, 1):
+            return None
+        report = json.loads(result.stdout)
+        if report.get("errors"):
+            return None
+        findings: dict[Finding, list[int]] = {}
         sources: dict[str, list[str]] = {}
-        for item in json.loads(result.stdout)["results"]:
+        for item in report["results"]:
📝 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
def _semgrep_findings(semgrep: list[str], root: Path) -> dict[Finding, list[int]] | None:
"""Findings under *root*, each with the lines it starts on. None when the scan did not run.
The target is the directory and not the files in it. Semgrep applies its default
ignore list (``tests/``, ``node_modules/``, minified files) to a directory it walks,
which is how CI scans, and skips that list for a file named on the command line.
"""
try:
result = subprocess.run(
[
*semgrep,
"--config",
str(REPO_ROOT / SEMGREP_RULES),
"--severity=WARNING",
"--severity=ERROR",
"--metrics=off",
"--quiet",
"--json",
".",
],
cwd=root,
env={**os.environ, "SEMGREP_ENABLE_VERSION_CHECK": "false"},
capture_output=True,
text=True,
timeout=_SEMGREP_TIMEOUT_SECONDS,
)
findings: dict[Finding, list[int]] = {}
sources: dict[str, list[str]] = {}
for item in json.loads(result.stdout)["results"]:
path = item["path"]
if path not in sources:
sources[path] = (root / path).read_text(errors="replace").splitlines()
start, end = item["start"]["line"], item["end"]["line"]
matched = "\n".join(line.strip() for line in sources[path][start - 1 : end])
# The id prefix encodes the rule file's path relative to the working directory.
# The last segment is the rule's own id.
rule = item["check_id"].rsplit(".", 1)[-1]
findings.setdefault((rule, path, matched), []).append(start)
except (OSError, subprocess.TimeoutExpired, ValueError, KeyError, TypeError):
# A missing uvx download, a crash and a timeout all end here, because none of them prints a report.
return None
return findings
def _semgrep_findings(semgrep: list[str], root: Path) -> dict[Finding, list[int]] | None:
"""Findings under *root*, each with the lines it starts on. None when the scan did not run.
The target is the directory and not the files in it. Semgrep applies its default
ignore list (``tests/``, ``node_modules/``, minified files) to a directory it walks,
which is how CI scans, and skips that list for a file named on the command line.
"""
try:
result = subprocess.run(
[
*semgrep,
"--config",
str(REPO_ROOT / SEMGREP_RULES),
"--severity=WARNING",
"--severity=ERROR",
"--metrics=off",
"--quiet",
"--json",
".",
],
cwd=root,
env={**os.environ, "SEMGREP_ENABLE_VERSION_CHECK": "false"},
capture_output=True,
text=True,
timeout=_SEMGREP_TIMEOUT_SECONDS,
)
if result.returncode not in (0, 1):
return None
report = json.loads(result.stdout)
if report.get("errors"):
return None
findings: dict[Finding, list[int]] = {}
sources: dict[str, list[str]] = {}
for item in report["results"]:
path = item["path"]
if path not in sources:
sources[path] = (root / path).read_text(errors="replace").splitlines()
start, end = item["start"]["line"], item["end"]["line"]
matched = "\n".join(line.strip() for line in sources[path][start - 1 : end])
# The id prefix encodes the rule file's path relative to the working directory.
# The last segment is the rule's own id.
rule = item["check_id"].rsplit(".", 1)[-1]
findings.setdefault((rule, path, matched), []).append(start)
except (OSError, subprocess.TimeoutExpired, ValueError, KeyError, TypeError):
# A missing uvx download, a crash and a timeout all end here, because none of them prints a report.
return None
return findings
🧰 Tools
🪛 ast-grep (0.45.3)

[error] 150-167: Command coming from incoming request
Context: subprocess.run(
[
*semgrep,
"--config",
str(REPO_ROOT / SEMGREP_RULES),
"--severity=WARNING",
"--severity=ERROR",
"--metrics=off",
"--quiet",
"--json",
".",
],
cwd=root,
env={**os.environ, "SEMGREP_ENABLE_VERSION_CHECK": "false"},
capture_output=True,
text=True,
timeout=_SEMGREP_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 03872caa-7a7b-4eb8-9ba2-ccfb532b6d3e

📥 Commits

Reviewing files that changed from the base of the PR and between 4f86c77 and 6c29787.

📒 Files selected for processing (3)
  • .agents/skills/running-ci-preflight/SKILL.md
  • tools/hogli-commands/hogli_commands/preflight_checks.py
  • tools/hogli-commands/hogli_commands/tests/test_preflight_checks.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

Comment thread tools/hogli-commands/hogli_commands/preflight_checks.py
@rnegron
rnegron force-pushed the raul/preflight-shift-left branch from 350caf9 to e740e67 Compare October 2, 2026 16:13
@rnegron
rnegron changed the base branch from master to raul/flox-semgrep October 2, 2026 16:13
@rnegron
rnegron added this pull request to stack #110974 October 2, 2026 16:13
@rnegron
rnegron marked this pull request as ready for review October 2, 2026 16:14
@parameterai

parameterai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Risk: No findings

This PR adds four diff-reading checks to the hogli ci:preflight dev CLI (frontend format, semgrep devex findings, dropped visual baselines, merge-queue lane) plus a new preflight_checks.py module that shells out to git, semgrep, and node. It is internal developer tooling with no production attack surface: all subprocess calls are list-form without a shell, untrusted inputs (git paths, semgrep/node JSON, YAML) are parsed safely (SafeLoader, exception guard that fails to "skipped"), and inputs come from the author's own branch. No security risks found.

Sentinel reviewed e740e67 · Review settings

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team October 2, 2026 16:15

This branch has not been deployed

No deployments
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.

2 participants