Skip to content

feat: match a customer-configured manifest filename - #2265

Open
pankajastro wants to merge 13 commits into
mainfrom
feat/custom-manifest-name
Open

pankajastro wants to merge 13 commits into
mainfrom
feat/custom-manifest-name

Conversation

@pankajastro

@pankajastro pankajastro commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Approach changed since earlier commits/review — replaced the customer-configured env var with automatic discovery. No configuration needed.

Discovery previously matched only files literally named manifest.json. Now a file is treated as a manifest when its name contains "manifest" (case-insensitive) and its content validates as one - covering manifest.json, manifest_full.json, manifest_by_schedule.json, etc. automatically.

Every manifest found gets its own slim companion, named after itself:

manifest.json      -> manifest.slim.json   (unchanged default)
manifest_full.json -> manifest_full.slim.json

So a directory with multiple manifests (e.g. per-schedule manifests, each read by a different DAG) gets one slim file per manifest instead of one contested file - no picking a winner, no collision. Three dbt projects with two manifests each produce six slim files, one per manifest.

Sidecar schema bumped to v2 in response to review (a directory can describe more than one manifest now, so a single top-level hash/slim pointer isn't enough). .astro/dbt_metadata.json keeps its fixed name, but filtered_manifest is replaced by manifests, a map keyed by source filename, each with its own hash and slim pointer:

{
  "schema": 2,
  "version": {"algo": "sha256-tree-v2", "hash": "…"},
  "manifests": {
    "manifest_full.json": {
      "version": {"algo": "sha256-manifest-v2", "hash": "…"},
      "slim_manifest": {"schema": 1, "path": "manifest_full.slim.json", "version": {...}}
    },
    "manifest_by_schedule.json": { "...": "..." }
  }
}

A consumer looks up its own manifest's entry instead of trusting the top-level version/pointer when a directory has several. The top-level version is kept for backward compat and is deterministic (alphabetically-first manifest in the directory).

This also closed a real concurrency hazard flagged in review: multiple manifests sharing a directory are hashed/slimmed in parallel, then written in one grouped pass per directory (one writer, no race) - verified with a 300-iteration stress test at real parallelism.

Cleanup matches a slim file by its .slim.json suffix instead of one fixed name, so it can find and remove any of them; ownership is still checked per file via its own _generated_by marker before deleting.

Also fixed in review: a failure slimming one manifest in a project root no longer aborts the whole unit - the project's tree-hash sidecar and any already-slimmed sibling manifest are no longer discarded.

Follow-up tracked separately: BOSS-738 for the Cosmos Boost plugin repo to consume the new manifests map instead of a single pointer.

Testing

  • go build/go vet/gofmt clean; full pkg/cosmosboost/... suite passing (golang:1.26 container - no local Go toolchain).
  • Key tests: TestRunSharedDirectorySidecarDescribesBothManifests (schema v2 map + top-level version determinism across repeated runs), TestRunDoesNotFlagOtherDbtArtifactsAsManifests, TestRunSlimsEveryManifestInProjectRoot, TestRunProjectRootSlimFailureIsolatedToOneManifest, TestRunHandlesMultipleProjectsWithDifferentManifestNames, TestIsManifestCandidateName, TestSlimNameFor, TestCleanupRemovesCustomNamedSlimManifest, TestCleanupKeepsForeignSlimJSONSuffixedFile.
  • E2E'd against a copy of a real customer directory holding two manifests (9275 vs 2300 nodes, each meant for a different DAG): both now get stamped with their own correctly-scoped slim file, no configuration needed.
  • E2E'd the exact "3 projects x 2 manifests each" scenario: confirmed 6 slim files, one per manifest.
  • Stress-tested the grouped-write path at GOMAXPROCS=8 across 300 iterations: zero corruption or lost updates in the sidecar.
  • Verified EnsureClean removes a stale slim file after a manifest is renamed between deploys (no orphaned files left behind).

🤖 Generated with Claude Code

@coveralls-official

coveralls-official Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37749960856

Coverage increased (+0.06%) to 44.913%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 5 uncovered changes across 1 file (123 of 128 lines covered, 96.09%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
pkg/cosmosboost/precompute/precompute.go 106 101 95.28%
Total (6 files) 128 123 96.09%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
pkg/cosmosboost/precompute/precompute.go 1 97.17%

Coverage Stats

Coverage Status
Relevant Lines: 60181
Covered Lines: 27029
Line Coverage: 44.91%
Coverage Strength: 9.95 hits per line

💛 - Coveralls

@pankajastro
pankajastro marked this pull request as ready for review September 28, 2026 11:13
@pankajastro
pankajastro requested a review from a team as a code owner September 28, 2026 11:13
Comment thread pkg/cosmosboost/precompute/precompute.go Outdated
Comment thread pkg/cosmosboost/predeploy.go Outdated
Comment thread pkg/cosmosboost/precompute/precompute.go Outdated
pankajastro and others added 8 commits September 30, 2026 00:23
ASTRO_COSMOS_BOOST_MANIFEST_NAME, when set, replaces manifest.json as
the only filename Cosmos Boost pre-deploy discovery matches. A
customer whose manifest isn't named manifest.json (and may keep other,
differently-purposed manifests in the same directory) had nothing
discovered at all; setting this env var to their manifest's actual
name picks up exactly that file, wherever it sits in the tree, and
leaves every other file untouched. Unset, behavior is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
ASTRO_COSMOS_BOOST_MANIFEST_NAME now holds a JSON object mapping a
directory (relative to the deployed path, "." for its root) to the
filename discovery should match there, instead of one name applied to
the whole deploy. A single global name couldn't represent different
projects using different manifest naming conventions in one deploy;
an unlisted directory still falls back to manifest.json. Also rejects
malformed JSON and any value that isn't a bare filename (a path there
would silently match nothing in findManifests' basename comparison),
failing the deploy step instead of stamping nothing silently.

Co-Authored-By: Claude <noreply@anthropic.com>
The plugin resolves both the hash sidecar and the slim manifest by
directory alone, with no check of which manifest produced them. If a
directory legitimately holds more than one valid dbt manifest (e.g.
per-schedule manifests read by different DAGs), stamping it for one
manifest would risk serving that manifest's slim copy and cache hash
to a DAG that actually points at the other.

hasSiblingDbtManifest checks, before writing anything, whether another
*.json file in the same directory also parses as a valid dbt manifest.
When it does, processManifest skips both artifacts for that unit (like
a non-dbt manifest, but with a Warning naming the reason) and
processProject skips only the slim attachment, since the project's own
tree-hash sidecar isn't manifest-specific and stays safe to write.

Co-Authored-By: Claude <noreply@anthropic.com>
- effectiveManifestName: drop the len(overrides)==0 fast path - a nil
  map lookup already returns ("", false) safely, so it was dead code.
- manifestNameOverrides: filepath.Base("") == "." != "", so the
  explicit name == "" check was redundant with the bare-filename check.
- Inline joinWarnings (used at exactly one call site) instead of a
  standalone helper.
- Drop TestRunManifestNameOverrideInProjectRootIsSlimmedNotDiscovered:
  fully subsumed by TestRunManifestNameOverridePerDirectory's dbt1 case.
- Replace two PreDeploy-level rejection tests (each spinning up a
  tempdir to hit an error that happens before any filesystem access)
  with one direct TestManifestNameOverrides covering unset/valid/
  invalid-JSON/path-valued cases against the parser itself.
- Shrink processManifest's doc comment to a pointer at
  hasSiblingDbtManifest instead of restating its rationale.

Co-Authored-By: Claude <noreply@anthropic.com>
No behavior change. Doc comments added across this PR's earlier
commits restated what the code already shows; cut each to the
non-obvious "why" (or the keying/format convention where that isn't
otherwise visible), dropping the rest.

Co-Authored-By: Claude <noreply@anthropic.com>
isDbtManifest checked only for metadata.dbt_schema_version's presence,
which every dbt artifact carries (run_results.json, catalog.json,
sources.json, semantic_manifest.json - not just manifest.json), each
under its own schema URL. On main this was harmless: it only ever ran
on a file already found by exact filename match. hasSiblingDbtManifest
breaks that assumption by scanning every *.json in a directory by
content - so a plain "dbt build" (which always writes run_results.json
next to manifest.json in target/) would flag the directory ambiguous
and silently stop stamping manifest.json for most real projects.

Tightened to require the schema URL contain "/manifest/", which every
dbt manifest schema does and no other dbt artifact's does (confirmed
against schemas.getdbt.com). Also fixed two related error-handling
gaps found in the same pass: processProject aborted the whole unit
(skipping its always-safe tree-hash sidecar) when the sibling check
itself failed to read the directory, instead of just skipping the
slim attachment the way an actually-ambiguous directory does; and
processManifest promoted that same read failure to a hard Err instead
of the same fail-safe Skipped+Warning an ambiguous directory gets.

Co-Authored-By: Claude <noreply@anthropic.com>
dir always descends from root (it only ever comes from walking root),
so filepath.Rel(root, dir) can't realistically fail here - and even if
it did, Rel returns "" on error, which already can't match a real
override key (the documented convention is "." for root). The
explicit early-return added nothing a plain fallthrough doesn't
already do correctly.

Co-Authored-By: Claude <noreply@anthropic.com>
The override key is a path relative to the whole deploy root, not to
the project's own directory - worth pinning since a dbt project living
under dags/ (dags/dbt/<name>/) is a common Astro layout, and it would
be easy to assume the key is just the project's own folder name.

Co-Authored-By: Claude <noreply@anthropic.com>
@pankajastro
pankajastro force-pushed the feat/custom-manifest-name branch from 34347fa to c6117a6 Compare September 29, 2026 18:54
@pankajastro
pankajastro requested a review from tatiana September 29, 2026 19:14
Rewritten from scratch, not incrementally: discovery no longer relies
on any customer-configured filename. A file is treated as a manifest
when its name contains "manifest" (case-insensitive) and its content
validates as one (isDbtManifest) - covering manifest.json,
manifest_full.json, manifest_by_schedule.json, etc. with nothing to
configure. ASTRO_COSMOS_BOOST_MANIFEST_NAME, Options.ManifestNames,
effectiveManifestName, and hasSiblingDbtManifest are all removed.

Every manifest found now gets its own slim companion, named after
itself (slimNameFor: "manifest_full.json" -> "manifest_full.slim.json",
"manifest.json" -> "manifest.slim.json" as before) - so N manifests in
one directory produce N slim files, never a single contested one. A
project root with multiple manifests slims each in turn. Cleanup
matches slim files by their .slim.json suffix instead of one fixed
name, so it can find and remove any of them; ownership is still
checked per file via its own _generated_by marker.

.astro/dbt_metadata.json is untouched - same fixed name, same schema,
same writer - so a directory with multiple manifests still gets one
sidecar, written by whichever unit's processing finishes last.

Co-Authored-By: Claude <noreply@anthropic.com>
@pankajastro
pankajastro force-pushed the feat/custom-manifest-name branch from bd6e318 to a82ea27 Compare September 30, 2026 11:11
@pankajastro

pankajastro commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

@kaxil heads up - the approach changed significantly since my earlier replies on this thread, so those are superseded.

Instead of a customer-configured env var (name override, then a JSON directory→filename map) plus an ambiguity guard that skipped stamping when a directory held more than one valid manifest, this now discovers manifests automatically: a file is treated as a manifest when its name contains "manifest" (case-insensitive) and its content validates as one - no configuration needed.

The bigger change: every manifest found now gets its own slim companion, named after itself (manifest_full.json → manifest_full.slim.json, manifest.json → manifest.slim.json as before). So the manifests_per_schedule/ case you originally flagged is no longer "skip the whole directory" - both manifests get correctly and separately stamped now, since there's no shared/fixed slim filename left to collide on.

PR description is updated to match. Would appreciate another look when you have time.

Comment thread pkg/cosmosboost/precompute/precompute.go
Comment thread pkg/cosmosboost/precompute/precompute.go Outdated

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

Thanks, @pankajastro , for implementing the original approach we had agreed with.

I left two comments inline, I consider them blockers.

Please, could you confirm if we have a follow-up ticket for the plugin work?

pankajastro and others added 3 commits October 7, 2026 20:08
dbt_metadata.json's schema bumps to 2: filtered_manifest (a single
pointer) becomes manifests, a map keyed by source filename. Each entry
carries that manifest's own content hash and its own slim_manifest
pointer, so a directory holding more than one manifest no longer loses
every sibling's identity to whichever one wrote last - the exact gap
kaxil's and tatiana's review comments both raised.

Writing is restructured to make that safe: manifest units still
compute (hash + slim bytes) concurrently, but are now written in a
second pass grouped by directory, so manifests sharing a directory
build one complete map together and write their shared sidecar
exactly once, from a single writer - not two goroutines racing the
same file. Stress-tested at 300 iterations under real parallelism
(GOMAXPROCS=8): zero corrupted or lost-update sidecars.

Also along the way:
- slimNameFor's suffix trim is now case-insensitive, matching
  isManifestCandidateName (a manifest named MANIFEST.JSON previously
  got a malformed MANIFEST.JSON.slim.json).
- A manifest candidate in a project root that can't even be read now
  surfaces a warning instead of vanishing silently (was conflated with
  "not a dbt manifest").
- Trimmed redundant tests/comments across the branch.

Co-Authored-By: Claude <noreply@anthropic.com>
processProject aborted the whole unit (discarding the project's tree
hash and any sibling manifest's already-written slim file) if any one
candidate's slim write failed. Isolate the failure to that candidate,
matching the existing read-error handling in the same loop.

Co-Authored-By: Claude <noreply@anthropic.com>
…idate

hashProject's tree walk aborted the entire project (CI's real
permission enforcement, unlike this sandbox's root user) when a
root-level manifest-candidate file couldn't be read. processProject
already re-reads and warns on that same file - skip it here instead
of failing the walk.

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

Copy link
Copy Markdown
Contributor Author

Thanks, @pankajastro , for implementing the original approach we had agreed with.

I left two comments inline, I consider them blockers.

Please, could you confirm if we have a follow-up ticket for the plugin work?

Thanks for the review. Addressed your feedback and created a follow-up issue for plugin work

Comment thread pkg/cosmosboost/precompute/metadata.go
Comment thread pkg/cosmosboost/precompute/precompute.go Outdated
@pankajkoti

Copy link
Copy Markdown
Contributor

Overall this rework is a good improvement. I left two inline comments (the schema bump, and a minor stale comment).

One more thing that is not in this diff so I could not inline it: the scaffold gitignore at pkg/airflowrt/include/gitignore:18 is still the literal **/.astro/manifest.slim.json. This PR now writes custom-named slim files like manifest_full.slim.json, which that line does not match, so they show up untracked. Same thing that was fixed once in #2242. Broadening it to **/.astro/*manifest*.slim.json covers all of them while still matching only the slim manifests this step writes.

…nifests

- precompute.go: fix comment claiming doc is never mutated after the
  slim marshal - hashDocument mutates it right below; the marshal just
  has to happen first.
- scaffold gitignore matched only the default manifest.slim.json, so a
  custom-named slim file (e.g. manifest_full.slim.json) showed up
  untracked. Same issue as #2242, broadened the same way.

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

Copy link
Copy Markdown
Contributor Author

Fixed the scaffold gitignore too - broadened to **/.astro/*manifest*.slim.json, same pattern as #2242.

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

Thanks for addressing the feedback, @pankajastro !

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.

4 participants