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

Reply via email to