Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
fbeeaea
dist, helm, contrib: drop manual zone-name labeling
koolzz May 13, 2026
ffed931
test/e2e/kubevirt: simplify worker selection for live-migration
koolzz May 13, 2026
1e20211
docs/installation: drop manual zone-name labeling step
koolzz May 13, 2026
be80cc0
test/e2e: drop dead multi-node-per-zone branching
koolzz May 13, 2026
7525f9c
e2e: wait for localnet multi network policy convergence
ricky-rav Jun 2, 2026
1d8db6e
Bump frr-k8s for next-hop API
trozet Jun 4, 2026
0afda7b
ovn: remove legacy external-gateway annotation handling
koolzz Jun 4, 2026
2d0bede
apbroute: stop honoring legacy external-gateway annotations
koolzz Jun 4, 2026
52634fa
util,node,config: drop legacy external-gateway annotations
koolzz Jun 4, 2026
be576f2
tests,e2e: remove legacy external-gateway annotation specs
koolzz Jun 4, 2026
c824eb8
node: run periodic external-gateway conntrack cleanup for APB namespaces
koolzz Jun 4, 2026
1355df2
tests: cover external-gateway route deletion for APB-managed routes
koolzz Jun 4, 2026
ebc865c
tests,e2e: drop orphaned external-gw-pod-ips constant
koolzz Jun 4, 2026
fc34b18
tests,e2e: add BFD route-cleanup coverage and fix stale wording
koolzz Jun 4, 2026
a0c9fa1
Set DPU host BGP next-hop for no-overlay routes
trozet May 18, 2026
fa24ddb
Fix using docker for dpu-sim CI lane
trozet Jun 4, 2026
cc84a97
Stop using deprecated frr-k8s DisableMP field
trozet Jun 4, 2026
351aa10
Bump OpenTelemetry to v1.41.0
trozet Jun 4, 2026
4ab105d
ci: wait for service reachability after dual-stack conversion
ricky-rav Jun 4, 2026
d9cefde
Use FRR-K8S next-hop API in kind installs
trozet Jun 4, 2026
b5773df
Merge pull request #6506 from trozet/dpu-ci-offload-docker
trozet Jun 5, 2026
09cf494
[FYI] prevent duplicate logical_router_static_route
freedge Jun 5, 2026
6c7e649
Merge pull request #6427 from trozet/dpu-next-hop-bgp
trozet Jun 5, 2026
0fe6e3d
dpulease: fix nil pointer panic in CNI health check
tsorya Jun 6, 2026
c83b6f3
e2e: configure framework TestContext to verify service accounts
jcaamano Jun 8, 2026
4b4d0b9
RA controller: rename evpn_rawconfig to rawconfig for future broader use
jcaamano May 29, 2026
208be41
Merge pull request #6498 from ricky-rav/fix-mnp-localnet-policy-race
npinaeva Jun 8, 2026
e08225f
RA contrioller: add unicast allowas-in origin to raw FRR config
jcaamano May 28, 2026
047d5d3
Sync reconcile pod-selector address sets at creation time
cathy-zhou Jun 5, 2026
27454c1
Merge pull request #6518 from tsorya/fix/dpulease-nil-receiver
trozet Jun 8, 2026
7a7f4ca
ovn: drop dead IPAMClaim watch after non-IC removal
cathy-zhou May 19, 2026
ca60863
ovn: drop unreachable IPAMClaim reconciler after non-IC removal
cathy-zhou May 19, 2026
d7ac03e
Support manual DPU simulator Helm installs
trozet May 18, 2026
7fc0f4b
Add DPU simulator no-overlay CI lane
trozet Jun 2, 2026
107c5d5
Handle renamed FRR-K8S controller in DPU sim
trozet Jun 4, 2026
95380fd
Peer DPU sim FRR over gateway network
trozet Jun 5, 2026
f8f3c2b
Route DPU host no-overlay traffic through OVN
trozet Jun 6, 2026
1160194
Make TFT report failure for DPU CI jobs
trozet Jun 6, 2026
2d545a0
contrib: refresh OVN pods on kind-helm.sh --deploy
koolzz May 13, 2026
40ee196
Merge pull request #6431 from trozet/dpu-sim-helm-install
trozet Jun 8, 2026
a11c389
area-maintainers: introduce kubevirt area with merge bot
tssurya Jun 1, 2026
9e8bb31
fix(evpn): gate neighbor programming on migration domain readiness
qinqon May 26, 2026
ff61143
refactor: rename LinkNeighAdd to LinkNeighSet
qinqon May 26, 2026
8fd2cad
refactor: rename LinkFDBAdd to LinkFDBSet, use NeighSet
qinqon May 26, 2026
94cf737
fix(evpn): remove dead EEXIST check from LinkFDBSet caller
qinqon May 26, 2026
08dee44
test(evpn): add unit tests for neighbor timing during live migration
qinqon May 27, 2026
8597950
e2e(virt): Add evpn failed migration test
qinqon May 27, 2026
e01ebbc
Merge pull request #6510 from freedge/duproute
trozet Jun 9, 2026
797423e
Merge pull request #6505 from booxter/license-adjust
trozet Jun 9, 2026
b8a3e69
Merge pull request #6499 from koolzz/worktree-remove-legacy-egressgw-…
trozet Jun 9, 2026
90411a5
Merge pull request #6507 from ricky-rav/fix-dual-stack-conversion-ser…
trozet Jun 9, 2026
d59e62b
cni: gracefully handle missing device info file for primary UDN
tsorya May 28, 2026
aabbf2e
Merge pull request #6466 from qinqon/kv-evpn-target-node-neighbors-wa…
kyrtapz Jun 10, 2026
268848f
e2e: fix UDN subnet overlap with downstream service CIDR
jcaamano Jun 9, 2026
8f86bc5
Merge pull request #6481 from jcaamano/unicast-allowas-in-origin
kyrtapz Jun 10, 2026
9e0a3bf
Merge pull request #6521 from jcaamano/e2e-verify-service-account
npinaeva Jun 10, 2026
1fabdec
Merge pull request #6408 from koolzz/kind-helm-deploy-pod-refresh
npinaeva Jun 10, 2026
e8d2541
Merge pull request #6428 from cathy-zhou/clanupIpamClaim
npinaeva Jun 10, 2026
9ebc3af
Adding missing labels for UDN workloads
jtaleric Jun 5, 2026
20ca6f1
Merge pull request #6515 from jtaleric/fix-udn-perf-workload
kyrtapz Jun 11, 2026
8a5781b
Merge pull request #6401 from koolzz/remove_labels
npinaeva Jun 11, 2026
b5e35d1
Merge pull request #6511 from cathy-zhou/syncAddressset
npinaeva Jun 11, 2026
75597a6
Add transport label to CUDN count metric
mattedallo Apr 17, 2026
8512a3b
Address review feedback on CUDN transport metric
mattedallo Jun 10, 2026
857ce9a
Add CLI flags for configuring TLS to ovnkube-identity
tpantelis May 27, 2026
3ac5bf9
Use TLS config params in the ovnkube-identity webhook
tpantelis May 28, 2026
d355cbb
Merge pull request #6472 from tpantelis/ovnk_identity_tls_args
kyrtapz Jun 12, 2026
05a6b10
Merge pull request #6490 from tssurya/area-maintainers-concept
tssurya Jun 12, 2026
dfcb242
Fix UDN controller startup timeout during pod sync
jluhrsen Jun 9, 2026
851b27e
Merge pull request #6486 from tsorya/fix/udn-skip-devinfo-dpu-host-v2
trozet Jun 12, 2026
07394f1
Merge remote-tracking branch 'upstream/master' into d/s-merge-06-15-2026
Jun 15, 2026
8e2a679
sync test annotations with upstream changes
Jun 15, 2026
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
338 changes: 338 additions & 0 deletions .github/scripts/area-merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
// Area Maintainer Merge Bot
//
// Parses CODEOWNERS to determine area maintainer authorization, then verifies
// file scope and CI status before merging PRs via /area-maintainer-approved.
//
// Called from .github/workflows/area-merge.yml via actions/github-script.

const fs = require('fs');

module.exports = async ({ github, context, core }) => {
// --- Determine which PRs to process and who triggered ---
let pullNumbers = [];
let commenter = null;

if (context.eventName === 'issue_comment') {
pullNumbers.push(context.payload.issue.number);
commenter = context.payload.comment.user.login.toLowerCase();
} else if (context.eventName === 'check_suite') {
const suite = context.payload.check_suite;
for (const pr of (suite.pull_requests || [])) {
pullNumbers.push(pr.number);
}
}

if (pullNumbers.length === 0) {
core.info('No pull requests to process');
return;
}

// --- Parse CODEOWNERS ---
// Extracts two things:
// 1. Pattern rules: which users are listed on each file pattern line (for review assignment)
// 2. Area maintainers: parsed from section header comments matching
// "Area Maintainer: @user1 @user2" — only these users can trigger merges
const codeownersContent = fs.readFileSync('CODEOWNERS', 'utf8');
const rules = [];
let currentAreaMaintainers = [];

for (const line of codeownersContent.split('\n')) {
const trimmed = line.trim();

// Parse area maintainer(s) from section header comments
// Format: # ... (Area Maintainer: @user1 @user2)
const maintainerMatch = trimmed.match(/Area Maintainer[s]?:\s*((?:@[\w-]+[\s,]*)+)/i);
if (maintainerMatch) {
currentAreaMaintainers = maintainerMatch[1]
.match(/@[\w-]+/g)
.map(u => u.replace('@', '').toLowerCase());
continue;
}

if (!trimmed || trimmed.startsWith('#')) continue;
const parts = trimmed.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
if (pattern === '*') continue;
const owners = parts.slice(1).map(o => o.replace('@', '').toLowerCase());
rules.push({ pattern, owners, areaMaintainers: [...currentAreaMaintainers] });
}

function matchPattern(pattern, filePath) {
const normalizedFile = '/' + filePath;
if (pattern.endsWith('/')) {
return normalizedFile.startsWith(pattern) ||
normalizedFile === pattern.slice(0, -1);
}
return normalizedFile === pattern;
}

// Last matching rule wins, mirroring CODEOWNERS precedence.
function findAreaMaintainers(filePath) {
let maintainers = null;
for (const rule of rules) {
if (matchPattern(rule.pattern, filePath)) {
maintainers = rule.areaMaintainers;
}
}
return maintainers;
}

async function fetchAllComments(prNumber) {
const allComments = [];
let page = 1;
while (true) {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
page: page,
});
if (comments.length === 0) break;
allComments.push(...comments);
if (comments.length < 100) break;
page++;
}
return allComments;
}

// --- Process each PR ---
for (const prNumber of pullNumbers) {
core.info(`Processing PR #${prNumber}`);

const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});

if (pr.state !== 'open') {
core.info(`PR #${prNumber} is ${pr.state}, skipping`);
continue;
}

// Prevent PR authors from merging their own PRs
const prAuthor = pr.user.login.toLowerCase();
if (commenter && commenter === prAuthor) {
core.info(`PR #${prNumber} author ${prAuthor} cannot approve their own PR`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `@${commenter} you cannot use \`/area-maintainer-approved\` on your own PR. ` +
`Another area maintainer or a repo maintainer must approve and merge it.`,
});
continue;
}

// For check_suite retries, find who previously ran /area-maintainer-approved
let requester = commenter;
let allComments = null;
if (!requester) {
allComments = await fetchAllComments(prNumber);
const mergeComment = [...allComments]
.reverse()
.find(c => c.body.includes('/area-maintainer-approved') &&
c.user.login !== 'github-actions[bot]' &&
c.user.login.toLowerCase() !== prAuthor);
if (mergeComment) {
requester = mergeComment.user.login.toLowerCase();
}
}

if (!requester) {
core.info(`No /area-maintainer-approved request found for PR #${prNumber}, skipping`);
continue;
}

// For check_suite, verify the bot posted a "waiting" comment for the current head SHA.
// If new commits were pushed after approval, the SHA won't match and we skip
// to prevent merging code the area maintainer never reviewed.
if (context.eventName === 'check_suite') {
if (!allComments) allComments = await fetchAllComments(prNumber);
const waitingMarker = `<!-- area-merge-sha:${pr.head.sha} -->`;
const isWaiting = allComments.some(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes(waitingMarker)
);
if (!isWaiting) {
core.info(`PR #${prNumber} has no waiting marker for current SHA ${pr.head.sha}, skipping`);
continue;
}
}

core.info(`/area-maintainer-approved requested by: ${requester}`);

// Get changed files (paginate for large PRs)
const changedFiles = [];
let page = 1;
while (true) {
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
page: page,
});
if (files.length === 0) break;
changedFiles.push(...files.map(f => f.filename));
if (files.length < 100) break;
page++;
}

core.info(`Changed files (${changedFiles.length}): ${changedFiles.join(', ')}`);

// Check authorization: requester must be an area maintainer for ALL changed files
const unauthorizedFiles = [];
for (const file of changedFiles) {
const maintainers = findAreaMaintainers(file);
if (!maintainers || !maintainers.includes(requester)) {
unauthorizedFiles.push(file);
}
}

if (unauthorizedFiles.length > 0) {
core.info(`Unauthorized files: ${unauthorizedFiles.join(', ')}`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `@${requester} cannot merge this PR via \`/area-maintainer-approved\`.\n\n` +
`You are not listed as an Area Maintainer for the following files in \`CODEOWNERS\`:\n` +
unauthorizedFiles.map(f => `- \`${f}\``).join('\n') +
`\n\nOnly area maintainers (listed in the \`Area Maintainer:\` section header in CODEOWNERS) can trigger merges. ` +
`A repo maintainer from \`ovn-kubernetes-committers\` is required to merge this PR.`,
});
continue;
}

// Check CI status
const ref = pr.head.sha;

const allCheckRuns = [];
let checkPage = 1;
while (true) {
const { data } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
per_page: 100,
page: checkPage,
});
allCheckRuns.push(...data.check_runs);
if (allCheckRuns.length >= data.total_count) break;
checkPage++;
}

const { data: combinedStatus } = await github.rest.repos.getCombinedStatusForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
});

const pendingChecks = [];
const failedChecks = [];

for (const run of allCheckRuns) {
if (run.name === 'area-merge') continue;
if (run.status !== 'completed') {
pendingChecks.push(run.name);
} else if (run.conclusion !== 'success' && run.conclusion !== 'neutral' && run.conclusion !== 'skipped') {
failedChecks.push(run.name);
}
}

// Gate on the aggregate state first — it covers ALL commit statuses
// regardless of pagination, so we never miss a failure or pending status.
// Then iterate the (possibly partial) statuses array for detailed names.
if (combinedStatus.state === 'failure') {
for (const status of combinedStatus.statuses) {
if (status.state === 'failure' || status.state === 'error') {
failedChecks.push(status.context);
}
}
if (failedChecks.length === 0) {
failedChecks.push('(commit status failure — details may be on a later page)');
}
} else if (combinedStatus.state === 'pending') {
for (const status of combinedStatus.statuses) {
if (status.state === 'pending') {
pendingChecks.push(status.context);
} else if (status.state === 'failure' || status.state === 'error') {
failedChecks.push(status.context);
}
}
if (pendingChecks.length === 0 && failedChecks.length === 0) {
pendingChecks.push('(commit status pending — details may be on a later page)');
}
}

if (failedChecks.length > 0) {
core.info(`Failed checks: ${failedChecks.join(', ')}`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `Cannot merge via \`/area-maintainer-approved\`: the following CI checks have failed:\n` +
failedChecks.map(c => `- \`${c}\``).join('\n') +
`\n\nPlease fix the failures and run \`/area-maintainer-approved\` again.`,
});
continue;
}

if (pendingChecks.length > 0) {
core.info(`Pending checks: ${pendingChecks.join(', ')}`);

const shaMarker = `<!-- area-merge-sha:${ref} -->`;
if (!allComments) allComments = await fetchAllComments(prNumber);
const alreadyWaiting = allComments.some(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes(shaMarker)
);
if (!alreadyWaiting) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `${shaMarker}\nArea maintainer @${requester} has approved this PR via \`/area-maintainer-approved\`. ` +
`Waiting on CI checks to complete — will merge automatically when all checks pass.\n\n` +
`Pending checks:\n` +
pendingChecks.map(c => `- \`${c}\``).join('\n'),
});
}
continue;
}

// All checks passed, authorized — merge!
core.info(`All checks passed. Merging PR #${prNumber}`);
try {
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
sha: ref,
merge_method: 'merge',
commit_title: `Merge pull request #${prNumber} (/area-maintainer-approved by @${requester})`,
});

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `PR merged by area maintainer @${requester} via \`/area-maintainer-approved\`.`,
});

core.info(`Successfully merged PR #${prNumber}`);
} catch (e) {
core.setFailed(`Failed to merge PR #${prNumber}: ${e.message}`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `Failed to merge this PR: \`${e.message}\`\n\n` +
`A repo maintainer may need to merge manually.`,
});
}
}
};
38 changes: 38 additions & 0 deletions .github/workflows/area-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: "Area Maintainer Merge"

on:
issue_comment:
types: [created]
check_suite:
types: [completed]

jobs:
area-merge:
if: >
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/area-maintainer-approved')) ||
github.event_name == 'check_suite'
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
runs-on: ubuntu-latest
steps:
- name: Checkout base branch (for CODEOWNERS and scripts)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: |
CODEOWNERS
.github/scripts/area-merge.js
persist-credentials: false

- name: Area maintainer merge check
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const script = require('./.github/scripts/area-merge.js');
await script({ github, context, core });
Loading