asf-tooling commented on issue #1210:
URL: 
https://github.com/apache/tooling-trusted-releases/issues/1210#issuecomment-4409674819

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `new_feature`  •  **Classification:** `actionable`  •  
**Confidence:** `medium`
   **Application domain(s):** `project_committee_management`, 
`admin_operations`, `release_lifecycle`
   
   ### Summary
   Issue #1210 requests adding metadata fields to the Committee model 
(specifically 'permission to use CI in building release artifacts') and an 
admin interface to manage these settings. The Committee model lives in 
atr/models/sql.py (not provided in full but referenced throughout). Admin route 
patterns in atr/admin/__init__.py show the established form+GET/POST pattern. 
The feature requires: (1) a schema migration adding a field like `ci_enabled` 
to sql.Committee, (2) an admin form/route pair to toggle this setting, and (3) 
potentially surfacing it in the committee view page.
   
   ### Where this lives in the code today
   
   #### `atr/datasources/apache.py` — `_update_committees` (lines 374-406)
   _currently does this_
   Shows the current committee fields being set during metadata sync; new CI 
metadata field must not be overwritten here.
   
   ```python
   async def _update_committees(
       data: db.Session, ldap_projects: LDAPProjectsData, committees_by_name: 
Mapping[str, Committee]
   ) -> tuple[int, int]:
       added_count = 0
       updated_count = 0
   
       # First create PMC committees
       for project in ldap_projects.projects:
           name = project.name
           # Skip non-PMC committees
           if project.pmc is not True:
               continue
   
           # Get or create PMC
           committee = await data.committee(key=name).get()
           if not committee:
               committee = sql.Committee(key=name)
               data.add(committee)
               added_count += 1
           else:
               updated_count += 1
   
           committee.committee_members = project.owners
           committee.committers = project.members
           # We create PMCs for now
           committee.is_podling = False
           committee_info = committees_by_name.get(name)
           if committee_info:
               committee.name = committee_info.display_name
   
           updated_count += 1
   
       return added_count, updated_count
   ```
   
   #### `atr/admin/__init__.py` — `DeleteCommitteeKeysForm` (lines 76-78)
   _extension point_
   Establishes the pattern for admin forms that operate on committees with 
SELECT widget for committee choice.
   
   ```python
   class DeleteCommitteeKeysForm(form.Form):
       committee_key: str = form.label("Committee", widget=form.Widget.SELECT)
       confirm_delete: Literal["DELETE KEYS"] = form.label("Confirmation", 
"Type DELETE KEYS to confirm.")
   ```
   
   #### `atr/admin/__init__.py` — `delete_committee_keys_get` (lines 145-171)
   _extension point_
   Demonstrates the pattern for admin GET routes that render forms with 
committee selection.
   
   ```python
   @admin.typed
   async def delete_committee_keys_get(
       _session: web.Committer, _delete_committee_keys: 
Literal["delete-committee-keys"]
   ) -> str | web.WerkzeugResponse:
       """
       URL: GET /delete-committee-keys
   
       Display the form to delete committee keys.
       """
       async with db.session() as data:
           all_committees = await 
data.committee(_public_signing_keys=True).order_by(sql.Committee.key).all()
           committees_with_keys = [c for c in all_committees if 
c.public_signing_keys]
   
       committee_choices = [(c.key, c.display_name) for c in 
committees_with_keys]
   
       rendered_form = await form.render(
           model_cls=DeleteCommitteeKeysForm,
           submit_label="Delete all keys for selected committee",
           defaults={"committee_key": committee_choices},
       )
       return await template.render(
           "admin-form.html",
           title="Delete committee keys",
           description="Delete committee keys",
           header="Delete all keys for a committee",
           form=rendered_form,
       )
   ```
   
   #### `atr/get/committees.py` — `view` (lines 35-70)
   _needs modification_
   Committee view page will likely need to display the new CI permission 
metadata.
   
   ```python
   @get.typed
   async def view(session: web.Public, _committees: Literal["committees"], 
name: safe.CommitteeKey) -> str:
       """
       URL: /committees/<name>
       """
       # TODO: Could also import this from keys.py
       async with db.session() as data:
           committee = await data.committee(
               key=str(name),
               _projects=True,
               _public_signing_keys=True,
           ).demand(base.ASFQuartException(f"Committee {name!s} not found", 
errorcode=404))
       project_list = list(committee.projects)
       committee_member = False
       if isinstance(session, web.Committer):
           committee_member = await 
session.prevent_confusing_ui_display_committee(name, False)
       for project in project_list:
           # Workaround for the usual loading problem
           project.committee = committee
       return await template.render(
           "committee-view.html",
           committee=committee,
           projects=project_list,
           algorithms=shared.algorithms,
           now=datetime.datetime.now(datetime.UTC),
           email_from_key=util.email_from_uid,
           is_committee_member=committee_member,
           update_committee_keys_form=await form.render(
               model_cls=shared.keys.UpdateCommitteeKeysForm,
               action=util.as_url(post.keys.keys),
               submit_label="Regenerate KEYS file",
               defaults={"committee_key": committee.key},
               empty=True,
           ),
           is_standing=util.committee_is_standing(committee.key),
       )
   ```
   
   #### `atr/datasources/apache.py` — `_update_tooling` (lines 506-535)
   _currently does this_
   Shows additional Committee fields (release_managers) and pattern of setting 
metadata — confirms the model already has multiple metadata fields.
   
   ```python
   async def _update_tooling(data: db.Session) -> tuple[int, int]:
       added_count = 0
       updated_count = 0
   
       # Tooling is not a committee
       # We add a special entry for Tooling, pretending to be a PMC, for 
debugging and testing
       tooling_committee = await data.committee(key="tooling").get()
       if not tooling_committee:
           tooling_committee = sql.Committee(key="tooling", name="Tooling")
           data.add(tooling_committee)
           tooling_project = sql.Project(key="tooling", name="Apache Tooling", 
committee=tooling_committee)
           data.add(tooling_project)
           added_count += 1
       else:
           updated_count += 1
   
       additional = config.get().TOOLING_USERS_ADDITIONAL
       if additional:
           extra = set(additional.split(","))
       else:
           extra = set()
   
       # Update Tooling PMC data
       tooling_users = list(await ldap.fetch_tooling_users(extra))
       tooling_committee.committee_members = tooling_users
       tooling_committee.committers = tooling_users
       tooling_committee.release_managers = tooling_users
       tooling_committee.is_podling = False
   
       return added_count, updated_count
   ```
   
   ### Where new code would go
   - `atr/models/sql.py` — within Committee class definition
     New boolean/JSON field (e.g., ci_enabled) must be added to the Committee 
SQLModel.
   - `atr/admin/__init__.py` — after delete_committee_keys_post
     New admin form class and GET/POST route pair for updating committee 
metadata.
   
   ### Proposed approach
   The implementation requires three parts:
   
   1. **Schema change**: Add a `ci_enabled: bool = False` field (or a more 
flexible JSON metadata field) to the `sql.Committee` model in 
`atr/models/sql.py`. Since SQLite with SQLModel is used, this will need a 
database migration or ALTER TABLE. The field should default to `False` so 
existing committees are unaffected.
   
   2. **Admin route**: Following the established pattern (e.g., 
`delete_committee_keys_get/post`), create a new `UpdateCommitteeMetadataForm` 
with a committee selector and a checkbox for CI permission, along with 
corresponding GET and POST admin routes. The POST handler would load the 
committee from the DB and update the `ci_enabled` field.
   
   3. **Display**: The committee view page (`atr/get/committees.py:view`) and 
template should surface this metadata for admin/PMC users.
   
   Importantly, the `_update_committees` function in 
`atr/datasources/apache.py` must NOT overwrite the manually-set `ci_enabled` 
field during metadata sync from Whimsy/LDAP.
   
   ### Suggested patches
   
   #### `atr/models/sql.py`
   Add ci_enabled field to Committee model (exact location within class 
unknown, proposed as addition)
   
   ````diff
   --- a/atr/models/sql.py
   +++ b/atr/models/sql.py
   @@ # Within the Committee class definition, add:
   +    # Whether this committee is permitted to use CI for building release 
artifacts
   +    ci_enabled: bool = sqlmodel.Field(default=False)
   ````
   
   #### `atr/admin/__init__.py`
   Add admin form and GET/POST routes for updating committee CI metadata
   
   ````diff
   --- a/atr/admin/__init__.py
   +++ b/atr/admin/__init__.py
   @@ -72,6 +72,11 @@ class DeleteCommitteeKeysForm(form.Form):
        confirm_delete: Literal["DELETE KEYS"] = form.label("Confirmation", 
"Type DELETE KEYS to confirm.")
    
    
   +class UpdateCommitteeMetadataForm(form.Form):
   +    committee_key: str = form.label("Committee", widget=form.Widget.SELECT)
   +    ci_enabled: bool = form.label("CI enabled", "Allow this committee to 
use CI for building release artifacts.")
   +
   +
    class DeleteReleaseForm(form.Form):
        releases_to_delete: form.StrList = form.label("Select releases to 
delete", widget=form.Widget.CUSTOM)
        confirm_delete: Literal["DELETE"] = form.label("Confirmation", "Type 
DELETE to confirm.")
   @@ # After delete_committee_keys_post, add new route pair:
   +
   [email protected]
   +async def committee_metadata_get(
   +    _session: web.Committer, _committee_metadata: 
Literal["committee-metadata"]
   +) -> str | web.WerkzeugResponse:
   +    """
   +    URL: GET /committee-metadata
   +
   +    Display the form to update committee metadata.
   +    """
   +    async with db.session() as data:
   +        all_committees = await 
data.committee().order_by(sql.Committee.key).all()
   +
   +    committee_choices = [(c.key, c.display_name) for c in all_committees]
   +
   +    rendered_form = await form.render(
   +        model_cls=UpdateCommitteeMetadataForm,
   +        submit_label="Update committee metadata",
   +        defaults={"committee_key": committee_choices},
   +    )
   +    return await template.render(
   +        "admin-form.html",
   +        title="Update committee metadata",
   +        description="Update committee metadata",
   +        header="Update committee metadata",
   +        form=rendered_form,
   +    )
   +
   +
   [email protected]
   +async def committee_metadata_post(
   +    session: web.Committer,
   +    _committee_metadata: Literal["committee-metadata"],
   +    metadata_form: UpdateCommitteeMetadataForm,
   +) -> str | web.WerkzeugResponse:
   +    """
   +    URL: POST /committee-metadata
   +
   +    Update metadata for a committee.
   +    """
   +    committee_key = metadata_form.committee_key
   +
   +    async with db.session() as data:
   +        async with data.begin():
   +            committee = await data.committee(key=committee_key).get()
   +            if not committee:
   +                await quart.flash(f"Committee '{committee_key}' not 
found.", "error")
   +                return await session.redirect(committee_metadata_get)
   +            committee.ci_enabled = metadata_form.ci_enabled
   +
   +    await quart.flash(
   +        f"Updated metadata for '{committee_key}': CI {'enabled' if 
metadata_form.ci_enabled else 'disabled'}.",
   +        "success",
   +    )
   +    return await session.redirect(committee_metadata_get)
   ````
   
   ### Open questions
   - The exact structure of the Committee model in atr/models/sql.py is not 
visible — need to confirm where to add the field and whether a database 
migration strategy (Alembic or manual ALTER TABLE) is needed.
   - Should `ci_enabled` be a simple boolean, or should the metadata be more 
structured (e.g., a JSON field allowing specification of which CI systems, 
repositories, or workflows are permitted)?
   - Should the form also allow configuring other future metadata fields, or 
should each field get its own admin route?
   - How does the `ci_enabled` flag interact with the GitHub Actions OIDC 
trusted publishing flow in atr/ssh.py and atr/models/github.py — should those 
check this flag before allowing CI-based releases?
   
   ### Files examined
   - `atr/admin/__init__.py`
   - `atr/blueprints/admin.py`
   - `atr/datasources/apache.py`
   - `atr/get/committees.py`
   
   ---
   *Draft from a triage agent. A human reviewer should validate before merging 
any change. The agent did not run tests or verify diffs apply.*


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to