This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit a52d17f35a5c7ad8fa75592fb3625276ae4346d9
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Jan 30 15:20:36 2026 +0000

    Associate check result ignores with projects not committees
---
 atr/api/__init__.py                             |  15 +--
 atr/db/__init__.py                              |   6 +-
 atr/get/checks.py                               |  14 +--
 atr/get/ignores.py                              |  16 +--
 atr/models/api.py                               |   4 +-
 atr/models/sql.py                               |   2 +-
 atr/post/ignores.py                             |  29 ++---
 atr/storage/readers/checks.py                   |  14 ++-
 atr/storage/readers/releases.py                 |   5 +-
 atr/storage/writers/checks.py                   |  22 +++-
 atr/templates/check-selected.html               |   4 +-
 migrations/versions/0045_2026.01.30_9664bcb9.py | 135 ++++++++++++++++++++++++
 12 files changed, 203 insertions(+), 63 deletions(-)

diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index 764b858..b5b28a0 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -396,8 +396,9 @@ async def ignore_add(data: models.api.IgnoreAddArgs) -> 
DictResponse:
     if not any(data.model_dump().values()):
         raise exceptions.BadRequest("At least one field must be provided")
     async with storage.write(asf_uid) as write:
-        wacm = write.as_committee_member(data.committee_name)
+        wacm = await write.as_project_committee_member(data.project_name)
         await wacm.checks.ignore_add(
+            data.project_name,
             data.release_glob,
             data.revision_number,
             data.checker_glob,
@@ -425,7 +426,7 @@ async def ignore_delete(data: models.api.IgnoreDeleteArgs) 
-> DictResponse:
     if not any(data.model_dump().values()):
         raise exceptions.BadRequest("At least one field must be provided")
     async with storage.write(asf_uid) as write:
-        wacm = write.as_committee_member(data.committee)
+        wacm = await write.as_project_committee_member(data.project_name)
         # TODO: This is more like discard
         # Should potentially check for rowcount, and raise an error if it's 0
         await wacm.checks.ignore_delete(data.id)
@@ -436,15 +437,15 @@ async def ignore_delete(data: 
models.api.IgnoreDeleteArgs) -> DictResponse:
 
 
 # TODO: Rename to ignores
[email protected]("/ignore/list/<committee_name>")
[email protected]("/ignore/list/<project_name>")
 @quart_schema.validate_response(models.api.IgnoreListResults, 200)
-async def ignore_list(committee_name: str) -> DictResponse:
+async def ignore_list(project_name: str) -> DictResponse:
     """
-    List ignores by committee name.
+    List ignores by project name.
     """
-    _simple_check(committee_name)
+    _simple_check(project_name)
     async with db.session() as data:
-        ignores = await 
data.check_result_ignore(committee_name=committee_name).all()
+        ignores = await 
data.check_result_ignore(project_name=project_name).all()
     return models.api.IgnoreListResults(
         endpoint="/ignore/list",
         ignores=ignores,
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 80ebbc5..eb454e0 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -196,7 +196,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
 
     def check_result_ignore(
         self,
-        committee_name: Opt[str] = NOT_SET,
+        project_name: Opt[str] = NOT_SET,
         release_glob: Opt[str] = NOT_SET,
         revision_number: Opt[str] = NOT_SET,
         checker_glob: Opt[str] = NOT_SET,
@@ -207,8 +207,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
     ) -> Query[sql.CheckResultIgnore]:
         query = sqlmodel.select(sql.CheckResultIgnore)
 
-        if is_defined(committee_name):
-            query = query.where(sql.CheckResultIgnore.committee_name == 
committee_name)
+        if is_defined(project_name):
+            query = query.where(sql.CheckResultIgnore.project_name == 
project_name)
         if is_defined(release_glob):
             query = query.where(sql.CheckResultIgnore.release_glob == 
release_glob)
         if is_defined(revision_number):
diff --git a/atr/get/checks.py b/atr/get/checks.py
index df36f9e..0d76c73 100644
--- a/atr/get/checks.py
+++ b/atr/get/checks.py
@@ -85,15 +85,12 @@ class FileStats(NamedTuple):
 
 async def get_file_totals(release: sql.Release, session: web.Committer | None) 
-> FileStats:
     """Get file level check totals after ignores are applied."""
-    if release.committee is None:
-        raise ValueError("Release has no committee")
-
     base_path = util.release_directory(release)
     paths = [path async for path in util.paths_recursive(base_path)]
 
     async with storage.read(session) as read:
         ragp = read.as_general_public()
-        match_ignore = await 
ragp.checks.ignores_matcher(release.committee.name)
+        match_ignore = await ragp.checks.ignores_matcher(release.project_name)
 
     _, totals = await _compute_stats(release, paths, match_ignore)
     return totals
@@ -119,7 +116,7 @@ async def selected(session: web.Committer | None, 
project_name: str, version_nam
 
     async with storage.read(session) as read:
         ragp = read.as_general_public()
-        match_ignore = await 
ragp.checks.ignores_matcher(release.committee.name)
+        match_ignore = await ragp.checks.ignores_matcher(release.project_name)
 
     per_file_stats, totals = await _compute_stats(release, paths, match_ignore)
 
@@ -510,16 +507,13 @@ def _render_header(page: htm.Block, release: sql.Release) 
-> None:
 
 
 def _render_ignores_section(page: htm.Block, release: sql.Release) -> None:
-    if release.committee is None:
-        return
-
     # TODO: We should choose a consistent " ..." or "... " style
     page.h2["Check ignores"]
     page.p[
-        "Committee members can configure rules to ignore specific check 
results. "
+        "Project committee members can configure rules to ignore specific 
check results. "
         "Ignored checks are excluded from the counts shown above.",
     ]
-    ignores_url = util.as_url(ignores.ignores, 
committee_name=release.committee.name)
+    ignores_url = util.as_url(ignores.ignores, 
project_name=release.project.name)
     page.div[htpy.a(".btn.btn-outline-primary", href=ignores_url)["Manage 
check ignores"],]
 
 
diff --git a/atr/get/ignores.py b/atr/get/ignores.py
index a6575e0..5c741d3 100644
--- a/atr/get/ignores.py
+++ b/atr/get/ignores.py
@@ -27,24 +27,24 @@ import atr.util as util
 import atr.web as web
 
 
[email protected]("/ignores/<committee_name>")
-async def ignores(session: web.Committer, committee_name: str) -> str | 
web.WerkzeugResponse:
[email protected]("/ignores/<project_name>")
+async def ignores(session: web.Committer, project_name: str) -> str | 
web.WerkzeugResponse:
     async with storage.read() as read:
         ragp = read.as_general_public()
-        ignores = await ragp.checks.ignores(committee_name)
+        ignores = await ragp.checks.ignores(project_name)
 
     content = htm.div[
         htm.h1["Ignored checks"],
-        htm.p[f"Manage ignored checks for committee {committee_name}."],
-        _add_ignore(committee_name),
+        htm.p[f"Manage ignored checks for project {project_name}."],
+        _add_ignore(project_name),
         _existing_ignores(ignores),
     ]
 
     return await template.blank("Ignored checks", content, 
javascripts=["ignore-form-change"])
 
 
-def _add_ignore(committee_name: str) -> htm.Element:
-    form_path = util.as_url(post.ignores.ignores, 
committee_name=committee_name)
+def _add_ignore(project_name: str) -> htm.Element:
+    form_path = util.as_url(post.ignores.ignores, project_name=project_name)
     block = htm.Block(htm.div)
     block.h2["Add ignore"]
     block.p["Add a new ignore for a check result."]
@@ -65,7 +65,7 @@ def _check_result_ignore_card(cri: sql.CheckResultIgnore) -> 
htm.Element:
 
     # Update form
     update_form_block = htm.Block(htm.div)
-    form_path_update = util.as_url(post.ignores.ignores, 
committee_name=cri.committee_name)
+    form_path_update = util.as_url(post.ignores.ignores, 
project_name=cri.project_name)
     status = shared.ignores.sql_to_ignore_status(cri.status)
     form.render_block(
         update_form_block,
diff --git a/atr/models/api.py b/atr/models/api.py
index 436f002..8a242f6 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -164,7 +164,7 @@ class DistributionRecordResults(schema.Strict):
 
 
 class IgnoreAddArgs(schema.Strict):
-    committee_name: str = schema.example("example")
+    project_name: str = schema.example("example")
     release_glob: str | None = schema.default_example(None, "example-0.0.*")
     revision_number: str | None = schema.default_example(None, "00001")
     checker_glob: str | None = schema.default_example(None, 
"atr.tasks.checks.license.files")
@@ -194,7 +194,7 @@ class IgnoreAddResults(schema.Strict):
 
 
 class IgnoreDeleteArgs(schema.Strict):
-    committee: str = schema.example("example")
+    project_name: str = schema.example("example")
     id: int = schema.example(1)
 
 
diff --git a/atr/models/sql.py b/atr/models/sql.py
index e178e2f..cdbc51f 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -946,7 +946,7 @@ class CheckResultIgnore(sqlmodel.SQLModel, table=True):
         sa_column=sqlalchemy.Column(UTCDateTime),
         **example(datetime.datetime(2025, 5, 1, 1, 2, 3, tzinfo=datetime.UTC)),
     )
-    committee_name: str = sqlmodel.Field(**example("example"))
+    project_name: str = sqlmodel.Field(foreign_key="project.name", 
**example("example"))
     release_glob: str | None = sqlmodel.Field(**example("example-0.0.*"))
     revision_number: str | None = sqlmodel.Field(**example("00001"))
     checker_glob: str | None = 
sqlmodel.Field(**example("atr.tasks.checks.license.files"))
diff --git a/atr/post/ignores.py b/atr/post/ignores.py
index 74e8815..974dbb2 100644
--- a/atr/post/ignores.py
+++ b/atr/post/ignores.py
@@ -23,32 +23,33 @@ import atr.storage as storage
 import atr.web as web
 
 
[email protected]("/ignores/<committee_name>")
[email protected]("/ignores/<project_name>")
 @post.form(shared.ignores.IgnoreForm)
 async def ignores(
-    session: web.Committer, ignore_form: shared.ignores.IgnoreForm, 
committee_name: str
+    session: web.Committer, ignore_form: shared.ignores.IgnoreForm, 
project_name: str
 ) -> web.WerkzeugResponse:
     """Handle forms on the ignores page."""
     match ignore_form:
         case shared.ignores.AddIgnoreForm() as add_form:
-            return await _add_ignore(session, add_form, committee_name)
+            return await _add_ignore(session, add_form, project_name)
 
         case shared.ignores.DeleteIgnoreForm() as delete_form:
-            return await _delete_ignore(session, delete_form, committee_name)
+            return await _delete_ignore(session, delete_form, project_name)
 
         case shared.ignores.UpdateIgnoreForm() as update_form:
-            return await _update_ignore(session, update_form, committee_name)
+            return await _update_ignore(session, update_form, project_name)
 
 
 async def _add_ignore(
-    session: web.Committer, add_form: shared.ignores.AddIgnoreForm, 
committee_name: str
+    session: web.Committer, add_form: shared.ignores.AddIgnoreForm, 
project_name: str
 ) -> web.WerkzeugResponse:
     """Add a new ignore."""
     status = shared.ignores.ignore_status_to_sql(add_form.status)  # pyright: 
ignore[reportArgumentType]
 
     async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
+        wacm = await write.as_project_committee_member(project_name)
         await wacm.checks.ignore_add(
+            project_name=project_name,
             release_glob=add_form.release_glob or None,
             revision_number=add_form.revision_number or None,
             checker_glob=add_form.checker_glob or None,
@@ -60,34 +61,34 @@ async def _add_ignore(
 
     return await session.redirect(
         get.ignores.ignores,
-        committee_name=committee_name,
+        project_name=project_name,
         success="Ignore added",
     )
 
 
 async def _delete_ignore(
-    session: web.Committer, delete_form: shared.ignores.DeleteIgnoreForm, 
committee_name: str
+    session: web.Committer, delete_form: shared.ignores.DeleteIgnoreForm, 
project_name: str
 ) -> web.WerkzeugResponse:
     """Delete an ignore."""
     async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
+        wacm = await write.as_project_committee_member(project_name)
         await wacm.checks.ignore_delete(id=delete_form.id)
 
     return await session.redirect(
         get.ignores.ignores,
-        committee_name=committee_name,
+        project_name=project_name,
         success="Ignore deleted",
     )
 
 
 async def _update_ignore(
-    session: web.Committer, update_form: shared.ignores.UpdateIgnoreForm, 
committee_name: str
+    session: web.Committer, update_form: shared.ignores.UpdateIgnoreForm, 
project_name: str
 ) -> web.WerkzeugResponse:
     """Update an ignore."""
     status = shared.ignores.ignore_status_to_sql(update_form.status)  # 
pyright: ignore[reportArgumentType]
 
     async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
+        wacm = await write.as_project_committee_member(project_name)
         await wacm.checks.ignore_update(
             id=update_form.id,
             release_glob=update_form.release_glob or None,
@@ -101,6 +102,6 @@ async def _update_ignore(
 
     return await session.redirect(
         get.ignores.ignores,
-        committee_name=committee_name,
+        project_name=project_name,
         success="Ignore updated",
     )
diff --git a/atr/storage/readers/checks.py b/atr/storage/readers/checks.py
index 15131bb..5af10c6 100644
--- a/atr/storage/readers/checks.py
+++ b/atr/storage/readers/checks.py
@@ -45,8 +45,6 @@ class GeneralPublic:
         self.__asf_uid = read.authorisation.asf_uid
 
     async def by_release_path(self, release: sql.Release, rel_path: 
pathlib.Path) -> types.CheckResults:
-        if release.committee is None:
-            raise ValueError("Release has no committee - Invalid state")
         if release.latest_revision_number is None:
             raise ValueError("Release has no revision - Invalid state")
 
@@ -63,7 +61,7 @@ class GeneralPublic:
         # Filter out any results that are ignored
         unignored_checks = []
         ignored_checks = []
-        match_ignore = await self.ignores_matcher(release.committee.name)
+        match_ignore = await self.ignores_matcher(release.project_name)
         for cr in all_check_results:
             if not match_ignore(cr):
                 unignored_checks.append(cr)
@@ -87,18 +85,18 @@ class GeneralPublic:
             member_results_list[member_rel_path].sort(key=lambda r: r.checker)
         return types.CheckResults(primary_results_list, member_results_list, 
ignored_checks)
 
-    async def ignores(self, committee_name: str) -> 
list[sql.CheckResultIgnore]:
+    async def ignores(self, project_name: str) -> list[sql.CheckResultIgnore]:
         results = await self.__data.check_result_ignore(
-            committee_name=committee_name,
+            project_name=project_name,
         ).all()
         return list(results)
 
     async def ignores_matcher(
         self,
-        committee_name: str,
+        project_name: str,
     ) -> Callable[[sql.CheckResult], bool]:
         ignores = await self.__data.check_result_ignore(
-            committee_name=committee_name,
+            project_name=project_name,
         ).all()
 
         def match(cr: sql.CheckResult) -> bool:
@@ -111,7 +109,7 @@ class GeneralPublic:
         return match
 
     def __check_ignore_match(self, cr: sql.CheckResult, cri: 
sql.CheckResultIgnore) -> bool:
-        # Does not check that the committee name matches
+        # Does not check that the project name matches
         if cr.status == sql.CheckResultStatus.SUCCESS:
             # Successes are never ignored
             return False
diff --git a/atr/storage/readers/releases.py b/atr/storage/readers/releases.py
index 8a3bff9..669fbfb 100644
--- a/atr/storage/readers/releases.py
+++ b/atr/storage/readers/releases.py
@@ -115,10 +115,7 @@ class GeneralPublic:
     async def __successes_errors_warnings(
         self, release: sql.Release, latest_revision_number: str, info: 
types.PathInfo
     ) -> None:
-        if release.committee is None:
-            raise ValueError("Release has no committee - Invalid state")
-
-        match_ignore = await 
self.__read_as.checks.ignores_matcher(release.committee.name)
+        match_ignore = await 
self.__read_as.checks.ignores_matcher(release.project_name)
 
         cs = types.ChecksSubset(
             release=release,
diff --git a/atr/storage/writers/checks.py b/atr/storage/writers/checks.py
index ca18f2c..452559a 100644
--- a/atr/storage/writers/checks.py
+++ b/atr/storage/writers/checks.py
@@ -99,6 +99,7 @@ class CommitteeMember(CommitteeParticipant):
 
     async def ignore_add(
         self,
+        project_name: str,
         release_glob: str | None = None,
         revision_number: str | None = None,
         checker_glob: str | None = None,
@@ -107,6 +108,7 @@ class CommitteeMember(CommitteeParticipant):
         status: sql.CheckResultStatusIgnore | None = None,
         message_glob: str | None = None,
     ) -> None:
+        await self.__validate_project_in_committee(project_name)
         _validate_ignore_patterns(
             release_glob,
             checker_glob,
@@ -117,7 +119,7 @@ class CommitteeMember(CommitteeParticipant):
         cri = sql.CheckResultIgnore(
             asf_uid=self.__asf_uid,
             created=datetime.datetime.now(datetime.UTC),
-            committee_name=self.__committee_name,
+            project_name=project_name,
             release_glob=release_glob,
             revision_number=revision_number,
             checker_glob=checker_glob,
@@ -134,6 +136,10 @@ class CommitteeMember(CommitteeParticipant):
         )
 
     async def ignore_delete(self, id: int) -> None:
+        cri = await self.__data.get(sql.CheckResultIgnore, id)
+        if cri is None:
+            raise storage.AccessError(f"Ignore {id} not found")
+        await self.__validate_project_in_committee(cri.project_name)
         via = sql.validate_instrumented_attribute
         await 
self.__data.execute(sqlmodel.delete(sql.CheckResultIgnore).where(via(sql.CheckResultIgnore.id)
 == id))
         await self.__data.commit()
@@ -153,6 +159,10 @@ class CommitteeMember(CommitteeParticipant):
         status: sql.CheckResultStatusIgnore | None = None,
         message_glob: str | None = None,
     ) -> None:
+        cri = await self.__data.get(sql.CheckResultIgnore, id)
+        if cri is None:
+            raise storage.AccessError(f"Ignore {id} not found")
+        await self.__validate_project_in_committee(cri.project_name)
         _validate_ignore_patterns(
             release_glob,
             checker_glob,
@@ -160,9 +170,6 @@ class CommitteeMember(CommitteeParticipant):
             member_rel_path_glob,
             message_glob,
         )
-        cri = await self.__data.get(sql.CheckResultIgnore, id)
-        if cri is None:
-            raise storage.AccessError(f"Ignore {id} not found")
         # The updating ASF UID is now responsible for the whole ignore
         cri.asf_uid = self.__asf_uid
         cri.release_glob = release_glob
@@ -177,3 +184,10 @@ class CommitteeMember(CommitteeParticipant):
             asf_uid=self.__asf_uid,
             cri=cri.model_dump_json(exclude_none=True),
         )
+
+    async def __validate_project_in_committee(self, project_name: str) -> None:
+        project = await self.__data.project(project_name, 
_committee=True).get()
+        if project is None:
+            raise storage.AccessError(f"Project {project_name} not found")
+        if project.committee_name != self.__committee_name:
+            raise storage.AccessError(f"Project {project_name} is not in 
committee {self.__committee_name}")
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index 7a77cc4..0a92433 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -145,7 +145,7 @@
       {% set ignored_errors_count = info.ignored_errors|length %}
       {% set ignored_warnings_count = info.ignored_warnings|length %}
       <p>
-        There were {{ ignored_errors_count }} {{ 'error' if 
ignored_errors_count == 1 else 'errors' }} and {{ ignored_warnings_count }} {{ 
'warning' if ignored_warnings_count == 1 else 'warnings' }} ignored by policy 
set by committee members. This does not include archive member checks.
+        There were {{ ignored_errors_count }} {{ 'error' if 
ignored_errors_count == 1 else 'errors' }} and {{ ignored_warnings_count }} {{ 
'warning' if ignored_warnings_count == 1 else 'warnings' }} ignored by policy 
set by project committee members. This does not include archive member checks.
       </p>
       <details>
         <summary>Show ignored checks</summary>
@@ -191,7 +191,7 @@
       <p>No ignored checks found.</p>
     {% endif %}
     <p>
-      You can also <a href="{{ as_url(get.ignores.ignores, 
committee_name=release.committee.name) }}">manage which check results are 
ignored</a>.
+      You can also <a href="{{ as_url(get.ignores.ignores, 
project_name=release.project_name) }}">manage which check results are 
ignored</a>.
     </p>
 
     <h3 id="debugging" class="mt-4">Debugging</h3>
diff --git a/migrations/versions/0045_2026.01.30_9664bcb9.py 
b/migrations/versions/0045_2026.01.30_9664bcb9.py
new file mode 100644
index 0000000..0d089d9
--- /dev/null
+++ b/migrations/versions/0045_2026.01.30_9664bcb9.py
@@ -0,0 +1,135 @@
+"""Associate check result ignores with projects not committees
+
+Revision ID: 0045_2026.01.30_9664bcb9
+Revises: 0044_2026.01.29_24d82084
+Create Date: 2026-01-30 14:44:47.168525+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0045_2026.01.30_9664bcb9"
+down_revision: str | None = "0044_2026.01.29_24d82084"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    # Add the new project name column
+    with op.batch_alter_table("checkresultignore", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("project_name", sa.String(), 
nullable=True))
+        batch_op.create_foreign_key(
+            batch_op.f("fk_checkresultignore_project_name_project"), 
"project", ["project_name"], ["name"]
+        )
+
+    bind = op.get_bind()
+    check_result_ignore = sa.table(
+        "checkresultignore",
+        sa.column("id", sa.Integer),
+        sa.column("asf_uid", sa.String),
+        sa.column("created", sa.DateTime),
+        sa.column("committee_name", sa.String),
+        sa.column("project_name", sa.String),
+        sa.column("release_glob", sa.String),
+        sa.column("revision_number", sa.String),
+        sa.column("checker_glob", sa.String),
+        sa.column("primary_rel_path_glob", sa.String),
+        sa.column("member_rel_path_glob", sa.String),
+        sa.column("status", sa.String),
+        sa.column("message_glob", sa.String),
+    )
+    project = sa.table(
+        "project",
+        sa.column("name", sa.String),
+        sa.column("committee_name", sa.String),
+    )
+
+    existing_ignores = bind.execute(
+        sa.select(
+            check_result_ignore.c.asf_uid,
+            check_result_ignore.c.created,
+            check_result_ignore.c.committee_name,
+            check_result_ignore.c.release_glob,
+            check_result_ignore.c.revision_number,
+            check_result_ignore.c.checker_glob,
+            check_result_ignore.c.primary_rel_path_glob,
+            check_result_ignore.c.member_rel_path_glob,
+            check_result_ignore.c.status,
+            check_result_ignore.c.message_glob,
+        ).where(check_result_ignore.c.project_name.is_(None))
+    ).fetchall()
+
+    # Expand each committee scoped ignore to cover all projects in its 
committee
+    for ignore in existing_ignores:
+        project_names = bind.execute(
+            sa.select(project.c.name).where(project.c.committee_name == 
ignore.committee_name)
+        ).fetchall()
+        for (project_name,) in project_names:
+            bind.execute(
+                check_result_ignore.insert().values(
+                    asf_uid=ignore.asf_uid,
+                    created=ignore.created,
+                    committee_name=ignore.committee_name,
+                    project_name=project_name,
+                    release_glob=ignore.release_glob,
+                    revision_number=ignore.revision_number,
+                    checker_glob=ignore.checker_glob,
+                    primary_rel_path_glob=ignore.primary_rel_path_glob,
+                    member_rel_path_glob=ignore.member_rel_path_glob,
+                    status=ignore.status,
+                    message_glob=ignore.message_glob,
+                )
+            )
+
+    # Remove the original committee scoped ignores
+    
bind.execute(check_result_ignore.delete().where(check_result_ignore.c.project_name.is_(None)))
+
+    # Complete the schema transition
+    with op.batch_alter_table("checkresultignore", schema=None) as batch_op:
+        batch_op.alter_column("project_name", nullable=False)
+        batch_op.drop_column("committee_name")
+
+
+def downgrade() -> None:
+    # Restore the committee name column
+    with op.batch_alter_table("checkresultignore", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("committee_name", sa.VARCHAR(), 
nullable=True))
+
+    bind = op.get_bind()
+    check_result_ignore = sa.table(
+        "checkresultignore",
+        sa.column("id", sa.Integer),
+        sa.column("committee_name", sa.String),
+        sa.column("project_name", sa.String),
+    )
+    project = sa.table(
+        "project",
+        sa.column("name", sa.String),
+        sa.column("committee_name", sa.String),
+    )
+
+    # Populate committee names from project ownership or delete orphaned 
ignores
+    project_committee_map = {
+        name: committee_name
+        for name, committee_name in bind.execute(sa.select(project.c.name, 
project.c.committee_name)).fetchall()
+    }
+    ignore_rows = bind.execute(sa.select(check_result_ignore.c.id, 
check_result_ignore.c.project_name)).fetchall()
+    for ignore_id, project_name in ignore_rows:
+        committee_name = project_committee_map.get(project_name)
+        if committee_name is None:
+            
bind.execute(check_result_ignore.delete().where(check_result_ignore.c.id == 
ignore_id))
+            continue
+        bind.execute(
+            check_result_ignore.update()
+            .where(check_result_ignore.c.id == ignore_id)
+            .values(committee_name=committee_name)
+        )
+
+    # Complete the schema rollback
+    with op.batch_alter_table("checkresultignore", schema=None) as batch_op:
+        
batch_op.drop_constraint(batch_op.f("fk_checkresultignore_project_name_project"),
 type_="foreignkey")
+        batch_op.drop_column("project_name")
+        batch_op.alter_column("committee_name", nullable=False)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to