Skip to content

Commit b5873d6

Browse files
refactor: handle branches and channels separately
This also makes an incremental move to displaying new evaluations based on branches as such, while preserving display of channel-oriented evaluations.
1 parent 4c84165 commit b5873d6

20 files changed

Lines changed: 309 additions & 166 deletions

src/shared/auth/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def ismaintainer(user: Any) -> bool:
1919
return NixMaintainer.objects.filter(
2020
github_id=user.socialaccount_set.get(provider="github").uid,
2121
nixderivationmeta__derivation__parent_evaluation__commit_sha1=F(
22-
"nixderivationmeta__derivation__parent_evaluation__channel__head_sha1_commit"
22+
"nixderivationmeta__derivation__parent_evaluation__branch__head_sha1_commit"
2323
),
2424
).exists()
2525

src/shared/cache_suggestions.py

Lines changed: 49 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
PackageOverlay,
2020
ReferenceUrlOverlay,
2121
)
22-
from shared.models.nix_evaluation import get_major_channel
2322

2423
logger = logging.getLogger(__name__)
2524

@@ -211,8 +210,9 @@ def cache_new_suggestions(suggestion: CVEDerivationClusterProposal) -> None:
211210
"metadata",
212211
"package_link__package",
213212
"parent_evaluation",
214-
"parent_evaluation__channel",
213+
"parent_evaluation__branch",
215214
)
215+
.prefetch_related("parent_evaluation__branch__channels")
216216
.prefetch_related(
217217
Prefetch(
218218
"metadata__maintainers",
@@ -316,69 +316,61 @@ def channel_structure(
316316
packages[attribute_path].derivation_ids.append(derivation.pk)
317317
if (
318318
derivation.metadata
319-
and derivation.parent_evaluation.channel.is_tracking_branch
319+
and derivation.parent_evaluation.branch.is_tracking_branch
320320
):
321321
packages[attribute_path].description = derivation.metadata.get_description()
322322
packages[attribute_path].maintainers = [
323323
CachedSuggestion.Maintainer.model_validate(to_dict(m))
324324
for m in derivation.metadata.prefetched_maintainers
325325
]
326-
# Get the branch from which that derivation originates
327-
branch_name = derivation.parent_evaluation.channel.channel_branch
328-
# Get primary ("major") channel to which that branch belongs
329-
major_channel = get_major_channel(branch_name)
330-
# FIXME This quietly drops unfamiliar branch names
331-
if major_channel:
332-
# XXX(@fricklerhandwerk): Here we assign package information to channel names in iteration order, which in the query we have established to be oldest-first by evaluation time.
333-
channels = packages[attribute_path].channels
334-
if major_channel not in channels:
335-
channels[major_channel] = CachedSuggestion.PackageOnPrimaryChannel(
336-
major_version=None,
337-
status=None,
338-
src_position=None,
339-
# XXX(@fricklerhandwerk): If this is not replaced in subsequent processing, it will display "-"
340-
uniform_versions=None,
341-
sub_branches=dict(),
342-
updated=None,
343-
)
344-
if branch_name == major_channel:
345-
channels[major_channel] = CachedSuggestion.PackageOnPrimaryChannel(
326+
all_channels = list(derivation.parent_evaluation.branch.channels.all())
327+
status = is_version_affected(
328+
[c.affects(package_version) for c in version_constraints]
329+
)
330+
src_position = get_src_position(derivation)
331+
updated = derivation.parent_evaluation.updated_at
332+
if len(all_channels) > 1:
333+
# Old style: multiple channels were evaluated separately.
334+
# Display under the primary channel with others nested.
335+
# FIXME(@fricklerhandwerk): Ideally we'd garbage-collect all those evaluations,
336+
# except the one with the latest commit.
337+
# That would require knowing the order of evaluation commits:
338+
# https://docs.github.com/en/rest/commits/commits?apiVersion=2026-03-10#compare-two-commits
339+
primary = next((c for c in all_channels if c.primary), None)
340+
if primary is None:
341+
continue
342+
group_name = primary.channel_branch
343+
packages[attribute_path].channels[group_name] = (
344+
CachedSuggestion.PackageOnPrimaryChannel(
346345
major_version=package_version,
347-
status=is_version_affected(
348-
[c.affects(package_version) for c in version_constraints]
349-
),
350-
src_position=get_src_position(derivation),
351-
uniform_versions=channels[major_channel].uniform_versions,
352-
sub_branches=channels[major_channel].sub_branches,
353-
updated=derivation.parent_evaluation.updated_at,
346+
status=status,
347+
src_position=src_position,
348+
uniform_versions=True,
349+
sub_branches={
350+
c.channel_branch: CachedSuggestion.PackageOnBranch(
351+
version=package_version,
352+
status=status,
353+
src_position=src_position,
354+
updated=updated,
355+
)
356+
for c in all_channels
357+
if not c.primary
358+
},
359+
updated=updated,
354360
)
355-
else:
356-
channels[major_channel].sub_branches[branch_name] = (
357-
CachedSuggestion.PackageOnBranch(
358-
version=package_version,
359-
status=is_version_affected(
360-
[c.affects(package_version) for c in version_constraints]
361-
),
362-
src_position=get_src_position(derivation),
363-
updated=derivation.parent_evaluation.updated_at,
364-
)
365-
)
366-
367-
for package_name in packages:
368-
channels = packages[package_name].channels
369-
for mc in channels.keys():
370-
uniform_versions = True
371-
major_version = channels[mc].major_version
372-
for _, branch in channels[mc].sub_branches.items():
373-
uniform_versions = (
374-
uniform_versions and str(major_version) == branch.version
375-
)
376-
channels[mc].uniform_versions = uniform_versions
377-
# We just sort branch names by length to get a good-enough order
378-
channels[mc].sub_branches = dict(
379-
sorted(
380-
channels[mc].sub_branches.items(),
381-
reverse=True,
361+
)
362+
else:
363+
# New style: evaluations per release branch.
364+
# Display the version for each branch separately
365+
group_name = derivation.parent_evaluation.branch.name
366+
packages[attribute_path].channels[group_name] = (
367+
CachedSuggestion.PackageOnPrimaryChannel(
368+
major_version=package_version,
369+
status=status,
370+
src_position=src_position,
371+
uniform_versions=True,
372+
sub_branches={},
373+
updated=updated,
382374
)
383375
)
384376
return packages

src/shared/channels.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
from shared.models.cve import Container
77
from shared.models.issue import NixpkgsIssue
88
from shared.models.linkage import CVEDerivationClusterProposal
9-
from shared.models.nix_evaluation import NixChannel, NixEvaluation
9+
from shared.models.nix_evaluation import NixEvaluation, NixpkgsBranch
1010

1111

1212
@dataclass
13-
class NixChannelUpdateChannel(TriggerChannel):
14-
model = NixChannel
13+
class NixpkgsBranchInsertChannel(TriggerChannel):
14+
model = NixpkgsBranch
1515
lock_notifications = True
1616

1717

1818
@dataclass
19-
class NixChannelInsertChannel(TriggerChannel):
20-
model = NixChannel
19+
class NixpkgsBranchUpdateChannel(TriggerChannel):
20+
model = NixpkgsBranch
2121
lock_notifications = True
2222

2323

src/shared/listeners/automatic_linkage.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
from shared.channels import ContainerChannel
2626
from shared.models.cve import Container, Cpe
2727
from shared.models.linkage import CVEDerivationClusterProposal, ProvenanceFlags
28-
from shared.models.nix_evaluation import NixChannel, NixDerivation, NixEvaluation
28+
from shared.models.nix_evaluation import (
29+
NixChannel,
30+
NixDerivation,
31+
NixEvaluation,
32+
)
2933

3034
logger = logging.getLogger(__name__)
3135

@@ -34,9 +38,9 @@ def produce_linkage_candidates(
3438
container: Container,
3539
filtered_affected: models.QuerySet,
3640
) -> models.QuerySet:
37-
latest_complete_channels = NixEvaluation.objects.filter(
38-
channel__state__in=NixChannel.TRACKED_STATES,
39-
).latest_completed_per_channel()
41+
latest_complete_evaluations = NixEvaluation.objects.filter(
42+
branch__channels__state__in=NixChannel.TRACKED_STATES,
43+
).latest_completed_per_branch()
4044

4145
package_names = (
4246
filtered_affected.exclude(package_name__isnull=True)
@@ -87,7 +91,7 @@ def produce_linkage_candidates(
8791
)
8892
.filter(
8993
package_q | product_q,
90-
parent_evaluation__in=list(latest_complete_channels),
94+
parent_evaluation__in=list(latest_complete_evaluations),
9195
)
9296
.select_related("metadata")
9397
.annotate(**annotations)

src/shared/listeners/nix_channels.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,59 @@
22

33
import pgpubsub
44

5-
from shared.channels import NixChannelInsertChannel, NixChannelUpdateChannel
6-
from shared.models import NixChannel, NixEvaluation
5+
from shared.channels import NixpkgsBranchInsertChannel, NixpkgsBranchUpdateChannel
6+
from shared.models import NixEvaluation
7+
from shared.models.nix_evaluation import NixpkgsBranch
78

89
logger = logging.getLogger(__name__)
910

1011

11-
def enqueue_evaluation_job(channel: NixChannel) -> tuple[NixEvaluation, bool]:
12+
def enqueue_evaluation_job(branch: NixpkgsBranch) -> tuple[NixEvaluation, bool]:
1213
eval_job, created = NixEvaluation.objects.get_or_create(
1314
defaults={
1415
# We will leave the scheduling to the evaluation channel
1516
# listener.
16-
"state": NixEvaluation.EvaluationState.WAITING,
17-
"channel": channel,
17+
"state": NixEvaluation.EvaluationState.WAITING
1818
},
19-
commit_sha1=channel.head_sha1_commit,
19+
branch=branch,
20+
commit_sha1=branch.head_sha1_commit,
2021
)
2122
# If the commit is shared by multiple channels, prefer the tracking branch.
2223
if (
2324
not created
24-
and channel.is_tracking_branch
25-
and not eval_job.channel.is_tracking_branch
25+
and branch.is_tracking_branch
26+
and not eval_job.branch.is_tracking_branch
2627
):
27-
eval_job.channel = channel
28-
eval_job.save(update_fields=["channel", "updated_at"])
28+
eval_job.branch = branch
29+
eval_job.save(update_fields=["branch", "updated_at"])
2930

3031
logger.info(
3132
f"Enqueued evaluation job {eval_job}{' (already existing!)' if not created else ''}"
3233
)
3334
return eval_job, created
3435

3536

36-
@pgpubsub.post_insert_listener(NixChannelInsertChannel)
37-
def start_evaluation_jobs_upon_insertion(old: NixChannel, new: NixChannel) -> None:
38-
logger.info("Nix channel created: %s", new.head_sha1_commit)
39-
if new.state in NixChannel.TRACKED_STATES:
40-
enqueue_evaluation_job(new)
37+
@pgpubsub.post_insert_listener(NixpkgsBranchInsertChannel)
38+
def start_evaluation_jobs_upon_insertion(
39+
old: NixpkgsBranch, new: NixpkgsBranch
40+
) -> None:
41+
logger.info("Nixpkgs branch created: %s at %s", new.name, new.head_sha1_commit)
42+
branch = NixpkgsBranch.objects.get(pk=new.name)
43+
if branch.is_tracked:
44+
enqueue_evaluation_job(branch)
4145

4246

4347
# XXX(@fricklerhandwerk): We can't reuse the same channel for different events
4448
# https://github.com/PaulGilmartin/django-pgpubsub/issues/86
45-
@pgpubsub.post_update_listener(NixChannelUpdateChannel)
46-
def start_evaluation_jobs_upon_updates(old: NixChannel, new: NixChannel) -> None:
49+
@pgpubsub.post_update_listener(NixpkgsBranchUpdateChannel)
50+
def start_evaluation_jobs_upon_updates(old: NixpkgsBranch, new: NixpkgsBranch) -> None:
4751
logger.info(
48-
"Nix channel updated: %s -> %s", old.head_sha1_commit, new.head_sha1_commit
52+
"Nixpkgs branch updated: %s %s -> %s",
53+
new.name,
54+
old.head_sha1_commit,
55+
new.head_sha1_commit,
4956
)
50-
if (
51-
old.head_sha1_commit != new.head_sha1_commit
52-
and new.state in NixChannel.TRACKED_STATES
53-
):
54-
enqueue_evaluation_job(new)
57+
if old.head_sha1_commit != new.head_sha1_commit:
58+
branch = NixpkgsBranch.objects.get(pk=new.name)
59+
if branch.is_tracked:
60+
enqueue_evaluation_job(branch)

src/shared/listeners/nix_evaluation.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async def realtime_batch_process_attributes(
121121
"%d attributes were ingested in %f seconds (%s, %s)",
122122
len(drvs),
123123
elapsed,
124-
parent_evaluation.channel,
124+
parent_evaluation.branch,
125125
parent_evaluation.commit_sha1[:8],
126126
)
127127

@@ -305,9 +305,7 @@ def _try_acquire_slot() -> bool:
305305

306306
@pgpubsub.post_insert_listener(NixEvaluationChannel)
307307
def run_evaluation_job(old: NixEvaluation, new: NixEvaluation) -> None:
308-
evaluation = NixEvaluation.objects.select_related("channel__release_branch").get(
309-
pk=new.pk
310-
)
308+
evaluation = NixEvaluation.objects.select_related("branch").get(pk=new.pk)
311309
average_evaluation_time = NixEvaluation.objects.aggregate(
312310
avg_eval_time=Avg("elapsed")
313311
)
@@ -316,14 +314,14 @@ def run_evaluation_job(old: NixEvaluation, new: NixEvaluation) -> None:
316314
if average_evaluation_time is not None:
317315
logger.info(
318316
"Nix evaluation requested: %s %s, expecting to finish in %f seconds",
319-
evaluation.channel,
317+
evaluation.branch,
320318
new.commit_sha1[:8],
321319
average_evaluation_time,
322320
)
323321
else:
324322
logger.info(
325323
"First nix evaluation requested: %s %s, no ETA",
326-
evaluation.channel,
324+
evaluation.branch,
327325
new.commit_sha1[:8],
328326
)
329327
# FIXME(@raitobezarius): Can we schedule this one instead of waiting on the lock?

src/shared/listeners/package_clustering.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ def cluster_after_evaluation(old: NixEvaluation, new: NixEvaluation) -> None:
1515
return
1616
if new.state != NixEvaluation.EvaluationState.COMPLETED:
1717
return
18-
evaluation = NixEvaluation.objects.select_related("channel").get(pk=new.pk)
18+
evaluation = NixEvaluation.objects.select_related("branch").get(pk=new.pk)
1919
logger.info("Clustering derivations from evaluation %s", evaluation)
2020
result = cluster_packages(
2121
NixDerivation.objects.filter(parent_evaluation_id=new.pk),
22-
update_packages=evaluation.channel.is_tracking_branch,
22+
update_packages=evaluation.branch.is_tracking_branch,
2323
)
2424
logger.info(
2525
f"Done. Clustered {result.derivations_processed} derivations: "

src/shared/management/commands/fetch_all_channels.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
class Command(BaseCommand):
13-
help = "Register Nix channels"
13+
help = "Fetch current Nixpkgs channel tips and update source branches"
1414

1515
def handle(self, *args: Any, **kwargs: Any) -> str | None:
1616
client = default_client()

src/shared/management/commands/garbage_collect.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,16 @@ def _delete_inactive_channels(self, batch_size: int, dry_run: bool) -> None:
220220
NixChannel.ChannelState.DEPRECATED,
221221
]
222222
)
223-
.exclude(evaluations__derivations__cve_links_proposals__isnull=False)
223+
.exclude(
224+
release_branch__evaluations__derivations__cve_links_proposals__isnull=False
225+
)
224226
.exclude(
225227
# No user input must be attached.
226228
# Currently only ignored/additional maintainers relate directly to derivations.
227-
evaluations__derivations__metadata__maintainers__maintaineroverlay__isnull=False
229+
release_branch__evaluations__derivations__metadata__maintainers__maintaineroverlay__isnull=False
228230
)
229-
.exclude(evaluations__derivations__isnull=False)
230-
.exclude(evaluations__isnull=False)
231+
.exclude(release_branch__evaluations__derivations__isnull=False)
232+
.exclude(release_branch__evaluations__isnull=False)
231233
.distinct()
232234
)
233235

0 commit comments

Comments
 (0)