diff --git a/.github/github-merge-queue.md b/.github/github-merge-queue.md
new file mode 100644
index 00000000..5ae23d0e
--- /dev/null
+++ b/.github/github-merge-queue.md
@@ -0,0 +1,340 @@
+# GitHub Merge Queue Setup
+
+This document describes the GitHub-based approval and merge system that replaces Prow Tide.
+
+## Overview
+
+The repository now uses GitHub workflows and merge queue instead of Prow Tide for managing PR approvals and merges. This provides:
+
+- Label-based approval system via comment commands
+- Required status checks for merge readiness
+- Automatic merging via GitHub merge queue
+
+## Workflow Diagram
+
+```mermaid
+flowchart TD
+ Start([PR Created/Updated]) --> AutoChecks{Automatic Checks}
+
+ AutoChecks --> WIP[Check WIP Status]
+ AutoChecks --> ReleaseNote[Check Release Note Label]
+ AutoChecks --> Rebase[Check Merge Conflicts]
+
+ WIP -->|WIP detected| WIPLabel[Add: do-not-merge/work-in-progress]
+ WIP -->|Ready| WIPRemove[Remove: do-not-merge/work-in-progress]
+
+ ReleaseNote -->|Missing label| RNLabel[Add: do-not-merge/release-note-label-needed]
+ ReleaseNote -->|Has label| RNRemove[Remove: do-not-merge/release-note-label-needed]
+
+ Rebase -->|Conflicts| RebaseLabel[Add: needs-rebase]
+ Rebase -->|Clean| RebaseRemove[Remove: needs-rebase]
+
+ WIPLabel --> ManualReview
+ WIPRemove --> ManualReview
+ RNLabel --> ManualReview
+ RNRemove --> ManualReview
+ RebaseLabel --> ManualReview
+ RebaseRemove --> ManualReview
+
+ ManualReview{Manual Review} -->|Reviewer comments /lgtm| LGTMLabel[Add: lgtm]
+ ManualReview -->|Maintainer comments /approve| ApprovedLabel[Add: approved]
+ ManualReview -->|Anyone comments /hold| HoldLabel[Add: do-not-merge/hold]
+
+ LGTMLabel --> MergeCheck
+ ApprovedLabel --> MergeCheck
+ HoldLabel --> MergeCheck
+
+ MergeCheck{Merge Readiness Check}
+
+ MergeCheck -->|Has lgtm + approved
No blocking labels| Ready[✅ Status: PASS]
+ MergeCheck -->|Missing lgtm or approved| NotReady[❌ Status: FAIL
Missing required labels]
+ MergeCheck -->|Has blocking labels| Blocked[❌ Status: FAIL
Has blocking labels]
+
+ Ready --> MergeQueue[Add to Merge Queue]
+ NotReady --> Wait[Wait for approvals]
+ Blocked --> Wait
+
+ Wait --> ManualReview
+
+ MergeQueue --> FinalChecks{All CI Checks Pass?}
+ FinalChecks -->|Yes| Merge([🎉 PR Merged])
+ FinalChecks -->|No| Failed([❌ Merge Failed])
+
+ style Start fill:#e1f5ff
+ style Merge fill:#d4edda
+ style Failed fill:#f8d7da
+ style Ready fill:#d4edda
+ style NotReady fill:#fff3cd
+ style Blocked fill:#f8d7da
+ style MergeQueue fill:#cfe2ff
+```
+
+### Key Points
+- **Green boxes** = Success states
+- **Yellow boxes** = Waiting/pending states
+- **Red boxes** = Blocked/failed states
+- **Blue boxes** = Active processing
+
+## Approval System
+
+### Commands
+
+Maintainers with **write** access can use the following commands in PR comments:
+
+#### `/lgtm` - Looks Good To Me
+Adds the `lgtm` label to indicate code review approval.
+
+```
+/lgtm
+```
+
+To remove the label:
+```
+/lgtm cancel
+```
+
+**Restrictions**:
+- Requires write access to the repository
+- ⚠️ PR authors and co-authors cannot `/lgtm` or `/approve` their own PRs
+
+#### `/approve` - Final Approval
+Adds the `approved` label to indicate final approval for merge.
+
+```
+/approve
+```
+
+To remove the label:
+```
+/approve cancel
+```
+
+**Restrictions**:
+- Requires write access to the repository
+- ⚠️ PR authors and co-authors cannot `/lgtm` or `/approve` their own PRs
+
+#### `/hold` - Hold PR from Merging
+Adds the `do-not-merge/hold` label to block a PR from merging. Both maintainers and PR authors can use this command.
+
+```
+/hold
+```
+
+To remove the hold:
+```
+/unhold
+```
+or
+```
+/hold cancel
+```
+
+### Permissions
+
+- Only users with **write** access to the repository can use approval commands
+- Commands must be placed at the start of a comment
+- The bot will react with emoji to indicate status:
+ - 👀 (eyes) - Processing command
+ - 👍 (+1) - Label added successfully
+ - 👎 (-1) - Label removed successfully
+ - 😕 (confused) - Insufficient permissions
+
+## Automatic Label Management
+
+Several workflows automatically manage labels based on PR state:
+
+### Work in Progress (WIP)
+- **Trigger**: PR title contains `[WIP]`, `WIP:`, `[Draft]`, `Draft:`, or 🚧 emoji, or PR is marked as draft
+- **Action**: Adds `do-not-merge/work-in-progress` label
+- **Resolution**: Remove WIP indicators from title or convert from draft to ready
+
+### Release Notes
+- **Trigger**: PR is missing a release note label
+- **Action**: Adds `do-not-merge/release-note-label-needed` label
+- **Resolution**: Add one of these labels manually, or use commands:
+ - `release-note` - For user-facing changes
+ - `release-note-action-required` - For breaking changes requiring user action
+ - `release-note-none` - For internal changes with no user impact
+ - **Commands**: `/release-note`, `/release-note-action-required`, `/release-note-none`
+ - **Permissions**: ⚠️ **PR author ONLY** (following Prow's behavior - this ensures the author takes responsibility for documenting their changes)
+
+### Merge Conflicts
+- **Trigger**: PR has merge conflicts with base branch
+- **Action**: Adds `needs-rebase` label
+- **Resolution**: Rebase or merge the base branch to resolve conflicts
+
+### Hold
+- **Trigger**: Someone comments `/hold`
+- **Action**: Adds `do-not-merge/hold` label
+- **Resolution**: Comment `/unhold` or `/hold cancel`
+- **Permissions**: PR author OR maintainers with write access
+
+## Blocking Labels
+
+The following labels will block a PR from merging (checked by the Merge Readiness workflow):
+
+- `do-not-merge/hold` - Manual hold requested
+- `do-not-merge/invalid-owners-file` - OWNERS file is invalid
+- `do-not-merge/release-note-label-needed` - Missing release note label
+- `do-not-merge/requires-unreleased-pipelines` - Depends on unreleased Tekton pipelines
+- `do-not-merge/work-in-progress` - PR is still in progress
+- `needs-ok-to-test` - PR needs approval to run tests
+- `needs-rebase` - PR has merge conflicts
+
+
+
+## Enabling Auto-Merge with Merge Queue
+
+Once both required labels are present:
+
+1. The "Merge Readiness Check" status will turn green
+2. The PR can be added to the merge queue
+3. Enable auto-merge on the PR to have it automatically merge when ready
+
+### Repository Settings
+
+To enable merge queue for the repository:
+
+1. Go to **Settings** → **General** → **Pull Requests**
+2. Enable **Allow auto-merge**
+3. Go to **Settings** → **Branches** → **Branch protection rules** for your main branch
+4. Enable **Require merge queue**
+5. Add **Merge Readiness Check** as a required status check
+6. Configure merge queue settings:
+ - Minimum PRs to merge: 1 (or as desired)
+ - Maximum PRs to merge: 5 (or as desired)
+ - Merge method: Squash, merge commit, or rebase (as per project preference)
+
+## Workflow Files
+
+All merge automation workflows are located in `.github/workflows/` with the `pr_` prefix.
+
+### [`pr_approval-labels.yaml`](workflows/pr_approval-labels.yaml)
+Handles `/lgtm` and `/approve` commands in PR comments. Manages label addition and removal based on user permissions.
+- **Permissions**: Requires write access to the repository
+- **Commands**: `/lgtm`, `/lgtm cancel`, `/approve`, `/approve cancel`
+- **Restrictions**: PR authors and co-authors cannot approve their own PRs (prevents self-approval)
+
+### [`pr_hold-label.yaml`](workflows/pr_hold-label.yaml)
+Handles `/hold` and `/unhold` commands. Allows maintainers and PR authors to block/unblock PRs from merging by adding/removing the `do-not-merge/hold` label.
+- **Permissions**: PR author or users with write access
+- **Commands**: `/hold`, `/unhold`, `/hold cancel`
+
+### [`pr_wip-label.yaml`](workflows/pr_wip-label.yaml)
+Automatically detects work-in-progress PRs by checking for:
+- PR titles starting with `[WIP]`, `WIP:`, `[Draft]`, or `Draft:`
+- Draft PR status
+- 🚧 emoji in title
+
+Adds/removes `do-not-merge/work-in-progress` label accordingly.
+
+### [`pr_release-notes-label.yaml`](workflows/pr_release-notes-label.yaml)
+Checks for release note labels on PRs. Adds `do-not-merge/release-note-label-needed` if missing one of:
+- `release-note` - User-facing changes
+- `release-note-action-required` - Requires action from users
+- `release-note-none` - No user-facing changes
+
+**NEW**: Also supports comment commands for easier label management:
+- **Commands**: `/release-note`, `/release-note-action-required`, `/release-note-none`
+- **Permissions**: ⚠️ **PR author ONLY** (matches Prow's release-note plugin behavior)
+- **Rationale**: Only the PR author can set release note labels to ensure they take responsibility for documenting their changes
+- **Features**: Automatically removes other release-note labels when applying a new one
+
+### [`pr_needs-rebase-label.yaml`](workflows/pr_needs-rebase-label.yaml)
+Automatically detects merge conflicts and adds/removes the `needs-rebase` label when conflicts are present.
+
+### [`pr_merge-readiness.yaml`](workflows/pr_merge-readiness.yaml)
+Provides a required status check that verifies both `lgtm` and `approved` labels are present and no blocking labels exist.
+
+## Migration from Prow
+
+This system replaces the Prow Tide configuration with native GitHub functionality:
+
+| Prow Tide | GitHub Workflows |
+|-----------|------------------|
+| `/lgtm` command | `/lgtm` command (via `pr_approval-labels.yaml`) |
+| `/approve` command | `/approve` command (via `pr_approval-labels.yaml`) |
+| `/hold` command | `/hold` command (via `pr_hold-label.yaml`) |
+| Release note plugin | Release note workflow (via `pr_release-notes-label.yaml`) with `/release-note-*` commands |
+| Tide merge pool | GitHub Merge Queue |
+| Tide status contexts | Merge Readiness Check |
+| Automatic merging | Auto-merge + Merge Queue |
+
+### Key Differences from Prow Tide
+
+#### Architecture
+- **Prow Tide**: Centralized service running on Kubernetes cluster, polls GitHub API for PRs matching criteria
+- **GitHub Workflows**: Distributed event-driven workflows, triggered by GitHub webhooks on PR events
+- **Result**: Lower latency (immediate response vs polling), no infrastructure to maintain
+
+#### Label Management
+- **Prow Tide**: Plugins (lgtm, approve, hold, wip) managed by central Prow plugins
+- **GitHub Workflows**: Individual workflows handle each command independently
+- **Result**: More modular, easier to customize individual behaviors
+
+#### Merge Process
+- **Prow Tide**:
+ - Batches multiple PRs together for testing
+ - Maintains merge pool with sync loop (default: 2m)
+ - Tests PRs together, merges if batch passes
+ - Bisects on failure to find culprit
+- **GitHub Merge Queue**:
+ - Tests each PR individually or in configurable groups
+ - Queue-based approach with configurable merge strategies
+ - Native GitHub UI for queue visibility
+ - No separate infrastructure needed
+- **Result**: Similar reliability, simpler setup, better UI
+
+#### Permission Model
+- **Prow Tide**: Uses GitHub team membership and OWNERS files
+- **GitHub Workflows**: Uses GitHub's native repository permissions (read/write/admin)
+- **Result**: More straightforward, one permission system to manage
+
+#### Status Checks
+- **Prow Tide**: Multiple required contexts, complex configuration
+- **GitHub Workflows**: Single "Merge Readiness Check" workflow
+- **Result**: Simpler to understand, single source of truth
+
+#### Commands
+- **Prow Tide**: Fixed set of commands defined in plugins config
+- **GitHub Workflows**: Flexible, can add new commands easily by creating new workflows
+- **Result**: More extensible and customizable
+
+#### What's the Same
+✅ `/lgtm` and `/approve` commands work identically
+✅ `/hold` blocks merging
+✅ `do-not-merge/*` labels prevent merging
+✅ Automatic WIP detection
+✅ Release note label enforcement
+✅ Merge conflict detection
+✅ Permission-based access control
+
+#### What's Different
+⚠️ **No batch merging** - GitHub merge queue tests PRs individually or in smaller groups
+⚠️ **No OWNERS file** - Uses GitHub repository permissions instead
+⚠️ **Different UI** - GitHub PR UI instead of Prow dashboard
+⚠️ **Faster response** - Event-driven instead of polling (2m sync period)
+⚠️ **Simpler setup** - No Prow infrastructure needed
+
+#### Migration Considerations
+- **OWNERS files**: If you rely on OWNERS files for approval, you'll need to migrate to GitHub CODEOWNERS
+- **Batch testing**: If you need to test multiple PRs together, configure merge queue groups
+- **Custom plugins**: Any custom Prow plugins need to be reimplemented as GitHub workflows
+- **Tide configuration**: Context requirements → Branch protection rules
+- **Bot accounts**: Replace Prow bot token with GitHub Actions bot or app
+
+## Troubleshooting
+
+### Commands not working
+- Verify you have write access to the repository
+- Ensure the command is at the start of the comment
+- Check workflow run logs in the Actions tab
+
+### Merge Readiness Check failing
+- Verify both `lgtm` and `approved` labels are present
+- Check the workflow logs for detailed status
+
+### Auto-merge not triggering
+- Ensure merge queue is enabled in repository settings
+- Verify all required status checks are passing
+- Check that auto-merge is enabled on the PR
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b8a72a15..543abbd8 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -31,7 +31,6 @@ jobs:
unit-tests:
name: Unit Tests
- needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -44,7 +43,6 @@ jobs:
linting:
name: Linting
- needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -73,7 +71,6 @@ jobs:
check-licenses:
name: License Check
- needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -87,7 +84,6 @@ jobs:
ko-resolve:
name: Ko Resolve (Multi-arch)
- needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
diff --git a/.github/workflows/kind-e2e.yaml b/.github/workflows/kind-e2e.yaml
index 51b37ee0..29222e25 100644
--- a/.github/workflows/kind-e2e.yaml
+++ b/.github/workflows/kind-e2e.yaml
@@ -20,7 +20,80 @@ defaults:
working-directory: ./
jobs:
+ # Wait for CI checks to pass before running E2E tests
+ wait-for-ci:
+ name: Wait for CI checks
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Wait for CI workflow
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const checkNames = [
+ 'Build',
+ 'Unit Tests',
+ 'Linting',
+ 'License Check',
+ 'Ko Resolve (Multi-arch)'
+ ];
+
+ const maxAttempts = 120; // 5 minutes max wait
+ const delayMs = 5000; // 5 seconds between checks
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const { data: checkRuns } = await github.rest.checks.listForRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: context.payload.pull_request.head.sha
+ });
+
+ const relevantChecks = checkRuns.check_runs.filter(check =>
+ checkNames.includes(check.name)
+ );
+
+ console.log(`Attempt ${attempt + 1}/${maxAttempts}`);
+ console.log('Check statuses:', relevantChecks.map(c => `${c.name}: ${c.status} (${c.conclusion})`).join(', '));
+
+ // Check if all required checks exist and are completed
+ const allChecksPresent = checkNames.every(name =>
+ relevantChecks.some(check => check.name === name)
+ );
+
+ if (allChecksPresent) {
+ const allCompleted = relevantChecks.every(check =>
+ check.status === 'completed'
+ );
+
+ if (allCompleted) {
+ const allPassed = relevantChecks.every(check =>
+ check.conclusion === 'success'
+ );
+
+ if (allPassed) {
+ console.log('✅ All CI checks passed!');
+ return;
+ } else {
+ const failedChecks = relevantChecks.filter(c => c.conclusion !== 'success');
+ console.log('❌ Some CI checks failed:', failedChecks.map(c => c.name).join(', '));
+ core.setFailed('CI checks failed. E2E tests will not run.');
+ return;
+ }
+ }
+ }
+
+ // Wait before next attempt
+ console.log(`Waiting ${delayMs/1000} seconds before next check...`);
+ await new Promise(resolve => setTimeout(resolve, delayMs));
+ }
+
+ console.log('⏱️ Timeout waiting for CI checks');
+ core.setFailed('Timeout waiting for CI checks to complete');
+
k8s:
+ needs: [wait-for-ci]
+ # Skip the wait-for-ci dependency on push/merge_group events
+ if: ${{ !failure() && (github.event_name != 'pull_request' || needs.wait-for-ci.result == 'success') }}
strategy:
fail-fast: false # Keep running if one leg fails.
matrix:
@@ -34,6 +107,9 @@ jobs:
pipelines-release: v0.65.0
# This job is for testing the latest LTS version of Tekton Pipelines
pipelines-lts:
+ needs: [wait-for-ci]
+ # Skip the wait-for-ci dependency on push/merge_group events
+ if: ${{ !failure() && (github.event_name != 'pull_request' || needs.wait-for-ci.result == 'success') }}
strategy:
fail-fast: false # Keep running if one leg fails.
matrix:
diff --git a/.github/workflows/pr_approval-labels.yaml b/.github/workflows/pr_approval-labels.yaml
new file mode 100644
index 00000000..45892d8f
--- /dev/null
+++ b/.github/workflows/pr_approval-labels.yaml
@@ -0,0 +1,239 @@
+---
+name: Approval Label Management
+# This workflow handles /lgtm and /approve commands in PR comments
+# Replaces Prow Tide functionality for label-based approvals
+
+'on':
+ issue_comment:
+ types: [created]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ handle_approval_commands:
+ name: Process approval commands
+ # Only run on pull request comments
+ if: github.event.issue.pull_request
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Process approval commands
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const comment_body = context.payload.comment.body.trim();
+ const commenter = context.payload.comment.user.login;
+
+ // Check if comment is an approval command
+ const isLgtmCommand = comment_body.startsWith('/lgtm');
+ const isApproveCommand = comment_body.startsWith('/approve');
+
+ if (!isLgtmCommand && !isApproveCommand) {
+ console.log('Not an approval command, skipping');
+ return;
+ }
+
+ console.log(`Processing command from ${commenter}: ${comment_body}`);
+
+ // React with eyes to show we're processing
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'eyes'
+ });
+
+ // Check user permissions
+ let hasPermission = false;
+ try {
+ const { data: collaborator } = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ username: commenter
+ });
+
+ console.log(`User ${commenter} has permission: ${collaborator.permission}`);
+ hasPermission = ['admin', 'write', 'maintain'].includes(collaborator.permission);
+ } catch (error) {
+ console.log(`Error checking permissions for ${commenter}:`, error.message);
+ hasPermission = false;
+ }
+
+ // Handle insufficient permissions
+ if (!hasPermission) {
+ console.log(`User ${commenter} does not have sufficient permissions`);
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'confused'
+ });
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `@${commenter} you don't have permission to use approval commands. Only users with write access or higher can approve PRs.`
+ });
+
+ return;
+ }
+
+ // Get PR details to check author and co-authors
+ const { data: pr } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.issue.number
+ });
+
+ const prAuthor = pr.user.login;
+
+ // Extract co-authors from commit messages and PR body
+ const coAuthors = new Set();
+
+ // Check PR body for co-authored-by
+ const prBody = pr.body || '';
+ const coAuthorMatches = prBody.matchAll(/(?:co-authored-by|Co-authored-by|CO-AUTHORED-BY):\s*([^<]+)\s*<([^>]+)>/gi);
+ for (const match of coAuthorMatches) {
+ // Extract username from email or name
+ const email = match[2].toLowerCase();
+ const username = email.split('@')[0].replace(/[^a-z0-9-]/g, '');
+ coAuthors.add(username);
+ }
+
+ // Check commits for co-authors
+ const { data: commits } = await github.rest.pulls.listCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.issue.number
+ });
+
+ for (const commit of commits) {
+ const message = commit.commit.message;
+ const commitCoAuthorMatches = message.matchAll(/(?:co-authored-by|Co-authored-by|CO-AUTHORED-BY):\s*([^<]+)\s*<([^>]+)>/gi);
+ for (const match of commitCoAuthorMatches) {
+ const email = match[2].toLowerCase();
+ const username = email.split('@')[0].replace(/[^a-z0-9-]/g, '');
+ coAuthors.add(username);
+ }
+
+ // Add commit author as co-author if different from PR author
+ if (commit.author && commit.author.login && commit.author.login !== prAuthor) {
+ coAuthors.add(commit.author.login.toLowerCase());
+ }
+ }
+
+ console.log(`PR author: ${prAuthor}`);
+ console.log(`Co-authors: ${Array.from(coAuthors).join(', ')}`);
+ console.log(`Commenter: ${commenter}`);
+
+ // Check if commenter is the PR author or co-author
+ const isAuthor = commenter.toLowerCase() === prAuthor.toLowerCase();
+ const isCoAuthor = coAuthors.has(commenter.toLowerCase());
+
+ if (isAuthor || isCoAuthor) {
+ console.log(`User ${commenter} is the PR author or co-author and cannot approve their own PR`);
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'confused'
+ });
+
+ const role = isAuthor ? 'author' : 'co-author';
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `@${commenter} as the PR ${role}, you cannot approve your own pull request. Please ask another maintainer to review and approve.`
+ });
+
+ return;
+ }
+
+ // Process /lgtm command
+ if (isLgtmCommand) {
+ const isCancel = comment_body === '/lgtm cancel';
+
+ if (isCancel) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ name: 'lgtm'
+ });
+ console.log('Removed lgtm label');
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '-1'
+ });
+ } catch (error) {
+ console.log('Label lgtm not found or already removed:', error.message);
+ }
+ } else if (comment_body === '/lgtm') {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: ['lgtm']
+ });
+ console.log('Added lgtm label');
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+ }
+ }
+
+ // Process /approve command
+ if (isApproveCommand) {
+ const isCancel = comment_body === '/approve cancel';
+
+ if (isCancel) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ name: 'approved'
+ });
+ console.log('Removed approved label');
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '-1'
+ });
+ } catch (error) {
+ console.log('Label approved not found or already removed:', error.message);
+ }
+ } else if (comment_body === '/approve') {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: ['approved']
+ });
+ console.log('Added approved label');
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+ }
+ }
diff --git a/.github/workflows/pr_hold-label.yaml b/.github/workflows/pr_hold-label.yaml
new file mode 100644
index 00000000..150263e6
--- /dev/null
+++ b/.github/workflows/pr_hold-label.yaml
@@ -0,0 +1,131 @@
+---
+name: Hold Label Management
+# Handles /hold and /unhold commands to block/unblock PRs from merging
+# Replaces Prow hold plugin
+
+'on':
+ issue_comment:
+ types: [created]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ handle_hold_command:
+ name: Process hold commands
+ if: github.event.issue.pull_request
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Process hold commands
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const comment_body = context.payload.comment.body.trim();
+ const commenter = context.payload.comment.user.login;
+
+ // Check if comment is a hold command
+ const isHoldCommand = comment_body === '/hold';
+ const isUnholdCommand = comment_body === '/unhold' || comment_body === '/hold cancel';
+
+ if (!isHoldCommand && !isUnholdCommand) {
+ console.log('Not a hold command, skipping');
+ return;
+ }
+
+ console.log(`Processing hold command from ${commenter}: ${comment_body}`);
+
+ // React with eyes to show we're processing
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'eyes'
+ });
+
+ // Check user permissions
+ let hasPermission = false;
+ try {
+ const { data: collaborator } = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ username: commenter
+ });
+
+ console.log(`User ${commenter} has permission: ${collaborator.permission}`);
+ hasPermission = ['admin', 'write', 'maintain'].includes(collaborator.permission);
+ } catch (error) {
+ console.log(`Error checking permissions for ${commenter}:`, error.message);
+ hasPermission = false;
+ }
+
+ // Allow PR author to hold their own PR
+ const pr = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.issue.number
+ });
+
+ const isPrAuthor = pr.data.user.login === commenter;
+
+ if (!hasPermission && !isPrAuthor) {
+ console.log(`User ${commenter} does not have sufficient permissions and is not the PR author`);
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'confused'
+ });
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `@${commenter} you don't have permission to use hold commands. Only the PR author or users with write access can hold/unhold PRs.`
+ });
+
+ return;
+ }
+
+ // Process /hold command
+ if (isHoldCommand) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: ['do-not-merge/hold']
+ });
+ console.log('Added do-not-merge/hold label');
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+ }
+
+ // Process /unhold command
+ if (isUnholdCommand) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ name: 'do-not-merge/hold'
+ });
+ console.log('Removed do-not-merge/hold label');
+
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+ } catch (error) {
+ console.log('Label do-not-merge/hold not found or already removed:', error.message);
+ }
+ }
diff --git a/.github/workflows/pr_merge-readiness.yaml b/.github/workflows/pr_merge-readiness.yaml
new file mode 100644
index 00000000..20e8fbcf
--- /dev/null
+++ b/.github/workflows/pr_merge-readiness.yaml
@@ -0,0 +1,86 @@
+---
+name: Merge Readiness Check
+# This workflow provides a required status check that only passes
+# when both 'lgtm' and 'approved' labels are present on the PR
+# This enables auto-merge with GitHub merge queue
+
+'on':
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - labeled
+ - unlabeled
+
+permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
+ checks: write
+
+jobs:
+ check_merge_readiness:
+ name: Check approval labels
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check for required labels and blocking labels
+ id: check_labels
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const pr = context.payload.pull_request;
+ const labels = pr.labels.map(label => label.name);
+
+ console.log('PR Labels:', labels);
+
+ // Required labels
+ const hasLgtm = labels.includes('lgtm');
+ const hasApproved = labels.includes('approved');
+
+ console.log('Has lgtm:', hasLgtm);
+ console.log('Has approved:', hasApproved);
+
+ // Blocking labels that prevent merge
+ const blockingLabels = [
+ 'do-not-merge/hold',
+ 'do-not-merge/invalid-owners-file',
+ 'do-not-merge/release-note-label-needed',
+ 'do-not-merge/requires-unreleased-pipelines',
+ 'do-not-merge/work-in-progress',
+ 'needs-ok-to-test',
+ 'needs-rebase'
+ ];
+
+ const presentBlockingLabels = labels.filter(label => blockingLabels.includes(label));
+
+ if (presentBlockingLabels.length > 0) {
+ console.log('❌ PR has blocking labels:', presentBlockingLabels.join(', '));
+ core.setOutput('ready', 'false');
+ core.setOutput('message', `PR is blocked by labels: ${presentBlockingLabels.join(', ')}`);
+ } else if (hasLgtm && hasApproved) {
+ console.log('✅ PR has both lgtm and approved labels and no blocking labels');
+ core.setOutput('ready', 'true');
+ core.setOutput('message', 'PR is ready to merge - both lgtm and approved labels are present, no blocking labels');
+ } else {
+ const missing = [];
+ if (!hasLgtm) missing.push('lgtm');
+ if (!hasApproved) missing.push('approved');
+
+ console.log('❌ PR is missing required labels:', missing.join(', '));
+ core.setOutput('ready', 'false');
+ core.setOutput('message', `PR is not ready to merge - missing labels: ${missing.join(', ')}`);
+ }
+
+ - name: Set success status
+ if: steps.check_labels.outputs.ready == 'true'
+ run: |
+ echo "✅ ${{ steps.check_labels.outputs.message }}"
+ exit 0
+
+ - name: Set failure status
+ if: steps.check_labels.outputs.ready == 'false'
+ run: |
+ echo "❌ ${{ steps.check_labels.outputs.message }}"
+ exit 1
diff --git a/.github/workflows/pr_needs-rebase-label.yaml b/.github/workflows/pr_needs-rebase-label.yaml
new file mode 100644
index 00000000..bbd96355
--- /dev/null
+++ b/.github/workflows/pr_needs-rebase-label.yaml
@@ -0,0 +1,80 @@
+---
+name: Needs Rebase Label
+# Automatically adds/removes needs-rebase label when PR has merge conflicts
+# Replaces Prow needs-rebase functionality
+
+'on':
+ pull_request_target:
+ types:
+ - opened
+ - reopened
+ - synchronize
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ check_rebase_needed:
+ name: Check if rebase is needed
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check PR mergeable state
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ // Get fresh PR data to check mergeable state
+ const { data: pr } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.payload.pull_request.number
+ });
+
+ console.log('PR mergeable state:', pr.mergeable_state);
+ console.log('PR mergeable:', pr.mergeable);
+
+ const labels = pr.labels.map(label => label.name);
+ const hasNeedsRebaseLabel = labels.includes('needs-rebase');
+
+ // PR needs rebase if it has conflicts (mergeable === false)
+ // mergeable_state can be: clean, dirty, unstable, blocked, unknown
+ const needsRebase = pr.mergeable === false || pr.mergeable_state === 'dirty';
+
+ console.log('Needs rebase:', needsRebase);
+ console.log('Has needs-rebase label:', hasNeedsRebaseLabel);
+
+ if (needsRebase && !hasNeedsRebaseLabel) {
+ // Add needs-rebase label
+ console.log('Adding needs-rebase label');
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: ['needs-rebase']
+ });
+
+ // Add a comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ body: `This PR has merge conflicts and needs to be rebased. The \`needs-rebase\` label will be automatically removed once conflicts are resolved.`
+ });
+ } else if (!needsRebase && hasNeedsRebaseLabel) {
+ // Remove needs-rebase label
+ console.log('Removing needs-rebase label');
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ name: 'needs-rebase'
+ });
+ } catch (error) {
+ console.log('Label not found or already removed:', error.message);
+ }
+ } else {
+ console.log('No label change needed');
+ }
diff --git a/.github/workflows/pr_release-notes-label.yaml b/.github/workflows/pr_release-notes-label.yaml
new file mode 100644
index 00000000..e6fb0be7
--- /dev/null
+++ b/.github/workflows/pr_release-notes-label.yaml
@@ -0,0 +1,227 @@
+---
+name: Release Notes Label Check
+# Checks for release-note label and blocks merge if missing
+# Replaces Prow release-note plugin
+
+'on':
+ pull_request_target:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - labeled
+ - unlabeled
+ issue_comment:
+ types:
+ - created
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ handle_comment_command:
+ name: Handle release note comment command
+ runs-on: ubuntu-latest
+ if: github.event_name == 'issue_comment' && github.event.issue.pull_request
+
+ steps:
+ - name: Check if comment contains release note command
+ id: check_command
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const comment = context.payload.comment.body.trim();
+ const commands = {
+ '/release-note-none': 'release-note-none',
+ '/release-note': 'release-note',
+ '/release-note-action-required': 'release-note-action-required'
+ };
+
+ for (const [cmd, label] of Object.entries(commands)) {
+ if (comment === cmd) {
+ core.setOutput('has_command', 'true');
+ core.setOutput('label', label);
+ return;
+ }
+ }
+ core.setOutput('has_command', 'false');
+
+ - name: Check permissions
+ id: check_permissions
+ if: steps.check_command.outputs.has_command == 'true'
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const commenter = context.payload.comment.user.login;
+ const prNumber = context.payload.issue.number;
+
+ // Get PR details to check if commenter is the PR author
+ const pr = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: prNumber
+ });
+
+ const isPrAuthor = pr.data.user.login === commenter;
+
+ console.log(`Commenter: ${commenter}, PR Author: ${pr.data.user.login}, Is PR Author: ${isPrAuthor}`);
+
+ // Following Prow's release-note plugin behavior: only PR authors can set release note labels
+ // This ensures the PR author takes responsibility for documenting their changes
+ if (!isPrAuthor) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: `@${commenter} only the PR author (@${pr.data.user.login}) can set release note labels. This ensures the author takes responsibility for documenting their changes.`
+ });
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'confused'
+ });
+ core.setFailed('Only PR author can use release note commands');
+ }
+
+ core.setOutput('has_permission', isPrAuthor);
+
+ - name: Apply release note label
+ if: steps.check_command.outputs.has_command == 'true' && steps.check_permissions.outputs.has_permission == 'true'
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const prNumber = context.payload.issue.number;
+ const labelToAdd = '${{ steps.check_command.outputs.label }}';
+
+ // All possible release note labels
+ const releaseNoteLabels = [
+ 'release-note',
+ 'release-note-action-required',
+ 'release-note-none'
+ ];
+
+ // Get current labels
+ const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber
+ });
+
+ const currentLabelNames = currentLabels.map(label => label.name);
+
+ // Remove other release note labels
+ for (const label of releaseNoteLabels) {
+ if (label !== labelToAdd && currentLabelNames.includes(label)) {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ name: label
+ });
+ }
+ }
+
+ // Add the new label
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ labels: [labelToAdd]
+ });
+
+ // Remove blocking label if present
+ if (currentLabelNames.includes('do-not-merge/release-note-label-needed')) {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ name: 'do-not-merge/release-note-label-needed'
+ });
+ }
+
+ // React to the comment
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+
+ check_release_note:
+ name: Check release note label
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+
+ steps:
+ - name: Check for release note labels
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const pr = context.payload.pull_request;
+ const labels = pr.labels.map(label => label.name);
+
+ console.log('PR labels:', labels);
+
+ // Valid release note labels
+ const releaseNoteLabels = [
+ 'release-note',
+ 'release-note-action-required',
+ 'release-note-none'
+ ];
+
+ const hasReleaseNoteLabel = labels.some(label => releaseNoteLabels.includes(label));
+ const hasBlockingLabel = labels.includes('do-not-merge/release-note-label-needed');
+
+ console.log('Has release note label:', hasReleaseNoteLabel);
+ console.log('Has blocking label:', hasBlockingLabel);
+
+ if (!hasReleaseNoteLabel && !hasBlockingLabel) {
+ // Add blocking label
+ console.log('Adding do-not-merge/release-note-label-needed label');
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: ['do-not-merge/release-note-label-needed']
+ });
+
+ // Add a comment explaining what's needed
+ const commentBody = [
+ 'This PR is missing a release note label. Please add one of the following labels:',
+ '- `release-note` - This PR has user-facing changes that should be in release notes',
+ '- `release-note-action-required` - This PR requires action from users',
+ '- `release-note-none` - This PR has no user-facing changes',
+ '',
+ 'Alternatively, you can comment with one of these commands:',
+ '- `/release-note`',
+ '- `/release-note-action-required`',
+ '- `/release-note-none`',
+ '',
+ 'The `do-not-merge/release-note-label-needed` label will be automatically removed once a release note label is added.'
+ ].join('\n');
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ body: commentBody
+ });
+ } else if (hasReleaseNoteLabel && hasBlockingLabel) {
+ // Remove blocking label
+ console.log('Removing do-not-merge/release-note-label-needed label');
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ name: 'do-not-merge/release-note-label-needed'
+ });
+ } catch (error) {
+ console.log('Label not found or already removed:', error.message);
+ }
+ } else {
+ console.log('No label change needed');
+ }
diff --git a/.github/workflows/pr_wip-label.yaml b/.github/workflows/pr_wip-label.yaml
new file mode 100644
index 00000000..83a662e9
--- /dev/null
+++ b/.github/workflows/pr_wip-label.yaml
@@ -0,0 +1,76 @@
+---
+name: Work In Progress Label
+# Automatically adds/removes do-not-merge/work-in-progress label based on PR title
+# Replaces Prow WIP plugin
+
+'on':
+ pull_request_target:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - edited
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ manage_wip_label:
+ name: Manage WIP label
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check PR title for WIP indicators
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const pr = context.payload.pull_request;
+ const title = pr.title.toLowerCase();
+
+ // WIP indicators in title
+ const wipPatterns = [
+ /^\[wip\]/i,
+ /^wip:/i,
+ /^wip\s/i,
+ /^\[draft\]/i,
+ /^draft:/i,
+ /^draft\s/i,
+ /🚧/,
+ ];
+
+ const isWip = wipPatterns.some(pattern => pattern.test(pr.title)) || pr.draft;
+
+ console.log('PR title:', pr.title);
+ console.log('Is draft:', pr.draft);
+ console.log('Is WIP:', isWip);
+
+ const labels = pr.labels.map(label => label.name);
+ const hasWipLabel = labels.includes('do-not-merge/work-in-progress');
+
+ if (isWip && !hasWipLabel) {
+ // Add WIP label
+ console.log('Adding do-not-merge/work-in-progress label');
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: ['do-not-merge/work-in-progress']
+ });
+ } else if (!isWip && hasWipLabel) {
+ // Remove WIP label
+ console.log('Removing do-not-merge/work-in-progress label');
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ name: 'do-not-merge/work-in-progress'
+ });
+ } catch (error) {
+ console.log('Label not found or already removed:', error.message);
+ }
+ } else {
+ console.log('No label change needed');
+ }
diff --git a/.github/workflows/reusable-e2e.yaml b/.github/workflows/reusable-e2e.yaml
index d3c63dcb..3d9873c9 100644
--- a/.github/workflows/reusable-e2e.yaml
+++ b/.github/workflows/reusable-e2e.yaml
@@ -14,7 +14,6 @@ defaults:
run:
shell: bash
working-directory: ./
-
jobs:
e2e-test:
name: e2e test