Skip to content

build(deps): bump org.jacoco:jacoco-maven-plugin from 0.8.14 to 0.8.15 #25

build(deps): bump org.jacoco:jacoco-maven-plugin from 0.8.14 to 0.8.15

build(deps): bump org.jacoco:jacoco-maven-plugin from 0.8.14 to 0.8.15 #25

name: Dependabot Auto Merge
# pull_request_target, not pull_request: a workflow Dependabot triggers gets a
# read-only token, and approving and merging needs a writable one.
#
# That trigger runs with the base branch's secrets, which is only safe as long
# as nothing here ever runs code from the pull request. Nothing does — there is
# deliberately no checkout step, and every step below talks to the GitHub API
# about the pull request rather than executing anything from it. Adding a
# checkout, a build, or any step that reads a file from the head branch would
# hand a dependency update write access to this repository, so do not.
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions:
contents: read
jobs:
auto-merge:
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
# Longer than the wait-and-merge fallback below, which polls for ~30 minutes.
timeout-minutes: 45
permissions:
contents: write # merge the pull request
pull-requests: write # approve it, and label or comment when merging is blocked
checks: read
statuses: read
steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Approve, wait for checks, and merge
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const selfJob = context.job;
const blockedLabel = "automerge-blocked";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function ensureLabel() {
try {
await github.rest.issues.getLabel({ owner, repo, name: blockedLabel });
} catch (error) {
if (error.status !== 404) return;
try {
await github.rest.issues.createLabel({
owner,
repo,
name: blockedLabel,
color: "b60205",
description: "Dependabot PR could not be auto-merged",
});
} catch (createError) {
core.info(`Could not create label: ${createError.message}`);
}
}
}
async function block(reason) {
core.warning(`Auto-merge blocked: ${reason}`);
await ensureLabel();
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [blockedLabel],
});
} catch (error) {
core.info(`Could not add label: ${error.message}`);
}
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: `🚧 **Auto-merge blocked** — ${reason}.\n\nThe pull request was left open for manual review. Fix the issue (or merge manually) and Dependabot / re-running this workflow will pick it up again.`,
});
} catch (error) {
core.info(`Could not comment: ${error.message}`);
}
}
async function clearBlock() {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: blockedLabel,
});
} catch (error) {
// Label was not present; nothing to clear.
}
}
// 1. A major bump can change behaviour no test here would notice, so it is
// left for a human to read the changelog rather than merged on green CI.
if (process.env.UPDATE_TYPE === "version-update:semver-major") {
await block("this is a major version update — review the changelog and merge manually");
return;
}
// 2. Approve the pull request (idempotent).
try {
await github.rest.pulls.createReview({
owner,
repo,
pull_number: prNumber,
event: "APPROVE",
body: "Auto-approving Dependabot pull request.",
});
} catch (error) {
core.info(`Approval skipped: ${error.message}`);
}
// 3. Choose a merge method the repository actually allows.
const { data: repoData } = await github.rest.repos.get({ owner, repo });
const mergeMethod = repoData.allow_squash_merge
? "squash"
: repoData.allow_rebase_merge
? "rebase"
: repoData.allow_merge_commit
? "merge"
: null;
if (!mergeMethod) {
await block("no merge method is enabled in the repository settings");
core.setFailed("No merge method is enabled for this repository.");
return;
}
// 4. Prefer GitHub's native auto-merge (used when branch protection is configured).
try {
const query = await github.graphql(
`query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) { id }
}
}`,
{ owner, repo, number: prNumber }
);
await github.graphql(
`mutation($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }) {
clientMutationId
}
}`,
{ pullRequestId: query.repository.pullRequest.id, mergeMethod: mergeMethod.toUpperCase() }
);
core.info(`Native auto-merge enabled (${mergeMethod}); GitHub will merge when required checks pass.`);
return;
} catch (error) {
const message = error.errors?.map((entry) => entry.message).join("; ") || error.message || "";
core.info(`Native auto-merge unavailable (${message}); falling back to wait-and-merge.`);
}
// 5. Fallback for repositories without branch protection: wait for checks, then merge directly.
const maxAttempts = 60; // ~30 minutes at 30s intervals.
const intervalMs = 30000;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
if (pr.merged) {
core.info("Pull request already merged.");
return;
}
if (pr.state !== "open") {
core.info(`Pull request is ${pr.state}; nothing to merge.`);
return;
}
const sha = pr.head.sha;
const checkRuns = (
await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: sha,
per_page: 100,
})
).filter((run) => run.name !== selfJob);
const { data: combined } = await github.rest.repos.getCombinedStatusForRef({ owner, repo, ref: sha });
const pendingChecks = checkRuns.filter((run) => run.status !== "completed");
const failedChecks = checkRuns.filter(
(run) => run.status === "completed" && !["success", "skipped", "neutral"].includes(run.conclusion)
);
const statuses = combined.statuses || [];
const statusPending = statuses.some((entry) => entry.state === "pending");
const statusFailed = combined.state === "failure" || combined.state === "error";
if (failedChecks.length > 0 || statusFailed) {
const names = failedChecks.map((run) => run.name).join(", ") || combined.state;
await block(`failing checks (${names})`);
return;
}
if (pendingChecks.length === 0 && !statusPending) {
if (pr.mergeable === false) {
await block("the pull request has a merge conflict");
return;
}
try {
await github.rest.pulls.merge({
owner,
repo,
pull_number: prNumber,
merge_method: mergeMethod,
});
await clearBlock();
core.info(`Merged pull request #${prNumber} using ${mergeMethod}.`);
return;
} catch (error) {
core.info(`Merge attempt ${attempt} failed: ${error.message}; retrying.`);
}
} else {
core.info(
`Waiting for checks (attempt ${attempt}/${maxAttempts}): ${pendingChecks.length} pending check run(s).`
);
}
await sleep(intervalMs);
}
await block("required checks did not complete within the auto-merge timeout");