feat(iac): offer dashboard tile alerts for Terraform import - #3076
feat(iac): offer dashboard tile alerts for Terraform import#3076jordan-simonovski wants to merge 15 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: ef53c21 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Greptile SummaryThe PR extends Terraform export eligibility to addressable dashboard-tile alerts while retaining fail-closed behavior for alerts whose tiles cannot be referenced safely.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains. The wall-clock race and spent-budget short circuit now keep the optional dashboard lookup within the manifest’s remaining deadline while returning the other manifest listings fail-closed.
|
| Filename | Overview |
|---|---|
| packages/api/src/utils/iacTileAlerts.ts | Resolves tile-alert addressability within the remaining request budget and fails closed on timeout or lookup failure. |
| packages/api/src/routers/api/iac.ts | Adds the bounded dashboard lookup and marks unaddressable tile alerts in the import manifest. |
| packages/api/src/routers/api/alerts.ts | Adds the server-computed tile-addressability marker to alert-list responses. |
| packages/common-utils/src/iacEligibility.ts | Extends alert eligibility to tile alerts and centralizes unique, nonblank tile-name validation. |
| packages/common-utils/src/iac.ts | Generates tile-alert imports with the required provider floor and durability guidance. |
| packages/app/src/components/alerts/AlertRowMenu.tsx | Offers Terraform export only for alerts that satisfy the shared eligibility contract. |
Reviews (13): Last reviewed commit: "Merge branch 'main' into jordansimonovsk..." | Re-trigger Greptile
Deep ReviewScope: 18 files vs base 🔴 P0/P1 — must fix
🟡 P2 — recommended
🔵 P3 nitpicks (4)
Reviewers (8): correctness, reliability, kieran-typescript, api-contract, adversarial, testing, maintainability, previous-comments. Testing gaps:
|
PR Review2 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 2 minor 2 posted as inline comment(s) on the changed lines. Severity is the reviewer's own estimate and is used for ordering, not filtering. |
`isImportableAlert` only accepted saved-search alerts, because that was all `clickhouse_clickstack_alert` modelled. Provider 3.26.0 adds `source = "tile"` with `dashboard_id` and `tile_id`, so tile alerts are now offered too — unless the provider could not address the tile: the `tile_ids` map it references is keyed by tile name and omits any tile whose name is blank or shared, and a provisioned dashboard's tiles are rewritten wholesale by ProvisionDashboardsTask. Both decisions are made server-side, on the import manifest and on the alerts listing, because neither response carries a dashboard's sibling tile names — the alerts response filters `dashboard.tiles` down to the alert's own tile, so a client-side check could never see a duplicate. A generated file that carries a tile alert asks for `>= 3.26.0` and explains the hand edit its config needs; an export without one still installs on 3.25.x.
… manifest budget The alerts listing computed `unaddressableTile` inside the `alert.dashboard` spread, so a tile alert whose dashboard had been deleted came back with no marker at all and the row menu offered it for import — the one case the manifest already withheld. The marker now hangs off the alert's source, where a null dashboard reaches `isTileAlertUnaddressable` and reads as unaddressable. The tile-name read is bounded by its own `$in`, not by the `limit` it carried: the ids come from a listing already capped at IAC_MANIFEST_LIMIT, so the cap could never bind and the comment describing what happens past it described nothing. It now takes what is left of the request's 10s budget rather than a fresh one, so sequencing it after the six listings cannot double the worst case. Also: the generated file's tile-alert notice said nothing about an export whose dashboard is absent, `providerVersionConstraint` claimed to compare two floors when it picks one (with a test to catch a base-floor bump past the tile-alert floor), and the manifest's schema comment pointed at the wrong predicate.
The e2e alerts spec still asserted the Terraform export was hidden for a tile alert. Its tile has a unique name on a dashboard nothing else manages, so the export is offered now and the assertion was wrong, not the behaviour. The dashboards read that resolves tile addressability decides one optional marker, but it runs last on a shared budget and a MaxTimeMSExpired took the whole manifest with it — the other six listings are the export. It now withholds every tile alert instead of failing the request, with a counter and a log line so the degraded export is visible rather than looking like a team that has no tile alerts. Also: the skip reasons lived in two places and had to be hand-edited in both when tile alerts became eligible, so both renderings now come from one table; the withheld counter described one of its three reasons; and the popover's doc comment still said the provider models only saved-search alerts.
188597a to
7c1f5a6
Compare
🟡 Tier 3 — StandardIntroduces new logic, modifies core functionality, or touches areas with non-trivial risk. Why this tier:
Review process: Full human review — logic, architecture, edge cases. Stats
|
E2E Test Results✅ All tests passed • 357 passed • 1 skipped • 1488s
Tests ran across 4 shards in parallel. |
| // Withheld rather than offered unchecked: the export comes up short, which | ||
| // both the generated file and the UI report, instead of carrying an alert | ||
| // whose reference cannot resolve. | ||
| if (dashboards == null) return allTileAlertIds(); |
There was a problem hiding this comment.
🔵 minor — A failed tile-name lookup is reported to the user as a tile-naming problem
The fail-closed path reuses unaddressableTile: true, which is the only signal the manifest carries, so a transient Mongo error or MaxTimeMSExpired makes IacMigrationSection render "N alerts will be skipped — ... an alerted tile needs a unique, non-blank name on a dashboard Terraform can own" (packages/app/src/components/TeamSettings/IacMigrationSection.tsx:95) and bake that same false reason into the downloaded hyperdx-import.tf header, with no error banner and only a server-side counter to say otherwise. Add a distinct manifest-level marker (e.g. tileAlertCheckUnavailable: true alongside truncatedTypes) so the UI and the generated file can say the check could not run instead of blaming the user's tile names.
| # generated for a dashboard, and applying that churns its tile ids and | ||
| # deletes the tile alerts attached to them. | ||
| # * Re-exporting later does NOT produce an additive file. It re-emits every | ||
| # requires ${providerVersionConstraint(tileAlerts)} — older providers write |
There was a problem hiding this comment.
🔵 minor — Generated file gives the 3.25.0 rationale for the 3.26.0 floor
The bullet now interpolates providerVersionConstraint(tileAlerts) but keeps the reason clause that belongs to the base floor, so a file with a tile alert reads "requires >= 3.26.0 — older providers write server-only ids into the config generated for a dashboard" — which is the provider #654 / 3.24.1 issue, not why 3.26.0 is needed. Keep the base sentence pinned to TERRAFORM_PROVIDER_VERSION_CONSTRAINT and let the tileAlertNotice below it own the 3.26.0 rationale it already states.
| // client cannot tell whether a sibling tile shares its name. | ||
| const canExport = | ||
| IS_IAC_EXPORT_ENABLED && | ||
| isImportableAlert({ |
There was a problem hiding this comment.
🔵 minor — Row menu hand-picks the predicate's inputs instead of passing the alert
isImportableAlert used to receive alert whole; it now receives a three-field literal, so a fourth eligibility input added to the predicate would reach collectImportableResources (packages/common-utils/src/iac.ts:390, which still passes the manifest entry whole) but silently arrive as undefined here — exactly the drift the comment above this call says the shared predicate prevents. AlertsPageItem now carries unaddressableTile, so isImportableAlert(alert) type-checks; revert to passing alert.
The branch put api's eslint-disable count and common-utils' as-any count above their baselines. The api one only silenced a warning on a test mock, and one of the two common-utils hits was prose in a doc comment matching `as any` — the phantom count the ratchet's own comment warns about. AlertRowMenu's suite waited out Mantine's dropdown transition with 5s timeouts and a 15s per-test budget, and still timed out under parallel workers. Turning transitions and portals off with `env="test"` makes the dropdown present on click, so the padding is gone. One assertion had to move ahead of its click: it read the menu after picking an item, which only worked while the closing transition kept the dropdown mounted.
| // client cannot tell whether a sibling tile shares its name. | ||
| const canExport = | ||
| IS_IAC_EXPORT_ENABLED && | ||
| isImportableAlert({ |
There was a problem hiding this comment.
🔵 minor — Hand-projecting the alert into isImportableAlert re-creates the sync burden the predicate exists to remove
isImportableAlert({ source, savedSearchId, unaddressableTile }) re-lists the predicate's inputs at the call site. AlertsPageItem is structurally assignable to the parameter type (source?: AlertSource widens to string?, and excess-property checking does not apply to a variable), so isImportableAlert(alert) still compiles — and it is the form that cannot go stale. As written, the next field added to the predicate arrives here as undefined with no compile error, which is precisely how a tile alert would be offered for import despite a server marker saying otherwise. Revert to passing alert.
The remaining budget was floored at a second, so a request that had already spent the ten-second ceiling on the six concurrent listings still handed the sequential dashboard read another second. The remainder is passed through unclamped now, and the read is skipped when there is nothing left of it — mongo reads `maxTimeMS: 0` as "no limit", so passing it through would have removed the bound rather than tightened it. Skipping withholds every tile alert, which is the answer an expired read already gave, and the log and counter that report it are now one helper shared by both paths.
…export-tile-alerts # Conflicts: # packages/app/src/components/alerts/__tests__/AlertRowMenu.test.tsx
| // passed through — and a floor under it would push the request past the | ||
| // ceiling this read is meant to stay inside. Skipped rather than run | ||
| // unbounded, which lands on the same answer an expired read gives. | ||
| if (maxTimeMS <= 0) { |
There was a problem hiding this comment.
🔵 minor — A failed or budget-starved tile-alert lookup is reported to the user as a tile-naming rule
Both fail-closed paths here (maxTimeMS <= 0 after the six listings consume IAC_MANIFEST_MAX_TIME_MS, and the dashboards == null rejection at line 112) return every tile alert id, so packages/api/src/routers/api/iac.ts:244 stamps unaddressableTile: true on alerts that are perfectly addressable. The client cannot tell the two apart: IacMigrationSection.tsx:95 then tells the user "N alerts will be skipped — ... an alerted tile needs a unique, non-blank name on a dashboard Terraform can own", which is false, and the same wrong reason is baked into the downloaded .tf header. A retry produces a different file with no explanation. Carry a distinct signal for the transient case (e.g. a manifest-level tileAlertsUnresolved flag) and word that notice as a lookup failure the user can retry, rather than as an eligibility rule.
| # generated for a dashboard, and applying that churns its tile ids and | ||
| # deletes the tile alerts attached to them. | ||
| # * Re-exporting later does NOT produce an additive file. It re-emits every | ||
| # requires ${providerVersionConstraint(tileAlerts)} — older providers write |
There was a problem hiding this comment.
🔵 minor — Generated file states the 3.25.0 rationale against the 3.26.0 floor
The bullet now interpolates providerVersionConstraint(tileAlerts), so a file carrying a tile alert reads "check your own declaration requires >= 3.26.0 — older providers write server-only ids into the config generated for a dashboard...". That justification belongs to provider #654, fixed in 3.24.1 (per the comment at line 34), so it is untrue of 3.24.1–3.25.x — the actual reason for the higher floor is the tile-alert bullet immediately below. Keep this bullet pinned to TERRAFORM_PROVIDER_VERSION_CONSTRAINT and let tileAlertNotice own the 3.26.0 requirement, so each version is stated with its own reason.
| // banner above is cached-manifest state, the file must describe what | ||
| // was actually written. | ||
| skipNotices: skipNoticesFor(freshSelection), | ||
| skipNotices: skippedRules(freshSelection).map(n => n.text), |
There was a problem hiding this comment.
🔵 minor — The file-header rendering of the skip notices is untested
skippedRules now serves both renderings, and the only difference between them is the willBeSkipped flag. Every assertion covers the banner variant (IacMigrationSection.test.tsx:118, :246); nothing asserts what reaches buildImportFile's skipNotices, and iac.test.ts never passes skipNotices either. Passing { willBeSkipped: true } at this call site — the exact mistake the shared table invites — would emit "1 alert will be skipped" under the file's "Some resources were left out as ineligible:" heading with every test still green. Add an assertion in the download test that the generated content contains the file-form notice (no "will be skipped").
| # this file too, replace them with references — | ||
| # clickhouse_clickstack_dashboard.<name>.id and | ||
| # ...tile_ids["<tile name>"] — before you apply. If it is not, the literals | ||
| # hold only while nothing else applies that dashboard, so import it under |
There was a problem hiding this comment.
🔵 minor — Tile-alert notice tells the user to import a dashboard the same file refuses to export
A tile alert is withheld only when its dashboard is missing or provisioned (isTileAlertUnaddressable, iacEligibility.ts:102) — not when the dashboard was skipped for unexportableTiles. So for a dashboard carrying a PromQL tile plus an alerted tile with a unique name, one file contains the alert's import block, the skip line "1 dashboard — a tile on them cannot be represented by the provider, and importing one would delete that tile on the next apply", and this bullet telling the user to "import it under Terraform before you do" — advice that, followed, deletes the very tile the skip line warns about. Qualify the sentence ("unless the dashboard is listed as skipped above"), or make the notice name only dashboards that passed isImportableDashboard.
| * the cost is withholding two alerts that would have worked. Wrong in the safe | ||
| * direction. | ||
| */ | ||
| function tileName(tile: { config?: unknown } | undefined): string | undefined { |
There was a problem hiding this comment.
🔵 minor — tileName re-implements the existing normalizeName without naming the twin
normalizeName at packages/api/src/utils/alerts.ts:53 is the same operation over the same untrusted-Mongo values (unknown → non-string is none, trim, blank is none), and is already used on tile.config.name at alerts.ts:118 for a tile alert. iacEligibility.ts cannot import from packages/api, so a twin may be necessary — but the DRY convention requires naming it: either move normalizeName into common-utils and have both call it, or doc-comment tileName with a pointer to packages/api/src/utils/alerts.ts:53.
|
@jordan-simonovski Lmk if P0 is concerning |
…export-tile-alerts # Conflicts: # packages/app/src/components/alerts/__tests__/AlertRowMenu.test.tsx
Provider #683 published in 3.28.0, not 3.26.0 — 3.26.0 and 3.27.x went out without it. A file asking for `>= 3.26.0` lets `terraform init` pick a provider that then rejects `source = "tile"` at apply time.
@wrn14897 I've resolved the P0. Thanks for reviewing! |
| noun: 'alert', | ||
| count: c => c.skippedAlerts, | ||
| reason: | ||
| 'the provider models saved-search and dashboard tile alerts, and an alerted tile needs a unique, non-blank name on a dashboard Terraform can own', |
There was a problem hiding this comment.
🔵 minor — Skipped-alert reason no longer explains inline alerts, the other thing that lands in that count
collectImportableResources increments the single skippedAlerts counter for both unaddressable tile alerts and inline alerts (isImportableAlert in packages/common-utils/src/iacEligibility.ts:29 returns false for source: 'inline', and inline alerts are a shipped feature — see EditInlineAlertModal). The old copy ("the provider only supports saved-search alerts") covered both; the new single reason only describes the tile-name rule, so a team whose skipped alerts are all inline reads "an alerted tile needs a unique, non-blank name on a dashboard Terraform can own" — which does not apply to any of them. This text is rendered both in the banner and, via skipNotices, into the committed .tf header. Extend the reason to name inline alerts too ("...and an inline alert's chart config has no provider representation"), or split skippedAlerts into two counted rules.
| * provider version the file asks for — the address and the import id of a | ||
| * tile alert match every other alert's. | ||
| */ | ||
| tileAlert?: boolean; |
There was a problem hiding this comment.
🔵 minor — tileAlert on the shared IacResourceRef is representable on resource types where it is meaningless
{ type: 'dashboard', id, tileAlert: true } type-checks, and buildImportFile (line 263) would then raise the whole file's provider floor to >= 3.28.0 for an export containing no alert at all — failing terraform init until 3.28.0 publishes, for nothing. Make IacResourceRef a discriminated union on type so tileAlert only exists on the 'alert' variant, or drop the field and have buildImportFile derive the floor from the manifest's source the way collectImportableResources already does at line 403.
| * the cost is withholding two alerts that would have worked. Wrong in the safe | ||
| * direction. | ||
| */ | ||
| function tileName(tile: { config?: unknown } | undefined): string | undefined { |
There was a problem hiding this comment.
🔵 minor — tileName re-implements an existing untrusted-name normalizer and tile lookup
normalizeName at packages/api/src/utils/alerts.ts:53 is the same operation (unknown → trimmed non-blank string or nullish), and deriveAlertDisplayFields at packages/api/src/utils/alerts.ts:112-118 already performs the exact composition isAddressableTile opens with — Array.isArray(dashboard.tiles), tiles.find(t => t.id === tileId), then normalize tile?.config?.name. Per the repo's REQUIRED DRY rule, either lift normalizeName (and the tile lookup) into common-utils and have both sites import it, or doc-comment tileName naming its twin and why they must coexist across the package boundary.
A comment naming a release goes stale silently the next time the floor moves. The constant and the assertions that check it still name the version — those fail loudly instead — and the comments now say what holds regardless: #683 is the provider change, and the shipping release is the thing to look up before moving the floor.
| label: 'Provider setup', | ||
| collapsible: true, | ||
| hint: 'Add once per Terraform module. Skip if your project already declares the ClickHouse provider.', | ||
| hint: `Add once per Terraform module. Skip if your project already declares the ClickHouse provider${ |
There was a problem hiding this comment.
🔵 minor — The >= 3.28.0 requirement is only visible inside the collapsed "Provider setup" block, whose own copy tells the affected users to skip it
The tile-alert version caveat is appended to the Provider setup hint, and that hint is rendered inside <Collapse> in TerraformHelperPanel.tsx:40-46 — so it is hidden until the user clicks "Show provider setup". The users who hit the failure are exactly the ones who already declare the ClickHouse provider (possibly pinned at the old >= 3.25.0 floor) and therefore never expand that section; the always-visible "Import block" hint mentions the hand edit but not the version. Add the floor to the Import block hint (e.g. "Requires ClickHouse provider >= 3.28.0."), or move the hint outside the Collapse in CollapsibleSnippet. Note the new test at AlertRowMenu.test.tsx:170 asserts toBeInTheDocument(), which Mantine's always-mounted Collapse satisfies while collapsed, so it does not pin visibility.
maxTimeMS caps what mongo spends executing, not the wait for a pool connection, server selection or the socket, so the sequential tile-alert read could still push GET /iac/import-manifest past the ten seconds it documents. A timer now races the read for what is left of the budget and lands on the same fail-closed answer the error and spent-budget branches already gave. The failure counter dedupes so one request cannot count as two, but both logs fire: when the read the timer walked away from rejects afterwards, that late error names the failure the timeout could only guess at.
| # generated for a dashboard, and applying that churns its tile ids and | ||
| # deletes the tile alerts attached to them. | ||
| # * Re-exporting later does NOT produce an additive file. It re-emits every | ||
| # requires ${providerVersionConstraint(tileAlerts)} — older providers write |
There was a problem hiding this comment.
🔵 minor — Generated file justifies the 3.28.0 floor with the 3.25.0 rationale, which is false for 3.25–3.27
The bullet now reads "check your own declaration requires >= 3.28.0 — older providers write server-only ids into the config generated for a dashboard, and applying that churns its tile ids…", but that consequence only applies below 3.25.0 (provider #654, per the comment on TERRAFORM_PROVIDER_VERSION_CONSTRAINT at iac.ts:31-37). Every tile-alert export tells the reader that 3.26.0/3.27.0 churn tile ids, which they do not. Keep the interpolated floor but make the reason conditional too — the source = "tile" reason is already stated in tileAlertNotice, so the tile-alert case only needs the version, not the dashboard-id-churn clause.
| // Withheld rather than offered unchecked: the export comes up short, which | ||
| // both the generated file and the UI report, instead of carrying an alert | ||
| // whose reference cannot resolve. | ||
| if (dashboards == null) return allTileAlertIds(); |
There was a problem hiding this comment.
🔵 minor — A failed or timed-out tile lookup is reported to the user as "the tile has no unique, non-blank name"
On a read failure, an exhausted budget, or the wall-clock timer (all reachable on a slow Mongo — this read runs last on what is left of IAC_MANIFEST_MAX_TIME_MS), every tile alert is returned with unaddressableTile: true. The client cannot distinguish that from a genuine verdict, so IacMigrationSection.tsx:95 renders "N alerts will be skipped — … an alerted tile needs a unique, non-blank name on a dashboard Terraform can own" for alerts that are perfectly addressable, and the downloaded file repeats it — the only accurate signal is the server-side counter. Return a separate manifest-level marker for "could not resolve" (like truncatedTypes) and give it its own copy, so the user is told to retry rather than to rename their tiles.
Terraform export skips every alert that is not a saved-search alert, because that was all the provider could model. terraform-provider-clickhouse#683 adds
source = "tile"withdashboard_idandtile_idtoclickhouse_clickstack_alert, and lands in provider 3.28.0, so this offers tile alerts for import too, minus the ones the provider could not address. Closes HDX-5285.This depends on provider 3.28.0 publishing first. #683 merged on 9 September and is in the v3.28.0 release, but that release is still a draft, so the newest published version is 3.27.0 and a file carrying a tile alert asks for a
>= 3.28.0that cannot resolve —terraform initfails for the whole file. Please hold this until v3.28.0 is out. The floor this PR originally carried,>= 3.26.0, was wrong in a worse way than not resolving: 3.26.0 and 3.27.0 both published withoutsource = "tile", so it would have let Terraform install a provider that accepts the file and then fails on apply, and a lockfile already pinned to 3.26.0 would have kept satisfying it.What changed
isImportableAlertnow acceptssource === 'tile'; inline alerts stay excluded, sinceclickhouse_clickstack_alerthas no attributes for the chart config one carries. A tile alert is withheld when the provider could not address its tile, and that judgement is made server-side in two places — the import manifest that Team Settings reads, and the alerts listing the row menu reads. Neither response ships a dashboard's sibling tile names, so the client cannot make the call itself: the alerts response filtersdashboard.tilesdown to the alert's own tile, which means a client-side duplicate check would pass for every alert.The generated file's provider floor is per-file rather than global. Only an export that actually contains a tile alert asks for
>= 3.28.0; anything else still installs on 3.25.x, so this does not force a provider upgrade on people exporting dashboards and saved searches. When a tile alert is present the file gains a note about the hand edit its generated config needs, and the per-alert popover carries a one-line version of the same caveat.Key decisions
Withholding an unaddressable tile alert rather than exporting it with a warning. The import itself would succeed — the import id is the alert's own — but the config it generates can only pin the literal tile id the server holds today, and the durable form is a
tile_ids["<tile name>"]reference that does not exist for a blank or duplicated name. The failure would land at apply time, on the alert, long after the export.Tile alerts on provisioned dashboards are withheld for the same reason
isImportableDashboardrefuses the dashboard:ProvisionDashboardsTaskrewrites those tiles wholesale, so the tile an alert is bound to can be replaced underneath Terraform. Alerts on a dashboard withheld for an unexportable tile are still offered, because that dashboard is not Terraform-managed at all and its tile ids therefore stay put.Tile names are compared trimmed. If the provider trims its
tile_idskeys then"Errors"and"Errors "collide, and treating them as distinct would emit a reference that cannot resolve; if it does not trim, the cost is withholding two alerts that would have worked.Background
The provider cannot let you author a tile id, because the server assigns them. It exposes them instead as a computed
tile_idsmap onclickhouse_clickstack_dashboard, keyed by tile name, which is what makes a unique non-blank name load-bearing for a tile alert. Per #683 the provider's tile-id carry-forward matches unique names before position, so an alerted tile with a unique name keeps its id across dashboard applies — the condition this change gates on is the same one that keeps the reference stable.Impact
Tile alerts appear in the Team Settings export and the alerts row menu once the provider ships. The skip copy names both exclusions. One extra bounded Mongo read on
GET /iac/import-manifest, skipped entirely for teams with no tile alerts and drawing on the request's existing 10s budget rather than a fresh one, and the alerts listing gains an optionalunaddressableTilemarker.Implementation detail
isTileAlertUnaddressableinpackages/common-utils/src/iacEligibility.tsis the single definition both surfaces use, composed fromisAddressableTileplus theprovisionedcheck. The manifest's tile-name lookup lives inpackages/api/src/utils/iacTileAlerts.ts— it needsalerts.itemsbefore it knows which dashboards to read, so it cannot join the existingPromise.all. It is team-scoped and bounded by its own$in, whose ids come from a listing already capped atIAC_MANIFEST_LIMIT. A dashboard that is gone, or belongs to another team, reads as unaddressable, which withholds an alert that would have worked rather than offering one that would not.IacResourceRef.tileAlertonly decides the version floor; the address and import id of a tile alert are unchanged.buildImportFilederives the floor from the resources rather than taking it as an argument, so a call site cannot forget to pass it.Tests:
isTileAlertUnaddressableand the trimmed-name rule inpackages/common-utils/src/__tests__/iac.test.ts, plus a guard that the tile-alert floor never drops below the base floor; the manifest marker for duplicate, blank, dangling-tile, provisioned, cross-team and deleted-dashboard cases iniac.int.test.ts; the alerts-listing marker for duplicate names, provisioned dashboards and a deleted dashboard inalerts.int.test.ts; and the row menu now opens the export panel and pins the>= 3.28.0floor, so the two surfaces cannot drift. That floor guard reads the provider'sversionline with an anchored match; unanchored, it hitrequired_version = ">= 1.5.0"first and compared 1.5.0 to itself, so it could not fail.One accepted gap: during a rolling deploy a new bundle against a not-yet-upgraded API pod sees no
unaddressableTileand offers every tile alert. The field's JSDoc documents it. The worst case is an import whose reference needs a hand fix, which did not seem worth a capability-handshake field on the manifest.