This is an automated email from the ASF dual-hosted git repository.
skrawcz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/burr.git
The following commit(s) were added to refs/heads/main by this push:
new 30d74e91 ci: repo governance workflows and assignment policy (#713)
30d74e91 is described below
commit 30d74e91e39a7ff3c4b76f1c3e6f4a50f9428002
Author: André Ahlert <[email protected]>
AuthorDate: Fri Apr 10 02:09:40 2026 -0300
ci: repo governance workflows and assignment policy (#713)
* ci: add stale PR lifecycle workflow
Weekly cron job (Mondays 09:00 UTC) that enforces the PR
lifecycle policy defined in #690:
- 14 days after review with no author response: label pr/stale
and convert to draft
- 90 days with no activity: close with comment
- Skip PRs with lifecycle/frozen, pr/do-not-merge, or from
dependabot
Closes #695
Signed-off-by: André Ahlert <[email protected]>
* ci: add auto-labeling for new issues and PRs
- New issues automatically get `status/needs-triage`
- PRs get `area/*` labels based on changed files via actions/labeler
- PRs get `pr/needs-rebase` when they have merge conflicts (auto-removed
when resolved)
Labeler config maps repo paths to area labels: core, ui, storage,
streaming, hooks, tracking, integrations, visualization, website,
ci, examples, typing.
Closes #696
Signed-off-by: André Ahlert <[email protected]>
* ci: add issue assignment policy and stale assignment workflow
Documents the assignment policy in CONTRIBUTING.rst:
- Assignee = actively working
- 14 days without activity: warning comment
- 21 days total: unassign + add help wanted
- lifecycle/frozen issues are exempt
Adds weekly workflow (Wednesdays 09:00 UTC) to enforce it
automatically.
Closes #709
Signed-off-by: André Ahlert <[email protected]>
---------
Signed-off-by: André Ahlert <[email protected]>
---
.github/labeler.yml | 77 +++++++++++++++
.github/workflows/auto-label.yml | 111 +++++++++++++++++++++
.github/workflows/stale-assignments.yml | 170 ++++++++++++++++++++++++++++++++
.github/workflows/stale-prs.yml | 167 +++++++++++++++++++++++++++++++
CONTRIBUTING.rst | 18 ++++
5 files changed, 543 insertions(+)
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 00000000..cf99246d
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,77 @@
+# Auto-labeling config for actions/labeler
+# Maps file path patterns to area/* labels for PRs
+# Part of #696
+
+"area/core":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/core/**
+ - burr/lifecycle/**
+ - burr/system.py
+
+"area/ui":
+ - changed-files:
+ - any-glob-to-any-file:
+ - telemetry/ui/**
+
+"area/storage":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/core/persistence.py
+ - burr/integrations/persisters/**
+ - burr/tracking/server/s3/**
+
+"area/streaming":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/core/parallelism.py
+
+"area/hooks":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/lifecycle/**
+
+"area/tracking":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/tracking/**
+ - burr/telemetry.py
+ - burr/visibility/**
+
+"area/integrations":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/integrations/**
+
+"area/visualization":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/visibility/**
+
+"area/website":
+ - changed-files:
+ - any-glob-to-any-file:
+ - website/**
+ - docs/**
+
+"area/ci":
+ - changed-files:
+ - any-glob-to-any-file:
+ - .github/**
+ - scripts/**
+ - .pre-commit-config.yaml
+ - .rat-excludes
+ - pyproject.toml
+ - setup.cfg
+
+"area/examples":
+ - changed-files:
+ - any-glob-to-any-file:
+ - examples/**
+ - burr/examples/**
+
+"area/typing":
+ - changed-files:
+ - any-glob-to-any-file:
+ - burr/core/typing.py
+ - burr/integrations/pydantic.py
diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml
new file mode 100644
index 00000000..c6f85d2e
--- /dev/null
+++ b/.github/workflows/auto-label.yml
@@ -0,0 +1,111 @@
+#<!--
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#-->
+# Auto-labeling for new issues and PRs
+# Part of #690 (repo governance) and #696 (auto-labeling)
+#
+# - New issues get status/needs-triage
+# - PRs get area/* labels based on changed files
+# - PRs get pr/needs-rebase when they have merge conflicts
+#
+# Security: this workflow only uses numeric IDs from context (issue_number,
+# pull_request.number). No untrusted string input is interpolated.
+
+name: Auto-label
+
+on:
+ issues:
+ types: [opened]
+ pull_request_target:
+ types: [opened, synchronize]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ triage-issues:
+ if: github.event_name == 'issues'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Add status/needs-triage to new issues
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: ['status/needs-triage']
+ });
+
+ label-prs:
+ if: github.event_name == 'pull_request_target'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Apply area/* labels based on changed files
+ uses: actions/labeler@v5
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ configuration-path: .github/labeler.yml
+ sync-labels: false
+
+ check-mergeable:
+ if: github.event_name == 'pull_request_target'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check for merge conflicts
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const LABEL = 'pr/needs-rebase';
+
+ // Wait for GitHub to compute mergeability
+ await new Promise(r => setTimeout(r, 5000));
+
+ const { data: pr } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.payload.pull_request.number,
+ });
+
+ const labels = pr.labels.map(l => l.name);
+ const hasLabel = labels.includes(LABEL);
+
+ if (pr.mergeable === false && !hasLabel) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: [LABEL],
+ });
+ console.log(`#${pr.number}: added ${LABEL}`);
+ } else if (pr.mergeable === true && hasLabel) {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ name: LABEL,
+ });
+ console.log(`#${pr.number}: removed ${LABEL}`);
+ } else {
+ console.log(`#${pr.number}: no change
(mergeable=${pr.mergeable}, hasLabel=${hasLabel})`);
+ }
diff --git a/.github/workflows/stale-assignments.yml
b/.github/workflows/stale-assignments.yml
new file mode 100644
index 00000000..d418958e
--- /dev/null
+++ b/.github/workflows/stale-assignments.yml
@@ -0,0 +1,170 @@
+#<!--
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#-->
+# Stale issue assignment check
+# Part of #690 (repo governance) and #709 (assignment policy)
+#
+# Policy (documented in CONTRIBUTING.rst):
+# - 14 days without activity on an assigned issue -> comment asking for
update
+# - 21 days total -> unassign + add help wanted
+# - Issues with lifecycle/frozen are exempt
+#
+# Security: only uses numeric IDs and login names from the GitHub API.
+# No untrusted string input is interpolated into shell commands.
+
+name: Stale Assignments
+
+on:
+ schedule:
+ - cron: "0 9 * * 3" # Every Wednesday at 09:00 UTC
+ workflow_dispatch:
+
+permissions:
+ issues: write
+
+jobs:
+ check-assignments:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check for stale assignments
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const WARN_DAYS = 14;
+ const UNASSIGN_DAYS = 21;
+ const SKIP_LABELS = ['lifecycle/frozen'];
+ const now = new Date();
+
+ let page = 1;
+ let allIssues = [];
+
+ // Paginate through all open issues
+ while (true) {
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ per_page: 100,
+ page: page,
+ });
+ if (issues.length === 0) break;
+ allIssues = allIssues.concat(issues);
+ page++;
+ }
+
+ for (const issue of allIssues) {
+ // Skip PRs (they show up in the issues API)
+ if (issue.pull_request) continue;
+
+ // Skip unassigned
+ if (!issue.assignees || issue.assignees.length === 0) continue;
+
+ const labels = issue.labels.map(l => l.name);
+
+ // Skip protected issues
+ if (SKIP_LABELS.some(skip => labels.includes(skip))) {
+ console.log(`#${issue.number}: skipped (protected)`);
+ continue;
+ }
+
+ const updatedAt = new Date(issue.updated_at);
+ const daysSinceUpdate = Math.floor((now - updatedAt) / (1000 *
60 * 60 * 24));
+ const assigneeLogins = issue.assignees.map(a => a.login);
+
+ // 21+ days -> unassign and add help wanted
+ if (daysSinceUpdate >= UNASSIGN_DAYS) {
+ const names = assigneeLogins.map(l => `@${l}`).join(', ');
+
+ console.log(`#${issue.number}: unassigning ${names}
(${daysSinceUpdate} days)`);
+
+ const unassignMsg = [
+ `Removing assignment from ${names} after ${daysSinceUpdate}
days of inactivity`,
+ '(per the [assignment
policy](../blob/main/CONTRIBUTING.rst#issue-assignment-policy)).',
+ '',
+ 'Feel free to comment if you want to pick this back up or if
someone else wants to take it.',
+ ].join('\n');
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body: unassignMsg,
+ });
+
+ await github.rest.issues.removeAssignees({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ assignees: assigneeLogins,
+ });
+
+ if (!labels.includes('help wanted')) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ labels: ['help wanted'],
+ });
+ }
+
+ continue;
+ }
+
+ // 14+ days -> warn
+ if (daysSinceUpdate >= WARN_DAYS) {
+ // Check if we already warned (avoid spamming)
+ const { data: comments } = await
github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ per_page: 5,
+ });
+
+ const recentWarn = comments.some(c =>
+ c.user.login === 'github-actions[bot]' &&
+ c.body.includes('assignment policy') &&
+ (now - new Date(c.created_at)) / (1000 * 60 * 60 * 24) < 10
+ );
+
+ if (recentWarn) {
+ console.log(`#${issue.number}: skipped (already warned
recently)`);
+ continue;
+ }
+
+ const names = assigneeLogins.map(l => `@${l}`).join(', ');
+
+ console.log(`#${issue.number}: warning ${names}
(${daysSinceUpdate} days)`);
+
+ const warnMsg = [
+ `${names}, this issue has been inactive for
${daysSinceUpdate} days.`,
+ 'Are you still working on it? Drop a comment to let us
know.',
+ '',
+ 'If there is no update within 7 days, the assignment will be
removed',
+ 'so someone else can pick it up',
+ '(per the [assignment
policy](../blob/main/CONTRIBUTING.rst#issue-assignment-policy)).',
+ ].join('\n');
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body: warnMsg,
+ });
+ }
+ }
diff --git a/.github/workflows/stale-prs.yml b/.github/workflows/stale-prs.yml
new file mode 100644
index 00000000..b1e7bc9f
--- /dev/null
+++ b/.github/workflows/stale-prs.yml
@@ -0,0 +1,167 @@
+#<!--
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#-->
+# Stale PR lifecycle management
+# Part of #690 (repo governance) and #695 (stale PR policy)
+#
+# Policy:
+# - 14 days after review with no author response -> pr/stale + convert to
draft
+# - 90 days with no activity at all -> close with comment
+# - PRs with lifecycle/frozen or pr/do-not-merge are always skipped
+
+name: Stale PR Lifecycle
+
+on:
+ schedule:
+ - cron: "0 9 * * 1" # Every Monday at 09:00 UTC
+ workflow_dispatch: # Allow manual trigger
+
+permissions:
+ pull-requests: write
+ issues: write
+
+jobs:
+ stale-prs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Mark stale and close inactive PRs
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const STALE_DAYS = 14;
+ const CLOSE_DAYS = 90;
+ const SKIP_LABELS = ['lifecycle/frozen', 'pr/do-not-merge'];
+ const STALE_LABEL = 'pr/stale';
+
+ const now = new Date();
+
+ const { data: prs } = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ sort: 'updated',
+ direction: 'asc',
+ per_page: 100,
+ });
+
+ for (const pr of prs) {
+ const labels = pr.labels.map(l => l.name);
+
+ // Skip protected PRs
+ if (SKIP_LABELS.some(skip => labels.includes(skip))) {
+ console.log(`#${pr.number}: skipped (protected label)`);
+ continue;
+ }
+
+ // Skip dependabot PRs
+ if (pr.user.login === 'dependabot[bot]') {
+ console.log(`#${pr.number}: skipped (dependabot)`);
+ continue;
+ }
+
+ const updatedAt = new Date(pr.updated_at);
+ const daysSinceUpdate = Math.floor((now - updatedAt) / (1000 *
60 * 60 * 24));
+
+ // 90+ days no activity -> close
+ if (daysSinceUpdate >= CLOSE_DAYS) {
+ const closeMsg = [
+ `This PR has had no activity for ${daysSinceUpdate} days.`,
+ 'Closing to keep the PR list manageable.',
+ '',
+ 'Feel free to reopen when you are ready to continue.',
+ 'If the branch has conflicts, a fresh PR against `main`
might be easier.',
+ ].join('\n');
+
+ console.log(`#${pr.number}: closing (${daysSinceUpdate} days
inactive)`);
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ body: closeMsg,
+ });
+
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: pr.number,
+ state: 'closed',
+ });
+
+ continue;
+ }
+
+ // 14+ days since last review with no author response -> stale +
draft
+ if (daysSinceUpdate >= STALE_DAYS &&
!labels.includes(STALE_LABEL)) {
+ // Check if there's a review that the author hasn't responded
to
+ const { data: reviews } = await github.rest.pulls.listReviews({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: pr.number,
+ });
+
+ const hasUnaddressedReview = reviews.some(r =>
+ r.user.login !== pr.user.login &&
+ r.state !== 'APPROVED' &&
+ r.state !== 'DISMISSED'
+ );
+
+ if (!hasUnaddressedReview) {
+ console.log(`#${pr.number}: skipped (no unaddressed
review)`);
+ continue;
+ }
+
+ console.log(`#${pr.number}: marking stale (${daysSinceUpdate}
days, unaddressed review)`);
+
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: [STALE_LABEL],
+ });
+
+ // Convert to draft via GraphQL
+ try {
+ await github.graphql(`
+ mutation($id: ID!) {
+ convertPullRequestToDraft(input: { pullRequestId: $id })
{
+ pullRequest { id }
+ }
+ }
+ `, { id: pr.node_id });
+ } catch (e) {
+ console.log(`#${pr.number}: could not convert to draft
(${e.message})`);
+ }
+
+ const staleMsg = [
+ `This PR has been inactive for ${daysSinceUpdate} days after
receiving review feedback.`,
+ 'Converting to draft.',
+ '',
+ 'Please mark it as ready for review when you have addressed
the comments.',
+ 'If no activity occurs within 90 days, it will be closed
automatically.',
+ ].join('\n');
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ body: staleMsg,
+ });
+ }
+ }
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index db48538f..1dd3f62c 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -41,6 +41,24 @@ Please:
#. Ensure all new features have tests
#. Add documentation for new features
+-----------------------
+Issue assignment policy
+-----------------------
+
+Assigning yourself to an issue signals that you are actively working on it.
+This applies equally to maintainers, committers, and external contributors.
+
+- **Only assign yourself** if you have a PR open or are about to start coding.
+- **14 days without visible activity** (PR, commit, or comment): a triager will
+ comment asking for a status update.
+- **21 days total without response**: the assignee is removed and ``help
wanted``
+ is added so someone else can pick it up.
+- **Re-assignment is welcome.** If you want to take over, comment on the issue.
+- **Umbrella and tracking issues** marked with ``lifecycle/frozen`` are exempt.
+
+This is enforced by a weekly automated check. If you need more time, just drop
a
+comment on the issue to reset the clock.
+
---------------
Developer notes