Skip to content
Open
Show file tree
Hide file tree
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
91 changes: 91 additions & 0 deletions .github/workflows/branch-protection-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Branch Protection Check

on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM UTC
workflow_dispatch:

permissions:
contents: read

jobs:
verify-config:
name: Verify Dependabot and auto-merge setup
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
Comment on lines +16 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false for defense in depth.

Although this workflow only reads the repository, disabling credential persistence prevents potential credential leakage through artifacts or logs.

🛡️ Proposed fix
       - name: Checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 16-17: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/branch-protection-check.yml around lines 16 - 17, Update
the "Checkout code" step that uses actions/checkout@v4 to explicitly set
persist-credentials: false to avoid leaving GitHub token credentials available
to later steps; locate the step with the uses: actions/checkout@v4 entry and add
the persist-credentials: false input under that step so credential persistence
is disabled.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin action to commit hash for supply chain security.

Using a mutable tag reference (@v4) instead of an immutable commit hash exposes the workflow to potential supply chain attacks if the action's tag is moved or compromised.

🔒 Proposed fix
-        uses: actions/checkout@v4
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 16-17: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/branch-protection-check.yml at line 17, The workflow
currently uses the mutable reference "uses: actions/checkout@v4"; replace that
mutable tag with the action's immutable commit SHA (e.g., "uses:
actions/checkout@<commit-sha>") by locating the step that contains "uses:
actions/checkout@v4" and swapping the tag for the pinned commit hash from the
actions/checkout repository's release commit; update any other actions in the
file that use mutable tags similarly and commit the change so the workflow uses
fixed, immutable references.


- name: Validate Dependabot and workflow configuration
run: |
set -euo pipefail
pip install --quiet pyyaml
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pin PyYAML version for reproducibility and security.

Installing PyYAML without a version constraint exposes the workflow to potential supply chain risks and non-reproducible builds if a new version introduces breaking changes or vulnerabilities.

📌 Proposed fix
-          pip install --quiet pyyaml
+          pip install --quiet pyyaml==6.0.2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pip install --quiet pyyaml
pip install --quiet pyyaml==6.0.2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/branch-protection-check.yml at line 22, The workflow
installs PyYAML without a version pin (the step containing "pip install --quiet
pyyaml"); change that command to install a specific, pinned PyYAML release (for
example replace it with a pinned version like pyyaml==6.0) so installs are
reproducible and mitigate supply-chain risks, and update the "pip install
--quiet pyyaml" invocation in the branch-protection-check.yml file accordingly
(optionally include a hash or use a requirements file if you prefer stronger
verification).

python3 <<'PY'
import sys
from pathlib import Path

import yaml

def fail(msg: str) -> None:
print(f"❌ {msg}")
sys.exit(1)

dependabot_path = Path(".github/dependabot.yml")
if not dependabot_path.is_file():
fail("Dependabot configuration missing (.github/dependabot.yml)")

with dependabot_path.open() as f:
cfg = yaml.safe_load(f)
if not isinstance(cfg, dict):
fail("dependabot.yml must be a YAML mapping")
if cfg.get("version") != 2:
fail("dependabot.yml: version must be 2")
updates = cfg.get("updates")
if not isinstance(updates, list) or not updates:
fail("dependabot.yml: updates must be a non-empty list")
for i, entry in enumerate(updates):
if not isinstance(entry, dict):
fail(f"dependabot.yml: updates[{i}] must be a mapping")
if not entry.get("package-ecosystem"):
fail(f"dependabot.yml: updates[{i}] missing package-ecosystem")
if "directory" not in entry:
fail(f"dependabot.yml: updates[{i}] missing directory")
Comment on lines +51 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate directory field value, not just presence.

The check only verifies the directory key exists, but doesn't validate its value. An empty string, None, or non-string value would pass this check but fail Dependabot validation.

✅ Proposed fix
-              if "directory" not in entry:
-                  fail(f"dependabot.yml: updates[{i}] missing directory")
+              if not entry.get("directory"):
+                  fail(f"dependabot.yml: updates[{i}] missing or empty directory")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if "directory" not in entry:
fail(f"dependabot.yml: updates[{i}] missing directory")
if not entry.get("directory"):
fail(f"dependabot.yml: updates[{i}] missing or empty directory")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/branch-protection-check.yml around lines 51 - 52, The
current check only ensures the "directory" key exists; update the validation
around entry and i to also ensure entry["directory"] is a non-empty string (not
None, not empty/whitespace, and of type str) before accepting it; if it fails
these checks call fail with a clear message (e.g., keep using
fail(f"dependabot.yml: updates[{i}] missing directory") or a slightly adjusted
message) so invalid values don't slip through.


print("✅ dependabot.yml structure is valid")
for entry in updates:
print(f" - {entry.get('package-ecosystem')} ({entry.get('directory')})")

workflow_path = Path(".github/workflows/dependabot-auto-merge.yml")
if not workflow_path.is_file():
fail("dependabot-auto-merge workflow missing")

with workflow_path.open() as f:
wf = yaml.safe_load(f)
if not isinstance(wf, dict):
fail("dependabot-auto-merge.yml must be a YAML mapping")
on = wf.get("on")
if not isinstance(on, dict) or "pull_request_target" not in on:
fail("dependabot-auto-merge.yml must use pull_request_target trigger")
jobs = wf.get("jobs")
if not isinstance(jobs, dict) or "auto-merge" not in jobs:
fail("dependabot-auto-merge.yml must define jobs.auto-merge")
job = jobs["auto-merge"]
if not isinstance(job, dict):
fail("jobs.auto-merge must be a mapping")
steps = job.get("steps")
if not isinstance(steps, list) or not steps:
fail("jobs.auto-merge must define steps")
uses = [
s.get("uses", "")
for s in steps
if isinstance(s, dict)
]
if not any("dependabot/fetch-metadata" in u for u in uses):
fail("jobs.auto-merge must include dependabot/fetch-metadata")

print("✅ dependabot-auto-merge.yml structure is valid")
print("")
print("ℹ️ dependabot-auto-merge.yml enables merge via GraphQL")
print(" enablePullRequestAutoMerge; the PR merges only after existing")
print(" required status checks pass (e.g. ci/prow/*). No extra CI job.")
PY
234 changes: 234 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
name: Dependabot Auto-Merge

on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: write
pull-requests: write
checks: read
actions: read

jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository_owner == 'openshift'
steps:
- name: Fetch Dependabot Metadata
id: metadata
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

- name: Enable Auto-Merge for Safe Updates
id: enable-auto-merge
Comment on lines +24 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ci/prow/* presence is not enforced before auto-merge is enabled.

This path goes from metadata lookup straight to enablePullRequestAutoMerge; there is no status/check-run query anywhere in the job. Any patch/minor Dependabot PR will therefore be put on the auto-merge path even when no ci/prow/* check exists for the head SHA, which is looser than the PR objective. Add a fail-closed guard before the GraphQL mutation.

Also applies to: 87-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-auto-merge.yml around lines 24 - 25, The
workflow step with id enable-auto-merge calls enablePullRequestAutoMerge without
verifying that ci/prow/* checks exist; add a fail-closed guard before invoking
enablePullRequestAutoMerge by querying the PR head SHA's check-runs/statuses
(e.g., using the Checks API or a GraphQL statusLookup) and ensure at least one
ci/prow/* check-run or required status is present; if none are found, skip or
abort auto-merge to enforce the ci/prow/* presence. Use the same guard logic for
the earlier block referenced (lines 87-113) and keep the step name/id
(enable-auto-merge / enablePullRequestAutoMerge) so the mutation is only called
when the check-run guard passes.

if: |
steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
steps.metadata.outputs.update-type == 'version-update:semver-minor'
Comment on lines +24 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

What output from the current dependabot/fetch-metadata@v2 action should a workflow use to distinguish digest updates from semver patch/minor/major updates?

💡 Result:

To distinguish digest updates from semver (major, minor, patch) updates using the dependabot/fetch-metadata@v2 action, you should use the update-type output [1][2]. The update-type output provides the highest semver change being made by the pull request [1][2]. When Dependabot performs a digest update (e.g., updating a Docker image by its SHA), the update-type output will typically be empty or null, as it does not correspond to a semver version increment [3]. In contrast, semver updates will return values such as: - version-update:semver-major [1] - version-update:semver-minor [1] - version-update:semver-patch [1] To implement this in a GitHub Actions workflow, you can check the value of this output [1]: - To target semver updates: if: ${{ steps.dependabot-metadata.outputs.update-type!= '' }} - To target digest updates (where update-type is null or empty): if: ${{ steps.dependabot-metadata.outputs.update-type == '' }} Note that for more complex scenarios, such as grouped updates or specific package ecosystems, you may also want to inspect the updated-dependencies-json output, which provides a detailed JSON structure containing the full information about each updated dependency, including the previous and new versions [1][2].

Citations:


🏁 Script executed:

# Look for any PR description, issue, or documentation that states the objective
git log --oneline --all -20

Repository: openshift/certman-operator

Length of output: 1528


🏁 Script executed:

# Check for any documentation about the workflow's intended behavior
fd -t f -e md | xargs rg -l "digest.*auto" -i 2>/dev/null || echo "No direct matches for digest auto-merge objective"

Repository: openshift/certman-operator

Length of output: 120


🏁 Script executed:

# Check the commit or PR that introduced this workflow
git log --oneline -- ".github/workflows/dependabot-auto-merge.yml" | head -5

Repository: openshift/certman-operator

Length of output: 125


🏁 Script executed:

# Get the full commit message for the workflow introduction
git show 80fe2a8 --format=fuller -s

Repository: openshift/certman-operator

Length of output: 584


🏁 Script executed:

# Search for any ROSA-745 references or related documentation
rg -i "rosa-745|digest.*update" --type md

Repository: openshift/certman-operator

Length of output: 52


Add digest update detection to the auto-merge gate.

The PR objective explicitly requires patch, minor, and digest updates to auto-merge, but this condition only checks for semver patch/minor. Digest updates produce an empty update-type and will not match either condition, causing them to fall through to the "not eligible" branch.

Add || steps.metadata.outputs.update-type == '' to detect digest updates:

Diff
      - name: Enable Auto-Merge for Safe Updates
        id: enable-auto-merge
        if: |
          steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
          steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
          steps.metadata.outputs.update-type == ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-auto-merge.yml around lines 24 - 28, The
auto-merge job gating condition in the step with id enable-auto-merge only
checks for semver patch/minor updates and thus misses digest updates (which
yield an empty update-type); update the if condition that references
steps.metadata.outputs.update-type to also accept an empty string by adding ||
steps.metadata.outputs.update-type == '' so digest updates are considered
eligible for auto-merge.

env:
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
PREVIOUS_VERSION: ${{ steps.metadata.outputs.previous-version }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
export GH_TOKEN

comment_count() {
local marker="$1"
local http_code
http_code=$(curl -sS -w "%{http_code}" -o /tmp/comments-list.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments")
if [[ "$http_code" != "200" ]]; then
echo "::warning::Could not list PR comments (HTTP ${http_code})" >&2
echo "1"
return
fi
jq --arg m "$marker" '[.[] | select(.body | contains($m))] | length' /tmp/comments-list.json
}

post_issue_comment() {
local body="$1"
local http_code
http_code=$(curl -sS -w "%{http_code}" -o /tmp/comment-response.json \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \
-d "$(jq -n --arg body "$body" '{body: $body}')")
if [[ ! "$http_code" =~ ^2 ]]; then
echo "❌ Failed to post PR comment. HTTP status: ${http_code}"
cat /tmp/comment-response.json
echo "::warning::PR comment could not be posted"
return 1
fi
}

graphql_auto_merge_ok() {
local http_code="$1"
[[ "$http_code" == "200" ]] || return 1
jq -e '(.errors // []) | length == 0' /tmp/response.json >/dev/null 2>&1 || return 1
jq -e '.data.enablePullRequestAutoMerge.pullRequest != null' /tmp/response.json >/dev/null 2>&1
}

graphql_error_summary() {
jq -c '(.errors // []) | if length > 0 then . else .data end' /tmp/response.json 2>/dev/null || cat /tmp/response.json
}

echo "Enabling auto-merge for ${UPDATE_TYPE} update"
echo "Dependency: ${DEPENDENCY_NAMES}"

pr_http_code=$(curl -sS -w "%{http_code}" -o /tmp/pr-response.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${REPOSITORY}/pulls/${PR_NUMBER}")

if [[ "$pr_http_code" != "200" ]]; then
echo "❌ Failed to fetch PR metadata. HTTP status: ${pr_http_code}"
cat /tmp/pr-response.json
echo "auto_merge_enabled=false" >> "$GITHUB_OUTPUT"
exit 1
fi

PR_NODE_ID=$(jq -r '.node_id' /tmp/pr-response.json)
if [[ -z "$PR_NODE_ID" || "$PR_NODE_ID" == "null" ]]; then
echo "❌ Failed to parse PR node ID from response"
cat /tmp/pr-response.json
echo "auto_merge_enabled=false" >> "$GITHUB_OUTPUT"
exit 1
fi

http_code=$(curl -sS -w "%{http_code}" -o /tmp/response.json \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/graphql" \
-d "{\"query\":\"mutation { enablePullRequestAutoMerge(input: { pullRequestId: \\\"$PR_NODE_ID\\\", mergeMethod: SQUASH }) { pullRequest { autoMergeRequest { enabledAt } } } }\"}")

if graphql_auto_merge_ok "$http_code"; then
echo "✅ Auto-merge enabled successfully via GraphQL"
cat /tmp/response.json
echo "auto_merge_enabled=true" >> "$GITHUB_OUTPUT"
else
api_detail=$(graphql_error_summary)
echo "❌ Failed to enable auto-merge. HTTP status: ${http_code}"
echo "Response body:"
cat /tmp/response.json
echo "auto_merge_enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::Could not enable auto-merge. PR may need manual review."
if [[ "$(comment_count 'Dependabot Auto-Merge Status')" -eq 0 ]]; then
failure_body=$(jq -rn \
--arg ut "$UPDATE_TYPE" \
--arg deps "$DEPENDENCY_NAMES" \
--arg prev "$PREVIOUS_VERSION" \
--arg new "$NEW_VERSION" \
--arg api "$api_detail" \
'@text "🤖 **Dependabot Auto-Merge Status**

This PR meets the criteria for auto-merge but could not be automatically merged.

**Details:**
- Update type: \($ut)
- Dependencies: \($deps)
- Previous version: \($prev)
- New version: \($new)
- API response: `\($api)`

Please review and merge manually if appropriate."')
post_issue_comment "$failure_body" || true
else
echo "Auto-merge status comment already posted; skipping duplicate"
fi
fi

- name: Comment on Major Version Updates
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
env:
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
PREVIOUS_VERSION: ${{ steps.metadata.outputs.previous-version }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
export GH_TOKEN

comments_http=$(curl -sS -w "%{http_code}" -o /tmp/comments-list.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments")
if [[ "$comments_http" != "200" ]]; then
echo "::warning::Could not list PR comments (HTTP ${comments_http})" >&2
exit 0
fi
existing=$(jq '[.[] | select(.body | contains("Major Version Update Detected"))] | length' /tmp/comments-list.json)

if [[ "$existing" -gt 0 ]]; then
echo "Major-version notice already posted; skipping duplicate comment"
exit 0
fi

major_body=$(jq -rn \
--arg deps "$DEPENDENCY_NAMES" \
--arg prev "$PREVIOUS_VERSION" \
--arg new "$NEW_VERSION" \
'@text "🚨 **Major Version Update Detected** 🚨

This PR contains a major version update that requires manual review:
- **Dependency:** \($deps)
- **Previous version:** \($prev)
- **New version:** \($new)

Please review the changelog and breaking changes before merging.

Auto-merge has been **disabled** for this PR."')

http_code=$(curl -sS -w "%{http_code}" -o /tmp/comment-response.json \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \
-d "$(jq -n --arg body "$major_body" '{body: $body}')")

if [[ ! "$http_code" =~ ^2 ]]; then
echo "❌ Failed to post major-version comment. HTTP status: ${http_code}"
cat /tmp/comment-response.json
echo "::warning::Major-version comment could not be posted"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Log Auto-Merge Decision
if: always() && steps.metadata.outcome == 'success'
env:
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
PR_NUMBER: ${{ github.event.pull_request.number }}
AUTO_MERGE_ENABLED: ${{ steps.enable-auto-merge.outputs.auto_merge_enabled }}
run: |
echo "Auto-merge decision for PR #${PR_NUMBER}:"
echo "- Update type: ${UPDATE_TYPE}"
echo "- Dependency: ${DEPENDENCY_NAMES}"

case "${UPDATE_TYPE}" in
version-update:semver-patch|version-update:semver-minor)
if [[ "${AUTO_MERGE_ENABLED}" == "true" ]]; then
echo "✅ Auto-merge ENABLED (GraphQL mutation succeeded)"
elif [[ "${AUTO_MERGE_ENABLED}" == "false" ]]; then
echo "❌ Auto-merge NOT enabled (GraphQL mutation failed — see enable step logs)"
else
echo "⚠️ Auto-merge enable step did not complete (check workflow logs)"
fi
;;
version-update:semver-major)
echo "❌ Auto-merge DISABLED: Major version update"
;;
*)
echo "❌ Auto-merge DISABLED: Update type not eligible for auto-merge (${UPDATE_TYPE})"
;;
esac