You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tooling.apache.org by "asf-tooling (via GitHub)" <gi...@apache.org> on 2026/05/08 20:37:53 UTC

Re: [I] Add committee metadata along with admin updates (tooling-trusted-releases)

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:
   +
   +@admin.typed
   +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,
   +    )
   +
   +
   +@admin.typed
   +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: dev-unsubscribe@tooling.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tooling.apache.org
For additional commands, e-mail: dev-help@tooling.apache.org