[Sync] Update project files from source repository (c939592) #115
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ------------------------------------------------------------------------------------ | |
| # 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!" |