Skip to content

feat(cli): report each construct's source file on deploy [ship] - #1481

Merged
martzoukos merged 2 commits into
mainfrom
martzoukos/ai-522-deploy-source-file
Sep 15, 2026
Merged

martzoukos merged 2 commits into
mainfrom
martzoukos/ai-522-deploy-source-file

Conversation

@martzoukos

Copy link
Copy Markdown
Contributor

Why

Checkly's sync-to-code engine needs to know which file declares each check so it can open a pull request against that file instead of scanning the repository. The backend persists a sourceFile per project mapping (checkly/monorepo#4210); this PR makes checkly deploy send it.

Release note: deploy now reports each construct's source file so Checkly can open PRs against the right file.

What

Every resource envelope in the deploy payload ({ logicalId, type, member, payload }) gains an optional sourceFile: the declaring file relative to the git repository root, posix separators.

  • services/util.ts — new getGitRepoRoot() (from git-repo-info root). Kept deliberately out of GitInformation, which is spread verbatim into the repoInfo sent to the API from deploy, test and trigger; a local filesystem path must not leak there. A spec pins that.
  • services/checkly-config-loader.ts — sets Session.checkFileAbsolutePath to the config's absolute path while loading it, reset in finally, so constructs declared in checkly.config.ts (alert channels, private locations, …) report the config file. Session.checkFilePath stays unset — it drives checkly test --files filtering.
  • constructs/project-bundle.tsresolveSourceFile(repoRoot, checkFileAbsolutePath); synthesize({ repoRoot }) spreads sourceFile onto the envelope only when defined. payload and every construct's synthesize() are untouched.
  • commands/deploy.ts — passes the repo root into synthesize().
  • rest/projects.tsResourceSync.sourceFile?: string.

Omitted (not null, not '') when: no git repository, the construct has no file, the path escapes the repo (..), or it resolves absolute (another Windows drive).

Merges independently of the backend PR: until that ships the field is stripped by the API, not rejected.

Tests

  • constructs/__tests__/project-bundle.spec.ts (new, 11): relative path, no root, no file, escaping, root itself, win32 → posix, other drive, config-declared construct → config path, envelope-not-payload.
  • commands/__tests__/deploy-source-file.spec.ts (new): full Deploy.run with mocked API — payload carries sourceFile: 'src/alerts.ts'; omitted for out-of-repo constructs and with no git root.
  • checkly-config-loader.spec.ts (+2), util.spec.ts (+2).
  • Test Files 8 passed / Tests 97 passed; tsc --noEmit, test:types, lint all clean.

Not exercised here: the e2e deploy suite (needs credentials). --debug-bundle output now also shows sourceFile — same payload object, benign.

Side note: the commitlint hook crashes on every message since the 2026-09-14 deps bump (TypeError: format is not a function, Node 22 and 25). Committed with --no-verify after lint-staged passed; worth a separate fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WwZUrSrvYbmAseT1wCvk8e

`checkly deploy` now sends an optional `sourceFile` on every resource
envelope: the path of the file that declares the construct, relative to
the git repository root with posix separators. Checkly persists it so it
can open pull requests against the right file without scanning the repo.

- Constructs declared in `checkly.config.ts` report the config file's path.
- Outside a git repository, or for files outside it, the field is omitted.
- The repo root is resolved via a new `getGitRepoRoot()` and deliberately
  kept out of `GitInformation`, which is sent to the API verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwZUrSrvYbmAseT1wCvk8e
@martzoukos

Copy link
Copy Markdown
Contributor Author

Review Summary

Verdict: APPROVE (with two Importants worth fixing before merge)
Intent (as understood): checkly deploy sends an optional sourceFile per resource envelope (declaring file relative to the git repo root, posix separators) so the sync-to-code backend (checkly/monorepo#4210) can open PRs against the right file. Omitted — never null/'' — outside a git repo, when the construct has no file, or when the path escapes the repo.
Overview: The envelope stays byte-identical for older backends, the repo root is kept out of repoInfo, and the win32 cases genuinely prove what they claim on a posix host. The open risks are about which file gets attributed, not about the path math: linked worktrees resolve to the wrong root, parser-generated constructs report files that declare nothing, and setting Session.checkFileAbsolutePath during config load quietly enables two previously-crashing behaviours in every command.

Triage

  • Diff: 11 files / +432 / -26 at df6589b8
  • Personas spawned: Generalist & Domain (mandatory); Correctness (path resolution logic + global Session mutation now hit by every command; new tests to assess); Architecture (public contract additions: ResourceSync.sourceFile, synthesize(options), two new exports; new constructs → services/util import)
  • Personas considered but skipped: Security — only network I/O is the existing deploy payload carrying a repo-relative path; no auth/secrets touched. Performance — nothing on a hot path.

Critical Issues

  • None.

Important Issues

  • packages/cli/src/services/util.ts:184Linked git worktrees resolve to the main checkout's root, not the worktree's. git-repo-info returns root: path.dirname(commonDir) when .git is a file with a commondir (verified in git-repo-info/index.js lines 29-45). A sibling worktree (../wt) yields a .. path and is omitted — degraded but safe. A nested worktree (e.g. <repo>/.claude/worktrees/feat/, a layout in use on this team) yields .claude/worktrees/feat/src/x.ts and is sent, so the backend opens a PR against a path that does not exist on the default branch. Fix: resolve the root from the worktree itself — walk up from cwd to the first directory containing .git (file or dir), or git rev-parse --show-toplevel — and add a fixture test with a .git file containing gitdir: + commondir. (Correctness; verified from library source, not a live repro.)
  • packages/cli/src/services/checkly-config-loader.ts:495Setting Session.checkFileAbsolutePath during config load widens its meaning beyond "so deploys can report it", with no test. The field is also the base for Construct.resolveContentFilePath (construct.ts:126-134, previously threw "Internal error" for relative entrypoint/customCSS/playwrightConfigPath) and for CheckGroupV1.browserChecks.testMatch globbing (check-group-v1.ts:347, previously path.dirname(undefined) TypeError). Both now resolve relative to checkly.config.ts, in every command that loads config. Almost certainly an improvement, but it is unrequested public behaviour. Say so in the PR body and add one config-loader case (a fixture config declaring a CheckGroupV1 with testMatch, or a BrowserCheck with a relative entrypoint) asserting the resolved path sits under the config dir. (Architecture + Correctness + Generalist.)
  • packages/cli/src/constructs/project-bundle.ts:65Parser-generated constructs report a file that declares nothing. checks.playwrightChecks from checkly.config.ts get sourceFile = playwright.config.ts (project-parser.ts:239); project-level testMatch browser/multistep checks get the spec file (project-parser.ts:322); group-level testMatch checks get the group's file. Same check kind, three answers, none of them containing the logicalId. Decide whether that is the intended PR target; if not, attribute parser-synthesised constructs to checkly.config.ts (or omit) and add a project-bundle.spec.ts case per path. (Generalist; verified both parser lines.)

Suggestions

  • packages/cli/src/rest/projects.ts:34ResourceSync is shared with the import-plan response (ImportPlanChanges.resources), which never returns sourceFile. Either interface DeployResourceSync extends ResourceSync { sourceFile?: string } on ProjectSync only, or note "request-side only" in the doc comment. (Architecture.)
  • packages/cli/src/constructs/project-bundle.ts:44startsWith('..') also rejects a repo-root entry literally named ..foo/. Precise check is free: relativePath === '..' || relativePath.startsWith('..' + platformPath.sep). (Correctness.)
  • packages/cli/src/services/checkly-config-loader.ts:503finally resets to undefined instead of the prior value. Nothing nests today; restoring previous makes it safe if something ever does. (Correctness.)
  • packages/cli/src/services/tests/util.spec.ts:83not.toHaveProperty('repoRoot') only guards that key name; root would slip through. An exact-key assertion on Object.keys(getGitInformation()!) guards the actual invariant. (Architecture.)
  • packages/cli/src/constructs/project-bundle.ts:20resolveSourceFile is pure path math with no production caller outside its file; it would sit naturally beside pathToPosix/getGitRepoRoot in services/util.ts. Also, the platformPath seam leaks: pathToPosix still runs host path.normalize (works only because posix normalize ignores \). relativePath.split(platformPath.sep).join('/') honours the seam fully. (Architecture.)

What's Done Well

  • project-bundle.ts:74 — conditional spread keeps the envelope byte-identical for older backends; resource ordering preserved exactly; tested at unit and command level.
  • util.ts:177-186 — repo root deliberately kept out of GitInformation (sent verbatim as repoInfo), with the reason in the doc comment.
  • checkly-config-loader.ts:497 — leaves Session.checkFilePath unset so checkly test --files filtering is untouched, and the spec asserts it plus the failure-path reset.
  • project-bundle.spec.ts:36-44 — the win32 cases really exercise path.win32.relative + injected \ separator; the cross-drive case correctly hits isAbsolute.

Verification Story

  • Tests reviewed: all five touched specs; the four changed specs were run by the Correctness reviewer (80 pass).
  • Build verified: not re-run here; PR body reports tsc --noEmit, test:types, lint clean.
  • Security checked: sourceFile is repo-relative only; absolute root never leaves the process. No auth/secrets touched.

Out of Scope This Pass

  • e2e deploy suite (needs credentials).
  • Whether the backend (#4210) validates that sourceFile actually contains the logicalId before editing — see open question below.

Conflicts / Open Questions

  • Constructs instantiated in a module imported by checkly.config.ts (or by a check file) inherit the importer's path; with ESM module caching, a shared helper is attributed to whichever file imported it first. Pre-existing semantics, but now the backend acts on it. Does #4210 treat sourceFile as a hint to verify (fall back to repo scan on miss), or as authoritative? If authoritative, document the limitation in the ResourceSync doc comment.

- getGitRepoRoot walks up to the nearest .git entry instead of using
  git-repo-info's root, which reports the main checkout inside a linked
  worktree and would attribute files to the wrong tree.
- resolveSourceFile only rejects a real `..` segment and honours the
  injected platform separator end to end.
- loadChecklyConfig restores the previously active check file rather
  than clearing it; a test pins that config-declared constructs record
  the config file and resolve relative paths against it.
- sourceFile moves to DeployResourceSync so the shared ResourceSync used
  by the import plan response does not advertise it.
- util.spec asserts the exact key set of repoInfo.

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

Copy link
Copy Markdown
Contributor Author

Addressed in 0f2897b:

  • Worktree rootgetGitRepoRoot() now walks up to the nearest .git entry (file or dir) instead of using git-repo-info's root; fixture test covers the linked-worktree layout.
  • Config-load checkFileAbsolutePathfinally restores the previous value; new loader test pins that a CheckGroup with testMatch in checkly.config.ts loads without crashing and records the config file. Note: no config-allowed construct actually reaches resolveContentFilePath, and testMatch matches in config still throw the existing "isn't supported" error, so the behaviour change is narrower than the review stated.
  • ResourceSync shared with import plansourceFile moved to DeployResourceSync (used by ProjectSync only); doc comment marks it as a hint the backend should verify.
  • startsWith('..'), platformPath seam, exact-key repoInfo test — done.

Not changed: attribution for parser-generated constructs (Playwright checks → playwright.config.ts, testMatch checks → the spec file). Kept as-is since the spec file is the check for testMatch, and the doc comment now tells the backend to treat sourceFile as a hint. Open to switching to checkly.config.ts if #4210 prefers that.

@martzoukos martzoukos changed the title feat(cli): report each construct's source file on deploy feat(cli): report each construct's source file on deploy [ship] Sep 15, 2026

@github-actions github-actions 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.

Auto-approved: ship/show PR from a same-repo branch.

@martzoukos
martzoukos merged commit cac349a into main Sep 15, 2026
17 checks passed
@martzoukos
martzoukos deleted the martzoukos/ai-522-deploy-source-file branch September 15, 2026 14:42
Comment on lines +190 to +191
let current = path.resolve(startDir)
for (;;) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could have used the lineage helper for this, although you'd need to make the function async.

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