Classify gov changes by hashtag, not topic

Gerrit hashtags are additive and support multiple tags per
change, making them better suited for classifying governance
changes. Older versions of git-review set the local branch
name as the topic on each push, causing topic mangling — an
issue that persists if submitters haven't updated their
git-review package.

See discussion on #openstack-tc [1].

[1] https://meetings.opendev.org/irclogs/%23openstack-tc/%23openstack-tc.2026-03-24.log.html

Change-Id: Ib585ab64b7c4b53025b407828c6da5e040d89c1a
Signed-off-by: Goutham Pacha Ravi <gouthampravi@gmail.com>
This commit is contained in:
Goutham Pacha Ravi
2026-03-23 13:40:59 -07:00
parent 86f273af38
commit d49ef47a5c
5 changed files with 50 additions and 31 deletions
+7 -4
View File
@@ -91,13 +91,16 @@ Status & Tracking
OR
:Etherpad: <put the link to etherpad where progress is tracked>
Gerrit Topic
------------
Gerrit Tracking
---------------
To facilitate tracking, commits related to this goal should use the
gerrit topic::
gerrit hashtag or topic::
<insert topic here>
<insert hashtag here>
Hashtags are preferred over topics because they are additive and
support multiple tags per change.
References
==========
+2 -2
View File
@@ -118,7 +118,7 @@ votes (ties mean the motion is rejected), and a minimum of positive votes of at
least one third of the total number of TC members (rounded up: in a 13-member
committee that means a minimum of 5 approvers).
Patches with motions should use the gerrit topic tag ``formal-vote``.
Patches with motions should use the gerrit hashtag ``formal-vote``.
Election for PTL seats
======================
@@ -327,5 +327,5 @@ motion, which needs to be approved by the affirmative vote of at least
two-thirds of the total number of TC members (rounded up: in a 13-member
committee that means a minimum of 9 approvers).
Patches with charter amendments should use the gerrit topic tag
Patches with charter amendments should use the gerrit hashtag
``charter-change``.
+9 -9
View File
@@ -22,7 +22,7 @@ by other means.
Typo fixes
----------
:Gerrit topic: ``typo-fix``
:Gerrit hashtag: ``typo-fix``
When the change fixes content that is obviously wrong (updates a PTL email
address, fixes a typo...) then any TC member (who is not the proposer) can
@@ -31,7 +31,7 @@ directly approve them.
Community-wide goal proposals
-----------------------------
:Gerrit topic: ``goal-proposal``
:Gerrit hashtag: ``goal-proposal``
The `process for choosing community goals`_ has two stages relevant to Gerrit
changes: defining goal proposals and selecting goals for a cycle. For changes
@@ -51,7 +51,7 @@ requires a formal-vote.
Code changes
------------
:Gerrit topic: ``code-change``
:Gerrit hashtag: ``code-change``
The `openstack/governance` repository also contains code to build and publish
pages on the governance.openstack.org website. For those we apply the normal
@@ -63,7 +63,7 @@ point.
Documentation changes
---------------------
:Gerrit topic: ``documentation-change``
:Gerrit hashtag: ``documentation-change``
The `openstack/governance` repository also contains documentation
related to internal operations of the TC but that does not represent
@@ -77,7 +77,7 @@ than the change owner) are posted (and no `RollCall-1`).
Election Results
----------------
:Gerrit topic: ``election-results``
:Gerrit hashtag: ``election-results``
The results of elections are documented in the `openstack/governance`
repository, but are not subject to "review" or "approval" by the TC,
@@ -92,7 +92,7 @@ RollCall votes being considered +-2): change will be approved once 2
Delegated metadata
------------------
:Gerrit topic: ``release-management``
:Gerrit hashtag: ``release-management``
The ``release-management`` setting for a deliverable is delegated to
the PTL of the Release Management team. When proposed or approved by
@@ -101,9 +101,9 @@ the PTL, changes can be directly approved by the chair.
Other project team updates
--------------------------
:Gerrit topic: ``project-update``
:Gerrit hashtag: ``project-update``
This topic is used for other changes within an existing project team, like
This hashtag is used for other changes within an existing project team, like
addition of a new git repository or retirement of an existing repository
maintained by a project team.
@@ -130,7 +130,7 @@ project, they can propose a revert which would then be discussed by our usual
Goal Updates from PTLs
----------------------
:Gerrit topic: ``goal-update``
:Gerrit hashtag: ``goal-update``
PTLs will acknowledge community-wide goals at the start of each cycle
by providing links to artifacts for tracking the work, or an
+2 -2
View File
@@ -25,7 +25,7 @@ TC Chair Election
* In case of single nomination, no election is needed and candidate can propose
a patch to the member list to add "chair" status next to their name and use
`election-results` as gerrit topic name.
`election-results` as gerrit hashtag.
* If there are multiple nominations, select a TC member who is not running for
the TC chair to establish a CIVS poll to choose the single winner.
@@ -37,4 +37,4 @@ TC Chair Election
voted.
* Winner will propose a patch to the member list to add "chair" status next to
their name and use `election-results` as gerrit topic name.
their name and use `election-results` as gerrit hashtag.
+30 -14
View File
@@ -187,8 +187,25 @@ def all_changes():
break
KNOWN_CATEGORIES = {
'on-hold',
'formal-vote',
'charter-change',
'goal-proposal',
'code-change',
'documentation-change',
'election-results',
'typo-fix',
'project-update',
'new-project',
'goal-update',
}
def get_one_status(change, delegates, tc_members):
topic = change.get('topic', 'unknown topic')
hashtags = change.get('hashtags', [])
matching = [h for h in hashtags if h in KNOWN_CATEGORIES | delegates.keys()]
hashtag = matching[0] if matching else 'Missing hashtag'
subject = change.get('subject')
owner = change.get('owner', {}).get('name')
url = 'https://review.opendev.org/{}\n'.format(change['_number'])
@@ -216,10 +233,10 @@ def get_one_status(change, delegates, tc_members):
can_approve = 'NO, verification failure'
earliest = 'when passing'
elif topic == 'on-hold':
elif hashtag == 'on-hold':
can_approve = 'on hold'
elif topic == 'formal-vote':
elif hashtag == 'formal-vote':
# https://governance.openstack.org/tc/reference/charter.html#motions
parts = []
@@ -274,7 +291,7 @@ def get_one_status(change, delegates, tc_members):
can_approve = ',\n'.join(parts)
elif topic == 'charter-change':
elif hashtag == 'charter-change':
# https://governance.openstack.org/tc/reference/charter.html#amendment
parts = []
@@ -314,7 +331,7 @@ def get_one_status(change, delegates, tc_members):
can_approve = ',\n'.join(parts)
elif topic in (
elif hashtag in (
'goal-proposal',
'code-change',
'documentation-change',
@@ -336,9 +353,9 @@ def get_one_status(change, delegates, tc_members):
else:
can_approve = 'CAN APPROVE'
elif topic in delegates.keys():
elif hashtag in delegates.keys():
# https://governance.openstack.org/tc/reference/house-rules.html#delegated-metadata
approver_name = delegates[topic]
approver_name = delegates[hashtag]
can_approve = 'delegated to {}'.format(approver_name)
if has_approved(approver_name, change):
can_approve += '\nYES'
@@ -347,7 +364,7 @@ def get_one_status(change, delegates, tc_members):
elif has_commented(approver_name, change):
can_approve += '\ndelegate has commented'
elif topic in ('project-update', 'new-project'):
elif hashtag in ('project-update', 'new-project'):
# https://governance.openstack.org/tc/reference/house-rules.html#other-project-team-updates
if votes[-1] or code_reviews[-1]:
@@ -357,7 +374,7 @@ def get_one_status(change, delegates, tc_members):
else:
can_approve = 'CAN APPROVE'
elif topic == 'goal-update':
elif hashtag == 'goal-update':
# https://governance.openstack.org/tc/reference/house-rules.html#goal-updates-from-ptls
# At least 7 days old.
@@ -371,8 +388,7 @@ def get_one_status(change, delegates, tc_members):
can_approve = 'CAN APPROVE'
else:
topic = 'unknown topic'
can_approve = 'unknown topic'
can_approve = ''
votes = '\n'.join([
'CR:' + format_votes(code_reviews),
@@ -396,7 +412,7 @@ def get_one_status(change, delegates, tc_members):
)
return {
'Topic': topic,
'Hashtag': hashtag,
'Subject': subject,
'Summary': '\n'.join([
subject.strip(),
@@ -409,7 +425,7 @@ def get_one_status(change, delegates, tc_members):
'Age': age.days,
'Date': latest_created.date(),
'Can Approve': can_approve,
'Status': '\n'.join([topic, can_approve,
'Status': '\n'.join([hashtag, can_approve,
'{} days old'.format(age.days),
'earliest: {}'.format(earliest)]),
'Earliest': earliest,
@@ -451,7 +467,7 @@ def main():
'release-management': release_team.ptl['name'],
}
for tag, name in sorted(delegates.items()):
print('Delegating {} tags to {}'.format(tag, name))
print('Delegating {} hashtag to {}'.format(tag, name))
status = sorted(
(get_one_status(change, delegates, tc_members)