From 83337a9437f0bdad7d5cf3b5ef0b8fc0d23649ac Mon Sep 17 00:00:00 2001 From: gopal-raj-suresh Date: Tue, 5 May 2026 13:45:06 -0500 Subject: [PATCH] feat(security): refine Dependabot notifications, auto-merge, and weekly scan schedule --- .github/workflows/code-scans.yaml | 2 +- .github/workflows/dependabot-auto-merge.yml | 37 ++- .github/workflows/dependabot-gchat-notify.yml | 257 ++++++++++++------ 3 files changed, 214 insertions(+), 82 deletions(-) diff --git a/.github/workflows/code-scans.yaml b/.github/workflows/code-scans.yaml index 8b58ff1..a985154 100644 --- a/.github/workflows/code-scans.yaml +++ b/.github/workflows/code-scans.yaml @@ -7,7 +7,7 @@ on: pull_request: types: [opened, synchronize, reopened, ready_for_review] schedule: - - cron: '0 6 * * 1' + - cron: '0 12 * * 5' concurrency: group: sdle-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index d99c47e..215cf2f 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -18,10 +18,43 @@ jobs: with: github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Classify update type + id: classify + env: + FM_UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + UPDATE_TYPE="${FM_UPDATE_TYPE}" + + if [[ -z "$UPDATE_TYPE" || "$UPDATE_TYPE" == "null" ]]; then + if [[ "$PR_TITLE" =~ ([0-9]+)\.([0-9]+)\.([0-9]+).*to.*([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + OLD_MAJOR="${BASH_REMATCH[1]}" + OLD_MINOR="${BASH_REMATCH[2]}" + OLD_PATCH="${BASH_REMATCH[3]}" + NEW_MAJOR="${BASH_REMATCH[4]}" + NEW_MINOR="${BASH_REMATCH[5]}" + NEW_PATCH="${BASH_REMATCH[6]}" + + if [[ "$OLD_MAJOR" != "$NEW_MAJOR" ]]; then + UPDATE_TYPE="version-update:semver-major" + elif [[ "$OLD_MINOR" != "$NEW_MINOR" ]]; then + UPDATE_TYPE="version-update:semver-minor" + elif [[ "$OLD_PATCH" != "$NEW_PATCH" ]]; then + UPDATE_TYPE="version-update:semver-patch" + else + UPDATE_TYPE="unknown" + fi + else + UPDATE_TYPE="unknown" + fi + fi + + echo "update-type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT" + - name: Enable auto-merge for minor and patch updates if: | - steps.metadata.outputs.update-type == 'version-update:semver-patch' || - steps.metadata.outputs.update-type == 'version-update:semver-minor' + steps.classify.outputs.update-type == 'version-update:semver-patch' || + steps.classify.outputs.update-type == 'version-update:semver-minor' run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/dependabot-gchat-notify.yml b/.github/workflows/dependabot-gchat-notify.yml index afdaf9e..e6bb917 100644 --- a/.github/workflows/dependabot-gchat-notify.yml +++ b/.github/workflows/dependabot-gchat-notify.yml @@ -2,7 +2,11 @@ name: Notify Google Chat on Dependabot Events on: pull_request_target: - types: [opened, closed] + types: [opened] + pull_request: + types: [closed] + push: + branches: [main] permissions: contents: read @@ -12,113 +16,208 @@ jobs: notify: name: Post Dependabot event to Google Chat runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' + # Trigger conditions: + # - opened/closed events: only when the PR was authored by dependabot[bot] + # - push events: only when the head commit looks like a Dependabot squash + # (commit message starts with chore(deps- which is our configured prefix) + if: | + (github.event_name != 'push' && github.event.pull_request.user.login == 'dependabot[bot]') || + (github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore(deps-')) steps: - - name: Fetch Dependabot metadata - id: meta + - name: Resolve PR for push event + id: pr_lookup + if: github.event_name == 'push' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.head_commit.id }} + run: | + # Find the PR that produced this squash commit on main. + PR_JSON=$(gh api "repos/${{ github.repository }}/commits/${COMMIT_SHA}/pulls" 2>/dev/null | jq '[.[] | select(.user.login == "dependabot[bot]")] | .[0] // empty') + if [[ -z "$PR_JSON" || "$PR_JSON" == "null" ]]; then + echo "No Dependabot PR associated with this commit; skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "pr_number=$(echo "$PR_JSON" | jq -r '.number')" >> "$GITHUB_OUTPUT" + echo "pr_url=$(echo "$PR_JSON" | jq -r '.html_url')" >> "$GITHUB_OUTPUT" + echo "pr_title=$(echo "$PR_JSON" | jq -r '.title')" >> "$GITHUB_OUTPUT" + echo "pr_head_ref=$(echo "$PR_JSON" | jq -r '.head.ref')" >> "$GITHUB_OUTPUT" + + - name: Stop early if push had no Dependabot PR + if: github.event_name == 'push' && steps.pr_lookup.outputs.skip == 'true' + run: exit 0 + + - name: Fetch Dependabot metadata (PR events) + id: meta_pr + if: github.event_name != 'push' + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Fetch Dependabot metadata (push events) + id: meta_push + if: github.event_name == 'push' && steps.pr_lookup.outputs.skip != 'true' uses: dependabot/fetch-metadata@v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" + pr-number: ${{ steps.pr_lookup.outputs.pr_number }} - - name: Classify event + - name: Classify event and update type id: classify + env: + FM_UPDATE_TYPE: ${{ steps.meta_pr.outputs.update-type || steps.meta_push.outputs.update-type }} + PR_TITLE: ${{ github.event.pull_request.title || steps.pr_lookup.outputs.pr_title }} + ACTION: ${{ github.event.action }} + MERGED: ${{ github.event.pull_request.merged }} + GH_EVENT: ${{ github.event_name }} run: | - EVENT="unknown" - if [[ "${{ github.event.action }}" == "opened" ]]; then - if [[ "${{ steps.meta.outputs.update-type }}" == "version-update:semver-major" ]]; then - EVENT="opened_major" + UPDATE_TYPE="${FM_UPDATE_TYPE}" + if [[ -z "$UPDATE_TYPE" || "$UPDATE_TYPE" == "null" ]]; then + if [[ "$PR_TITLE" =~ ([0-9]+)\.([0-9]+)\.([0-9]+).*to.*([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + OM="${BASH_REMATCH[1]}"; Om="${BASH_REMATCH[2]}" + NM="${BASH_REMATCH[4]}"; Nm="${BASH_REMATCH[5]}" + if [[ "$OM" != "$NM" ]]; then UPDATE_TYPE="version-update:semver-major" + elif [[ "$Om" != "$Nm" ]]; then UPDATE_TYPE="version-update:semver-minor" + else UPDATE_TYPE="version-update:semver-patch"; fi else - EVENT="opened_minor_patch" + UPDATE_TYPE="unknown" fi - elif [[ "${{ github.event.action }}" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then + fi + + EVENT="skip" + if [[ "$GH_EVENT" == "pull_request_target" && "$ACTION" == "opened" ]]; then + if [[ "$UPDATE_TYPE" == "version-update:semver-major" ]]; then EVENT="opened_major" + else EVENT="opened_minor_patch"; fi + elif [[ "$GH_EVENT" == "pull_request" && "$ACTION" == "closed" && "$MERGED" == "true" ]]; then + EVENT="merged" + elif [[ "$GH_EVENT" == "push" ]]; then EVENT="merged" - else - echo "Not a tracked event. Skipping notification." - echo "event=skip" >> "$GITHUB_OUTPUT" - exit 0 fi + + case "$UPDATE_TYPE" in + version-update:semver-patch) UPDATE_LABEL="patch update";; + version-update:semver-minor) UPDATE_LABEL="minor update";; + version-update:semver-major) UPDATE_LABEL="major update (may contain breaking changes)";; + *) UPDATE_LABEL="update";; + esac + echo "event=$EVENT" >> "$GITHUB_OUTPUT" + echo "update-type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT" + echo "update-label=$UPDATE_LABEL" >> "$GITHUB_OUTPUT" - - name: Send Google Chat card + - name: Send Google Chat card (threaded per repo) if: steps.classify.outputs.event != 'skip' env: WEBHOOK_URL: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} EVENT: ${{ steps.classify.outputs.event }} - UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} - DEP_NAMES: ${{ steps.meta.outputs.dependency-names }} - ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }} - DIRECTORY: ${{ steps.meta.outputs.directory }} - PREVIOUS_VERSION: ${{ steps.meta.outputs.previous-version }} - NEW_VERSION: ${{ steps.meta.outputs.new-version }} - CVSS: ${{ steps.meta.outputs.cvss || '0' }} - DEP_GROUP: ${{ steps.meta.outputs.dependency-group }} - PR_URL: ${{ github.event.pull_request.html_url }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} + UPDATE_LABEL: ${{ steps.classify.outputs.update-label }} + DEP_NAMES: ${{ steps.meta_pr.outputs.dependency-names || steps.meta_push.outputs.dependency-names }} + ECOSYSTEM: ${{ steps.meta_pr.outputs.package-ecosystem || steps.meta_push.outputs.package-ecosystem }} + DIRECTORY: ${{ steps.meta_pr.outputs.directory || steps.meta_push.outputs.directory }} + PREVIOUS_VERSION: ${{ steps.meta_pr.outputs.previous-version || steps.meta_push.outputs.previous-version }} + NEW_VERSION: ${{ steps.meta_pr.outputs.new-version || steps.meta_push.outputs.new-version }} + DEP_GROUP: ${{ steps.meta_pr.outputs.dependency-group || steps.meta_push.outputs.dependency-group }} + CVSS: ${{ steps.meta_pr.outputs.cvss || steps.meta_push.outputs.cvss || '0' }} + PR_URL: ${{ github.event.pull_request.html_url || steps.pr_lookup.outputs.pr_url }} + PR_NUMBER: ${{ github.event.pull_request.number || steps.pr_lookup.outputs.pr_number }} + PR_TITLE: ${{ github.event.pull_request.title || steps.pr_lookup.outputs.pr_title }} REPO: ${{ github.repository }} - MERGED_BY: ${{ github.event.pull_request.merged_by.login }} + MERGED_BY: ${{ github.event.pull_request.merged_by.login || github.event.head_commit.author.username || 'app/github-actions' }} run: | + case "$ECOSYSTEM" in + npm_and_yarn) ECO_LABEL="npm";; + pip) ECO_LABEL="pip";; + github_actions) ECO_LABEL="GitHub Actions";; + docker) ECO_LABEL="Docker";; + *) ECO_LABEL="$ECOSYSTEM";; + esac + ECO_DISPLAY="$ECO_LABEL" + if [[ -n "$DIRECTORY" && "$DIRECTORY" != "/" ]]; then + ECO_DISPLAY="$ECO_LABEL ($DIRECTORY)" + fi + if [[ -n "$PREVIOUS_VERSION" && -n "$NEW_VERSION" ]]; then + VERSION_TEXT="$PREVIOUS_VERSION -> $NEW_VERSION" + else + VERSION_TEXT="see PR for versions" + fi + if [[ -n "$DEP_GROUP" ]]; then + PKG_COUNT=$(echo "$DEP_NAMES" | tr ',' '\n' | wc -l | tr -d ' ') + PKG_SUMMARY="$DEP_GROUP group ($PKG_COUNT packages)" + else + PKG_SUMMARY="$DEP_NAMES" + fi case "$EVENT" in opened_minor_patch) - TITLE="Dependabot PR opened — auto-merge enabled" - SUBTITLE="${REPO} · #${PR_NUMBER}" - STATUS_TEXT="Auto-merge enabled. Will merge once CI passes." - STATUS_LABEL="Status" - ACTION_BUTTON_TEXT="View PR" + HEADER_TITLE="Safe update - auto-merging after CI" + STATUS_LABEL="Action Required" + STATUS_TEXT="No action needed. Auto-merge enabled; will merge once CI passes." + BUTTON_TEXT="View PR" ;; opened_major) - TITLE="MAJOR version bump — human review required" - SUBTITLE="${REPO} · #${PR_NUMBER}" - STATUS_TEXT="This is a MAJOR version upgrade and may contain breaking changes. Please review and merge manually if safe." + HEADER_TITLE="MAJOR version bump - human review required" STATUS_LABEL="Action Required" - ACTION_BUTTON_TEXT="Review PR" + STATUS_TEXT="This is a MAJOR version upgrade and may contain breaking changes. Please review and merge manually if safe." + BUTTON_TEXT="Review PR" ;; merged) - TITLE="Dependabot PR merged" - SUBTITLE="${REPO} · #${PR_NUMBER}" - STATUS_TEXT="Merged into default branch${MERGED_BY:+ by ${MERGED_BY}}." - STATUS_LABEL="Status" - ACTION_BUTTON_TEXT="View PR" + HEADER_TITLE="Merged into main" + STATUS_LABEL="Merged By" + if [[ -n "$MERGED_BY" && "$MERGED_BY" != "null" ]]; then + STATUS_TEXT="$MERGED_BY" + else + STATUS_TEXT="dependabot[bot] (auto-merge)" + fi + BUTTON_TEXT="View PR" ;; esac + SUBTITLE="$REPO - PR #$PR_NUMBER" + CARD_ID="dependabot-$EVENT-$(date +%s)" + THREAD_KEY=$(echo "$REPO" | tr '[:upper:]' '[:lower:]' | tr '/' '-') + THREADED_URL="${WEBHOOK_URL}&threadKey=${THREAD_KEY}&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" - VERSION_TEXT="${PREVIOUS_VERSION:-?} → ${NEW_VERSION:-?}" - [[ "$VERSION_TEXT" == "? → ?" ]] && VERSION_TEXT="see PR for versions" - - GROUP_LINE="" - if [[ -n "$DEP_GROUP" ]]; then - GROUP_LINE=",{\"decoratedText\": {\"topLabel\": \"Group\", \"text\": \"${DEP_GROUP}\"}}" - fi + jq -n \ + --arg cardId "$CARD_ID" \ + --arg title "$HEADER_TITLE" \ + --arg subtitle "$SUBTITLE" \ + --arg statusLabel "$STATUS_LABEL" \ + --arg statusText "$STATUS_TEXT" \ + --arg prTitle "$PR_TITLE" \ + --arg pkgSummary "$PKG_SUMMARY" \ + --arg versionText "$VERSION_TEXT" \ + --arg updateLabel "$UPDATE_LABEL" \ + --arg ecoDisplay "$ECO_DISPLAY" \ + --arg buttonText "$BUTTON_TEXT" \ + --arg prUrl "$PR_URL" \ + --arg cvss "$CVSS" \ + '{ + cardsV2: [{ + cardId: $cardId, + card: { + header: { + title: $title, + subtitle: $subtitle, + imageUrl: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png", + imageType: "CIRCLE" + }, + sections: [{ + widgets: ( + [ + {decoratedText: {topLabel: $statusLabel, text: $statusText, wrapText: true}}, + {decoratedText: {topLabel: "PR Title", text: $prTitle, wrapText: true}}, + {decoratedText: {topLabel: "Package(s)", text: $pkgSummary, wrapText: true}}, + {decoratedText: {topLabel: "Version", text: $versionText}}, + {decoratedText: {topLabel: "Update Type", text: $updateLabel}}, + {decoratedText: {topLabel: "Ecosystem", text: $ecoDisplay}} + ] + + (if ($cvss | IN("0","0.0","")) then [] else [{decoratedText: {topLabel: "CVSS Score (Common Vulnerability Scoring System, 0 to 10 scale)", text: $cvss}}] end) + + [{buttonList: {buttons: [{text: $buttonText, onClick: {openLink: {url: $prUrl}}}]}}] + ) + }] + } + }] + }' > payload.json - cat > payload.json <