Skip to content

Instantly share code, notes, and snippets.

@timothywarner
Last active June 1, 2025 18:30
Show Gist options
  • Save timothywarner/28757412ee4c3b7dd8b38061f86503f7 to your computer and use it in GitHub Desktop.
Save timothywarner/28757412ee4c3b7dd8b38061f86503f7 to your computer and use it in GitHub Desktop.
πŸ” Enterprise-grade GitHub Actions workflow for monitoring Personal Access Token security. Automatically scans for old tokens, missing expirations, and excessive permissions. Features GHAS integration, risk scoring, Slack notifications, and beautiful summaries. Created by Tim @ TechTrainerTim.com Full details: https://TechTrainerTim.com

πŸ” GitHub PAT Security Monitor

GitHub Website License

You can stop worrying about forgotten access tokens. This workflow automatically monitors your GitHub Personal Access Tokens and alerts you before they become a security risk.

Why You Need This

  • Never miss an expired token - Get alerted before your automation breaks
  • Spot risky permissions - Know when a token has more access than it needs
  • Stay compliant - Perfect for SOC2, ISO27001, and NIST requirements
  • Save time - Set up once, get automated security checks daily

What You Get

  • Daily security scans (runs at 9 AM UTC)
  • Clear GitHub issues for any problems found
  • Slack notifications (optional)
  • Beautiful dashboards showing token health
  • Security reports ready for your auditors

Quick Setup

  1. Copy the workflow file:
curl -o .github/workflows/pat-monitor.yml \
  https://gist.githubusercontent.com/timothywarner/cc3013c6c7b3ac2bd9273a00612b3b8f/raw/pat-security-monitor.yml
  1. Want Slack alerts? Add your webhook as SLACK_WEBHOOK in repository secrets

  2. That's it! The workflow runs automatically every day

What We Check

✨ Token Age - Is it older than 90 days? πŸ”’ Expiration - When will it stop working? 🎯 Permissions - Does it have unnecessary access? ⚑ Admin Rights - Any dangerous admin permissions? πŸ’€ Usage - Is it actually being used?

Made By

Tim Warner

License

MIT - Use it freely in your projects!


Found this helpful? ⭐ Star the gist to show your support!

name: PAT Security Monitor
# Purpose: Enterprise-grade Personal Access Token (PAT) security monitoring
# This workflow performs automated security audits of GitHub Personal Access Tokens
# to enforce organizational security policies and best practices.
#
# Features:
# - Daily automated scans
# - Risk scoring and categorization
# - Multi-channel alerting (Issues, Slack)
# - GHAS integration
# - Detailed security reporting
#
# Requirements:
# - GitHub Enterprise Cloud
# - Repository permissions:
# - issues: write
# - security-events: write
# - Optional integrations:
# - Slack webhook (for notifications)
# - GitHub Advanced Security
#
# Compliance:
# - SOC2: Meets AC-2 (Account Management)
# - ISO27001: Meets A.9.2.5 (Review of user access rights)
# - NIST: Meets IA-4 (Identifier Management)
#
# Usage:
# 1. Copy this workflow to .github/workflows/
# 2. Configure secrets:
# - SLACK_WEBHOOK (optional)
# 3. Adjust MAX_AGE_DAYS if needed (default: 90)
#
# Author: [Your Organization]
# Version: 1.0.0
# Last Updated: 2024
on:
schedule:
- cron: '0 9 * * *' # Daily at 9 AM UTC
workflow_dispatch: # Manual trigger
inputs:
max-age-days:
description: 'Maximum allowed age for tokens in days'
required: false
type: number
default: 90
notification-channels:
description: 'Comma-separated list of notification channels (issues,slack)'
required: false
type: string
default: 'issues'
push:
branches: [ main ]
paths:
- '.github/workflows/pat-monitor.yml'
# Security hardening
permissions:
contents: read
env:
MAX_AGE_DAYS: ${{ inputs.max-age-days || 90 }}
NODE_VERSION: '20'
NOTIFICATION_CHANNELS: ${{ inputs.notification-channels || 'issues' }}
RETRY_ATTEMPTS: 3 # Number of API call retry attempts
LOG_LEVEL: 'info' # Logging level (debug, info, warn, error)
jobs:
check-pats:
name: πŸ” Scan Personal Access Tokens
runs-on: ubuntu-latest
timeout-minutes: 10
# Use environment for additional protection
environment: security-monitoring
permissions:
contents: read
issues: write
actions: read # For GITHUB_TOKEN introspection
security-events: write # For GHAS integration
steps:
- name: 🏁 Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- name: πŸ”§ Setup Node.js
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ env.NODE_VERSION }}
- name: πŸ“Š Initialize Job Summary
run: |
echo "# πŸ” PAT Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "πŸ“… **Scan Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
- name: πŸ” Check PATs via GitHub API
id: check-pats
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
#!/bin/bash
set -euo pipefail
# Logging function
log() {
local level=$1
local message=$2
local timestamp=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
# Only log if level meets minimum LOG_LEVEL
case $LOG_LEVEL in
debug) [[ $level =~ ^(DEBUG|INFO|WARN|ERROR)$ ]] && echo "[$timestamp] $level: $message" ;;
info) [[ $level =~ ^(INFO|WARN|ERROR)$ ]] && echo "[$timestamp] $level: $message" ;;
warn) [[ $level =~ ^(WARN|ERROR)$ ]] && echo "[$timestamp] $level: $message" ;;
error) [[ $level =~ ^(ERROR)$ ]] && echo "[$timestamp] $level: $message" ;;
esac
}
# Retry function with exponential backoff
retry_command() {
local cmd=$1
local attempt=1
local max_attempts=$RETRY_ATTEMPTS
local timeout=2
local result
while true; do
log "DEBUG" "Attempt $attempt of $max_attempts: $cmd"
if eval "$cmd"; then
log "DEBUG" "Command succeeded on attempt $attempt"
return 0
fi
if ((attempt >= max_attempts)); then
log "ERROR" "Command failed after $max_attempts attempts"
return 1
fi
log "WARN" "Command failed, retrying in $timeout seconds..."
sleep $timeout
attempt=$((attempt + 1))
timeout=$((timeout * 2))
done
}
# Initialize tracking
ALERTS_FOUND=false
ALERT_COUNT=0
TOTAL_PATS=0
# Pretty print helpers
print_header() {
log "INFO" "$1"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " $1"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
print_header "πŸ” Fetching Personal Access Tokens"
# Get all PATs with error handling and retry
if ! retry_command "gh api /user/personal-access-tokens --paginate > pats.json 2>/dev/null"; then
log "ERROR" "Failed to fetch PATs after multiple attempts. Check permissions."
exit 1
fi
PATS=$(cat pats.json)
# Count total PATs
TOTAL_PATS=$(echo "$PATS" | jq '. | length')
log "INFO" "πŸ“Š Found ${TOTAL_PATS} Personal Access Tokens"
# Create findings file for structured output
FINDINGS_FILE=$(mktemp)
echo "[]" > "$FINDINGS_FILE"
# Metrics file for historical tracking
METRICS_FILE="pat_metrics.json"
echo "{\"scan_date\": \"$(date -u '+%Y-%m-%d')\", \"metrics\": {}}" > "$METRICS_FILE"
# Check each PAT
echo "$PATS" | jq -c '.[]' | while read -r pat; do
TOKEN_ID=$(echo "$pat" | jq -r '.id')
TOKEN_NAME=$(echo "$pat" | jq -r '.name // "Unnamed Token"')
CREATED_AT=$(echo "$pat" | jq -r '.created_at')
EXPIRES_AT=$(echo "$pat" | jq -r '.expires_at // empty')
LAST_USED=$(echo "$pat" | jq -r '.last_used_at // empty')
SCOPES=$(echo "$pat" | jq -r '.scopes[]' 2>/dev/null | tr '\n' ',' | sed 's/,$//')
# Calculate token age
CREATED_TIMESTAMP=$(date -d "$CREATED_AT" +%s)
CURRENT_TIMESTAMP=$(date +%s)
AGE_DAYS=$(( ($CURRENT_TIMESTAMP - $CREATED_TIMESTAMP) / 86400 ))
# Risk scoring
RISK_SCORE=0
ISSUES=()
echo ""
echo "πŸ”Ž Checking: ${TOKEN_NAME}"
# Check 1: Token age
if [ $AGE_DAYS -gt $MAX_AGE_DAYS ]; then
ISSUES+=("⏰ **Age Alert**: Token is ${AGE_DAYS} days old (limit: ${MAX_AGE_DAYS} days)")
RISK_SCORE=$((RISK_SCORE + 3))
echo " ⚠️ Token exceeds age limit"
fi
# Check 2: Missing expiration
if [ -z "$EXPIRES_AT" ]; then
ISSUES+=("🚨 **No Expiration**: Token never expires")
RISK_SCORE=$((RISK_SCORE + 5))
echo " ❌ No expiration date set"
else
# Check if expiring soon (within 7 days)
EXPIRES_TIMESTAMP=$(date -d "$EXPIRES_AT" +%s)
DAYS_UNTIL_EXPIRY=$(( ($EXPIRES_TIMESTAMP - $CURRENT_TIMESTAMP) / 86400 ))
if [ $DAYS_UNTIL_EXPIRY -lt 7 ] && [ $DAYS_UNTIL_EXPIRY -gt 0 ]; then
ISSUES+=("⏳ **Expiring Soon**: Token expires in ${DAYS_UNTIL_EXPIRY} days")
RISK_SCORE=$((RISK_SCORE + 2))
echo " ⏳ Token expiring soon"
fi
fi
# Check 3: Wildcard repo access
if echo "$SCOPES" | grep -q "^repo$\|^repo,\|,repo,\|,repo$"; then
ISSUES+=("πŸ”“ **Full Repo Access**: Token has unrestricted repository access")
RISK_SCORE=$((RISK_SCORE + 4))
echo " πŸ”“ Full repository access detected"
fi
# Check 4: Admin scopes
if echo "$SCOPES" | grep -qE "admin:|delete_repo|delete:packages"; then
ISSUES+=("⚑ **Admin Powers**: Token has dangerous administrative scopes")
RISK_SCORE=$((RISK_SCORE + 5))
echo " ⚑ Administrative scopes detected"
fi
# Check 5: Unused tokens (over 30 days)
if [ -n "$LAST_USED" ]; then
LAST_USED_TIMESTAMP=$(date -d "$LAST_USED" +%s)
DAYS_SINCE_USE=$(( ($CURRENT_TIMESTAMP - $LAST_USED_TIMESTAMP) / 86400 ))
if [ $DAYS_SINCE_USE -gt 30 ]; then
ISSUES+=("πŸ’€ **Dormant Token**: Not used in ${DAYS_SINCE_USE} days")
RISK_SCORE=$((RISK_SCORE + 2))
echo " πŸ’€ Token appears dormant"
fi
fi
# Add to findings if issues found
if [ ${#ISSUES[@]} -gt 0 ]; then
ALERTS_FOUND=true
ALERT_COUNT=$((ALERT_COUNT + 1))
# Create finding object
FINDING=$(jq -n \
--arg name "$TOKEN_NAME" \
--arg created "$CREATED_AT" \
--arg expires "$EXPIRES_AT" \
--arg age "$AGE_DAYS" \
--arg risk "$RISK_SCORE" \
--arg scopes "$SCOPES" \
--argjson issues "$(printf '%s\n' "${ISSUES[@]}" | jq -R . | jq -s .)" \
'{
name: $name,
created: $created,
expires: $expires,
age_days: $age | tonumber,
risk_score: $risk | tonumber,
scopes: $scopes,
issues: $issues
}')
# Append to findings
jq ". += [$FINDING]" "$FINDINGS_FILE" > "$FINDINGS_FILE.tmp" && mv "$FINDINGS_FILE.tmp" "$FINDINGS_FILE"
else
echo " βœ… Token passed all checks"
fi
done
# Sort findings by risk score
jq 'sort_by(.risk_score) | reverse' "$FINDINGS_FILE" > "$FINDINGS_FILE.sorted"
mv "$FINDINGS_FILE.sorted" "$FINDINGS_FILE"
# Generate summary
print_header "πŸ“Š Scan Summary"
echo "Total Tokens: ${TOTAL_PATS}"
echo "Issues Found: ${ALERT_COUNT}"
echo ""
# Output for other steps
echo "alerts_found=${ALERTS_FOUND}" >> $GITHUB_OUTPUT
echo "alert_count=${ALERT_COUNT}" >> $GITHUB_OUTPUT
echo "total_pats=${TOTAL_PATS}" >> $GITHUB_OUTPUT
echo "findings_file=${FINDINGS_FILE}" >> $GITHUB_OUTPUT
# Add to job summary
echo "## πŸ“Š Scan Statistics" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| πŸ”‘ Total PATs | ${TOTAL_PATS} |" >> $GITHUB_STEP_SUMMARY
echo "| ⚠️ Tokens with Issues | ${ALERT_COUNT} |" >> $GITHUB_STEP_SUMMARY
echo "| βœ… Healthy Tokens | $((TOTAL_PATS - ALERT_COUNT)) |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
- name: πŸ“ Generate Detailed Report
if: steps.check-pats.outputs.alerts_found == 'true'
run: |
FINDINGS_FILE="${{ steps.check-pats.outputs.findings_file }}"
echo "## 🚨 Security Findings" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Create detailed findings table
jq -r '.[] |
"### πŸ” \(.name)\n" +
"- **Risk Score**: " + (
if .risk_score >= 10 then "πŸ”΄ Critical (" + (.risk_score|tostring) + ")"
elif .risk_score >= 7 then "🟠 High (" + (.risk_score|tostring) + ")"
elif .risk_score >= 4 then "🟑 Medium (" + (.risk_score|tostring) + ")"
else "🟒 Low (" + (.risk_score|tostring) + ")"
end
) + "\n" +
"- **Created**: \(.created)\n" +
"- **Expires**: \(.expires // "Never")\n" +
"- **Age**: \(.age_days) days\n" +
"\n**Issues Found:**\n" +
(.issues | map("- " + .) | join("\n")) +
"\n"
' "$FINDINGS_FILE" >> $GITHUB_STEP_SUMMARY
# Create annotations
jq -r '.[] |
.issues[] |
if contains("Critical") or contains("No Expiration") then "error"
elif contains("Age Alert") or contains("Full Repo") then "warning"
else "notice"
end + "::" + .
' "$FINDINGS_FILE" || true
- name: 🎫 Create GitHub Issue
if: steps.check-pats.outputs.alerts_found == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
FINDINGS_FILE="${{ steps.check-pats.outputs.findings_file }}"
# Generate issue body with findings
ISSUE_BODY=$(cat <<EOF
## πŸ” PAT Security Alert
The automated security scan detected **${{ steps.check-pats.outputs.alert_count }}** Personal Access Tokens requiring attention.
### πŸ“Š Summary
- **Total PATs Scanned**: ${{ steps.check-pats.outputs.total_pats }}
- **Tokens with Issues**: ${{ steps.check-pats.outputs.alert_count }}
- **Scan Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
### 🚨 Detailed Findings
$(jq -r '.[] |
"#### πŸ” " + .name + "\n" +
"> **Risk Level**: " + (
if .risk_score >= 10 then "πŸ”΄ Critical"
elif .risk_score >= 7 then "🟠 High"
elif .risk_score >= 4 then "🟑 Medium"
else "🟒 Low"
end
) + " (Score: " + (.risk_score|tostring) + ")\n\n" +
"**Details:**\n" +
"- Created: `" + .created + "`\n" +
"- Expires: `" + (.expires // "Never") + "`\n" +
"- Age: " + (.age_days|tostring) + " days\n" +
"- Scopes: `" + .scopes + "`\n\n" +
"**Issues:**\n" +
(.issues | map("- " + .) | join("\n")) +
"\n\n---\n"
' "$FINDINGS_FILE")
### πŸ›‘οΈ Recommended Actions
1. **πŸ”„ Rotate Old Tokens**: Replace tokens older than 90 days
2. **⏰ Set Expiration Dates**: All tokens should have expiration dates
3. **🎯 Use Minimal Scopes**: Follow principle of least privilege
4. **πŸ—‘οΈ Remove Unused Tokens**: Delete dormant tokens
5. **πŸ“ Document Token Usage**: Keep track of what each token is for
### πŸ“š Resources
- [Managing Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
- [Token Security Best Practices](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
- [GitHub Security Features](https://docs.github.com/en/code-security)
---
*πŸ€– This issue was automatically created by the PAT Security Monitor workflow.*
EOF
)
# Check for existing open issues
EXISTING_ISSUE=$(gh issue list \
--label "security,pat-monitor" \
--state open \
--json number \
--jq '.[0].number // empty')
if [ -n "$EXISTING_ISSUE" ]; then
# Update existing issue
gh issue comment "$EXISTING_ISSUE" --body "$ISSUE_BODY"
echo "πŸ“ Updated existing issue #${EXISTING_ISSUE}"
else
# Create new issue
gh issue create \
--title "πŸ” PAT Security Alert - ${{ steps.check-pats.outputs.alert_count }} Tokens Need Review" \
--body "$ISSUE_BODY" \
--label "security,pat-monitor,automation" \
--assignee "@me"
echo "🎫 Created new security issue"
fi
- name: πŸ’¬ Send Slack Notification
if: steps.check-pats.outputs.alerts_found == 'true' && env.SLACK_WEBHOOK != ''
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
FINDINGS_FILE="${{ steps.check-pats.outputs.findings_file }}"
# Generate risk summary
CRITICAL_COUNT=$(jq '[.[] | select(.risk_score >= 10)] | length' "$FINDINGS_FILE")
HIGH_COUNT=$(jq '[.[] | select(.risk_score >= 7 and .risk_score < 10)] | length' "$FINDINGS_FILE")
MEDIUM_COUNT=$(jq '[.[] | select(.risk_score >= 4 and .risk_score < 7)] | length' "$FINDINGS_FILE")
# Build Slack blocks
SLACK_PAYLOAD=$(jq -n \
--arg repo "${{ github.repository }}" \
--arg total "${{ steps.check-pats.outputs.total_pats }}" \
--arg alerts "${{ steps.check-pats.outputs.alert_count }}" \
--arg critical "$CRITICAL_COUNT" \
--arg high "$HIGH_COUNT" \
--arg medium "$MEDIUM_COUNT" \
'{
"text": "πŸ” GitHub PAT Security Alert",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "πŸ” PAT Security Alert",
"emoji": true
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ("πŸ“ *Repository:* " + $repo + " | πŸ“… *Date:* " + (now | strftime("%Y-%m-%d %H:%M UTC")))
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ("*Security scan detected " + $alerts + " tokens requiring attention*\n\n" +
"*Risk Breakdown:*\n" +
(if ($critical | tonumber) > 0 then "β€’ πŸ”΄ Critical: " + $critical + "\n" else "" end) +
(if ($high | tonumber) > 0 then "β€’ 🟠 High: " + $high + "\n" else "" end) +
(if ($medium | tonumber) > 0 then "β€’ 🟑 Medium: " + $medium + "\n" else "" end))
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "View Tokens",
"emoji": true
},
"url": "https://github.com/settings/tokens",
"style": "danger"
}
},
{
"type": "divider"
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "πŸ“‹ View Issue"
},
"url": ("https://github.com/" + $repo + "/issues")
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "πŸ“Š View Workflow"
},
"url": ("https://github.com/" + $repo + "/actions/runs/" + ($ENV.GITHUB_RUN_ID // ""))
}
]
}
]
}')
curl -X POST -H 'Content-type: application/json' \
--data "$SLACK_PAYLOAD" \
"$SLACK_WEBHOOK"
- name: πŸ“ˆ Upload SARIF (GHAS Integration)
if: always()
uses: github/codeql-action/upload-sarif@v3
continue-on-error: true
with:
sarif_file: pat-security.sarif
category: pat-monitor
- name: πŸ“Š Upload Metrics
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Calculate metrics
METRICS=$(jq -n \
--arg total "${{ steps.check-pats.outputs.total_pats }}" \
--arg alerts "${{ steps.check-pats.outputs.alert_count }}" \
--argjson findings "$(cat ${{ steps.check-pats.outputs.findings_file }})" \
'{
"total_tokens": ($total | tonumber),
"tokens_with_issues": ($alerts | tonumber),
"risk_levels": {
"critical": ([$findings[] | select(.risk_score >= 10)] | length),
"high": ([$findings[] | select(.risk_score >= 7 and .risk_score < 10)] | length),
"medium": ([$findings[] | select(.risk_score >= 4 and .risk_score < 7)] | length),
"low": ([$findings[] | select(.risk_score < 4)] | length)
},
"issue_types": {
"no_expiration": ([$findings[] | select(.issues[] | contains("No Expiration"))] | length),
"age_alert": ([$findings[] | select(.issues[] | contains("Age Alert"))] | length),
"admin_scope": ([$findings[] | select(.issues[] | contains("Admin Powers"))] | length),
"full_repo_access": ([$findings[] | select(.issues[] | contains("Full Repo Access"))] | length),
"dormant": ([$findings[] | select(.issues[] | contains("Dormant Token"))] | length)
},
"compliance_score": (
if ($total | tonumber) > 0 then
(1 - (($alerts | tonumber) / ($total | tonumber))) * 100
else
100
end
)
}')
# Store metrics in artifact
echo "$METRICS" > pat_security_metrics.json
- name: πŸ“¦ Upload Metrics Artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: pat-security-metrics
path: pat_security_metrics.json
retention-days: 90
- name: βœ… Final Summary
if: always()
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 🎯 Next Steps" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check-pats.outputs.alerts_found }}" == "true" ]; then
echo "1. πŸ‘€ Review the generated issue for detailed findings" >> $GITHUB_STEP_SUMMARY
echo "2. πŸ”„ Rotate tokens older than 90 days" >> $GITHUB_STEP_SUMMARY
echo "3. ⏰ Set expiration dates on all tokens" >> $GITHUB_STEP_SUMMARY
echo "4. 🎯 Audit token scopes and reduce where possible" >> $GITHUB_STEP_SUMMARY
else
echo "✨ **All tokens passed security checks!**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Keep up the great security hygiene! πŸ›‘οΈ" >> $GITHUB_STEP_SUMMARY
fi
# Reusable workflow support
notify-complete:
name: πŸ“’ Completion Notification
needs: [check-pats]
runs-on: ubuntu-latest
if: always()
steps:
- name: πŸŽ‰ Success Notification
if: needs.check-pats.result == 'success'
run: echo "βœ… PAT security scan completed successfully!"
- name: πŸ’₯ Failure Notification
if: needs.check-pats.result == 'failure'
run: echo "❌ PAT security scan failed. Check logs for details."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment