Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/shared/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def create_gh_issue(
cached_suggestion: CachedSuggestions,
tracker_issue_uri: str,
comment: str | None = None,
publisher=None,
# FIXME(@fricklerhandwerk): [tag:todo-github-connection] Make an application-level "GitHub connection" object instead.
# Instantiating the connection at definition time makes mocking it away for tests rather cumbersome.
# Ideally we'd have a generic mock that would abstract away regular book keeping such as app authentication, and tests would override only relevant behavior.
Expand Down Expand Up @@ -150,8 +151,9 @@ def additional_comment() -> str:
cached_suggestion.payload["pk"],
)

publisher_str = f" Published by @{publisher.username}" if publisher else ""
body = f"""\
- [{cached_suggestion.payload["cve_id"]}](https://nvd.nist.gov/vuln/detail/{quote(cached_suggestion.payload["cve_id"])})
- [{cached_suggestion.payload["cve_id"]}](https://nvd.nist.gov/vuln/detail/{quote(cached_suggestion.payload["cve_id"])}){publisher_str}
- [Nixpkgs security tracker issue]({tracker_issue_uri})
{maintainers()}
## Description
Expand Down
21 changes: 21 additions & 0 deletions src/shared/migrations/0071_nixpkgsissue_publisher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.2.9 on 2026-03-12 17:50

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('shared', '0070_cvederivationclusterproposal_rejection_reason'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name='nixpkgsissue',
name='publisher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='published_issues', to=settings.AUTH_USER_MODEL),
),
]
11 changes: 10 additions & 1 deletion src/shared/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from django.core.exceptions import ValidationError
from django.db import IntegrityError, models
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -38,6 +39,13 @@ class NixpkgsIssue(models.Model):
suggestion = models.OneToOneField(
CVEDerivationClusterProposal, on_delete=models.PROTECT
)
publisher = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="published_issues",
)

status = models.CharField(
max_length=text_length(IssueStatus),
Expand All @@ -61,7 +69,7 @@ def status_string(self) -> str:

@classmethod
def create_nixpkgs_issue(
cls, suggestion: CVEDerivationClusterProposal
cls, suggestion: CVEDerivationClusterProposal, publisher=None
) -> "NixpkgsIssue":
"""
Create a NixpkgsIssue from a suggestion and save it in the database. Note
Expand All @@ -75,6 +83,7 @@ def create_nixpkgs_issue(
# end.
status=IssueStatus.AFFECTED,
suggestion=suggestion,
publisher=publisher,
)
issue.save()
return issue
Expand Down
3 changes: 2 additions & 1 deletion src/webview/suggestions/views/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,15 @@ def post(self, request: HttpRequest, suggestion_id: int) -> HttpResponse:
elif new_status == "published":
try:
with transaction.atomic():
tracker_issue = NixpkgsIssue.create_nixpkgs_issue(suggestion)
tracker_issue = NixpkgsIssue.create_nixpkgs_issue(suggestion, publisher=request.user)
tracker_issue_link = request.build_absolute_uri(
reverse("webview:issue_detail", args=[tracker_issue.code])
)
github_issue_link = create_gh_issue(
suggestion_context.suggestion.cached,
tracker_issue_link,
new_comment,
publisher=request.user,
).html_url
NixpkgsEvent.objects.create(
issue=tracker_issue,
Expand Down
2 changes: 1 addition & 1 deletion src/webview/templates/components/issue.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{% if github_issue %}
<a href="{{ github_issue }}" target="_blank" class="permalink">GitHub issue</a>
{% endif %}
published on {{ issue.created }}
published {% if issue.publisher %}by <a href="https://github.com/{{ issue.publisher.username }}" target="_blank">@{{ issue.publisher.username }}</a> {% endif %}on {{ issue.created }}
</div>
</div>

Expand Down
6 changes: 6 additions & 0 deletions src/webview/tests/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def test_publish_gh_issue_empty_title(
link = as_staff.get_by_role("link", name="View")
expect(link).to_be_visible()
mock.assert_called()

# Check publisher name exists in kwargs
assert mock.call_args[1]["publisher"].username == "staff"

if no_js:
error = as_staff.locator("#messages")
Expand All @@ -89,6 +92,7 @@ def test_publish_gh_issue_empty_title(

issue_link = suggestion.locator("..").get_by_role("link", name="GitHub issue")
expect(issue_link).to_be_visible()
expect(suggestion.locator("..").filter(has_text="by @staff")).to_be_visible()
# FIXME(@fricklerhandwerk): Instrument the GitHub mock to produce a controlled link and check for that in the UI.
# This would assert we're actually displaying the right URL.
expect(issue_link).not_to_have_attribute("href", "")
Expand Down Expand Up @@ -177,3 +181,5 @@ def mock_get_maintainer_username(
assert f"@{maintainer_handle}" not in issue_body
else:
assert f"@{maintainer_handle}" in issue_body

assert "Published by @staff" in issue_body