Skip to content

Configure twin-server change-copy ignores - #162

Merged
rameziophobia merged 11 commits into
mainfrom
ramez/twin-server-copy-ignore
Aug 6, 2026
Merged

Configure twin-server change-copy ignores#162
rameziophobia merged 11 commits into
mainfrom
ramez/twin-server-copy-ignore

Conversation

@rameziophobia

@rameziophobia rameziophobia commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add safe default ignore rules for copying host changes into twin-server containers
  • keep audit-results/, compare-results/, and compare-bisect-results/ on the host by default
  • apply the rules to automatic sync and expose project-level configuration overrides
  • match live auto-sync overrides from the documented Git-root-relative path, including nested Docker build contexts
  • reject unknown copyIgnore keys so configuration typos fail validation
  • document copy-ignore behavior and cover defaults, overrides, and sync integration

History

This branch was rebased directly onto current main; it does not include the deferred demo bisect seed-history work from #161. The granular commits are preserved.

Validation

  • yarn workspace shaka-perf build
  • 7 affected twin-server suites: 114 tests passed
  • schema and config suites: 55 tests passed

Comment on lines +17 to +22
export const CopyIgnoreConfigSchema = z.object({
/** Repository-relative directory patterns excluded from change copying. */
folders: z.array(z.string().min(1)).optional(),
/** Repository-relative file patterns excluded from change copying. */
files: z.array(z.string().min(1)).optional(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CopyIgnoreConfigSchema doesn't call .strict(), unlike SetupCommandSchema (line 15) and TwinServersConfigSchema (line 65) in this same file. Zod's default object mode silently strips unrecognized keys instead of failing validation, so a typo like copyIgnore: { folder: [...] } (missing the trailing s) passes schema validation with the override silently dropped. resolveConfig (config.ts) then merges validConfig.copyIgnore (now effectively {}) over the packaged defaults, so the user's intended list is silently replaced by compare-results/compare-bisect-results with no error or warning anywhere. Recommend adding .strict() here to match this file's existing convention and surface typos.

Comment on lines +936 to +948
@@ -943,6 +945,7 @@ export async function runServersMenu(
let deleted = 0;
const errors: Error[] = [];
for (const rel of batch) {
if (isCopyIgnored(copyIgnore, rel)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

README-twin-servers.md documents copyIgnore patterns as "relative to the Git repository root," and that's what getChangedFiles does for sync-changes/copy-changes-to-ssh (both resolve sourceDir via getGitRootDirectory before filtering). Here, though, copyIgnore is matched (line 948) against rel, which comes from fs.watch(experimentBuildDir, ...) further down (~line 1035) — i.e. paths relative to the Docker build context (config.dockerBuildDir), not the git root.

When dockerBuildDir isn't the git root — which the twin-servers setup guide's own reference config achieves via dockerBuildDir: '..', and deeper monorepo nesting is explicitly supported — a slash-containing pattern like the README's own example (tmp/traces) gets anchored to a different base by this live auto-sync path than by the other two copy mechanisms, so identical config filters files inconsistently depending on which sync path runs. The shipped defaults (compare-results, compare-bisect-results) are unaffected since gitignore matches slash-less names at any depth, but this will silently misbehave for anyone adding a custom, path-qualified copyIgnore entry in a project whose build context isn't the repo root.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review summary

Reviewed the copyIgnore feature (default host-artifact ignores for twin-server change copying, plus project-level overrides). Overall the implementation is clean: the ignore package is reused consistently with the existing dockerignore/manifest matching in this codebase, the shallow-merge-over-defaults semantics in resolveConfig match the documented "each array replaces its default" behavior, and the git-based paths (sync-changes, copy-changes-to-ssh) correctly resolve to the git root before filtering. Left two inline comments on things worth fixing:

  1. CopyIgnoreConfigSchema missing .strict() (types.ts) — inconsistent with the other schemas in the file; typos in copyIgnore config keys are silently dropped rather than rejected, silently falling back to the packaged defaults.
  2. Base-directory mismatch for the live auto-sync path (commands/servers-menu.ts) — the README documents copyIgnore patterns as git-root-relative, and that holds for sync-changes/copy-changes-to-ssh, but the fs.watch-driven auto-sync in servers-menu.ts matches the same patterns against Docker-build-context-relative paths instead. Slash-qualified patterns (like the README's own tmp/traces example) can therefore behave inconsistently across the three copy mechanisms when dockerBuildDir isn't the git root.

No security concerns — this only affects local file-copy filtering driven by a trusted local config file, no untrusted/network input involved. Test coverage for resolveConfig and getChangedFiles is solid; the new filtering wired into servers-menu.ts's auto-sync has no dedicated test, though that matches the file's existing (untested) state.

@rameziophobia
rameziophobia marked this pull request as ready for review August 6, 2026 11:25
let deleted = 0;
const errors: Error[] = [];
for (const rel of batch) {
if (isCopyIgnored(copyIgnore, rel)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: copyIgnore is matched against the wrong root here, so multi-segment overrides silently no-op in live auto-sync.

rel comes from fs.watch(experimentBuildDir, …) (line 1035), so it's relative to dockerBuildDir (line 925: experimentBuildDir = dockerBuildDirForSide(config, 'experiment')). But README-twin-servers.md's new copy-ignore section, and both other call sites (sync-changes.ts, copy-changes-to-ssh.ts), define/resolve copyIgnore patterns as relative to the git repository rootsync-changes.ts explicitly does getGitRootDirectory(sideBuildDir) before calling getChangedFiles.

dockerBuildDir is not guaranteed to equal the git root — this PR's own commands.test.ts test "maps git-root paths into a nested build context" exercises exactly that divergence (dockerBuildDir nested under the repo root), and the sibling helpers/dockerignore.ts's findUnignoredShakaResultDirs already has to prefix patterns to compensate for this same gap.

Concretely: with a nested build dir, a user-configured copyIgnore.folders: ['packages/web/tmp/traces'] (git-root-relative, per the docs) would correctly exclude that folder for sync-changes/copy-changes-to-ssh, but silently fail to match here, because rel for that path would arrive as tmp/traces/... (no packages/web/ prefix). It only happens to work for the default patterns (compare-results, compare-bisect-results) because those are single-segment and gitignore-style patterns without a / match at any depth regardless of root — which is likely why no test caught this.

Worth resolving rel against the git root (or prefixing patterns the way findUnignoredShakaResultDirs does) before matching against copyIgnore here.

] as const;

/** Host-only files excluded from change copying unless config overrides them. */
export const DEFAULT_COPY_IGNORE_FILES: readonly string[] = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Per this repo's CLAUDE.md ("Breaking changes"): any change that can break an existing consumer's abtests.config.ts behavior — including "a changed default" — must get an Unreleased entry in BREAKING_CHANGES.md. Before this PR, getChangedFiles/sync-changes/copy-changes-to-ssh copied every git-changed file to the twin-server containers; after this PR, compare-results/ and compare-bisect-results/ are silently excluded by default, with no config change required from existing users. That's a new default behavior change (even if it's a strict improvement), so it looks like it should get a BREAKING_CHANGES.md Unreleased entry rather than being introduced silently.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review summary

Solid feature overall — the copyIgnore config, helpers/copy-ignore.ts matcher, and test coverage for resolveConfig/getChangedFiles are clean and follow existing conventions (reusing the already-present ignore package rather than adding a dependency).

Left two inline comments:

  1. Correctness bug in servers-menu.ts's live auto-sync loop: copyIgnore patterns are documented (and correctly resolved in sync-changes.ts/copy-changes-to-ssh.ts) as relative to the git repository root, but the auto-sync loop matches them against paths relative to dockerBuildDir instead. These two roots can diverge (a scenario this PR's own commands.test.ts exercises for sync-changes), so any multi-segment copyIgnore override would silently fail to apply during live auto-sync even though it works for manual sync/SSH copy. Only the single-segment defaults happen to be root-agnostic, which is likely why this wasn't caught by tests.
  2. Process: per this repo's CLAUDE.md, a changed default for abtests.config.ts-driven behavior should get a BREAKING_CHANGES.md Unreleased entry. This PR silently changes what gets copied to twin-server containers by default (previously everything; now compare-results//compare-bisect-results/ excluded) — worth a short entry even though it's a strict improvement.

No security or other correctness issues found; the ignore-based matching, config merge semantics (override replaces the corresponding default list, per the added tests), and docs are otherwise consistent.

Comment on lines +939 to +948
@@ -943,6 +945,7 @@ export async function runServersMenu(
let deleted = 0;
const errors: Error[] = [];
for (const rel of batch) {
if (isCopyIgnored(copyIgnore, rel)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: copyIgnore is matched against the wrong path base here, breaking anchored (slash-containing) override patterns in the auto-sync path.

rel comes from fs.watch(experimentBuildDir, ...) (line 1035-1037), so it's relative to the Docker build context (dockerBuildDirForSide). But config.copyIgnore is documented — and used everywhere else — as relative to the Git repository root:

  • sync-changes.ts filters getChangedFiles(sourceDir, config.copyIgnore) where sourceDir = getGitRootDirectory(sideBuildDir).
  • copy-changes-to-ssh.ts does the same via getGitRootDirectory(config.dockerBuildDir).
  • README-twin-servers.md explicitly says: "Manual sync, running-menu auto-sync, and copy-changes-to-ssh always leave audit-results/... Paths use gitignore pattern syntax and are relative to the Git repository root."

sync-changes.ts even has a comment acknowledging the two bases differ: "Git reports paths from the repository root, while the bind mount and build manifest are relative to the Docker build context."

For the default unslashed folder names (audit-results, compare-results, compare-bisect-results) this happens to work regardless of base, because unanchored gitignore patterns match at any depth. But for a project override containing a path separator — exactly the example given in this PR's own README and abtests.config.ts template ('tmp/traces') — gitignore anchoring semantics mean the pattern only matches paths that literally start with that prefix. Whenever the Docker build context isn't identical to the git root (e.g. dockerBuildDir: '.', the template's default, in a repo where the project isn't itself the git root), a tmp/traces override will correctly exclude files in sync-changes/copy-changes-to-ssh but silently fail to exclude them in the live running-menu auto-sync loop — the exact scenario this PR is meant to prevent.

There's no test exercising this path (no test file for helpers/copy-ignore.ts, and no servers-menu test covers performAutoSync's copyIgnore filtering), so this regressed silently.

Fix: derive rel's path relative to the git root (or re-anchor the copyIgnore patterns to the build-context base) before calling isCopyIgnored, the same way sync-changes.ts reconciles sourceDir vs sideBuildDir.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Reviewed the diff. Overall the feature is clean: schema/defaults/merge logic in config.ts + types.ts are correct (verified the override-replaces-default behavior against the tests), and the ignore-based matcher in helpers/copy-ignore.ts is a sensible reuse of gitignore semantics.

One real bug, flagged inline on servers-menu.ts:939-948: the running-menu auto-sync path filters config.copyIgnore against paths relative to the Docker build context (fs.watch(experimentBuildDir, ...)), while sync-changes.ts and copy-changes-to-ssh.ts — and the README — treat copyIgnore patterns as relative to the Git repository root. For the shipped defaults (bare folder names) this is unobservable, but for a slash-containing override — the exact tmp/traces example this PR's own docs/template show — the auto-sync loop will silently fail to exclude it whenever the Docker build context isn't the git root (e.g. the template's default dockerBuildDir: '.' in a nested project). No test covers performAutoSync's new copyIgnore filtering or helpers/copy-ignore.ts directly, which is how this slipped through.

Everything else (types, docs, test updates for the default/override merge behavior) looks correct and matches the stated intent.

Comment on lines +17 to +22
export const CopyIgnoreConfigSchema = z.object({
/** Repository-relative directory patterns excluded from change copying. */
folders: z.array(z.string().min(1)).optional(),
/** Repository-relative file patterns excluded from change copying. */
files: z.array(z.string().min(1)).optional(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CopyIgnoreConfigSchema is missing .strict(), unlike every other schema in this file (SetupCommandSchema, TwinServersConfigSchema). Because a plain z.object() defaults to "strip unknown keys" rather than erroring, a typo in a user's abtests.config.ts — e.g. copyIgnore: { folder: [...] } instead of folders — silently parses successfully and falls back to the packaged defaults. The user gets no error and no indication their override was ignored; they'll only notice when unexpected files show up in (or stay out of) the synced volume.

Suggest adding .strict() here to match the rest of the file's convention:

Suggested change
export const CopyIgnoreConfigSchema = z.object({
/** Repository-relative directory patterns excluded from change copying. */
folders: z.array(z.string().min(1)).optional(),
/** Repository-relative file patterns excluded from change copying. */
files: z.array(z.string().min(1)).optional(),
});
export const CopyIgnoreConfigSchema = z.object({
/** Repository-relative directory patterns excluded from change copying. */
folders: z.array(z.string().min(1)).optional(),
/** Repository-relative file patterns excluded from change copying. */
files: z.array(z.string().min(1)).optional(),
}).strict();

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review summary

Reviewed the copy-ignore feature for correctness across its three call sites (sync-changes, copy-changes-to-ssh, and the auto-sync watcher in servers-menu.ts).

What looks solid:

  • The core matching logic (createCopyIgnoreMatcher / isCopyIgnored in helpers/copy-ignore.ts) correctly leans on the well-tested ignore package for gitignore-style directory-recursive matching, including the trailing-slash fallback needed to catch bare directory-change events from fs.watch.
  • Path-relativization is handled correctly and consistently: sync-changes.ts and copy-changes-to-ssh.ts already resolve sourceDir to the git root before calling getChangedFiles, so no extra conversion is needed there. The auto-sync watcher in servers-menu.ts is the one path where the watched root (experimentBuildDir) can differ from the git root (nested Docker build context), and it correctly uses the new repositoryRelativeCopyPath helper to normalize watcher-relative paths to git-root-relative before matching — this is exercised by the new copy-ignore.test.ts nested-context test.
  • The default-merge semantics in resolveConfig ({ ...defaultCopyIgnoreConfig(), ...validConfig.copyIgnore }) correctly let a user override just files while keeping the default folders list, and vice versa — matches the documented behavior and is covered by tests.
  • All call sites of getChangedFiles were updated for the new required copyIgnoreConfig parameter; no stale call sites left.

Issue found (left as inline comment):

  • CopyIgnoreConfigSchema in types.ts is missing .strict(), unlike every other schema in that file. A typo'd key (e.g. folder instead of folders) will silently parse and fall back to defaults instead of raising a config error, which is inconsistent with this codebase's established convention for catching config typos.

Minor note (not blocking): this PR changes the default behavior of sync-changes/copy-changes-to-ssh/auto-sync (previously: sync everything; now: silently skip audit-results/, compare-results/, compare-bisect-results/ by default). Worth double-checking whether this falls under the project's BREAKING_CHANGES.md policy for changed defaults, even though it doesn't invalidate any existing abtests.config.ts.

@rameziophobia
rameziophobia merged commit 122714c into main Aug 6, 2026
2 checks passed
Comment on lines +70 to +73
export interface CopyIgnoreConfig {
folders: string[];
files: string[];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CopyIgnoreConfig fills the same role on ResolvedConfig that SetupCommand does (setupCommands: SetupCommand[]), but twin-servers/index.ts re-exports SetupCommand alongside ResolvedConfig while CopyIgnoreConfig is left out:

export type { TwinServersConfig, TwinServersConfigInput, ResolvedConfig, SetupCommand } from './types';

Consumers of the public twin-servers API can currently import ResolvedConfig but have no way to name the type of its .copyIgnore field. Worth adding CopyIgnoreConfig to that export list for parity.

Comment on lines +153 to +156
copyIgnore: {
...defaultCopyIgnoreConfig(),
...validConfig.copyIgnore,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor UX footgun: because folders/files are replaced wholesale rather than merged/appended, a project that only wants to add one extra ignored path (e.g. tmp/traces) and writes copyIgnore: { folders: ['tmp/traces'] } silently loses the audit-results/compare-results/compare-bisect-results defaults — exactly the multi-GB result dirs this PR is meant to keep off the wire. The README example does show repeating the full default list, so this is mitigated by docs, but it's an easy trap since nothing warns when a supplied list omits the defaults. Consider a merge-by-default (with an explicit opt-out) or at least a runtime warning when overriding.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review summary

Went through the new copyIgnore feature (schema, defaults, matcher, git/sync integration, auto-sync in servers-menu.ts, docs, tests).

Overall: the design is sound. The repo-root-relative path normalization (repositoryRelativeCopyPath) correctly handles the nested-build-context case the PR calls out, getChangedFiles filtering lines up with the fact that git already reports paths relative to the repo root when sourceDir/cwd is resolved to the git root in both sync-changes.ts and copy-changes-to-ssh.ts, and the .strict() schema addition for typo-catching is a nice touch. Test coverage for the new matcher, config merge behavior, and the nested-context path translation all look reasonable.

Two inline notes posted, both minor/non-blocking:

  1. packages/shaka-perf/src/twin-servers/types.ts — the new CopyIgnoreConfig type isn't re-exported from twin-servers/index.ts, even though its sibling SetupCommand (same role: a nested field type on ResolvedConfig) is. Consumers of the public API can get ResolvedConfig but can't name .copyIgnore's type.
  2. packages/shaka-perf/src/twin-servers/config.ts — the folders/files override is a full replace, not a merge. A project adding just one extra ignored path (copyIgnore: { folders: ['tmp/traces'] }) silently drops the audit-results/compare-results/compare-bisect-results defaults — the exact multi-GB directories this feature exists to protect. It's documented (the README example repeats the full list), but nothing warns at runtime if someone doesn't follow that pattern.

No security or correctness issues found in the matcher/path-normalization logic itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant