Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions .github/workflows/dapr-maintainer-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@

name: Auto-approve & merge SDK docs PRs (per directory/team)

# Run on PRs (including forks) but act with repo-level permissions.
# We DO NOT check out PR code; we only read PR metadata via the API.
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, edited]

# Token scopes needed: label, review, and merge.
permissions:
contents: write # required to merge
pull-requests: write # required to approve/merge
issues: write # required to create/add labels

jobs:
sdk-docs-automerge:
runs-on: ubuntu-latest
# Ignore drafts
if: ${{ github.event.pull_request.draft == false }}

steps:
- name: Evaluate PR for SDK docs eligibility & add label
id: check
uses: actions/github-script@v7
with:
# Optional: override org/merge method/colors via env
# env:
# ORG: dapr
# MERGE_METHOD: squash
# LABEL_COLOR_DEFAULT: '6A9286'
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const number = pr.number;

// --- Mapping: directory prefixes -> team slug + label ---
// Each entry can have multiple prefixes for the same SDK if needed.
const MAPPINGS = [
{ label: 'automerge: dotnet', teamSlug: 'maintainers-dotnet-sdk', prefixes: ['sdkdocs/dotnet/'] },
{ label: 'automerge: go', teamSlug: 'maintainers-go-sdk', prefixes: ['sdkdocs/go/'] },
{ label: 'automerge: java', teamSlug: 'maintainers-java-sdk', prefixes: ['sdkdocs/java/content/en/'] },
{ label: 'automerge: js', teamSlug: 'maintainers-js-sdk', prefixes: ['sdkdocs/js/'] },
{ label: 'automerge: php', teamSlug: 'maintainers-php-sdk', prefixes: ['sdkdocs/php/'] },
{ label: 'automerge: python', teamSlug: 'maintainers-python-sdk', prefixes: ['sdkdocs/python/'] },
{ label: 'automerge: rust', teamSlug: 'maintainers-rust-sdk', prefixes: ['sdkdocs/rust/content/en/'] },
];

const org = owner;
const defaultLabelColor = '6A9286';
const username = pr.user.login;

// 1) List changed files
const files = await github.paginate(
github.rest.pulls.listFiles,
{ owner, repo, pull_number: number, per_page: 100 }
);

if (files.length === 0) {
core.info('No files changed in PR; skipping.');
core.setOutput('eligible', 'false');
return;
}

// 2) Determine which single SDK mapping the PR targets
// - All files must match ONE mapping's prefixes
// - If files touch multiple mappings or outside any mapping, skip

let currentMapping = null; // holds the mapping object we've locked onto
let ineligible = false;

for (const f of files) {
const path = f.filename;

// find the first mapping whose prefixes match this file
let matched = null;
for (const m of MAPPINGS) {
if (m.prefixes.some(p => path.startsWith(p))) {
matched = m;
break;
}
}

// if no mapping matched, we can stop: not eligible
if (!matched) {
ineligible = true;
break;
}

// if we haven't locked onto a mapping yet, set it now
if (!currentMapping) {
currentMapping = matched;
} else if (currentMapping !== matched) {
// different SDK mapping from the one already selected => not eligible
ineligible = true;
break;
}
}

if (ineligible || !currentMapping) {
core.info('PR is not eligible: outside mapped paths or touches multiple SDK directories.');
core.setOutput('eligible', 'false');
return;
}

const mapping = currentMapping;
const labelName = mapping.label;
const teamSlug = mapping.teamSlug;
const lang = mapping.label.split(': ')[1] || 'sdk';

// 3) Verify author is active in the corresponding team
// teams.getMembershipForUserInOrg: GET /orgs/{org}/teams/{team_slug}/memberships/{username}
// Requires team visibility to the token. [3](https://docs.github.com/rest/teams/members)
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org,
team_slug: teamSlug,
username
});
if (membership.data.state !== 'active') {
core.info(`User ${username} is not active in team ${teamSlug}.`);
core.setOutput('eligible', 'false');
return;
}
} catch (err) {
if (err.status === 404) {
core.info(`User ${username} is not a member of team ${teamSlug}.`);
core.setOutput('eligible', 'false');
return;
}
throw err;
}

// 4) Ensure label exists; then add it to the PR
try {
await github.rest.issues.getLabel({ owner, repo, name: labelName });
} catch (e) {
if (e.status === 404) {
await github.rest.issues.createLabel({
owner, repo, name: labelName, color: defaultLabelColor,
description: 'Auto-merged language-specific SDK docs'
});
} else {
throw e;
}
}
await github.rest.issues.addLabels({
owner, repo, issue_number: number, labels: [labelName]
});

// 5) Expose mapping for next step
core.setOutput('eligible', 'true');
core.setOutput('label', labelName);
core.setOutput('teamSlug', teamSlug);
core.setOutput('lang', lang);

- name: Auto-approve & merge (only if eligible)
if: steps.check.outputs.eligible == 'true'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const number = context.payload.pull_request.number;
const lang = core.getInput('lang') || '${{ steps.check.outputs.lang }}';
const mergeMethod = process.env.MERGE_METHOD || 'squash';

// 6) Auto-approve review
try {
await github.rest.pulls.createReview({
owner, repo, pull_number: number,
event: 'APPROVE',
body: `Auto-approval: ${lang} SDK docs`
});
} catch (e) {
core.warning(`Failed to create review: ${e.message}`);
}

// 7) Poll until PR is mergeable (clean/unstable)
const wait = ms => new Promise(r => setTimeout(r, ms));
let attempt = 0;
while (attempt < 12) { // up to ~60s
attempt++;
const pr = await github.rest.pulls.get({ owner, repo, pull_number: number });
const state = pr.data.mergeable_state;
core.info(`mergeable=${pr.data.mergeable}, mergeable_state=${state}`);
if (pr.data.mergeable && (state === 'clean' || state === 'unstable')) break;
await wait(5000);
}

// 8) Merge the PR
await github.rest.pulls.merge({
owner, repo, pull_number: number,
merge_method: mergeMethod,
commit_title: `${lang}: ${context.payload.pull_request.title}`,
commit_message: `Auto-merged by SDK maintainer merge bot (${lang})`
});
Loading