Skip to content

Merge release-1.11 into develop - #10236

Closed
infrahub-github-bot-app[bot] wants to merge 123 commits into
developfrom
release-1.11
Closed

Merge release-1.11 into develop#10236
infrahub-github-bot-app[bot] wants to merge 123 commits into
developfrom
release-1.11

Conversation

@infrahub-github-bot-app

@infrahub-github-bot-app infrahub-github-bot-app Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Merging release-1.11 into develop after merging pull request #10235.


Summary by cubic

Brings release-1.11 to develop. Improves branch deletion safety and retryability; hardens schema updates/rebases and migration retries; fixes Git default-branch/remote-import behavior; consolidates and budgets events; stabilizes recompute scoping and tagging; rejects empty artifact payloads and tightens JSON Schema; advances number pools past taken values; validates timezones and improves date rendering; dedupes large diff trees; and preserves earliest creator during merges.

  • Branch deletion: Branch.delete() now raises; delete via bounded, retryable BranchDataDeleter and BranchDeleteOrchestrator. Follow‑ups (cancel proposed changes, emit event, delete Git branch) run only when this attempt removed the branch. Default/global branches are refused. Graph migration m075 resumes DELETING branches and finishes cleanup; the GraphQL mutation and flow allow retrying a DELETING branch.
  • Schema/rebase/migrations: rebases compare against the branch’s common‑ancestor schema but roll back the registry to the branch’s own pre‑rebase schema on failure; schema and graph migrations retry transient Neo4j errors inside a transaction; attribute‑rename migrations close branch‑owned edges instead of shadowing them. Kind‑update migrations relabel Profile{Kind}/Template{Kind} vertices; vertex labels derive from schema get_labels().
  • Git: workers clone on each repository’s default branch and resolve missing checkout refs from the graph; merges push back to the mapped default_branch and raise on rejection; branches discovered on a remote are created with sync_with_git enabled.
  • Events: group member/ancestor mutations consolidate roles; related resources are capped with reserved headroom for Prefect’s run‑context; event filters/readers match consolidated and legacy formats and de‑duplicate by id.
  • Recompute: coalesced submissions carry the branch tag at creation so branch‑scoped task queries see them; redundant tag refresh removed so branch/node tags persist; kinds with no read fields remain kind‑level dependencies.
  • Artifacts/JSON Schema: a transform returning no payload fails clearly; JSON/YAML/text serialization is consistent; Python closures auto‑detect only the entry file and rely on watch.files for helpers; exported JSON Schema forbids unknown fields and declares read‑only fields as deprecated.
  • Number pools: for globally unique attributes, allocation skips values already present on the target kind and advances to the next free one.
  • Preferences/UI: set‑preference validates timezone against IANA names (rejects localtime, posix/, right/, posixrules); empty strings normalize to unset. UI previews render in the edited timezone and hint the browser fallback if a stored zone is unrenderable; future dates render in the preferred format and DateTime detail fields keep the time.
  • Diff UI: deduplicates nodes re‑shipped as page context to fix a crash on large diffs.
  • Merge attribution: Attributes and Relationships set created_by to the earliest creator during merges.

Review and rollout

  • Run database migrations (graph version 75), including m075 to finish branches left in DELETING.
  • Replace any direct Branch.delete() calls with BranchDataDeleter or the branch‑delete task/mutation.
  • For Git repositories using a non‑main default, set default_branch on CoreRepository; read‑only repositories continue to use ref.
  • For Python artifacts and generators, declare helper modules or directories under watch.files; auto‑detection now includes only the entry file.
  • Clients setting timezone preferences must send a valid IANA name or omit the field to clear it.

Written for commit 3972f9a. Summary will update on new commits.

Review in cubic

ogenstad and others added 30 commits August 7, 2026 14:06
GroupMutatedEvent.get_related() built an unbounded related-resources
list (2 entries per member, 3 per ancestor), so a single mutation
changing a few hundred members exceeded the Prefect maximum and the
whole event was silently dropped: no activity log, no membership
automations.

Members and ancestors are now a single entry each (dropping the
duplicate related.node and the dead group.update roles) and the list is
truncated at the Prefect maximum, so the event is always recorded. The
event query filter and related-nodes output read all three related-node
roles and dedupe by id, keeping output identical across the consolidated
format and older events still in Prefect retention.

Fixes #10127
The resource loop in sort_key wrote the resource priority to type_cost
instead of item_cost. Every item carries both a type marker and a
resource marker, so that loop always ran and always erased the type
priority the first loop had computed.

Items were therefore sorted by resource kind only, and an integration
test could run before the smoke test of the same resource. Nothing
covered the ordering, so add unit tests on the collection hook.
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](dorny/paths-filter@v4.0.2...v4.0.3)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…le/dorny/paths-filter-4.0.3

chore(deps): bump dorny/paths-filter from 4.0.2 to 4.0.3
The "Example:" preview and the (i) source tooltip in the preferences
forms formatted through an ad-hoc date-fns format() call that took no
timezone, so they showed browser-local time — off by the zone offset,
and at some hours by a whole calendar day (with ISO_8601 they also
stated a false offset). The duplicate formatter is deleted; previews now
render through the shared preference-aware mechanism, exposed as
formatWithPreferences, using the unsaved timezone the form currently
holds. The user form falls back to the effective inherited zone, while
the global editor falls back to the browser zone, since an unset global
timezone means "browser default" for every viewer.

Fixes #10175

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ative

DateDisplay's compact/relative heuristic compared the day difference
with a signed value, so any future date — however distant — fell through
to the relative branch and rendered as "in 4 years" instead of the
user's preferred date format. The window is now a week either side of
now, keeping the relative phrase where it reads best and the preferred
date beyond.

Fixes #10173

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The object detail page's DateTime attribute renderer entered
DateDisplay's metadata-oriented age heuristic, so user-authored
timestamps collapsed to a bare date (or a relative phrase) while the
list view showed the full preferred datetime. The detail renderer now
opts out with fullTimestamp, matching the table cell.

Fixes #10172

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4 to 5.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](CodSpeedHQ/action@v4...v5)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](actions/stale@v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… cleared

The date-format example fell back to the caller's effective timezone while
the form's timezone field was empty. When that effective value came from
the caller's own override, clearing the override previewed the zone being
removed rather than the one about to apply. The fallback is now the
inherited zone, expressed as a domain rule: a USER source resolves to
null, since the API resolves the inherited value away once an override
wins and the browser zone is the only honest stand-in.

Also trims the comments added across these fixes down to the load-bearing
rationale, per the repo's code-documentation style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…preview

inheritedTimezone excluded a USER source but let any other source through,
so a DEFAULT source carrying a value would have been reported as inherited
— contradicting the pattern resolver, which discards a DEFAULT value even
when one is present. Only GLOBAL is an inherited layer, so whitelist it.

No behaviour change against today's API, which always pairs DEFAULT with a
null value; this keeps the two rules consistent if that ever loosens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A worker without a local copy of a repository clones it on demand. That path only carries the repository id and name, so the repository object never learns which git ref to track. It fell back to the platform default branch. Git then failed on any repository whose default branch is not `main`, the repository went to error, and artifact generation failed with it.

It takes more than one worker to see this. Each worker keeps its own git directory, so only the worker that added the repository has the clone.

`InfrahubRepositoryBase` gets one abstract method, `resolve_checkout_ref`. The re-clone path calls it and passes the result to `create_locally`. `CoreRepository` reads `default_branch`, `CoreReadOnlyRepository` reads `ref`. The lookup only runs when the clone is missing, so an existing clone costs nothing extra.

Fixes #8749
Both branch-details suites assert that no "default" text is visible on a
non-default branch, to prove the default badge is absent. The assertion
used a substring match, so renaming the attribute label to "Schema
differs from default branch" made it match that label instead and the
tests failed for a page that is rendering correctly. The positive
assertion was updated with the rename; this negative one was missed.

Matching exactly still catches a stray badge, whose text is only
"default", without tripping on prose that contains the word.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(backend): delete branch data in bounded batches

Deleting a large branch ran as a single Cypher statement whose peak
transaction memory scaled with the size of the branch: it collected the
element id of every vertex touched by a deleted edge into two lists and
concatenated them. On a big enough branch that exceeded
dbms.memory.transaction.total.max, and because the inner writes committed
in batches the failure left the branch stranded in DELETING -- invisible
in the branch list -- with most of its data still in the graph.

Replace it with a set of bounded queries driven from Python by a new
BranchDeleter component: the agnostic peers of branch-only nodes first,
then one batch of edges per relationship type until none are left. Peak
transaction memory is now a function of the batch size rather than the
branch, and naming the relationship type lets the branch range index
serve the match instead of scanning every edge in the database.

Each batch also deletes the vertices its edge deletions left bare. That
is only sound because every branch edge is removed by the batch's DELETE
and both endpoints are re-examined afterwards, so the batch that removes
a vertex's last edge is the one that sees it at degree zero. A DETACH
DELETE would break it by removing edges that never reach a batch of their
own, stranding the vertices on their far side.

Branch.delete now raises instead of silently dropping only the Branch
vertex; callers use BranchDeleter.

On a branch of 1,074,217 edges this deletes the same 416,021 vertices as
before, in 27.8s rather than 87.9s, and completes at a 512 MiB
transaction limit where the previous implementation ran out of memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): finish deleting branches abandoned in DELETING

A branch delete that ran out of transaction memory committed part of its
work before failing, leaving the branch with the DELETING status and the
rest of its data in the graph. That state was unreachable: the branch is
filtered out of the branch list, and Branch.get_by_name hides it by
default, so the delete could not be retried and the space could not be
reclaimed.

Add graph migration 075, which finds every branch still in DELETING and
runs it through BranchDeleter, then removes the branch node. It reuses the
normal delete path rather than reimplementing it, loading each branch via
the ignore_deleting escape hatch. Finding them needs a dedicated query
because the shared branch list query filters DELETING out
unconditionally.

BranchDeleter now returns the number of edges it removed so the migration
can report progress on the migration console. `infrahub db migrate` raises
the infrahub log level to WARNING, which hides the deleter's own logging,
and deleting a large branch takes long enough that silence looks like a
hung upgrade.

The migration is a no-op where no branch is in DELETING, and safe to
re-run: an interrupted delete resumes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): allow retrying a branch delete left in DELETING

A delete that failed part way through set the DELETING status before doing
any cleanup, and both the BranchDelete mutation and the branch-delete flow
looked the branch up with the default lookup, which hides that status. The
retry reported the branch as missing, so the only way to reclaim the data
was the upgrade migration -- which runs once, leaving any later failure
stranded for good. Both lookups now pass ignore_deleting=False.

Two consequences worth knowing: the mutation will accept a delete for a
branch whose delete is still running, and nothing serialises the two runs
(each query involved is idempotent); and a retry re-emits
BranchDeletedEvent. Making the surrounding flow steps idempotent is left
for later.

Cap the agnostic cleanup at 500 rows rather than query_size_limit. Those
batches count Nodes, and each one can drag an unbounded number of peer
vertices into the transaction with it, unlike the edge batches where one
row is one edge. 500 is what this phase used before the batching work, and
raising it to 5000 was incidental rather than deliberate.

Drop the edge accounting comment claiming the vertex cleanup can remove
extra edges. That was true while the cleanup used DETACH DELETE; it now
only deletes vertices that are already bare.

Add the missing tests: Branch.delete refusing, the default/global guards
that moved onto BranchDeleter, and retrying a branch left in DELETING.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(backend): own the branch in the delete-retry test

The test created the branch with `create_branch`, which leaves created_by
as the system user, so the BranchDelete mutation took its permission
branch and the assertion depended on how a super-admin grant resolves
against a specific DELETE_BRANCH check. That resolved differently in CI
and the test failed with PermissionDeniedError.

Use the existing first_account / session_first_account fixtures and make
that account the branch owner, so created_by matches the requesting
account, the permission check is skipped, and the result turns on the
DELETING status alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): make the DELETING branch cleanup migration robust

Address review findings on migration 075 and the deleter's accounting.

The migration's branch lookup suppressed the generated pagination. A read
query with no limit of its own is executed page by page, so with the
SKIP/LIMIT suppressed every page re-read the whole set and the paging
never reached a short page: with at least query_size_limit stranded
branches the upgrade would never finish. Pagination is enabled again and
the lookup orders by branch name, which is unique, so the pages are
disjoint.

Each branch is now deleted in its own try. A failure is reported as
"branch '<name>': <error>" and the loop continues, so an operator gets the
names of everything that still needs a re-run instead of the first
exception and an unknown remainder. Failing to list the branches at all
still aborts, since there is then nothing to iterate.

Cap the agnostic cleanup at min(batch_size, 500) rather than a fixed 500,
so lowering the configured batch size to fit a constrained database is not
answered with a larger batch than was asked for.

Count the agnostic cleanup's edges towards the total the deleter reports.
It detaches peer vertices, so leaving it out made the migration's
per-branch progress undercount for any branch with agnostic data.

The 075 test asserted the stalled branch's node was unreachable from main,
which it always was -- it only ever existed on that branch, so the
assertion held whether or not the migration ran. It now reads the node on
its own branch, before and after. A third case covers one branch of three
failing, using a FailingBranchDeleter that delegates to the real deleter
for the others so the test proves they were reclaimed rather than merely
attempted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): only run post-delete work when this attempt removed the branch

BranchDeleter.delete returned the number of edges it removed, which only
the upgrade migration used, for its progress output. It said nothing about
whether this attempt was the one that removed the branch -- so now that a
delete can be retried, two attempts on the same branch could both go on to
cancel the proposed changes, emit BranchDeletedEvent and delete the Git
branch.

Return a BranchDeleteResult carrying both branch_deleted and
edges_removed. Removing the vertex is itself the claim: two attempts are
serialised on it, so exactly one reports nodes_deleted, with no window of
the kind a read followed by a delete would leave. The branch-delete flow
returns early when it did not make the claim, and the migration reports
whether the branch was still there.

Skip the DELETING status write when the status is already set. It is a
wasted query for a branch whose earlier delete failed part way through,
and it fails outright if the branch has meanwhile been removed -- which
made delete() unsafe to call twice at all, whatever it returned.

A narrower window remains: an attempt that has not yet written the status,
and whose write lands after another attempt removed the vertex, still
raises rather than reporting false. It fails the run instead of
double-processing, and closing it properly wants a branch-scoped lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* make sure branch is deleted from git during concurrent deletes

* move delete_branch flow logic to a new component for easier testing

* formatting

* add default/global branch delete guard at higher level

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
update Branch.get_by_name to include ignore_deleting=True so that a
delete can be retried
pass the "low_context" into the branch delete orchestrator for follow-up
tasks
Keep stable's behavior from #10195 on top of the 1.11 frontend architecture:
git-repository.tsx links to the node's concrete __typename using the moved
NodeCore/object-urls modules, and the profiles-field test is ported onto
getObjectForEditingFromApi, which replaced generateObjectEditFormQuery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…C-3001]

The git agent relied on the SDK client default for sync_with_git when creating
an imported branch in the graph. That default flipped from True to False in the
SDK, so branches discovered on a repository remote were created without the
flag.

Merging such a branch, running its repository checks and generating its
artifacts are all gated on sync_with_git, so the git side of a merge was
silently skipped: the git branch was never merged into the repository default
branch and the recorded commit never advanced.

Closes #10208
…d-run

A dependency discovered after Vite's initial scan triggers a re-optimization
reload that resets vi.mock, which surfaces as "mockClear is not a function"
in whichever spec happens to be running. CI hit it on two different files
across consecutive runs. @radix-ui/react-dropdown-menu is a declared app
dependency the scan misses, so pre-bundling it removes that trigger.

Two entries already in this list, @dagrejs/dagre and html-to-image, still
reload: they belong to a workspace package consumed as source and do not
resolve from the app, so pre-bundling silently skips them. Fixing that needs
them declared here, which is a dependency change of its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(core): cap and consolidate group mutation event related resources
infrahub-github-bot-app Bot and others added 3 commits August 17, 2026 12:51
Merge stable into release-1.11
…10254)

* fix: close branch-owned edges when renaming an attribute on a branch

The rename ended the old attribute by creating a deleted edge beside it. That is the
only option for an edge owned by the default branch, which a branch cannot modify, but
it was applied to every edge — so a node created on the branch kept the old attribute
active and deleted at the same time.

Rebasing then re-stamped both edges to the rebase time, leaving the resolution of "the
latest edge for this attribute" a tie between them. The rename dispatched again after
the rebase could therefore resolve the still-active edge, rename a second time, and
leave the node with two copies of the renamed attribute.

Edges the branch owns are now closed with a `to` time, the way the default branch
already handled them, and only edges owned by another branch are shadowed.

The per-edge-type query rendering is gone with it: direction now comes from the stored
edge rather than from the schema's declared direction, so the ten generated UNION
branches collapse into four subqueries keyed on direction, and the node match uses a
label alternation instead of a three-way UNION.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite changelog

* fix: keep the rename node-selection overridable and cover the rerun

Inlining the node match into query_init made Migration012RenameTypeAttributeData's
render_match() override dead code, dropping the guard that skips nodes already holding
the new attribute name. The match moves back behind render_match() so a subclass can
still narrow it.

The regression test now re-executes the migration and asserts nothing further is
created, matching its sibling tests. That is the pass the rebase makes in practice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add a "Format schema files" section to the Create and load schema page:
default key-ordering behaviour, --diff / --check, and the opt-in
--strip-defaults / --sort-by-order-weight / --backfill-order-weight flags.

DO NOT MERGE until an infrahub-sdk release that ships `infrahubctl schema
format` is published (feature merged in opsmill/infrahub-sdk-python#1189).
Merging earlier would document a command absent from the released CLI.

Co-authored-by: Alex Gittings <agitting96@gmail.com>
@codspeed-hq

codspeed-hq Bot commented Aug 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 13 untouched benchmarks


Comparing release-1.11 (8c3f5d0) with develop (e6e28c2)

Open in CodSpeed

BaptisteGi and others added 23 commits August 18, 2026 09:07
docs: rework IPAM documentation
`infrahub tasks flush stale-runs` only reset flow runs stuck in RUNNING, so
runs picked up by a worker that died before the flow started stayed PENDING
forever. Prefect's start_time filter falls back to expected_start_time, so
never-started runs are aged correctly by the existing cutoff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Playwright's `name` option substring-matches by default, and the branch
selector renders the current branch name on every page. A base36 random
suffix can therefore spell a word the suites locate by, making that
locator match two elements and killing the test with a strict-mode
violation.

That is what took down object-relationships.spec.ts on #10287: the branch
was object-relationshipsaveyj8q5g6r, where the prefix's trailing "s" and a
suffix starting "ave" spell "save", so getByRole("button", { name: "Save" })
matched both the Save button and the branch selector. It presents as an
unrelated test failing for no reason and passing on re-run, which is why it
kept being written off as noise.

Switch the TypeScript generator to hex, which the pytest helper already
used. Measured over 2M generations against the names the suites locate by:
base36 produces 168 collisions (about 1 in 12k), hex produces 0. It is not
only "save" -- the sample also threw up branchesmainzvsrjh8,
...u6vl6xpathb and ...t6nname02n, colliding with "main", "path" and "name".

None of the 436 distinct locator names across both suites is spellable in
hex, so this closes the class rather than the one instance. Both helpers
now carry the constraint so a wider alphabet does not creep back in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4cf2b30)
The fix rests on the branch suffix staying hexadecimal, but that was
guarded only by the comments added alongside it. Prose already failed once
here: these two helpers silently diverged despite the Python one
documenting itself as a port of the TypeScript one, which is how base36
survived long enough to spell "save" in a branch name.

Assert the suffix matches [0-9a-f]{12} on both sides. Verified by
regression, not just by passing: reverting the TypeScript helper to
Math.random().toString(36) fails both of its cases, and perturbing the
Python helper's output fails its.

The Python test needs a shard marker because CI selects with
-m shard_<name> and an unmarked test is deselected in every shard. It also
inherits the suite's compose stack, since pytest-base-url's autouse
_verify_url pulls in base_url; that costs nothing in CI where the stack is
already running, and there is nowhere else the helper is importable from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit ecb56d0)
Bump the prefect-redis pin from 0.2.12 to the latest release, 0.2.14.
Infrahub only consumes it through the Prefect server env vars
(PREFECT_MESSAGING_BROKER/CACHE, PREFECT_SERVER_EVENTS_CAUSAL_ORDERING,
PREFECT_SERVER_CONCURRENCY_LEASE_STORAGE), so the change is confined to
the Prefect server side.

- pyproject.toml: prefect-redis==0.2.12 -> ==0.2.14
- uv.lock: regenerated with uv 0.11.8 (CI's version); prefect 3.7.5 and
  redis 6.0.0 unchanged, no other package moved except a pendulum marker
  refresh that was already stale in the lock

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…igration (#10298)

NodeRelationshipRemoveMigration overrides SchemaMigration.execute to open its
own transaction rather than delegating to the base, which discarded the
retry_db_transaction decorator applied to the base method. A transient error
inside that transaction failed the migration instead of being replayed.

Document the rule where migrations are authored, since an override silently
dropping an inherited decorator is easy to repeat.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stable merge collided on `infrahub tasks flush stale-runs`: release-1.11
moved the purge behind `FlowRunRetention`, while stable widened the state
filter to include PENDING via `STALE_FLOW_RUN_STATES`. Keep both — stable's
call site alone would not import, since release-1.11 dropped `PrefectTask`.

`FlowRunRetention.purge` filters on the same `start_time before cutoff`, so
never-started runs still age off Prefect's `expected_start_time` fallback as
the original fix intended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adjacency conflict in `requires-dist`: stable bumped prefect-redis to 0.2.14
and release-1.11's prometheus-client pin sits on the following line. Keep
both entries; `uv lock --check` passes against pyproject.toml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…table

test: generate hex branch names so they cannot spell locator words (backport #10291)
* fix(backend): omit task related nodes with unresolvable kind

A Prefect flow run can be tagged with a related node ID that no longer
resolves to an active node (deleted definition, stale tag). The kind then
stays None and the non-nullable TaskRelatedNode.kind GraphQL field crashed
the resolver, breaking the Tasks page.

Filter such entries out of the related-nodes accessors so the task is
returned without the unrenderable node instead of erroring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(backend): assert resolvable related node survives kind filtering

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ease-1-11

# Conflicts:
#	backend/infrahub/task_manager/event/models.py
`stable` fixed `RelatedNodesInfo.get_related_nodes` in
`task_manager/models.py` (#9664), filtering out related nodes whose kind
never resolved so the non-nullable `TaskRelatedNode.kind` GraphQL field
stops crashing the Tasks page.

`release-1.11` split that flat module into `task_manager/event/` and
`task_manager/flow_run/`. Git's rename detection followed the split to
`event/models.py` and reported the conflict there, but `RelatedNodesInfo`
actually landed in `flow_run/models.py` — every consumer
(`flow_run/enrichment.py`, `reader.py`, `service.py`) imports it from
`.models` there, and `event/models.py` does not reference it at all.

Drop the incoming block from `event/models.py` (restoring it byte-for-byte
to the `release-1.11` version) and apply the two-hunk fix to
`flow_run/models.py`, where the target methods were still identical to the
merge-base so the patch applies with exact context.
`release-1.11` added `TestRelatedNodesInfo` in #9810; `stable` added the
kind filter in #9664. Neither side touched the other's file, so the merge
was clean and five of those tests then failed.

They assert registration mechanics (`add_node`, `add_nodes`, cross-flow
sharing, dedup) but observe through `get_related_nodes`, which now omits
nodes whose kind never resolved. `add_node` leaves `kind=None`, so each
saw an empty list.

Set a kind on the registered nodes, following the idiom already used by
`test_get_related_nodes_as_dict` in this file. The assertions are otherwise
untouched and no longer depend on whether the accessor filters.

Add two tests for the filter's contract, which previously had only
component-level coverage:

- `test_get_related_nodes_omits_unresolvable_kind` pins the fix across all
  three accessors. Verified it fails when the filter is reverted.
- `test_get_unique_related_node_ids_keeps_unresolved_kind` guards the other
  direction: `flow_run/enrichment.py` resolves kinds from that list, so
  filtering it would leave every kind permanently unresolved.
The default test stack carried no com.github.* labels at all, and the
cluster stack only had them on database-core2/core3, so containers left
behind on the runners could not be traced back to the run that spawned
them.

com.github.job falls back to GITHUB_JOB because only the three E2E
workflow jobs export JOB_NAME, while the integration, functional,
docker-integration, benchmark and version-upgrade jobs — the ones that
actually build testcontainers stacks — do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge stable into release-1.11
…10250)

* refactor: consolidate graph-integrity checks behind verify_graph()

Six overlapping graph-integrity checks were split across
infrahub.database.validation and tests.helpers.db_validation, each called
individually, so a suite had to know all six existed and new suites routinely
covered only a subset. They now live in one module behind a single entry point:
verify_graph() raises GraphValidationError listing every violation found rather
than stopping at the first failing check, and collect_graph_violations() returns
them for tests that assert a damaged state. The individual checks are private.

An optional kinds filter narrows every check at its MATCH anchor, so a suite can
scope a run to the kinds it touches; checks anchored on Attribute or Relationship
vertices resolve the label through the Node the vertex hangs off.

The duplicate-attributes check no longer takes a branch. It derives the branch
set from the graph, so one call covers every branch that touched a node instead
of only the one the caller passed, and it prunes to nodes actually holding two
same-named Attribute vertices so a healthy graph does almost no work. It also
groups per vertex rather than per uuid, since a kind update leaves several
vertices sharing one uuid.

The three checks that resolve default-branch visibility now use branched_from
rather than created_at as the fork point, matching the production read path. A
rebased branch does see default-branch edges written after its creation, so
created_at under-included them and the checks could miss a violation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: correct the graph checks surfaced by the consolidated verify_graph()

Running every check at every call site exposed gaps in the checks themselves.

An empty kinds list rendered an empty label expression, so `MATCH (n:)` made every
check fail rather than run unfiltered. It now means no filter, as callers expect.

The orphaned-edge and duplicate-attribute checks selected a default-branch edge as
visible from a branch, then required it to be open, so an edge closed after the branch
forked counted as inactive even though the branch still reads it. Both now allow the
same `to > branched_from` exception the relationship-edge-count check already applied.

The edges-after-delete check compared a branch's edges against the default branch's
delete time. A rebase re-stamps every open edge to the fork point, including the ones
recording the branch's own deletion, so those edges looked like writes to an already
deleted node. An edge on another branch is now only flagged when it was written
strictly after the fork point.

The orphaned-edge check also carried a deletion timestamp through four projections
without ever reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: rewrite the orphaned-edge check around the delete that caused it

The check resolved, per field and peer, the latest second-level edge visible from the
branch that deleted the field, then asked whether it was active. Reading a default
branch edge through a branch's fork point made a value the default branch had since
replaced look live, so an ordinary update after a branch deleted a field was reported
as an orphan.

It now starts from the delete itself. Every branch holding a deleted or closed
first-level edge for a node and field is a candidate; the edges that branch holds
decide whether it deleted the field at all, and the last of them to close is when the
field went away. A delete writes the field's edges at that same moment, so what is
left open, or carries a later time, is what did not follow the delete.

Grouping on the node uuid rather than the vertex replaces the guard for kind updates:
those copy the node vertex, and both copies point at the same field, so an edge left
open on either of them means the field is still in use.

Edges on a branch that forked after the delete are checked too, since that branch
inherits it. A rebase re-stamps the edges it carries to the fork point, so only what
comes after that was written with the delete in view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: scope an inherited delete to the branches that can see it

A delete only reaches a branch that was cut after it from the branch it happened on.
Branches are cut from the default branch, so a delete on one branch is invisible to
every other, but the check treated any branch forked later as having inherited it and
reported that branch's own writes as orphaned. Two sibling branches, one deleting a
field and the other adding to it, were enough to trigger it.

The violation also named the branch that did the deleting rather than the branch
holding the edge, which pointed at the wrong place to go looking. It now names where
the edge lives, and where the delete came from when they differ.

The duplicate-attribute check now counts a default-branch edge that closed after a
branch forked, which the branch still reads as open. Unlike the orphaned-edge check
this resolves one edge per field vertex, so a value the default branch replaced cannot
be counted twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix migration

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group/backend Issue related to the backend (API Server, Git Agent) group/ci Issue related to the CI pipeline group/frontend Issue related to the frontend (React) type/documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.