From aee8a4d45263089459ad5efd58cf38514f399ee5 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:15:24 +0100 Subject: [PATCH 01/12] docs: define official Drips contribution flow --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f46667b..4fa6220 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,22 @@ Thank you for contributing to StellarForge CLI. The project is being built as pr 5. Ask for clarification in the issue before making architecture-expanding assumptions. 6. Do not disclose suspected vulnerabilities in public issues; follow `SECURITY.md`. +## Drips Stellar Wave Contributions + +Stellar Wave assignments are managed through the Drips Wave application, not by first-come-first-served GitHub comments. + +1. Complete the Drips participant requirements and apply for the issue in Drips. +2. Wait until a maintainer accepts the application and the contributor is assigned on GitHub. +3. Do not begin substantial work before official assignment. +4. Work on one active StellarForge issue at a time unless a maintainer explicitly approves otherwise. +5. Open one focused pull request that uses exactly one closing reference, for example `Closes #123`. +6. Post a progress update if no commit or pull-request update is visible for 48 hours. +7. Respond promptly to review feedback so the contribution can be resolved during the active Wave. + +The `/claim` and `/unclaim` comments do not create or release Drips assignments. They only return guidance to use the official Drips process. A GitHub issue becomes active in the Stellar Wave program only when a maintainer explicitly adds it through Drips or authorizes the exact `Stellar Wave` label. + +Maintainers follow `docs/contributing/drips-wave-operations.md` for activation, application review, assignment, stale-work escalation, PR review, and resolution. + ## Development Workflow 1. Fork or clone the repository. From 5a524df3c2ee2d46bb43c07edaa88ed74e0fae0e Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:15:34 +0100 Subject: [PATCH 02/12] ci: route Wave claims through Drips --- .github/workflows/wave-claim.yml | 59 +++++++------------------------- 1 file changed, 13 insertions(+), 46 deletions(-) diff --git a/.github/workflows/wave-claim.yml b/.github/workflows/wave-claim.yml index 7dcf61c..75cb59e 100644 --- a/.github/workflows/wave-claim.yml +++ b/.github/workflows/wave-claim.yml @@ -1,4 +1,4 @@ -name: Wave Claim +name: Wave Application Guidance on: issue_comment: @@ -9,13 +9,13 @@ permissions: issues: write jobs: - claim: - name: Process contributor claim command + guidance: + name: Direct claim commands to Drips if: github.event.issue.pull_request == null runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Claim or release Wave issue + - name: Explain the official assignment process uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | @@ -26,61 +26,28 @@ jobs: const command = (context.payload.comment.body || '').trim().toLowerCase(); if (!['/claim', '/unclaim'].includes(command)) return; - const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: issueNumber }); + const { data: issue } = await github.rest.issues.get({ + owner, repo, issue_number: issueNumber, + }); const labels = new Set(issue.labels.map((label) => label.name)); - if (!labels.has('Stellar Wave')) { - core.setFailed('Claim commands are limited to Stellar Wave issues.'); - return; - } - if (command === '/claim') { - if (!labels.has('intake: ready') || labels.has('blocked')) { - await github.rest.issues.createComment({ - owner, repo, issue_number: issueNumber, - body: 'This issue is not currently open for claims. Please choose an issue labeled intake: ready.', - }); - return; - } - if (issue.assignees.length > 0 && !issue.assignees.some((a) => a.login === actor)) { - await github.rest.issues.createComment({ - owner, repo, issue_number: issueNumber, - body: 'This issue is already assigned. Please choose another ready issue.', - }); - return; - } - await github.rest.issues.addAssignees({ owner, repo, issue_number: issueNumber, assignees: [actor] }); - await github.rest.issues.addLabels({ owner, repo, issue_number: issueNumber, labels: ['status: in progress'] }); - for (const name of ['help wanted', 'status: stale']) { - try { - await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name }); - } catch (error) { - if (error.status !== 404) throw error; - } - } + if (!labels.has('Stellar Wave')) { await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, - body: 'Assigned to @' + actor + '. Keep the PR focused on this issue, link it in the PR body, and follow the frozen contributor architecture.', + body: '@' + actor + ', this issue is not currently active in the Drips Stellar Wave program. Please wait for a maintainer to activate the issue before applying.', }); return; } - if (!issue.assignees.some((a) => a.login === actor)) { + if (command === '/claim') { await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, - body: '@' + actor + ', you can only unclaim an issue currently assigned to you.', + body: '@' + actor + ', Stellar Wave issues are assigned through the Drips application process, not by GitHub self-claim. Please apply for this issue in Drips and wait for maintainer acceptance and GitHub assignment before starting substantial work.', }); return; } - await github.rest.issues.removeAssignees({ owner, repo, issue_number: issueNumber, assignees: [actor] }); - try { - await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name: 'status: in progress' }); - } catch (error) { - if (error.status !== 404) throw error; - } - await github.rest.issues.addLabels({ - owner, repo, issue_number: issueNumber, labels: ['help wanted', 'intake: ready'], - }); + await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, - body: '@' + actor + ' released this issue. It is available for another contributor.', + body: '@' + actor + ', release or withdraw your assignment through Drips and notify the maintainer. This command does not automatically change the official Drips or GitHub assignment.', }); From df53e50bcf51dc5589746d7f03022054d82cbc7b Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:15:49 +0100 Subject: [PATCH 03/12] ci: use Wave-length stale escalation --- .github/workflows/wave-stale-prs.yml | 95 +++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/.github/workflows/wave-stale-prs.yml b/.github/workflows/wave-stale-prs.yml index 2573ca5..07dbf5e 100644 --- a/.github/workflows/wave-stale-prs.yml +++ b/.github/workflows/wave-stale-prs.yml @@ -8,7 +8,11 @@ on: permissions: contents: read issues: write - pull-requests: write + pull-requests: read + +concurrency: + group: wave-stale-review + cancel-in-progress: false jobs: stale: @@ -16,7 +20,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Warn and close stale Wave PRs + - name: Warn and escalate stale Wave PRs uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | @@ -24,39 +28,100 @@ jobs: const repo = context.repo.repo; const now = Date.now(); const DAY = 24 * 60 * 60 * 1000; - const WARN_AFTER = 7 * DAY; - const CLOSE_AFTER = 21 * DAY; - const pulls = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 }); + const WARN_AFTER = 2 * DAY; + const ESCALATE_AFTER = 4 * DAY; + const WARNING_MARKER = ''; + const ESCALATION_MARKER = ''; + const pulls = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100, + }); for (const pr of pulls) { - if (pr.draft) continue; - const age = now - new Date(pr.updated_at).getTime(); const labels = new Set(pr.labels.map((label) => label.name)); if (labels.has('do-not-close')) continue; - const ref = (pr.body || '').match(/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/i); + const ref = (pr.body || '').match( + /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/i, + ); if (!ref) continue; + let issue; try { - ({ data: issue } = await github.rest.issues.get({ owner, repo, issue_number: Number(ref[1]) })); + ({ data: issue } = await github.rest.issues.get({ + owner, repo, issue_number: Number(ref[1]), + })); } catch { continue; } if (!issue.labels.some((label) => label.name === 'Stellar Wave')) continue; - if (age >= CLOSE_AFTER) { + const [commits, comments] = await Promise.all([ + github.paginate(github.rest.pulls.listCommits, { + owner, repo, pull_number: pr.number, per_page: 100, + }), + github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: pr.number, per_page: 100, + }), + ]); + + const activityDates = [new Date(pr.created_at).getTime()]; + for (const commit of commits) { + const date = commit.commit?.committer?.date || commit.commit?.author?.date; + if (date) activityDates.push(new Date(date).getTime()); + } + for (const comment of comments) { + if (comment.user?.login === pr.user.login) { + activityDates.push(new Date(comment.created_at).getTime()); + } + } + + const lastContributorActivity = Math.max(...activityDates); + const inactiveFor = now - lastContributorActivity; + const workflowComments = comments.filter((comment) => + [WARNING_MARKER, ESCALATION_MARKER].some((marker) => + (comment.body || '').includes(marker), + ), + ); + const warnedSinceActivity = workflowComments.some((comment) => + (comment.body || '').includes(WARNING_MARKER) && + new Date(comment.created_at).getTime() > lastContributorActivity, + ); + const escalatedSinceActivity = workflowComments.some((comment) => + (comment.body || '').includes(ESCALATION_MARKER) && + new Date(comment.created_at).getTime() > lastContributorActivity, + ); + + if (inactiveFor < WARN_AFTER) { + if (labels.has('status: stale')) { + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr.number, name: 'status: stale', + }); + } catch (error) { + if (error.status !== 404) throw error; + } + } + continue; + } + + if (!labels.has('status: stale')) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr.number, labels: ['status: stale'], + }); + } + + if (inactiveFor >= ESCALATE_AFTER && !escalatedSinceActivity) { await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, - body: 'Closing this contributor PR after 21 days without activity. The linked issue can be reclaimed. A maintainer can reopen the PR if work resumes.', + body: ESCALATION_MARKER + '\nThis Stellar Wave PR has no contributor commit or progress comment for 4 days. A maintainer should now review whether to retain or release the assignment. The automation does not close the PR or change the Drips assignment.', }); - await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' }); continue; } - if (age >= WARN_AFTER && !labels.has('status: stale')) { - await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ['status: stale'] }); + + if (!warnedSinceActivity) { await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, - body: 'This PR has had no activity for 7 days. Please post an update or push progress. It may be closed after 21 days of inactivity.', + body: WARNING_MARKER + '\nThis Stellar Wave PR has no contributor commit or progress comment for 48 hours. Please post a concise update, push current progress, or tell the maintainer if the issue should be released.', }); } } From bef86d907bfe2135b76115e50b8e7534c16fdaa7 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:15:55 +0100 Subject: [PATCH 04/12] ci: align Wave PR guard with Drips assignment --- .github/workflows/wave-pr-guard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wave-pr-guard.yml b/.github/workflows/wave-pr-guard.yml index 0179666..311f5e3 100644 --- a/.github/workflows/wave-pr-guard.yml +++ b/.github/workflows/wave-pr-guard.yml @@ -73,7 +73,7 @@ jobs: } if (!issue.assignees.some((a) => a.login === pr.user.login)) { - core.setFailed('PR author must claim and be assigned to the linked Wave issue before merge.'); + core.setFailed('PR author must be accepted through Drips and assigned to the linked Wave issue before merge.'); return; } From 25e5cd74d00122aaeb572d2c2b3faf3eb1328198 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:16:15 +0100 Subject: [PATCH 05/12] docs: add Drips Wave operations runbook --- docs/contributing/drips-wave-operations.md | 128 +++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/contributing/drips-wave-operations.md diff --git a/docs/contributing/drips-wave-operations.md b/docs/contributing/drips-wave-operations.md new file mode 100644 index 0000000..eefe8a6 --- /dev/null +++ b/docs/contributing/drips-wave-operations.md @@ -0,0 +1,128 @@ +# Drips Stellar Wave Operations + +**Status:** Pre-Wave operating policy for StellarForge CLI maintainers. + +This runbook defines how StellarForge CLI issues move from the GitHub backlog into the Drips Stellar Wave program and how applications, assignments, pull requests, reviews, and resolutions are handled. + +## Authority and activation + +- The Drips Maintainer Dashboard is the authority for Wave applications and assignments. +- An issue is not active in the program merely because its body lists intended labels or points. +- Only a maintainer may activate an issue through Drips or authorize the exact `Stellar Wave` label. +- No repository workflow may bulk-add the `Stellar Wave` label. +- Contributors must not begin substantial Wave work before their Drips application is accepted and GitHub assignment is visible. + +The current planning inventory contains SW-001 through SW-120: + +| Complexity | Issues | Points each | Planned points | +| --- | ---: | ---: | ---: | +| Trivial / low | 36 | 100 | 3,600 | +| Medium | 61 | 150 | 9,150 | +| High | 23 | 200 | 4,600 | +| **Total** | **120** | | **17,350** | + +The current repository budget reported in Drips is 25,000 points. That leaves 7,650 points of headroom, but maintainers must re-check both repository and organization usage before activation because unresolved issues can carry over between Waves. + +## Before a Wave + +1. Confirm the repository remains approved and synchronized in Drips. +2. Confirm the active Wave dates, repository budget, organization budget, and any program-specific rules. +3. Revalidate every candidate issue before activation: + - scope is implementable during the Wave; + - acceptance criteria are objectively reviewable; + - dependencies are closed or intentionally sequenced; + - complexity matches the work; + - protected surfaces and security impact are declared; + - there is no conflicting active issue or pull request. +4. Add issues through the Drips Maintainer Dashboard so complexity is set deliberately. +5. Do not activate an issue solely because it exists in the SW backlog. +6. Confirm Drips applied the exact Wave label and that the issue appears in the program issue list. + +## Application and assignment + +1. Review applications in Drips rather than accepting GitHub self-claims. +2. Check the applicant's relevant experience, proposed approach, communication, availability, and existing active assignments. +3. Prefer one active StellarForge issue per contributor until their first accepted contribution. +4. Accept one applicant and confirm the same GitHub user is assigned to the issue. +5. Reject unsuitable or surplus applications promptly so applicants can pursue other work. +6. Record material scope clarifications in the public issue. + +The `/claim` and `/unclaim` commands provide guidance only. They never create, release, or replace an official assignment. + +## Pull-request intake + +A Wave contributor pull request must: + +- use exactly one closing reference such as `Closes #123`; +- be authored by the contributor assigned to that issue; +- remain within the issue scope; +- include testing and security sections; +- avoid unresolved dependencies; +- avoid undeclared protected surfaces; +- avoid overlapping protected or source files with another active pull request; +- pass the repository's required CI and security checks. + +The Contributor PR Guard enforces the machine-verifiable subset. Passing automation is necessary but never replaces maintainer review. + +## Automated readiness + +The Wave PR Readiness workflow observes the required Wave workflows for the pull request's current head commit: + +- CI; +- CodeQL; +- Dependency Review; +- E2E Smoke; +- Contributor PR Guard. + +When all five complete successfully, the workflow applies `review: ready`. If the current head does not have a complete green set, it removes that label. + +A maintainer then reviews correctness, scope, architecture, tests, security, documentation, and release impact. After approval, use GitHub native squash auto-merge when repository settings allow it. Do not give automation a branch-protection bypass. + +## Inactivity policy + +Wave time is limited, so stale handling uses contributor activity rather than bot comments: + +- after 48 hours without a contributor commit or progress comment, automation adds `status: stale` and requests an update; +- after 4 days without contributor activity, automation requests maintainer review of the assignment; +- automation does not close the pull request, unassign the contributor, or change Drips state; +- a maintainer decides whether to retain, release, or reassign the issue; +- `do-not-close` exempts exceptional work from automated reminders. + +## Daily maintainer cadence + +At least twice daily during an active Wave: + +1. Review and accept or reject pending applications. +2. Check newly assigned issues for questions or blockers. +3. Review `review: ready` pull requests. +4. Respond to requested changes and contributor updates. +5. Inspect stale assignments and conflicting pull requests. +6. Confirm completed work is merged and resolved in time for the active Wave. +7. Keep security reports in private vulnerability-reporting channels. + +## Merge and resolution + +- Prefer squash merge for issue-sized contributor work. +- Require human approval and all configured checks. +- Do not merge solely to award points. +- Do not bypass branch protections for convenience. +- Ensure the linked issue closes as completed and Drips records the intended contributor and points. +- If external project constraints prevent merging otherwise qualifying work, document the decision and follow the current Drips resolution rules. + +## Emergency pause + +If CI is unreliable, a security incident is active, issue metadata is incorrect, or assignments cannot be reviewed safely: + +1. stop activating new issues; +2. do not remove or add Wave labels in bulk; +3. tell affected contributors what is paused; +4. repair the baseline through maintainer-reviewed work; +5. resume only after required checks and assignment records are trustworthy. + +## After a Wave + +1. Confirm resolved issues and point allocations in Drips. +2. Review unresolved issues before allowing rollover. +3. Remove obsolete issues from the program when appropriate. +4. Complete contributor reviews within the Drips review window. +5. Record operational lessons as maintainer policy changes or scoped contributor issues. From 26e8d2e0cf195c03d74c2d0dd417a5844ca4016e Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:16:30 +0100 Subject: [PATCH 06/12] ci: label green Wave PRs for review --- .github/workflows/wave-pr-readiness.yml | 142 ++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 .github/workflows/wave-pr-readiness.yml diff --git a/.github/workflows/wave-pr-readiness.yml b/.github/workflows/wave-pr-readiness.yml new file mode 100644 index 0000000..be98a8c --- /dev/null +++ b/.github/workflows/wave-pr-readiness.yml @@ -0,0 +1,142 @@ +name: Wave PR Readiness + +on: + workflow_run: + workflows: + - CI + - CodeQL + - Dependency Review + - E2E Smoke + - Contributor PR Guard + types: [completed] + +permissions: + actions: read + contents: read + issues: write + pull-requests: read + +concurrency: + group: wave-pr-readiness-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + +jobs: + readiness: + name: Mark green Wave PRs ready for review + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Reconcile review readiness + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const labelName = 'review: ready'; + const requiredWorkflows = new Set([ + 'CI', + 'CodeQL', + 'Dependency Review', + 'E2E Smoke', + 'Contributor PR Guard', + ]); + + try { + await github.rest.issues.getLabel({ owner, repo, name: labelName }); + } catch (error) { + if (error.status !== 404) throw error; + await github.rest.issues.createLabel({ + owner, + repo, + name: labelName, + color: '0e8a16', + description: 'Current PR head passed the required automated Wave checks', + }); + } + + const pullRefs = context.payload.workflow_run.pull_requests || []; + for (const pullRef of pullRefs) { + const { data: pr } = await github.rest.pulls.get({ + owner, repo, pull_number: pullRef.number, + }); + const prLabels = new Set(pr.labels.map((label) => label.name)); + + const removeReadyLabel = async () => { + if (!prLabels.has(labelName)) return; + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr.number, name: labelName, + }); + } catch (error) { + if (error.status !== 404) throw error; + } + }; + + if (pr.state !== 'open' || pr.draft) { + await removeReadyLabel(); + continue; + } + + const ref = (pr.body || '').match( + /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/i, + ); + if (!ref) { + await removeReadyLabel(); + continue; + } + + let issue; + try { + ({ data: issue } = await github.rest.issues.get({ + owner, repo, issue_number: Number(ref[1]), + })); + } catch { + await removeReadyLabel(); + continue; + } + if (!issue.labels.some((label) => label.name === 'Stellar Wave')) { + await removeReadyLabel(); + continue; + } + if (!issue.assignees.some((assignee) => assignee.login === pr.user.login)) { + await removeReadyLabel(); + continue; + } + + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner, + repo, + head_sha: pr.head.sha, + event: 'pull_request', + per_page: 100, + }, + ); + const latestByWorkflow = new Map(); + for (const run of runs) { + if (!requiredWorkflows.has(run.name)) continue; + const current = latestByWorkflow.get(run.name); + if (!current || new Date(run.created_at) > new Date(current.created_at)) { + latestByWorkflow.set(run.name, run); + } + } + + const green = [...requiredWorkflows].every((name) => { + const run = latestByWorkflow.get(name); + return run?.status === 'completed' && run?.conclusion === 'success'; + }); + + if (!green) { + await removeReadyLabel(); + continue; + } + + if (!prLabels.has(labelName)) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr.number, labels: [labelName], + }); + } + core.info('Wave PR #' + pr.number + ' is ready for maintainer review.'); + } From 673189ae3d9c6a133bd1e091aaa0dd0aacb8771b Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:16:42 +0100 Subject: [PATCH 07/12] test: lock Wave operations safeguards --- tests/wave-operations.test.ts | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/wave-operations.test.ts diff --git a/tests/wave-operations.test.ts b/tests/wave-operations.test.ts new file mode 100644 index 0000000..26a0410 --- /dev/null +++ b/tests/wave-operations.test.ts @@ -0,0 +1,73 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const root = join(import.meta.dirname, '..'); +const read = (...path: string[]) => readFileSync(join(root, ...path), 'utf8'); + +describe('Drips Wave operations policy', () => { + const contributing = read('CONTRIBUTING.md'); + const claimWorkflow = read('.github', 'workflows', 'wave-claim.yml'); + const staleWorkflow = read('.github', 'workflows', 'wave-stale-prs.yml'); + const readinessWorkflow = read( + '.github', + 'workflows', + 'wave-pr-readiness.yml', + ); + const runbook = read( + 'docs', + 'contributing', + 'drips-wave-operations.md', + ); + + it('requires official Drips application and assignment', () => { + expect(contributing).toContain('managed through the Drips Wave application'); + expect(contributing).toContain( + 'Wait until a maintainer accepts the application', + ); + expect(contributing).toContain('one active StellarForge issue at a time'); + expect(contributing).toContain('Closes #123'); + }); + + it('does not allow GitHub claim comments to mutate assignments', () => { + expect(claimWorkflow).toContain( + 'assigned through the Drips application process', + ); + expect(claimWorkflow).not.toContain('addAssignees'); + expect(claimWorkflow).not.toContain('removeAssignees'); + expect(claimWorkflow).not.toContain("labels: ['Stellar Wave']"); + }); + + it('uses Wave-length reminders without automatic closure', () => { + expect(staleWorkflow).toContain('const WARN_AFTER = 2 * DAY'); + expect(staleWorkflow).toContain('const ESCALATE_AFTER = 4 * DAY'); + expect(staleWorkflow).toContain('lastContributorActivity'); + expect(staleWorkflow).not.toContain('github.rest.pulls.update'); + expect(staleWorkflow).not.toContain("state: 'closed'"); + }); + + it('labels only current green assigned Wave PRs for human review', () => { + for (const name of [ + 'CI', + 'CodeQL', + 'Dependency Review', + 'E2E Smoke', + 'Contributor PR Guard', + ]) { + expect(readinessWorkflow).toContain(name); + } + expect(readinessWorkflow).toContain("const labelName = 'review: ready'"); + expect(readinessWorkflow).toContain("label.name === 'Stellar Wave'"); + expect(readinessWorkflow).toContain('issue.assignees.some'); + expect(readinessWorkflow).not.toContain('pulls.merge'); + expect(readinessWorkflow).not.toContain('enablePullRequestAutoMerge'); + }); + + it('documents the approved budget without activating issues', () => { + expect(runbook).toContain('25,000 points'); + expect(runbook).toContain('17,350'); + expect(runbook).toContain( + 'No repository workflow may bulk-add the `Stellar Wave` label', + ); + }); +}); From b41554e0e03a8d9a3b160d939856672329fa8177 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 17:18:44 +0100 Subject: [PATCH 08/12] style: format Wave operations test --- tests/wave-operations.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/wave-operations.test.ts b/tests/wave-operations.test.ts index 26a0410..6e23c5a 100644 --- a/tests/wave-operations.test.ts +++ b/tests/wave-operations.test.ts @@ -14,14 +14,12 @@ describe('Drips Wave operations policy', () => { 'workflows', 'wave-pr-readiness.yml', ); - const runbook = read( - 'docs', - 'contributing', - 'drips-wave-operations.md', - ); + const runbook = read('docs', 'contributing', 'drips-wave-operations.md'); it('requires official Drips application and assignment', () => { - expect(contributing).toContain('managed through the Drips Wave application'); + expect(contributing).toContain( + 'managed through the Drips Wave application', + ); expect(contributing).toContain( 'Wait until a maintainer accepts the application', ); From 667e5e6db89aedbba39ee0ddd6c976e7cc075444 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 23:21:47 +0100 Subject: [PATCH 09/12] fix: reconcile readiness for fork pull requests --- .github/workflows/wave-pr-readiness.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wave-pr-readiness.yml b/.github/workflows/wave-pr-readiness.yml index be98a8c..de8490a 100644 --- a/.github/workflows/wave-pr-readiness.yml +++ b/.github/workflows/wave-pr-readiness.yml @@ -55,7 +55,15 @@ jobs: }); } - const pullRefs = context.payload.workflow_run.pull_requests || []; + const run = context.payload.workflow_run; + let pullRefs = run.pull_requests || []; + if (!pullRefs.length) { + const openPulls = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100, + }); + pullRefs = openPulls.filter((pull) => pull.head.sha === run.head_sha); + } + for (const pullRef of pullRefs) { const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: pullRef.number, From 60c12c766c7dcd37cd283d4c883acd3efa68c47f Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 23:21:55 +0100 Subject: [PATCH 10/12] fix: measure contributor activity accurately --- .github/workflows/wave-stale-prs.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wave-stale-prs.yml b/.github/workflows/wave-stale-prs.yml index 07dbf5e..b8f3406 100644 --- a/.github/workflows/wave-stale-prs.yml +++ b/.github/workflows/wave-stale-prs.yml @@ -55,21 +55,25 @@ jobs: } if (!issue.labels.some((label) => label.name === 'Stellar Wave')) continue; - const [commits, comments] = await Promise.all([ + const [commits, comments, reviewComments] = await Promise.all([ github.paginate(github.rest.pulls.listCommits, { owner, repo, pull_number: pr.number, per_page: 100, }), github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100, }), + github.paginate(github.rest.pulls.listReviewComments, { + owner, repo, pull_number: pr.number, per_page: 100, + }), ]); const activityDates = [new Date(pr.created_at).getTime()]; for (const commit of commits) { - const date = commit.commit?.committer?.date || commit.commit?.author?.date; + if (commit.author?.login !== pr.user.login) continue; + const date = commit.commit?.author?.date || commit.commit?.committer?.date; if (date) activityDates.push(new Date(date).getTime()); } - for (const comment of comments) { + for (const comment of [...comments, ...reviewComments]) { if (comment.user?.login === pr.user.login) { activityDates.push(new Date(comment.created_at).getTime()); } From fed70086a287a0cc053eb29d5559d89414c3e30b Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 23:22:03 +0100 Subject: [PATCH 11/12] test: cover Wave automation edge cases --- tests/wave-operations.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/wave-operations.test.ts b/tests/wave-operations.test.ts index 6e23c5a..988127b 100644 --- a/tests/wave-operations.test.ts +++ b/tests/wave-operations.test.ts @@ -40,6 +40,10 @@ describe('Drips Wave operations policy', () => { expect(staleWorkflow).toContain('const WARN_AFTER = 2 * DAY'); expect(staleWorkflow).toContain('const ESCALATE_AFTER = 4 * DAY'); expect(staleWorkflow).toContain('lastContributorActivity'); + expect(staleWorkflow).toContain('listReviewComments'); + expect(staleWorkflow).toContain( + "commit.author?.login !== pr.user.login", + ); expect(staleWorkflow).not.toContain('github.rest.pulls.update'); expect(staleWorkflow).not.toContain("state: 'closed'"); }); @@ -57,6 +61,9 @@ describe('Drips Wave operations policy', () => { expect(readinessWorkflow).toContain("const labelName = 'review: ready'"); expect(readinessWorkflow).toContain("label.name === 'Stellar Wave'"); expect(readinessWorkflow).toContain('issue.assignees.some'); + expect(readinessWorkflow).toContain( + 'pull.head.sha === run.head_sha', + ); expect(readinessWorkflow).not.toContain('pulls.merge'); expect(readinessWorkflow).not.toContain('enablePullRequestAutoMerge'); }); From c00aaef862aa567b1f2dac43806ad4493fc79193 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 13 Oct 2026 23:23:40 +0100 Subject: [PATCH 12/12] style: format Wave edge-case tests --- tests/wave-operations.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/wave-operations.test.ts b/tests/wave-operations.test.ts index 988127b..9d29bd0 100644 --- a/tests/wave-operations.test.ts +++ b/tests/wave-operations.test.ts @@ -41,9 +41,7 @@ describe('Drips Wave operations policy', () => { expect(staleWorkflow).toContain('const ESCALATE_AFTER = 4 * DAY'); expect(staleWorkflow).toContain('lastContributorActivity'); expect(staleWorkflow).toContain('listReviewComments'); - expect(staleWorkflow).toContain( - "commit.author?.login !== pr.user.login", - ); + expect(staleWorkflow).toContain('commit.author?.login !== pr.user.login'); expect(staleWorkflow).not.toContain('github.rest.pulls.update'); expect(staleWorkflow).not.toContain("state: 'closed'"); }); @@ -61,9 +59,7 @@ describe('Drips Wave operations policy', () => { expect(readinessWorkflow).toContain("const labelName = 'review: ready'"); expect(readinessWorkflow).toContain("label.name === 'Stellar Wave'"); expect(readinessWorkflow).toContain('issue.assignees.some'); - expect(readinessWorkflow).toContain( - 'pull.head.sha === run.head_sha', - ); + expect(readinessWorkflow).toContain('pull.head.sha === run.head_sha'); expect(readinessWorkflow).not.toContain('pulls.merge'); expect(readinessWorkflow).not.toContain('enablePullRequestAutoMerge'); });