Skip to content

Commit a70313e

Browse files
Leiyksbwoebi
andauthored
Restructure & document the shared Claude config (project/ code-guide) (#4035)
* Consolidate Claude config: shared vs personal split + project.md Add a portable, repo-side convention so each developer can keep personal Claude config alongside the shared team config without committing it, and fold project-level knowledge into the committed common config. - .claude/.gitignore: ignore per-developer paths (personal/, commands/, scripts/, skills/omc-reference/, settings.local.json, worktrees/, .omc/) in place; keep personal/README.md committed. Previously this relied on each developer's global gitignore, which isn't portable and silently hid new shared files. - .claude/personal/: git-ignored drop zone for per-developer config; CLAUDE.md optionally imports personal/CLAUDE.md if present. - .claude/project.md: project knowledge (overview, layout, architecture, INI, conventions) in the terse house style, cross-referencing the existing build/CI/debug docs instead of duplicating them. Reflects the ext/ -> tracer/ split (#3912); versions reference VERSION / Cargo.toml / rust-toolchain.toml rather than being pinned inline. - CLAUDE.md: link general.md, project.md, ci/index.md and import personal. * Auto-load common config via @import (general.md, project.md) Plain markdown links in CLAUDE.md are not injected into context by Claude Code — only @-imports are. Import general.md and project.md so the shared operating rules and project knowledge are always loaded alongside each developer's personal config. ci/index.md stays a link (large on-demand CI reference). * Restructure shared config: scripts/ and debugging/ folders; shareable commands Addresses PR review feedback: - .claude/.gitignore: commands/ is now personal-by-default via `/commands/*` (instead of ignoring the whole dir), so the team CAN commit shared commands by un-ignoring them while personal ones stay ignored. Claude Code discovers commands by filesystem regardless of git status, so both coexist. - Scripts: shared helper scripts moved into .claude/scripts/ (dd_php_release_url, find_map_region.py, parse_ucontext.py); personal scripts go in .claude/scripts/local/ (git-ignored). crash-analysis skill refs updated. - Debugging docs grouped under .claude/debugging/ with an index.md (like ci/): gdb.md, appsec-integration.md (was debugging.md), system-tests.md (was debugging-system-tests.md). All inbound/outbound links updated. Note: ci/ keeps its own co-located tooling (dockerh, check-ci, ...) since those are referenced throughout the ci/ docs as a self-contained module. * Keep personal skill ignores out of shared gitignore Personal skills live in .claude/skills/ alongside shared ones (Claude Code discovers by filesystem, ignoring git status). Their dir name is the skill name, so there's no generic pattern; document that each dev ignores their own personal skill dirs in .git/info/exclude (per-clone) instead of publishing personal skill names into the committed shared config. * Drop in-repo personal mechanism; personal config lives in ~/.claude Personal Claude config now lives entirely in ~/.claude (global), so the repo's .claude/ is purely shared team config: - Remove .claude/personal/ (README) and the @.claude/personal/CLAUDE.md import from CLAUDE.md. - Slim .claude/.gitignore to just local/runtime artifacts (settings.local.json, worktrees/, .omc/); document that personal config goes in ~/.claude. * Remove .claude/.gitignore; personal tool artifacts ignored globally The repo .claude/ carries no ignore file now — it's purely shared config. Local/runtime artifacts are handled outside the shared config: settings.local.json via the repo root .gitignore, and per-developer tooling output (OMC .omc/, Claude Code agent worktrees/checkpoints/etc.) via each dev's own global ~/.gitignore. * Expand project.md into a project/ code-guide folder Turn the single project.md into a ci/-style folder: a concise always-imported index.md (overview, layout, subsystem map, architecture summary, config, conventions, pointers) plus one terse orientation file per subsystem — ext, tracer, userland (src/), components (+components-rs/ZAI), sidecar, appsec, profiling. Each covers what it is / key files & dirs / how it fits / gotchas, cross-linked, with build/CI/debug content linked out (not duplicated). Produced via an analyze→write→review pass over the actual source: per-subsystem code analysis, drafting, then a source-accuracy + house-style review. Versions are referenced by source-of-truth (VERSION, Cargo.toml, rust-toolchain.toml); no hardcoded versions or source line numbers. CLAUDE.md now imports project/index.md. * project/: add data-flow to tracer/ext/sidecar; link architecture & CONTRIBUTING Targeted depth-increase (on-demand area files only; index stays lean): - tracer.md: ordered span→upload data-flow (RINIT → headers → span create → uhook → close → sampling → serializer → auto_flush → sidecar/coms → agent). - ext.md: fix the incorrect "MSHUTDOWN tears down in reverse" claim — it isn't (sidecar shuts down late, after config is freed); tighten MINIT/RINIT/RSHUTDOWN ordering (logging-first, zend_extension registration, dd_rinit_once). - sidecar.md: submit→upload flow (serialize → send_traces_to_sidecar → IPC → TraceFlusher batches ~5s/~1MB → agent upload; fork preserves session ID). - index.md: link architecture.md (background sender / components / version code) and CONTRIBUTING.md (setup/testing) from Pointers. Produced via the OMC analyze→write→review pipeline over live source; verified accurate (flush thresholds, ordering, function anchors). No line numbers. * project/: address Codex review — fix 4 doc inaccuracies All four verified against live source: - appsec.md: C++ helper client/runner/engine/service are .cpp/.hpp files, not dirs (only network/, remote_config/, subscriber/ are dirs). - appsec.md: the helper is enabled via the sidecar — ext/sidecar.c calls the appsec module's dd_appsec_maybe_enable_helper -> ddog_sidecar_enable_appsec (resolved Rust/C++ path) before ddappsec's first RINIT — not spawned by ddappsec MINIT/RINIT. - tracer.md + index.md: the sidecar sender is only the default on PHP 8.3+ / Windows (DD_SIDECAR_TRACE_SENDER_DEFAULT); on PHP 7.0-8.2 the in-process coms.c sender is the default. - components.md: drop container_id from the component list — it's a stale build-artifact dir with no source; container ID is in the Rust bridge (ddtrace_get_container_id). * project/: address review — drop FFI-surface note; fix cbindgen regen trigger Per @bwoebi: the "keep FFI surface small" line isn't actionable (dropped), and cbindgen headers regenerate when any components-rs/libdatadog source changes, not just lib.rs. * Update .claude/project/tracer.md Co-authored-by: Bob Weinand <bob.weinand@datadoghq.com> * feat(ci): add --jobs named-subset filter to check-ci Add an opt-in --jobs "pat1,pat2" option (comma-separated, case-insensitive substring patterns) to the check-ci monitor. When set, blocking-wait completion and failure/timeout accounting consider only jobs whose name matches any pattern, so the monitor finishes as soon as the matched subset is terminal instead of waiting for the whole pipeline. --list-jobs shows only matched jobs. Applies to both GitLab jobs and GitHub Actions jobs. Default (no --jobs) keeps whole-pipeline monitoring unchanged. * docs(claude): add CI flakiness & debugging gotchas to shared config --------- Co-authored-by: Bob Weinand <bob.weinand@datadoghq.com>
1 parent 024fe4a commit a70313e

25 files changed

Lines changed: 716 additions & 18 deletions

.claude/ci/appsec-gradle-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ If you need to inspect sidecar/helper or PHP issues:
278278
(`pref -f dd-ipc-helper`) or to PHP (usually an apache or an FPM worker -- if
279279
you're investigating code run during processes it will not be the master
280280
process). sidecar requires as a first command `file /proc/<pid>/exe`).
281-
* See [gdb.md](../gdb.md) for more information on how to run gdb. Always read
281+
* See [gdb.md](../debugging/gdb.md) for more information on how to run gdb. Always read
282282
this file before attempting to use gdb.
283283

284284
## Gotchas

.claude/ci/building-locally.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ This is needed for `build-appsec.sh` on centos-7 but not on bookworm
6969
the package pipeline's two-phase build). For local testing, always use
7070
`make` unless you specifically need the split build.
7171

72+
### Linting the libdatadog submodule (fmt + clippy)
73+
74+
To validate Rust changes in the `libdatadog/` submodule, run from inside it:
75+
76+
```bash
77+
cd libdatadog
78+
cargo +nightly fmt --all --quiet && \
79+
cargo clippy --workspace --all-targets --all-features -- -D warnings
80+
```
81+
82+
A rustc ≥1.87 toolchain override is active in the repo, so plain `cargo clippy`
83+
picks up the correct toolchain — do NOT force `+stable` (the default stable is
84+
older) or a pinned `+1.87.0`, and do not lint per-crate with `-p`. Only `fmt`
85+
needs `+nightly`.
86+
7287
## Tracer Extension (ddtrace.so)
7388

7489
### For test jobs (bookworm, debug build)

.claude/ci/check-ci

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,25 @@ def parse_args():
4646
parser.add_argument("--max-failures", type=int, default=50, help="Stop after this many failures (default: 50)")
4747
parser.add_argument("--timeout", type=int, default=7200, help="Overall timeout in seconds (default: 7200 = 2 hours)")
4848
parser.add_argument("--list-jobs", action="store_true", help="List all jobs grouped by pipeline and exit")
49+
parser.add_argument("--jobs", type=str, default=None, help="Comma-separated substring patterns; monitor only matching jobs (case-insensitive)")
4950
return parser.parse_args()
5051

5152

53+
def parse_job_patterns(spec: str | None) -> list[str]:
54+
"""Split a comma-separated --jobs spec into lowercased substring patterns."""
55+
if not spec:
56+
return []
57+
return [p.strip().lower() for p in spec.split(",") if p.strip()]
58+
59+
60+
def job_name_matches(name: str, patterns: list[str]) -> bool:
61+
"""Return True if name contains any pattern (case-insensitive substring). Empty patterns match all."""
62+
if not patterns:
63+
return True
64+
lname = name.lower()
65+
return any(p in lname for p in patterns)
66+
67+
5268
def resolve_sha(ref: str) -> str:
5369
result = subprocess.run(["git", "rev-parse", ref], capture_output=True, text=True)
5470
if result.returncode != 0:
@@ -383,8 +399,9 @@ async def gh_download_job_log(session: aiohttp.ClientSession, job_id: int, log_d
383399
# list-jobs mode
384400
# ---------------------------------------------------------------------------
385401

386-
async def list_jobs(session: aiohttp.ClientSession, root_id: int):
387-
"""List all GitLab jobs grouped by pipeline, then exit."""
402+
async def list_jobs(session: aiohttp.ClientSession, root_id: int, patterns: list[str] | None = None):
403+
"""List all GitLab jobs grouped by pipeline, then exit. Filtered to matched jobs when patterns given."""
404+
patterns = patterns or []
388405
pipeline_ids, pipeline_names, jobs_by_pipeline, pipeline_statuses = await discover_pipelines_and_jobs(session, root_id)
389406

390407
for pid in pipeline_ids:
@@ -394,6 +411,10 @@ async def list_jobs(session: aiohttp.ClientSession, root_id: int):
394411
info = pipeline_statuses.get(pid, {"status": "unknown", "name": str(pid)})
395412
display_name = pipeline_names.get(pid, info["name"])
396413
jobs = jobs_by_pipeline.get(pid, [])
414+
if patterns:
415+
jobs = [j for j in jobs if job_name_matches(j.get("name", ""), patterns)]
416+
if not jobs:
417+
continue
397418
print(f"\nPipeline {pid} '{display_name}' (status: {info['status']}):")
398419
for job in jobs:
399420
job_status = job.get("status", "unknown")
@@ -402,8 +423,9 @@ async def list_jobs(session: aiohttp.ClientSession, root_id: int):
402423
print(f" {job_status:<10}({job_id}) {job_name}")
403424

404425

405-
async def list_github_jobs(session: aiohttp.ClientSession, sha: str):
406-
"""List GitHub Actions workflow runs and their jobs for a SHA."""
426+
async def list_github_jobs(session: aiohttp.ClientSession, sha: str, patterns: list[str] | None = None):
427+
"""List GitHub Actions workflow runs and their jobs for a SHA. Filtered to matched jobs when patterns given."""
428+
patterns = patterns or []
407429
try:
408430
runs = await gh_discover_runs(session, sha, timeout=15)
409431
except GitHubAuthError:
@@ -420,6 +442,10 @@ async def list_github_jobs(session: aiohttp.ClientSession, sha: str):
420442
run_conclusion = run.get("conclusion") or ""
421443
status_str = run_status if not run_conclusion else f"{run_status}, conclusion: {run_conclusion}"
422444
jobs = await gh_get_run_jobs(session, run_id)
445+
if patterns:
446+
jobs = [j for j in jobs if job_name_matches(j.get("name", ""), patterns)]
447+
if not jobs:
448+
continue
423449
print(f"\nGitHub Actions run {run_id} '{run_name}' (status: {status_str}):")
424450
for job in sorted(jobs, key=lambda j: j.get("name", "")):
425451
job_status = job.get("status", "unknown")
@@ -493,10 +519,14 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
493519
else:
494520
root_id = await discover_pipeline(gl_session, sha, args.discovery_timeout)
495521

522+
patterns = parse_job_patterns(args.jobs)
523+
if patterns:
524+
print(f"Filtering to jobs matching (any of): {patterns}")
525+
496526
if args.list_jobs:
497-
await list_jobs(gl_session, root_id)
527+
await list_jobs(gl_session, root_id, patterns)
498528
if gh_session and sha:
499-
await list_github_jobs(gh_session, sha)
529+
await list_github_jobs(gh_session, sha, patterns)
500530
return
501531

502532
# Set up working directory
@@ -557,6 +587,8 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
557587

558588
pipeline_ids, _, jobs_by_pipeline, pipeline_statuses = gl_result
559589
all_jobs = [job for jobs in jobs_by_pipeline.values() for job in jobs]
590+
if patterns:
591+
all_jobs = [job for job in all_jobs if job_name_matches(job.get("name", ""), patterns)]
560592

561593
# Phase 2: fetch GH run-jobs while we can (no GL dependency); process GL jobs locally.
562594
if gh_monitoring:
@@ -609,6 +641,7 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
609641
gh_passed = 0
610642
gh_failed_count = 0
611643
gh_all_done = True
644+
gh_matched_total = 0
612645
gh_new_failures: list[tuple[dict, dict]] = []
613646
gh_fail_log_dir = wdir / "gh_fail_logs"
614647

@@ -618,6 +651,9 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
618651
if run.get("status", "unknown") != "completed":
619652
gh_all_done = False
620653
for job in run_jobs:
654+
if patterns and not job_name_matches(job.get("name", ""), patterns):
655+
continue
656+
gh_matched_total += 1
621657
jid = job["id"]
622658
job_status = job.get("status", "unknown")
623659
conclusion = job.get("conclusion") or ""
@@ -695,7 +731,15 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
695731
print(f"[{now}] pipelines={len(pipeline_ids)} jobs={len(all_jobs)} running={gl_running_count} passed={passed} failed={failed} allow_fail={allow_fail}")
696732

697733
# Check completion
698-
all_done = all_gl_done and (gl_running_count == 0) and (not gh_monitoring or gh_all_done)
734+
if patterns:
735+
# Finish as soon as the MATCHED subset is terminal (no matched job still
736+
# running), regardless of the rest of the pipeline. Require at least one
737+
# matched job to exist so we keep polling while jobs are still being created.
738+
matched_total = len(all_jobs) + gh_matched_total
739+
matched_running = gl_running_count + gh_running
740+
all_done = matched_total > 0 and matched_running == 0
741+
else:
742+
all_done = all_gl_done and (gl_running_count == 0) and (not gh_monitoring or gh_all_done)
699743

700744
if all_done:
701745
total_failures = failure_count

.claude/ci/diagnosing-failures.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Diagnosing CI Failures & Flakiness
2+
3+
Transferable triage techniques to apply before proposing a fix for a failing or
4+
flaky CI job. For reproducing a specific job locally, see the group docs linked
5+
from [index.md](index.md).
6+
7+
## Determine the failure mode first: hang vs test failure
8+
9+
Before blaming any FAIL/diff line in the log, check the job **duration**. A
10+
duration equal to the job timeout (e.g. `windows test_c` dies at ~3601s = the
11+
1 h timeout) means the job died from a **hang/timeout**, not from the assertion
12+
failures printed earlier. Those FAIL lines may be real, but are separate,
13+
non-fatal issues.
14+
15+
- Grep the tail for a hang signature (`execution took longer than`,
16+
`Failed to terminate process`) vs an assertion `FAIL`/diff (a test bug).
17+
- **`run-tests.php -g FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP` suppresses PASS
18+
output.** So the last log line is NOT necessarily the hang point — it is just
19+
the last *reported* (failed/skipped) test before a run of silently-passing
20+
ones. To find the real hang point, add `-v` (sets `$DETAILED`, printing
21+
`TEST <path>` before each test): the last `TEST` line that never gets a
22+
result is the culprit. Confirm determinism against a second matrix variant.
23+
- Timed-out jobs skip `after_script`, so they upload only `job.log` (no
24+
`sidecar.log`/dumps). A passing sibling version is both the comparison
25+
baseline and the source of the artifacts the failing job never emitted.
26+
27+
## All-versions deterministic flip with no source change = mutable image
28+
29+
If a test flips green→red **deterministically across every PHP version/SAPI**
30+
with no corresponding source/commit change, suspect a **mutable CI image tag
31+
being republished** under the same tag (the `bookworm-{N}` images are rebuilt
32+
in place with `docker buildx bake --no-cache --pull`), not a code regression.
33+
Verify the mechanism empirically before fixing:
34+
35+
- Check that pipelines on the *same* sha previously passed — if so, the trigger
36+
is the image republish, not the commit.
37+
- Diff green-vs-red job **artifacts** (`[ddtrace]` logs, sidecar data) rather
38+
than theorizing about log levels / worker recycling.
39+
- Watch for env vars already forced by the Makefile test target (e.g.
40+
`run_tests_debug` forces `DD_TRACE_DEBUG=1`), so a "config" fix isn't a silent
41+
no-op.
42+
43+
A slower rebuilt image commonly loses a pre-existing **retrieval-window race**:
44+
e.g. a `/telemetry` poll that stops as soon as it sees `spans_created` can miss
45+
the later batch carrying `logs_created`. Fix the race (accumulate the awaited
46+
metrics across requests); don't relax the assertion.
47+
48+
## Retry-trait cascade masks the real failure
49+
50+
Tests using the persistent-server retry trait
51+
(`tests/Common/RetryTraitVersionGeneric.php`) re-run the whole test against an
52+
already-warmed server on failure, and **only the last attempt's exception is
53+
reported**. The reported failure can therefore be a downstream symptom of the
54+
first attempt's real break:
55+
56+
- One-shot-per-server-lifecycle metrics (e.g. `logs_created`, emitted only on
57+
the first telemetry lifecycle of a server process; zero-valued metrics aren't
58+
sent) fail on every *warmed* retry — so a reported first-window `logs_created`
59+
failure is often really a later-window break cascading through the retry.
60+
- To see the true cause, temporarily `fwrite(STDERR, ...)` the swallowed
61+
exception in the trait's `catch` block.
62+
- Hard-coded dependency-version assertions on `^x.y` fixtures break silently
63+
when a fresh `composer update` resolves a new upstream patch release; verify
64+
what actually resolves in the container with `composer show <pkg>`.
65+
66+
## Root-cause sidecar error logs — don't suppress them
67+
68+
Recurring sidecar error logs (`Failed synchronously flushing traces:
69+
Kind(TimedOut)`, `connection reset by peer (os error 104)`, `The sidecar
70+
transport is closed. Reconnecting...`) are **symptoms** of a real flush-timeout
71+
/ transport-reset problem. Do NOT green a flaky test by silencing them
72+
(`DD_TRACE_LOG_LEVEL=off`, per-target `datadog_ipc=off,datadog_sidecar=off`, or
73+
grep-ignoring the line). Investigate why the flush times out / the IPC transport
74+
resets and fix that root cause; silencing leaves the defect to resurface
75+
elsewhere.
76+
77+
## Merge gate: no `allow_failure` for known-flaky jobs
78+
79+
Known-flaky jobs are NOT hidden with `allow_failure`. A single `merge-gate` job
80+
(last stage, `when: always`, no skip rules) runs `.gitlab/merge-gate.sh`, which
81+
collects every failed job across the parent + child pipelines via the GitLab API
82+
(`/pipelines/:id/bridges` → downstream ids → `/pipelines/:id/jobs?scope[]=failed`)
83+
and matches each against glob patterns in `.gitlab/flaky-jobs.txt`. A failure
84+
matching no pattern fails the gate; matches are treated as known-flaky and
85+
ignored. Branch protection requires only the single `merge-gate` check.
86+
87+
To mark a job flaky, add its glob to `.gitlab/flaky-jobs.txt` (matrix jobs reduce
88+
to `base:*`, so any failing version marks all versions flaky; non-matrix jobs
89+
stay exact). Never reach for `allow_failure`.

.claude/ci/index.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ tooling/bin/download-artifacts --job-id 98765 -o /tmp/artifacts
229229

230230
---
231231

232+
## Diagnosing failures & flakiness
233+
234+
**[diagnosing-failures.md](diagnosing-failures.md)**
235+
Triage techniques to apply before proposing a fix: distinguishing a hang from a
236+
test failure (duration + `-v`), spotting a mutable-image republish behind an
237+
all-versions flip, seeing through retry-trait cascades, root-causing sidecar
238+
error logs instead of silencing them, and the `merge-gate` / `flaky-jobs.txt`
239+
mechanism (no `allow_failure`).
240+
241+
---
242+
232243
## Building artifacts locally
233244

234245
**[building-locally.md](building-locally.md)**

.claude/ci/system-tests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ cd /path/to/system-tests
300300
architecture in the filename. CI always sets this variable.
301301
Alternatively, build an arm64 `.so` with `make` (not `make static`)
302302
and use the `.so` override path — see
303-
[../debugging-system-tests.md](../debugging-system-tests.md).
303+
[../debugging/system-tests.md](../debugging/system-tests.md).
304304

305305
- Artifacts are collected from `system-tests/logs_parametric/` and
306306
`system-tests/logs/` -- these directories are always uploaded

.claude/ci/windows-tests.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,14 @@ docker stop -t 5 $CONTAINER && docker rm -f $CONTAINER
8686

8787
- **Windows `switch-php` is a PowerShell script**, not the Linux bash version —
8888
it configures `C:\php\` rather than symlinking into `/usr/local/bin/`.
89+
90+
- **CGI-SAPI `.phpt` tests can hang the whole job on PHP 7.2/7.3.** CGI tests
91+
(`--GET--`/`--POST--`) run their SKIPIF/CLEAN under `php-cgi`, not the CLI. On
92+
PHP 7.2/7.3, a ddtrace log line emitted during a CGI request blocks forever on
93+
a synchronous buffered stderr write, because the Windows CGI stderr pipe is
94+
never drained ("On Windows cgi skips stderr output"). Only tests that enable
95+
ddtrace logging (e.g. raising `datadog.trace.log_level` above the default
96+
`error`) hit it; the job then dies at the full timeout instead of a FAIL.
97+
7.4+ are unaffected. When `windows test_c` fails on 7.2/7.3, check duration
98+
before the FAIL lines — see
99+
[diagnosing-failures.md](diagnosing-failures.md#determine-the-failure-mode-first-hang-vs-test-failure).
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The appsec Gradle integration tests run inside Docker containers. Use
88
attach gdb to processes inside the container (PHP-FPM workers, sidecar).
99

1010
See [gdb.md](gdb.md) for gdb-specific instructions.
11-
See [ci/appsec-gradle-integration.md](ci/appsec-gradle-integration.md) for
11+
See [ci/appsec-gradle-integration.md](../ci/appsec-gradle-integration.md) for
1212
Gradle test details.
1313

1414
## Workflow
File renamed without changes.

.claude/debugging/index.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Debugging guides
2+
3+
How to debug this project's components locally. Each file covers one scenario.
4+
5+
| Guide | Covers |
6+
|---|---|
7+
| [gdb.md](gdb.md) | gdb-via-tmux fundamentals: attaching, scripting, watchpoints, reading optimized-out vars, C-vs-Rust language mode, attaching to the sidecar. Start here for any native (C/Rust) debugging. |
8+
| [appsec-integration.md](appsec-integration.md) | Debugging the appsec Gradle integration tests: driving containers with `jdb` (`--debug-jvm`) + gdb, breakpoint strategy, keeping the container alive, the sidecar watchdog. |
9+
| [system-tests.md](system-tests.md) | Debugging system tests locally (arm64): pytest `--pdb` + gdb inside the weblog container. |
10+
11+
Related: for *building* the binaries you debug, see
12+
[../ci/building-locally.md](../ci/building-locally.md); for reproducing a
13+
specific CI job, see [../ci/index.md](../ci/index.md).

0 commit comments

Comments
 (0)