Skip to content

Dependabot auto-merge #313

Dependabot auto-merge

Dependabot auto-merge #313

name: Dependabot auto-merge
on:
workflow_run:
workflows: ["Quality", "Security", "Docker", "Dockerfile Lint", "Workflow Lint"]
types: [completed]
permissions:
actions: read
contents: write
pull-requests: write
jobs:
automerge:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
env:
SEEKARR_AUTOMERGE_TOKEN: ${{ secrets.SEEKARR_AUTOMERGE }}
steps:
- name: Validate Dependabot PR + CI success
id: guard
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
let prNumber;
let headSha;
prNumber = context.payload.workflow_run?.pull_requests?.[0]?.number;
headSha = context.payload.workflow_run?.head_sha;
if (!prNumber || !headSha) {
core.info('No PR context found; skipping.');
core.setOutput('ok', 'false');
return;
}
const { data: pr } = await github.rest.pulls.get({
...context.repo,
pull_number: prNumber,
});
if (pr.state !== 'open') {
core.info(`PR #${prNumber} is not open; skipping.`);
core.setOutput('ok', 'false');
return;
}
if (pr.user?.login !== 'dependabot[bot]') {
core.info(`PR #${prNumber} is not from dependabot[bot]; skipping.`);
core.setOutput('ok', 'false');
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
...context.repo,
pull_number: prNumber,
per_page: 100,
});
const filenames = files.map((f) => f.filename);
const hasNonGithubChanges = filenames.some((f) => !f.startsWith('.github/'));
const hasWorkflowChanges = filenames.some((f) => f.startsWith('.github/workflows/'));
core.setOutput('has_workflow_changes', String(hasWorkflowChanges));
const requiredWorkflows = [];
if (hasNonGithubChanges) {
requiredWorkflows.push('Quality', 'Security', 'Docker', 'Dockerfile Lint');
}
if (hasWorkflowChanges) {
requiredWorkflows.push('Workflow Lint');
}
if (requiredWorkflows.length === 0) {
core.info('No required CI workflows matched changed files; skipping.');
core.setOutput('ok', 'false');
return;
}
const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
...context.repo,
event: 'pull_request',
head_sha: headSha,
per_page: 100,
});
const latestByName = new Map();
for (const run of runs) {
const current = latestByName.get(run.name);
if (!current || new Date(run.created_at) > new Date(current.created_at)) {
latestByName.set(run.name, run);
}
}
const missing = [];
const notSuccessful = [];
for (const workflowName of requiredWorkflows) {
const run = latestByName.get(workflowName);
if (!run) {
missing.push(workflowName);
continue;
}
if (run.status !== 'completed' || run.conclusion !== 'success') {
notSuccessful.push(`${workflowName} (${run.status}/${run.conclusion})`);
}
}
if (missing.length || notSuccessful.length) {
core.info(`Missing required workflow runs: ${missing.join(', ') || 'none'}`);
core.info(`Required workflow runs not successful yet: ${notSuccessful.join(', ') || 'none'}`);
core.setOutput('ok', 'false');
return;
}
const { data: commit } = await github.rest.repos.getCommit({
...context.repo,
ref: headSha,
});
const message = commit.commit.message || '';
const updateTypeMatch = message.match(/update-type:\s*([^\s]+)/);
const updateType = updateTypeMatch ? updateTypeMatch[1] : '';
if (!updateType) {
core.info('Could not determine Dependabot update-type from commit message; skipping.');
core.setOutput('ok', 'false');
return;
}
core.info(`PR #${prNumber} eligible. update-type=${updateType}`);
core.setOutput('ok', 'true');
core.setOutput('pr_number', String(prNumber));
core.setOutput('update_type', updateType);
- name: Auto-approve (patch/minor only)
if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type)
uses: hmarr/auto-approve-action@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
pull-request-number: ${{ steps.guard.outputs.pr_number }}
- name: Workflow update needs elevated token
if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) && steps.guard.outputs.has_workflow_changes == 'true' && env.SEEKARR_AUTOMERGE_TOKEN == ''
run: |
echo "Dependabot PR touches .github/workflows. GitHub blocks mergePullRequest for app tokens without workflows permission."
echo "Set SEEKARR_AUTOMERGE (classic PAT with repo+workflow scopes or fine-grained token with Workflows write + Pull requests write) to auto-merge these PRs."
- name: Enable auto-merge (squash, workflow updates)
if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) && steps.guard.outputs.has_workflow_changes == 'true' && env.SEEKARR_AUTOMERGE_TOKEN != ''
uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ env.SEEKARR_AUTOMERGE_TOKEN }}
pull-request-number: ${{ steps.guard.outputs.pr_number }}
merge-method: squash
- name: Enable auto-merge (squash)
if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) && steps.guard.outputs.has_workflow_changes != 'true'
uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
pull-request-number: ${{ steps.guard.outputs.pr_number }}
merge-method: squash