Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/cla-close-stale.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Close Stale Unsigned CLA PRs

on:
schedule:
# Runs daily at 01:30 UTC.
- cron: '30 1 * * *'
workflow_dispatch: {}

permissions:
contents: read
pull-requests: write
issues: write

# Configurable intervals (days).
# INTERVAL_1 = grace period before the warning comment.
# INTERVAL_2 = further period after the warning before closing.
env:
INTERVAL_1: 14
INTERVAL_2: 14

jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Close stale unsigned-CLA PRs
uses: actions/stale@v9
with:
# ---- Guards: only act on PRs carrying the CLA label ----
only-labels: 'awaiting cla'

# Never touch issues — PRs only.
days-before-issue-stale: -1
days-before-issue-close: -1

# ---- Timing ----
# INTERVAL_1: days of inactivity before the warning comment.
days-before-pr-stale: ${{ env.INTERVAL_1 }}
# INTERVAL_2: days after being marked stale before closing.
days-before-pr-close: ${{ env.INTERVAL_2 }}

# ---- Warning comment (posted once when marked stale) ----
stale-pr-message: >
This PR will be automatically closed if the CLA is not signed
within ${{ env.INTERVAL_2 }} days.

# ---- Close comment ----
close-pr-message: >
This PR has been automatically closed as the CLA has not been
signed. We are happy to have it reopened if the CLA is signed
subsequently.

# A dedicated marker label so we can track stale state without
# interfering with the "awaiting cla" label.
stale-pr-label: 'cla-stale'

# If the PR is updated after being marked stale, remove the marker
# so the warning-then-close cycle restarts cleanly.
remove-pr-stale-when-updated: true

# Process enough PRs per run for busy repos.
operations-per-run: 200
87 changes: 87 additions & 0 deletions .github/workflows/cla-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CLA Label Sync

on:
issue_comment:
types: [created, edited]
pull_request_target:
types: [opened, synchronize, reopened]

permissions:
pull-requests: write
issues: write
contents: read

jobs:
sync-label:
# Only run for PRs (issue_comment fires for issues too)
if: >-
github.event_name == 'pull_request_target' ||
(github.event.issue.pull_request != null)
runs-on: ubuntu-latest
steps:
- name: Sync "awaiting cla" label
uses: actions/github-script@v7
env:
AWAITING_LABEL: 'awaiting cla'
# Bot login that posts the CLA comment. Common values:
# 'github-actions[bot]', 'CLAassistant', 'cla-assistant[bot]'
CLA_BOT_LOGINS: 'CLAassistant,cla-assistant[bot],github-actions[bot]'
# Regex (case-insensitive) that matches an UNSIGNED CLA comment
NOT_SIGNED_REGEX: 'have not signed|has not signed|please sign|we need.*sign|\[ \]'
# Regex (case-insensitive) that matches a SIGNED CLA comment
SIGNED_REGEX: 'all committers have signed|all contributors have signed|has signed the cla|have signed the cla'
with:
script: |
const { AWAITING_LABEL } = process.env;
const botLogins = process.env.CLA_BOT_LOGINS.split(',').map(s => s.trim().toLowerCase());
const notSigned = new RegExp(process.env.NOT_SIGNED_REGEX, 'i');
const signed = new RegExp(process.env.SIGNED_REGEX, 'i');

// Resolve PR number for either trigger
const prNumber = context.eventName === 'pull_request_target'
? context.payload.pull_request.number
: context.payload.issue.number;

const { owner, repo } = context.repo;

// Pull the full comment history to find the latest CLA bot comment
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber, per_page: 100,
});

const claComments = comments.filter(c =>
botLogins.includes((c.user?.login || '').toLowerCase()) &&
(notSigned.test(c.body) || signed.test(c.body))
);

if (claComments.length === 0) {
core.info('No CLA Assistant comment found yet; nothing to do.');
return;
}

const latest = claComments[claComments.length - 1];
const isSigned = signed.test(latest.body) && !notSigned.test(latest.body);

core.info(`Latest CLA comment (id ${latest.id}) => signed=${isSigned}`);

// Current labels
const { data: issue } = await github.rest.issues.get({
owner, repo, issue_number: prNumber,
});
const hasLabel = issue.labels.some(l =>
(typeof l === 'string' ? l : l.name) === AWAITING_LABEL
);

if (isSigned && hasLabel) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: AWAITING_LABEL,
}).catch(e => core.warning(`removeLabel failed: ${e.message}`));
core.info(`Removed "${AWAITING_LABEL}".`);
} else if (!isSigned && !hasLabel) {
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: [AWAITING_LABEL],
});
core.info(`Added "${AWAITING_LABEL}".`);
} else {
core.info('Label already in the correct state.');
}
Loading