Skip to content

[Sync] Update project files from source repository (c939592) #115

[Sync] Update project files from source repository (c939592)

[Sync] Update project files from source repository (c939592) #115

# ------------------------------------------------------------------------------------
# Dependabot Auto-merge Workflow
#
# Purpose: Automatically merge Dependabot updates based on configurable rules
# for different update types (patch, minor, major) and dependency types
# (development, production). Security updates get special handling.
#
# Configuration: All settings are loaded from modular .github/env/ files for
# centralized management across all workflows.
#
# Triggers: Pull request events for immediate response to Dependabot PRs
#
# Auto-merge Rules (configurable via .github/env/):
# - Patch updates: Auto-merge by default
# - Minor dev dependencies: Auto-merge by default
# - Minor prod dependencies: Manual review by default
# - Major updates: Always require manual review with alert
# - Security updates: Auto-merge non-major by default
#
# Maintainer: @mrz1836
#
# ------------------------------------------------------------------------------------
name: Dependabot Auto-merge
# --------------------------------------------------------------------
# Trigger Configuration
#
# Only `opened` and `synchronize` are listed because:
# - `opened`: first event for any new Dependabot PR.
# - `synchronize`: fires when Dependabot rebases its own PR (e.g., after
# a base-branch update or a conflict). The decision tree
# must re-evaluate so the rebased commit can be auto-merged.
# `reopened` and `ready_for_review` are intentionally omitted β€” Dependabot
# never reopens closed PRs (it creates new ones) and never opens drafts,
# so those events would only fire for human PRs which the job-level `if:`
# below would skip anyway. Skipping them at the trigger level avoids the
# corresponding skipped runs in the UI.
# --------------------------------------------------------------------
on:
pull_request:
types: [opened, synchronize]
# Security: Restrict default permissions (jobs must explicitly request what they need)
permissions: {}
# --------------------------------------------------------------------
# Concurrency Control
# --------------------------------------------------------------------
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# --------------------------------------------------------------------
# Environment Variables
# --------------------------------------------------------------------
# Note: Configuration variables are loaded from modular .github/env/ files
jobs:
# ----------------------------------------------------------------------------------
# Dependabot processing (single consolidated job)
#
# Workflow phases (each preserved as a step):
# 🌍 Load environment β†’ reads .github/env/*
# πŸ”§ Extract configuration β†’ exports all DEPENDABOT_* vars to env
# πŸ“Š Fetch metadata β†’ official Dependabot metadata
# πŸ“‹ Log dependency details β†’ trace logging
# πŸ”’ Check for security β†’ classify security vs non-security
# 🎯 Determine action β†’ decision tree over config + metadata
# ⚠️ Alert (major/security) β†’ comment + manual-review label
# πŸ” Alert (minor prod) β†’ comment for review
# πŸš€ Auto-merge approved β†’ approve + enable auto-merge
# 🏷️ Add tracking labels β†’ dependency/update-type labels
# πŸ“Š Generate summary β†’ always
# ----------------------------------------------------------------------------------
dependabot:
name: πŸ€– Dependabot Auto-merge
runs-on: ubuntu-24.04
timeout-minutes: 10
# Only run on Dependabot PRs
if: github.event.pull_request.user.login == 'dependabot[bot]'
permissions:
contents: write # Required: Enables auto-merge for Dependabot PRs
pull-requests: write # Required: Update and merge Dependabot PRs
issues: write # Required: Comment on related dependency issues
steps:
# --------------------------------------------------------------------
# Check out code to access env file
# --------------------------------------------------------------------
- name: πŸ“₯ Checkout code (sparse)
# SECURITY: pin the checkout to the trusted base ref so the local
# ./.github/actions/load-env action (run below with contents/pull-requests/issues
# write) can never be a PR-controlled version. Default checkout on pull_request
# events resolves to the PR head. Lower risk here (job is gated to dependabot[bot]),
# but pinned for defense-in-depth and consistency with the other PR workflows.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.base_ref || github.ref }}
persist-credentials: false
sparse-checkout: |
.github/env
.github/actions/load-env
# --------------------------------------------------------------------
# Load and parse environment file
# --------------------------------------------------------------------
- name: 🌍 Load environment variables
id: load-env
uses: ./.github/actions/load-env
# --------------------------------------------------------------------
# Extract Dependabot configuration up-front
# --------------------------------------------------------------------
- name: πŸ”§ Extract configuration
env:
ENV_JSON: ${{ steps.load-env.outputs.env-json }}
GH_PAT_TOKEN: ${{ secrets.GH_PAT_TOKEN }}
run: |
echo "πŸ“‹ Extracting Dependabot configuration from environment..."
# Validate required configuration
MAINTAINER=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MAINTAINER_USERNAME')
if [[ -z "$MAINTAINER" ]] || [[ "$MAINTAINER" == "null" ]]; then
echo "❌ ERROR: DEPENDABOT_MAINTAINER_USERNAME not set in configuration" >&2
exit 1
fi
# Single jq pass for all config we need across steps
{
echo "MAINTAINER=$MAINTAINER"
echo "AUTO_MERGE_PATCH=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH')"
echo "AUTO_MERGE_MINOR_DEV=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_DEV')"
echo "AUTO_MERGE_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_PROD')"
echo "AUTO_MERGE_PATCH_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH_INDIRECT')"
echo "AUTO_MERGE_MINOR_INDIRECT=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_INDIRECT')"
echo "AUTO_MERGE_SECURITY=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_SECURITY_NON_MAJOR')"
echo "ALERT_ON_MAJOR=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_ALERT_ON_MAJOR')"
echo "ALERT_ON_MINOR_PROD=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_ALERT_ON_MINOR_PROD')"
echo "MANUAL_REVIEW_LABEL=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_MANUAL_REVIEW_LABEL')"
echo "AUTO_MERGE_LABELS=$(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_LABELS')"
} >> "$GITHUB_ENV"
PREFERRED_TOKEN=$(echo "$ENV_JSON" | jq -r '.PREFERRED_GITHUB_TOKEN')
# Log configuration
echo "πŸ” Configuration loaded:"
echo " πŸ‘€ Maintainer: @$MAINTAINER"
echo " πŸ”§ Auto-merge patch: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH')"
echo " πŸ”§ Auto-merge minor dev: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_DEV')"
echo " πŸ”§ Auto-merge minor prod: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_PROD')"
echo " πŸ”§ Auto-merge patch indirect: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_PATCH_INDIRECT')"
echo " πŸ”§ Auto-merge minor indirect: $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_MINOR_INDIRECT')"
echo " πŸ”’ Auto-merge security (non-major): $(echo "$ENV_JSON" | jq -r '.DEPENDABOT_AUTO_MERGE_SECURITY_NON_MAJOR')"
if [[ "$PREFERRED_TOKEN" == "GH_PAT_TOKEN" && -n "$GH_PAT_TOKEN" ]]; then
echo " πŸ”‘ Token: Personal Access Token (PAT)"
else
echo " πŸ”‘ Token: Default GITHUB_TOKEN"
fi
# --------------------------------------------------------------------
# Get official Dependabot metadata
# --------------------------------------------------------------------
- name: πŸ“Š Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# --------------------------------------------------------------------
# Log dependency information
# --------------------------------------------------------------------
- name: πŸ“‹ Log dependency details
env:
# Metadata via env: (never string-spliced into the shell)
PR_NUMBER: ${{ github.event.pull_request.number }}
DEPENDENCY: ${{ steps.metadata.outputs.dependency-names }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
DEP_TYPE: ${{ steps.metadata.outputs.dependency-type }}
PKG_ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }}
PREV_VERSION: ${{ steps.metadata.outputs.previous-version }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
run: |
echo "πŸ” Analyzing Dependabot PR #${PR_NUMBER}..."
echo "════════════════════════════════════════════════════════════════"
echo "πŸ“¦ Dependency: $DEPENDENCY"
echo "πŸ”„ Update type: $UPDATE_TYPE"
echo "πŸ“ Dependency type: $DEP_TYPE"
echo "🌐 Package ecosystem: $PKG_ECOSYSTEM"
echo "⬆️ Version: $PREV_VERSION β†’ $NEW_VERSION"
echo "════════════════════════════════════════════════════════════════"
# --------------------------------------------------------------------
# Check if this is a security update
# --------------------------------------------------------------------
- name: πŸ”’ Check for security update
id: check-security
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
# Structured Dependabot metadata β€” routed via env: (never string-spliced into
# shell or the GraphQL query). PR_BODY is intentionally NOT read: it embeds the
# upstream-controlled changelog, so free-text matching there let non-security
# updates self-escalate onto the auto-merge-security path.
DEPENDENCY: ${{ steps.metadata.outputs.dependency-names }}
PKG_ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }}
PREV_VERSION: ${{ steps.metadata.outputs.previous-version }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "════════════════════════════════════════════════════════════════"
echo "πŸ”’ SECURITY UPDATE DETECTION"
echo "════════════════════════════════════════════════════════════════"
# Check 1: PR title and labels for security indicators
echo "πŸ“‹ Check 1: Analyzing PR labels and title..."
echo " Labels: $PR_LABELS"
echo " Title: $PR_TITLE"
# Using environment variables to prevent script injection
if [[ "${PR_LABELS,,}" == *"security"* ]]; then
echo " βœ… MATCH: 'security' found in PR labels"
echo "is_security=true" >> $GITHUB_OUTPUT
echo "════════════════════════════════════════════════════════════════"
echo "🎯 RESULT: Security update detected (via labels)"
echo "════════════════════════════════════════════════════════════════"
exit 0
elif [[ "${PR_TITLE,,}" == *"security"* ]]; then
echo " βœ… MATCH: Security keyword found in PR title"
echo "is_security=true" >> $GITHUB_OUTPUT
echo "════════════════════════════════════════════════════════════════"
echo "🎯 RESULT: Security update detected (via title)"
echo "════════════════════════════════════════════════════════════════"
exit 0
else
echo " ❌ NO MATCH: No security indicators in labels/title"
fi
# Check 2 (removed): the PR body embeds the upstream-controlled changelog, so
# free-text matching there ("security"/"CVE-"/"GHSA-") let a non-security update
# self-escalate onto the auto-merge-security path. Security is now derived only
# from PR labels/title (above) and the structured advisory match below.
# Check 3: Structured advisory match. Treat this as a security update ONLY when a
# published advisory for the CORRECT ecosystem has a first-patched version that
# THIS bump actually crosses (prev < patched <= new). Replaces the old
# "any historical GO advisory = security" heuristic that let unrelated/long-fixed
# advisories flip a routine update onto the auto-merge-security path.
echo ""
echo "🌐 Check 3: Structured GitHub Security Advisory match..."
echo " Package: $DEPENDENCY ($PKG_ECOSYSTEM) bump: $PREV_VERSION β†’ $NEW_VERSION"
# Map Dependabot's package-ecosystem to the GraphQL SecurityAdvisoryEcosystem enum.
case "$PKG_ECOSYSTEM" in
go_modules|gomod) GQL_ECOSYSTEM="GO" ;;
npm_and_yarn) GQL_ECOSYSTEM="NPM" ;;
pip) GQL_ECOSYSTEM="PIP" ;;
composer) GQL_ECOSYSTEM="COMPOSER" ;;
cargo) GQL_ECOSYSTEM="RUST" ;;
nuget) GQL_ECOSYSTEM="NUGET" ;;
bundler) GQL_ECOSYSTEM="RUBYGEMS" ;;
maven|gradle) GQL_ECOSYSTEM="MAVEN" ;;
pub) GQL_ECOSYSTEM="PUB" ;;
swift) GQL_ECOSYSTEM="SWIFT" ;;
github_actions) GQL_ECOSYSTEM="ACTIONS" ;;
*) GQL_ECOSYSTEM="" ;;
esac
IS_SECURITY=false
if [[ -z "$GQL_ECOSYSTEM" ]]; then
echo " ⏭️ No advisory ecosystem mapping for '$PKG_ECOSYSTEM' β€” cannot confirm; treating as non-security"
elif [[ -z "$DEPENDENCY" || "$DEPENDENCY" == "null" ]]; then
echo " ⏭️ No dependency name β€” treating as non-security"
elif [[ -z "$PREV_VERSION" || -z "$NEW_VERSION" ]]; then
echo " ⏭️ Missing previous/new version β€” treating as non-security"
else
# $GQL_ECOSYSTEM (from the fixed map) and $DEPENDENCY are passed as GraphQL
# variables (parameterized) β€” never concatenated into the query text.
PATCHED=$(gh api graphql -f query='
query($ecosystem: SecurityAdvisoryEcosystem!, $package: String!) {
securityVulnerabilities(first: 100, ecosystem: $ecosystem, package: $package) {
nodes { firstPatchedVersion { identifier } }
}
}' -f ecosystem="$GQL_ECOSYSTEM" -f package="$DEPENDENCY" \
--jq '.data.securityVulnerabilities.nodes[] | select(.firstPatchedVersion != null) | .firstPatchedVersion.identifier' 2>/dev/null || echo "")
# version_lt A B β†’ true when A < B by version sort (a leading "v" is ignored).
version_lt() {
local a="${1#v}" b="${2#v}"
[[ "$a" == "$b" ]] && return 1
[[ "$(printf '%s\n%s\n' "$a" "$b" | sort -V | head -n1)" == "$a" ]]
}
while IFS= read -r patched; do
[[ -z "$patched" ]] && continue
# Vulnerable before the fix AND fixed at/after it β†’ this bump IS a security fix.
if version_lt "$PREV_VERSION" "$patched" && ! version_lt "$NEW_VERSION" "$patched"; then
echo " βœ… Advisory match: $PREV_VERSION < $patched <= $NEW_VERSION"
IS_SECURITY=true
break
fi
done <<< "$PATCHED"
fi
echo "════════════════════════════════════════════════════════════════"
if [[ "$IS_SECURITY" == "true" ]]; then
echo "is_security=true" >> "$GITHUB_OUTPUT"
echo "🎯 RESULT: Security update detected (advisory patched-version match)"
else
echo " ❌ NO MATCH: bump does not cross a known advisory's patched version"
echo "is_security=false" >> "$GITHUB_OUTPUT"
echo "🎯 RESULT: Not a security update"
fi
echo "════════════════════════════════════════════════════════════════"
# --------------------------------------------------------------------
# Determine action based on configuration and update type
# --------------------------------------------------------------------
- name: 🎯 Determine action
id: determine-action
env:
# Metadata / prior-step outputs via env: (never string-spliced into the shell)
UPDATE_TYPE_IN: ${{ steps.metadata.outputs.update-type }}
DEP_TYPE_IN: ${{ steps.metadata.outputs.dependency-type }}
IS_SECURITY_IN: ${{ steps.check-security.outputs.is_security }}
run: |
echo "════════════════════════════════════════════════════════════════"
echo "🎯 ACTION DETERMINATION LOGIC"
echo "════════════════════════════════════════════════════════════════"
UPDATE_TYPE="$UPDATE_TYPE_IN"
DEP_TYPE="$DEP_TYPE_IN"
IS_SECURITY="$IS_SECURITY_IN"
ACTION="none"
echo "πŸ“Š Input Variables:"
echo " Update Type: $UPDATE_TYPE"
echo " Dependency Type: $DEP_TYPE"
echo " Is Security: $IS_SECURITY"
echo ""
echo "πŸ“‹ Active Configuration:"
echo " AUTO_MERGE_SECURITY: ${{ env.AUTO_MERGE_SECURITY }}"
echo " AUTO_MERGE_PATCH: ${{ env.AUTO_MERGE_PATCH }}"
echo " AUTO_MERGE_PATCH_INDIRECT: ${{ env.AUTO_MERGE_PATCH_INDIRECT }}"
echo " AUTO_MERGE_MINOR_DEV: ${{ env.AUTO_MERGE_MINOR_DEV }}"
echo " AUTO_MERGE_MINOR_PROD: ${{ env.AUTO_MERGE_MINOR_PROD }}"
echo " AUTO_MERGE_MINOR_INDIRECT: ${{ env.AUTO_MERGE_MINOR_INDIRECT }}"
echo " ALERT_ON_MAJOR: ${{ env.ALERT_ON_MAJOR }}"
echo " ALERT_ON_MINOR_PROD: ${{ env.ALERT_ON_MINOR_PROD }}"
echo ""
echo "πŸ”„ Evaluating decision tree..."
echo "────────────────────────────────────────────────────────────────"
# Security updates (if enabled)
echo "πŸ” Checking Rule 1: Security updates..."
if [[ "$IS_SECURITY" == "true" ]]; then
echo " βœ… IS_SECURITY == true"
if [[ "${{ env.AUTO_MERGE_SECURITY }}" == "true" ]]; then
echo " βœ… AUTO_MERGE_SECURITY == true"
if [[ "$UPDATE_TYPE" != "version-update:semver-major" ]]; then
echo " βœ… UPDATE_TYPE != major"
ACTION="auto-merge-security"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Non-major security update with auto-merge enabled"
else
echo " ❌ UPDATE_TYPE == major"
ACTION="alert-security-major"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Major security update requires manual review"
fi
else
echo " ❌ AUTO_MERGE_SECURITY == false"
echo " ⏭️ Skipping security auto-merge (disabled)"
fi
else
echo " ❌ IS_SECURITY == false"
echo " ⏭️ Not a security update, checking other rules..."
fi
echo ""
# Patch updates - direct dependencies
if [[ "$ACTION" == "none" ]]; then
echo "πŸ” Checking Rule 2: Patch updates (direct dependencies)..."
if [[ "$UPDATE_TYPE" == "version-update:semver-patch" ]]; then
echo " βœ… UPDATE_TYPE == patch"
if [[ "$DEP_TYPE" == "direct:production" ]]; then
# SECURITY: never auto-merge NON-security production patches. A malicious or
# broken patch release of a production dependency ships straight to users, so
# require a human. Genuine security patches are handled earlier by Rule 1.
echo " πŸ”’ DEP_TYPE == direct:production β€” excluded from patch auto-merge"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Production patch requires manual review (supply-chain safety)"
elif [[ "$DEP_TYPE" != "indirect" ]]; then
echo " βœ… DEP_TYPE is direct:development"
if [[ "${{ env.AUTO_MERGE_PATCH }}" == "true" ]]; then
echo " βœ… AUTO_MERGE_PATCH == true"
ACTION="auto-merge-patch"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Patch update for direct dev dependency with auto-merge enabled"
else
echo " ❌ AUTO_MERGE_PATCH == false"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Patch auto-merge is disabled in configuration"
fi
else
echo " ❌ DEP_TYPE == indirect"
echo " ⏭️ Skipping, will check indirect patch rule next"
fi
else
echo " ❌ UPDATE_TYPE != patch"
echo " ⏭️ Not a patch update"
fi
fi
echo ""
# Patch updates - indirect dependencies
if [[ "$ACTION" == "none" ]]; then
echo "πŸ” Checking Rule 3: Patch updates (indirect dependencies)..."
if [[ "$UPDATE_TYPE" == "version-update:semver-patch" ]]; then
echo " βœ… UPDATE_TYPE == patch"
if [[ "$DEP_TYPE" == "indirect" ]]; then
echo " βœ… DEP_TYPE == indirect"
if [[ "${{ env.AUTO_MERGE_PATCH_INDIRECT }}" == "true" ]]; then
echo " βœ… AUTO_MERGE_PATCH_INDIRECT == true"
ACTION="auto-merge-patch-indirect"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Patch update for indirect dependency with auto-merge enabled"
else
echo " ❌ AUTO_MERGE_PATCH_INDIRECT == false"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Indirect patch auto-merge is disabled in configuration"
fi
else
echo " ❌ DEP_TYPE != indirect"
echo " ⏭️ Not an indirect dependency"
fi
else
echo " ❌ UPDATE_TYPE != patch"
echo " ⏭️ Not a patch update"
fi
fi
echo ""
# Minor updates - development dependencies
if [[ "$ACTION" == "none" ]]; then
echo "πŸ” Checking Rule 4: Minor updates (development dependencies)..."
if [[ "$UPDATE_TYPE" == "version-update:semver-minor" ]]; then
echo " βœ… UPDATE_TYPE == minor"
if [[ "$DEP_TYPE" == "direct:development" ]]; then
echo " βœ… DEP_TYPE == direct:development"
if [[ "${{ env.AUTO_MERGE_MINOR_DEV }}" == "true" ]]; then
echo " βœ… AUTO_MERGE_MINOR_DEV == true"
ACTION="auto-merge-minor-dev"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Minor update for dev dependency with auto-merge enabled"
else
echo " ❌ AUTO_MERGE_MINOR_DEV == false"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Minor dev dependency auto-merge is disabled"
fi
else
echo " ❌ DEP_TYPE != direct:development (is: $DEP_TYPE)"
echo " ⏭️ Not a development dependency"
fi
else
echo " ❌ UPDATE_TYPE != minor"
echo " ⏭️ Not a minor update"
fi
fi
echo ""
# Minor updates - production dependencies
if [[ "$ACTION" == "none" ]]; then
echo "πŸ” Checking Rule 5: Minor updates (production dependencies)..."
if [[ "$UPDATE_TYPE" == "version-update:semver-minor" ]]; then
echo " βœ… UPDATE_TYPE == minor"
if [[ "$DEP_TYPE" == "direct:production" ]]; then
echo " βœ… DEP_TYPE == direct:production"
if [[ "${{ env.AUTO_MERGE_MINOR_PROD }}" == "true" ]]; then
echo " βœ… AUTO_MERGE_MINOR_PROD == true"
ACTION="auto-merge-minor-prod"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Minor update for prod dependency with auto-merge enabled"
elif [[ "${{ env.ALERT_ON_MINOR_PROD }}" == "true" ]]; then
echo " ❌ AUTO_MERGE_MINOR_PROD == false"
echo " βœ… ALERT_ON_MINOR_PROD == true"
ACTION="alert-minor-prod"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Minor prod dependency - alerting maintainer per config"
else
echo " ❌ AUTO_MERGE_MINOR_PROD == false"
echo " ❌ ALERT_ON_MINOR_PROD == false"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Minor prod dependency auto-merge and alerts disabled"
fi
else
echo " ❌ DEP_TYPE != direct:production (is: $DEP_TYPE)"
echo " ⏭️ Not a production dependency"
fi
else
echo " ❌ UPDATE_TYPE != minor"
echo " ⏭️ Not a minor update"
fi
fi
echo ""
# Minor updates - indirect dependencies
if [[ "$ACTION" == "none" ]]; then
echo "πŸ” Checking Rule 6: Minor updates (indirect dependencies)..."
if [[ "$UPDATE_TYPE" == "version-update:semver-minor" ]]; then
echo " βœ… UPDATE_TYPE == minor"
if [[ "$DEP_TYPE" == "indirect" ]]; then
echo " βœ… DEP_TYPE == indirect"
if [[ "${{ env.AUTO_MERGE_MINOR_INDIRECT }}" == "true" ]]; then
echo " βœ… AUTO_MERGE_MINOR_INDIRECT == true"
ACTION="auto-merge-minor-indirect"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Minor update for indirect dependency with auto-merge enabled"
else
echo " ❌ AUTO_MERGE_MINOR_INDIRECT == false"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Indirect minor auto-merge is disabled in configuration"
fi
else
echo " ❌ DEP_TYPE != indirect"
echo " ⏭️ Not an indirect dependency"
fi
else
echo " ❌ UPDATE_TYPE != minor"
echo " ⏭️ Not a minor update"
fi
fi
echo ""
# Major updates
if [[ "$ACTION" == "none" ]]; then
echo "πŸ” Checking Rule 7: Major updates..."
if [[ "$UPDATE_TYPE" == "version-update:semver-major" ]]; then
echo " βœ… UPDATE_TYPE == major"
if [[ "${{ env.ALERT_ON_MAJOR }}" == "true" ]]; then
echo " βœ… ALERT_ON_MAJOR == true"
ACTION="alert-major"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Major update - alerting maintainer for breaking changes"
else
echo " ❌ ALERT_ON_MAJOR == false"
ACTION="manual-review"
echo " 🎯 MATCH! Action: $ACTION"
echo " πŸ“ Reason: Major update with alerts disabled - requires manual review"
fi
else
echo " ❌ UPDATE_TYPE != major"
echo " ⏭️ Not a major update"
fi
fi
echo ""
# Fallback
if [[ "$ACTION" == "none" ]]; then
echo "⚠️ No rules matched - applying fallback..."
ACTION="manual-review"
echo " 🎯 FALLBACK Action: $ACTION"
echo " πŸ“ Reason: No matching rule found - defaulting to manual review"
echo " ⚠️ This shouldn't happen - check workflow logic!"
fi
echo ""
echo "════════════════════════════════════════════════════════════════"
echo "🎯 FINAL DECISION: $ACTION"
echo "════════════════════════════════════════════════════════════════"
echo "action=$ACTION" >> $GITHUB_OUTPUT
# --------------------------------------------------------------------
# Handle major version alerts
# --------------------------------------------------------------------
- name: πŸ“Š Log major version alert decision
run: |
ACTION="${{ steps.determine-action.outputs.action }}"
if [[ "$ACTION" == "alert-major" ]] || [[ "$ACTION" == "alert-security-major" ]]; then
echo "════════════════════════════════════════════════════════════════"
echo "⚠️ MAJOR VERSION ALERT - WILL EXECUTE"
echo "════════════════════════════════════════════════════════════════"
echo "Action: $ACTION"
echo "Next step: Creating alert comment and adding manual-review label"
else
echo "════════════════════════════════════════════════════════════════"
echo "⚠️ MAJOR VERSION ALERT - SKIPPED"
echo "════════════════════════════════════════════════════════════════"
echo "Action: $ACTION"
echo "Reason: Not a major version update requiring alert"
echo "Skipping: Major version alert step"
fi
- name: ⚠️ Alert on major version bump
if: steps.determine-action.outputs.action == 'alert-major' || steps.determine-action.outputs.action == 'alert-security-major'
env:
# Route metadata through env: and read it via process.env in the script so
# PR-derived values can never break out of the JS string literals.
DEPENDENCY: ${{ steps.metadata.outputs.dependency-names }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
PREV_VERSION: ${{ steps.metadata.outputs.previous-version }}
DEP_TYPE: ${{ steps.metadata.outputs.dependency-type }}
PKG_ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }}
IS_SECURITY: ${{ steps.check-security.outputs.is_security }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueNumber = context.issue.number;
const dependency = process.env.DEPENDENCY;
const newVersion = process.env.NEW_VERSION;
const previousVersion = process.env.PREV_VERSION;
const maintainer = process.env.MAINTAINER;
const depType = process.env.DEP_TYPE;
const packageEcosystem = process.env.PKG_ECOSYSTEM;
const isSecurity = process.env.IS_SECURITY === 'true';
const emoji = isSecurity ? '🚨' : '⚠️';
const prefix = isSecurity ? '**SECURITY** - ' : '';
const commentBody = `${emoji} @${maintainer} – ${prefix}**Major version update detected**
**Dependency:** \`${dependency}\`
**Version:** \`${previousVersion}\` β†’ \`${newVersion}\`
**Type:** ${depType}
**Ecosystem:** ${packageEcosystem}
${isSecurity ? '\nπŸ”’ **This is a security update with potential breaking changes**' : ''}
This requires manual review for potential breaking changes.
**Review checklist:**
- [ ] Check changelog/release notes for breaking changes
- [ ] Review migration guide if available
- [ ] Test functionality affected by this dependency
- [ ] Update code if necessary to handle breaking changes`;
// Check for existing alert comment to avoid duplicates
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100
});
const alertExists = comments.some(comment =>
comment.body.includes('Major version update detected') &&
comment.body.includes(dependency) &&
comment.user.login === 'github-actions[bot]'
);
if (!alertExists) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: commentBody
});
// Add label for tracking
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['${{ env.MANUAL_REVIEW_LABEL }}']
});
} else {
console.log('Major version alert already exists, skipping duplicate comment');
}
# --------------------------------------------------------------------
# Handle minor production dependency alerts
# --------------------------------------------------------------------
- name: πŸ“Š Log minor prod dependency alert decision
run: |
ACTION="${{ steps.determine-action.outputs.action }}"
if [[ "$ACTION" == "alert-minor-prod" ]]; then
echo "════════════════════════════════════════════════════════════════"
echo "πŸ” MINOR PROD DEPENDENCY ALERT - WILL EXECUTE"
echo "════════════════════════════════════════════════════════════════"
echo "Action: $ACTION"
echo "Next step: Creating alert comment for maintainer review"
else
echo "════════════════════════════════════════════════════════════════"
echo "πŸ” MINOR PROD DEPENDENCY ALERT - SKIPPED"
echo "════════════════════════════════════════════════════════════════"
echo "Action: $ACTION"
echo "Reason: Not a minor production dependency requiring alert"
echo "Skipping: Minor prod alert step"
fi
- name: πŸ” Alert on minor production dependency
if: steps.determine-action.outputs.action == 'alert-minor-prod'
env:
# Route metadata through env: and read it via process.env in the script so
# PR-derived values can never break out of the JS string literals.
DEPENDENCY: ${{ steps.metadata.outputs.dependency-names }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
PREV_VERSION: ${{ steps.metadata.outputs.previous-version }}
PKG_ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueNumber = context.issue.number;
const dependency = process.env.DEPENDENCY;
const newVersion = process.env.NEW_VERSION;
const previousVersion = process.env.PREV_VERSION;
const maintainer = process.env.MAINTAINER;
const packageEcosystem = process.env.PKG_ECOSYSTEM;
const commentBody = `πŸ” @${maintainer} – **Minor production dependency update**
**Dependency:** \`${dependency}\`
**Version:** \`${previousVersion}\` β†’ \`${newVersion}\`
**Type:** Production dependency
**Ecosystem:** ${packageEcosystem}
Please review for potential feature changes or compatibility issues.
**Quick review checklist:**
- [ ] Check release notes for new features
- [ ] Verify no deprecation warnings
- [ ] Confirm compatibility with current code`;
// Check for existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100
});
const commentExists = comments.some(comment =>
comment.body.includes('Minor production dependency update') &&
comment.body.includes(dependency) &&
comment.user.login === 'github-actions[bot]'
);
if (!commentExists) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: commentBody
});
}
# --------------------------------------------------------------------
# Auto-merge approved updates
# --------------------------------------------------------------------
- name: πŸ“Š Log auto-merge decision
env:
# Metadata via env: (never string-spliced into the shell)
DEPENDENCY: ${{ steps.metadata.outputs.dependency-names }}
PREV_VERSION: ${{ steps.metadata.outputs.previous-version }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
run: |
ACTION="${{ steps.determine-action.outputs.action }}"
if [[ "$ACTION" == auto-merge-* ]]; then
echo "════════════════════════════════════════════════════════════════"
echo "πŸš€ AUTO-MERGE - WILL EXECUTE"
echo "════════════════════════════════════════════════════════════════"
echo "Action: $ACTION"
echo "Dependency: $DEPENDENCY"
echo "Version: $PREV_VERSION β†’ $NEW_VERSION"
echo "Next steps:"
echo " 1. Approve the PR with appropriate message"
echo " 2. Enable auto-merge (squash)"
echo " 3. PR will merge automatically when CI passes"
else
echo "════════════════════════════════════════════════════════════════"
echo "πŸš€ AUTO-MERGE - SKIPPED"
echo "════════════════════════════════════════════════════════════════"
echo "Action: $ACTION"
echo "Reason: Action does not start with 'auto-merge-'"
if [[ "$ACTION" == "manual-review" ]]; then
echo ""
echo "⚠️ MANUAL REVIEW REQUIRED"
echo "This PR was not auto-merged due to configuration settings."
echo "Possible reasons:"
echo " - Auto-merge is disabled for this update type"
echo " - Update type doesn't match any auto-merge rules"
echo " - Major version update requiring review"
echo ""
echo "Please review the decision tree output above for specific reason."
elif [[ "$ACTION" == alert-* ]]; then
echo ""
echo "πŸ“’ ALERT ACTION TAKEN"
echo "An alert comment was created instead of auto-merge."
echo "Check the alert step output for details."
fi
echo "Skipping: Auto-merge step"
fi
# --------------------------------------------------------------------
# Idempotency pre-check (prevents duplicate approvals on synchronize)
#
# Dependabot rebases its PR on every base-branch change / conflict,
# firing a `synchronize` event that re-runs this workflow. Without a
# guard, the approve step below re-runs `gh pr review --approve` on
# each event and posts a brand-new approval review every time, spamming
# the PR timeline. This step computes whether the PR is *already*
# approved and whether auto-merge is *already* armed so the next step
# can skip the redundant calls. Mirrors the idempotency pattern used in
# auto-merge-on-approval.yml (latest-review-per-user + pr.auto_merge).
# --------------------------------------------------------------------
- name: πŸ”Ž Check existing approval & auto-merge state
id: merge-state
if: |
startsWith(steps.determine-action.outputs.action, 'auto-merge-')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.issue.number;
// Is auto-merge already armed? (persists across pushes once enabled)
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const autoMergeEnabled = Boolean(pr.auto_merge);
// Is the PR already approved? Reduce to the latest review per user
// so a stale/dismissed approval (state !== 'APPROVED') correctly
// triggers a re-approval for the new head commit.
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
});
const latestReviews = {};
reviews.forEach(review => {
// review.user is null for deleted accounts - fall back to a unique
// key so the review still counts instead of throwing.
const userId = review.user?.id ?? `deleted-user:${review.id}`;
if (!latestReviews[userId] || review.submitted_at > latestReviews[userId].submitted_at) {
latestReviews[userId] = review;
}
});
const alreadyApproved = Object.values(latestReviews).some(r => r.state === 'APPROVED');
console.log('════════════════════════════════════════════════════════════════');
console.log('πŸ”Ž IDEMPOTENCY PRE-CHECK');
console.log('════════════════════════════════════════════════════════════════');
console.log(` Already approved: ${alreadyApproved}`);
console.log(` Auto-merge enabled: ${autoMergeEnabled}`);
if (alreadyApproved) {
console.log(' ⏭️ Will skip re-approval (prevents duplicate review comments)');
}
if (autoMergeEnabled) {
console.log(' ⏭️ Will skip enabling auto-merge (already armed)');
}
console.log('════════════════════════════════════════════════════════════════');
core.setOutput('already_approved', String(alreadyApproved));
core.setOutput('auto_merge_enabled', String(autoMergeEnabled));
- name: πŸš€ Auto-merge approved updates
if: |
startsWith(steps.determine-action.outputs.action, 'auto-merge-')
env:
PR_URL: ${{ github.event.pull_request.html_url }}
# SECURITY (least privilege): GH_PAT_TOKEN should be a FINE-GRAINED, single-repo
# PAT scoped to ONLY "Pull requests: write" + "Contents: write" (nothing else).
# It is used to approve + arm auto-merge; because approving as the CODEOWNER can
# satisfy required-review/CODEOWNERS branch protection, its scope must be minimal.
# Do NOT grant it admin, workflow, or org scopes. Configure at repo β†’ Settings β†’
# Secrets β†’ Actions β†’ GH_PAT_TOKEN.
GH_TOKEN: ${{ secrets.GH_PAT_TOKEN || secrets.GITHUB_TOKEN }}
TOKEN_TYPE: ${{ secrets.GH_PAT_TOKEN && 'PAT' || 'GITHUB_TOKEN' }}
ALREADY_APPROVED: ${{ steps.merge-state.outputs.already_approved }}
AUTO_MERGE_ENABLED: ${{ steps.merge-state.outputs.auto_merge_enabled }}
# Metadata via env: β€” this step holds GH_PAT_TOKEN, so PR-derived values
# (dependency name / versions) must never reach the shell parser via ${{ }}.
DEPENDENCY: ${{ steps.metadata.outputs.dependency-names }}
PREV_VERSION: ${{ steps.metadata.outputs.previous-version }}
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
run: |
echo "════════════════════════════════════════════════════════════════"
echo "πŸš€ EXECUTING AUTO-MERGE"
echo "════════════════════════════════════════════════════════════════"
ACTION="${{ steps.determine-action.outputs.action }}"
# DEPENDENCY / PREV_VERSION / NEW_VERSION come from env: (never string-spliced)
VERSION_CHANGE="$PREV_VERSION β†’ $NEW_VERSION"
echo "Processing: $ACTION"
echo "Dependency: $DEPENDENCY"
echo "Version: $VERSION_CHANGE"
echo ""
# Check token availability (use env var to avoid expanding secrets in run block)
# TOKEN_TYPE is passed as environment variable to indicate which token is being used
if [[ "$TOKEN_TYPE" == "PAT" ]]; then
echo "πŸ”‘ Using Personal Access Token for enhanced permissions"
else
echo "⚠️ Using default GITHUB_TOKEN - auto-merge may fail for Dependabot PRs"
fi
# Determine approval message based on action type
case "$ACTION" in
"auto-merge-patch")
APPROVAL_MSG="βœ… Auto-approving patch update"
;;
"auto-merge-patch-indirect")
APPROVAL_MSG="βœ… Auto-approving patch update (indirect dependency)"
;;
"auto-merge-minor-dev")
APPROVAL_MSG="βœ… Auto-approving minor development dependency update"
;;
"auto-merge-minor-prod")
APPROVAL_MSG="βœ… Auto-approving minor production dependency update"
;;
"auto-merge-minor-indirect")
APPROVAL_MSG="βœ… Auto-approving minor update (indirect dependency)"
;;
"auto-merge-security")
APPROVAL_MSG="πŸ”’ Auto-approving security update"
;;
*)
APPROVAL_MSG="βœ… Auto-approving dependency update"
;;
esac
# Approve the PR
echo "Step 1: Approving PR..."
echo "────────────────────────────────────────────────────────────────"
# Idempotency guard: skip re-approval if the PR is already approved.
# Dependabot fires a `synchronize` event on every rebase; without this
# guard each event would post a duplicate approval review (timeline spam).
if [[ "$ALREADY_APPROVED" == "true" ]]; then
echo "βœ… PR already approved by automation β€” skipping re-approval (prevents duplicate reviews)"
echo ""
elif ! gh pr review --approve "$PR_URL" --body "$APPROVAL_MSG: $DEPENDENCY ($VERSION_CHANGE)"; then
echo "❌ Failed to approve PR"
echo "════════════════════════════════════════════════════════════════"
echo "🚫 AUTO-MERGE FAILED"
echo "════════════════════════════════════════════════════════════════"
echo "Reason: Could not approve PR via GitHub CLI"
echo "This is likely a permissions or network issue."
exit 1
else
echo "βœ… PR approved successfully"
echo ""
fi
# Attempt to enable auto-merge
echo "Step 2: Enabling auto-merge..."
echo "────────────────────────────────────────────────────────────────"
# Idempotency guard: skip if auto-merge is already armed. Auto-merge
# persists across pushes once enabled, so re-enabling on every
# `synchronize` is redundant noise.
if [[ "$AUTO_MERGE_ENABLED" == "true" ]]; then
echo "βœ… Auto-merge already enabled β€” skipping"
MERGE_OK=true
else
# Capture output so a concurrency race ("already enabled") can be
# treated as success rather than a failure (mirrors auto-merge-on-approval.yml).
MERGE_OUTPUT=$(gh pr merge --auto --squash "$PR_URL" 2>&1) && MERGE_OK=true || MERGE_OK=false
echo "$MERGE_OUTPUT"
if [[ "$MERGE_OK" != "true" ]] && grep -qi "already enabled" <<< "$MERGE_OUTPUT"; then
echo "ℹ️ Auto-merge already enabled by another workflow run β€” treating as success"
MERGE_OK=true
fi
fi
if [[ "$MERGE_OK" == "true" ]]; then
echo "βœ… Auto-merge enabled successfully"
echo ""
echo "════════════════════════════════════════════════════════════════"
echo "βœ… AUTO-MERGE COMPLETED"
echo "════════════════════════════════════════════════════════════════"
echo "Status: PR approved and auto-merge enabled"
echo "Action: $ACTION"
echo "Dependency: $DEPENDENCY"
echo "Version: $VERSION_CHANGE"
echo "Merge method: squash"
echo ""
echo "Next steps:"
echo " βœ“ PR will automatically merge when:"
echo " - All required checks pass"
echo " - All required reviews are approved"
echo " βœ“ Branch will be deleted after merge"
echo "════════════════════════════════════════════════════════════════"
else
echo "❌ Auto-merge command failed"
echo ""
echo "════════════════════════════════════════════════════════════════"
echo "⚠️ AUTO-MERGE PARTIALLY COMPLETED"
echo "════════════════════════════════════════════════════════════════"
echo "Status: PR approved but auto-merge failed"
echo "Action: $ACTION"
echo "Dependency: $DEPENDENCY"
echo "Version: $VERSION_CHANGE"
echo ""
echo "Reason: Auto-merge command failed (permissions issue)"
echo ""
echo "Common causes:"
echo " - Using GITHUB_TOKEN instead of GH_PAT_TOKEN"
echo " - PAT doesn't have 'repo' scope"
echo " - Auto-merge not enabled in repository settings"
echo " - Branch protection rules prevent auto-merge"
echo ""
echo "Resolution:"
echo " βœ“ PR is approved and ready"
echo " βœ“ You can manually merge when CI passes"
echo " βœ“ Or add GH_PAT_TOKEN secret with repo permissions"
echo "════════════════════════════════════════════════════════════════"
# Fallback: Set up for manual merge
echo "action-taken=approved-ready-for-merge" >> $GITHUB_OUTPUT
# Don't exit with error - the PR is still approved
fi
# --------------------------------------------------------------------
# Add tracking labels
# --------------------------------------------------------------------
- name: 🏷️ Add tracking labels
if: |
startsWith(steps.determine-action.outputs.action, 'auto-merge-') ||
startsWith(steps.determine-action.outputs.action, 'alert-')
env:
# Route metadata / prior-step outputs through env: and read via process.env so
# PR-derived values never break out of the JS string literals below.
ACTION: ${{ steps.determine-action.outputs.action }}
DEP_TYPE: ${{ steps.metadata.outputs.dependency-type }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
IS_SECURITY: ${{ steps.check-security.outputs.is_security }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const action = process.env.ACTION;
const labels = [];
// Add auto-merge labels if applicable (AUTO_MERGE_LABELS is a job-level env var)
if (action.startsWith('auto-merge-')) {
const autoMergeLabels = (process.env.AUTO_MERGE_LABELS || '').split(',').map(l => l.trim()).filter(Boolean);
labels.push(...autoMergeLabels);
}
// Add dependency type label
const depType = process.env.DEP_TYPE;
if (depType === 'direct:development') {
labels.push('dev-dependency');
} else if (depType === 'direct:production') {
labels.push('prod-dependency');
} else if (depType === 'indirect') {
labels.push('indirect-dependency');
}
// Add update type label
const updateType = process.env.UPDATE_TYPE;
if (updateType === 'version-update:semver-patch') {
labels.push('patch-update');
} else if (updateType === 'version-update:semver-minor') {
labels.push('minor-update');
} else if (updateType === 'version-update:semver-major') {
labels.push('major-update');
}
// Add security label if applicable
if (process.env.IS_SECURITY === 'true') {
labels.push('security');
}
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: labels
});
console.log(`Added labels: ${labels.join(', ')}`);
}
# --------------------------------------------------------------------
# Generate workflow summary report
# --------------------------------------------------------------------
- name: πŸ“Š Generate workflow summary
if: always()
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
DEPENDENCY_TYPE: ${{ steps.metadata.outputs.dependency-type }}
ACTION: ${{ steps.determine-action.outputs.action }}
run: |
echo "πŸ“Š Generating workflow summary..."
# Determine action taken
case "$ACTION" in
"auto-merge-patch") ACTION_DESC="βœ… Auto-merged (patch update)" ;;
"auto-merge-patch-indirect") ACTION_DESC="βœ… Auto-merged (patch update - indirect dependency)" ;;
"auto-merge-minor-dev") ACTION_DESC="βœ… Auto-merged (minor dev dependency)" ;;
"auto-merge-minor-prod") ACTION_DESC="βœ… Auto-merged (minor prod dependency)" ;;
"auto-merge-minor-indirect") ACTION_DESC="βœ… Auto-merged (minor update - indirect dependency)" ;;
"auto-merge-security") ACTION_DESC="πŸ”’ Auto-merged (security update)" ;;
"approved-ready-for-merge") ACTION_DESC="βœ… Approved and ready for manual merge (auto-merge failed)" ;;
"alert-major") ACTION_DESC="⚠️ Manual review required (major update)" ;;
"alert-security-major") ACTION_DESC="🚨 Manual review required (major security update)" ;;
"alert-minor-prod") ACTION_DESC="πŸ” Manual review suggested (minor prod update)" ;;
"manual-review") ACTION_DESC="πŸ‘€ Manual review required" ;;
*) ACTION_DESC="❓ Unknown action" ;;
esac
{
echo "# πŸ€– Dependabot Auto-merge Summary"
echo ""
echo "**⏰ Processed:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "**πŸ“‹ PR:** #$PR_NUMBER"
echo ""
echo "## πŸ“¦ Dependency Information"
echo ""
echo "| Property | Value |"
echo "|----------|-------|"
echo "| **Dependency** | ${DEPENDENCY_NAMES} |"
echo "| **Update Type** | ${UPDATE_TYPE} |"
echo "| **Dependency Type** | ${DEPENDENCY_TYPE} |"
echo ""
echo "## 🎯 Action Taken"
echo ""
echo "$ACTION_DESC"
echo ""
echo "### πŸ”§ Current Configuration"
echo ""
echo "| Setting | Value |"
echo "|---------|-------|"
echo "| Auto-merge patch | ${AUTO_MERGE_PATCH} |"
echo "| Auto-merge minor dev | ${AUTO_MERGE_MINOR_DEV} |"
echo "| Auto-merge minor prod | ${AUTO_MERGE_MINOR_PROD} |"
echo "| Auto-merge patch indirect | ${AUTO_MERGE_PATCH_INDIRECT} |"
echo "| Auto-merge minor indirect | ${AUTO_MERGE_MINOR_INDIRECT} |"
echo "| Auto-merge security | ${AUTO_MERGE_SECURITY} |"
echo "| Maintainer | @${MAINTAINER} |"
echo ""
echo "---"
echo "πŸ€– _Automated by GitHub Actions_"
} >> $GITHUB_STEP_SUMMARY
# --------------------------------------------------------------------
# Report final workflow status (stdout)
# --------------------------------------------------------------------
- name: πŸ“’ Report workflow status
if: always()
env:
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
DEPENDENCY_TYPE: ${{ steps.metadata.outputs.dependency-type }}
ACTION: ${{ steps.determine-action.outputs.action }}
run: |
echo "=== πŸ€– Dependabot Auto-merge Summary ==="
echo "πŸ“¦ Dependency: $DEPENDENCY_NAMES"
echo "πŸ”„ Update type: $UPDATE_TYPE"
echo "πŸ“ Dependency type: $DEPENDENCY_TYPE"
case "$ACTION" in
auto-merge-*) echo "βœ… Action: Auto-merge enabled" ;;
approved-ready-for-merge) echo "βœ… Action: Approved and ready for manual merge" ;;
alert-*) echo "⚠️ Action: Alert sent, manual review required" ;;
manual-review) echo "πŸ‘€ Action: Manual review required" ;;
*) echo "❓ Action: $ACTION" ;;
esac
echo "πŸ• Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "βœ… Workflow completed!"