-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathhooks.json
More file actions
54 lines (54 loc) · 20 KB
/
Copy pathhooks.json
File metadata and controls
54 lines (54 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"description": "Block hardcoded secrets in file writes (CLI + VS Code) \u2014 denies with session-level additionalContext per v4 pattern",
"matcher": "create|edit|bash|run_in_terminal",
"timeoutSec": 10,
"bash": "if ! command -v jq >/dev/null 2>&1; then\n cat <<'JSON_EOF'\n{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Hook enforcement unavailable: jq is not installed. Install jq (brew install jq / apt install jq / choco install jq).\",\"additionalContext\":\"REPOSITORY POLICY: enforcement hooks require jq. Do NOT retry via alternate tools. Install jq and re-run.\",\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Hook enforcement unavailable: jq is not installed.\"}}\nJSON_EOF\n exit 0\nfi\nINPUT=$(cat)\nTOOL=$(echo \"$INPUT\" | jq -r '.toolName // .tool_name // empty' 2>/dev/null)\nARGS=$(echo \"$INPUT\" | jq -c 'if (.toolArgs|type)==\"string\" then (try (.toolArgs|fromjson) catch {}) elif (.toolArgs|type)==\"object\" then .toolArgs elif (.tool_input|type)==\"object\" then .tool_input else {} end' 2>/dev/null)\nCONTENT=$(echo \"$ARGS\" | jq -r '.content // .new_string // .newString // empty' 2>/dev/null)\nCMD=$(echo \"$ARGS\" | jq -r '.command // empty' 2>/dev/null)\n\nhit_content=0\nif [ -n \"$CONTENT\" ]; then\n hit_content=$(echo \"$CONTENT\" | while IFS= read -r line; do\n echo \"$line\" | grep -qiE '\"?(password|secret|token|api[_-]?key)\"?\\s*[:=]' 2>/dev/null || continue\n echo \"$line\" | sed -E 's/^.*[:=][[:space:]]*//' | grep -qE '^[$][{(]' 2>/dev/null && continue\n echo \"$line\" | grep -qiE '[:=]\\s*.{8,}' 2>/dev/null && echo HIT\n done | grep -c HIT)\nfi\n\nhit_cmd=0\ncase \"$TOOL\" in\n bash|run_in_terminal)\n if [ -n \"$CMD\" ]; then\n # scan line-by-line: only exempt individual lines that themselves use ${...}\n # so a heredoc with a real secret + unrelated ${{ github.ref }} elsewhere cannot bypass\n if echo \"$CMD\" | grep -qE '(>|>>|<<|<<<|cat[[:space:]]|printf[[:space:]]|echo[[:space:]])' 2>/dev/null; then\n hit_cmd=$(echo \"$CMD\" | while IFS= read -r line; do\n echo \"$line\" | grep -qiE '\"?(password|secret|token|api[_-]?key)\"?\\s*[:=]' 2>/dev/null || continue\n echo \"$line\" | sed -E 's/^.*[:=][[:space:]]*//' | grep -qE '^[$][{(]' 2>/dev/null && continue\n echo \"$line\" | grep -qiE '[:=]\\s*[^$[:space:]][^[:space:]]{7,}' 2>/dev/null && echo HIT\n done | grep -c HIT)\n fi\n fi\n ;;\nesac\n\nif [ \"${hit_content:-0}\" -gt 0 ] || [ \"${hit_cmd:-0}\" -gt 0 ]; then\n R='Blocked: hardcoded secret detected. Use GitHub Secrets (${ secrets.NAME }) instead.'\n AC='REPOSITORY POLICY: hardcoded secrets are not permitted in any write path, including shell redirects/heredocs. Do NOT retry through alternate tools. Replace literal secret values with GitHub Actions secrets context references.'\n jq -n --arg r \"$R\" --arg ac \"$AC\" '{permissionDecision:\"deny\",permissionDecisionReason:$r,additionalContext:$ac,hookSpecificOutput:{hookEventName:\"PreToolUse\",permissionDecision:\"deny\",permissionDecisionReason:$r,additionalContext:$ac}}'\nelse\n echo '{}'\nfi"
},
{
"type": "command",
"description": "Guard destructive ops; allow CI archival (CLI + VS Code) \u2014 denies with session-level additionalContext per v4 pattern",
"matcher": "bash|run_in_terminal",
"timeoutSec": 10,
"bash": "if ! command -v jq >/dev/null 2>&1; then\n cat <<'JSON_EOF'\n{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Hook enforcement unavailable: jq is not installed. Install jq (brew install jq / apt install jq / choco install jq).\",\"additionalContext\":\"REPOSITORY POLICY: enforcement hooks require jq. Do NOT retry via alternate tools. Install jq and re-run.\",\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Hook enforcement unavailable: jq is not installed.\"}}\nJSON_EOF\n exit 0\nfi\nINPUT=$(cat)\nARGS=$(echo \"$INPUT\" | jq -c 'if (.toolArgs|type)==\"string\" then (try (.toolArgs|fromjson) catch {}) elif (.toolArgs|type)==\"object\" then .toolArgs elif (.tool_input|type)==\"object\" then .tool_input else {} end' 2>/dev/null)\nCMD=$(echo \"$ARGS\" | jq -r '.command // empty' 2>/dev/null)\n[ -z \"$CMD\" ] && { echo '{}'; exit 0; }\n\nallow_ci_src_regex='^(Jenkinsfile|\\.travis\\.yml|\\.gitlab-ci\\.yml|\\.drone\\.yml|bitbucket-pipelines\\.yml|azure-pipelines\\.yml|bamboo-specs/.+|\\.circleci/.+)$'\ndenied=0\nreason=''\n\nis_ci_archive() {\n # Accept either the relative form or an absolute path anchored at the hook's PWD.\n # Reject arbitrary external directories (e.g. /tmp/x/.github/ci-archive/) that\n # merely happen to end with the same suffix.\n local p=\"${1%/}\"\n case \"$p\" in\n .github/ci-archive|.github/ci-archive/*) return 0 ;;\n esac\n local base=\"${PWD}/.github/ci-archive\"\n case \"$p\" in\n \"$base\"|\"$base\"/*) return 0 ;;\n esac\n return 1\n}\n\nset_deny() {\n denied=1\n reason=\"$1\"\n}\n\nwhile IFS= read -r raw; do\n seg=$(echo \"$raw\" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')\n [ -z \"$seg\" ] && continue\n\n # Bug 8 fix \u2014 unwrap shell-wrapper commands so the destructive-op regex can see the\n # actual payload. bash -c 'rm README.md' would otherwise slip past the (^|space)rm(space|$)\n # anchor because the character before rm is a quote.\n if echo \"$seg\" | grep -qE '(^|[[:space:]])(bash|sh|zsh|dash|ksh|ash)[[:space:]]+(-[a-zA-Z]*c|--command)([[:space:]]|$)'; then\n inner=$(echo \"$seg\" | sed -E \"s/^.*[[:space:]](-[a-zA-Z]*c|--command)[[:space:]]+['\\\"]?(.*)$/\\2/\" | sed -E \"s/['\\\"][[:space:]]*$//\")\n # Additionally split the inner payload on shell operators \u2014 a wrapped multi-command\n # like bash -c 'rm A; mv B' can hide multiple destructive ops behind one segment.\n [ -n \"$inner\" ] && seg=\"$inner\"\n fi\n\n op=''\n echo \"$seg\" | grep -qE '(^|[[:space:]])find[[:space:]].*-delete([[:space:]]|$)' && op='finddelete'\n [ -z \"$op\" ] && echo \"$seg\" | grep -qE '(^|[[:space:]])(sudo[[:space:]]+|command[[:space:]]+|builtin[[:space:]]+|/usr/bin/env[[:space:]]+)*(\\/bin\\/|\\/usr\\/bin\\/)?rm([[:space:]]|$)' && op='rm'\n [ -z \"$op\" ] && echo \"$seg\" | grep -qE '(^|[[:space:]])(sudo[[:space:]]+|command[[:space:]]+|builtin[[:space:]]+|/usr/bin/env[[:space:]]+)*(\\/bin\\/|\\/usr\\/bin\\/)?unlink([[:space:]]|$)' && op='unlink'\n [ -z \"$op\" ] && echo \"$seg\" | grep -qE '(^|[[:space:]])git[[:space:]]+rm([[:space:]]|$)' && op='gitrm'\n [ -z \"$op\" ] && echo \"$seg\" | grep -qE '(^|[[:space:]])git[[:space:]]+mv([[:space:]]|$)' && op='gitmv'\n [ -z \"$op\" ] && echo \"$seg\" | grep -qE '(^|[[:space:]])(sudo[[:space:]]+|command[[:space:]]+|builtin[[:space:]]+|/usr/bin/env[[:space:]]+)*(\\/bin\\/|\\/usr\\/bin\\/)?mv([[:space:]]|$)' && op='mv'\n [ -z \"$op\" ] && continue\n\n seg_clean=$(echo \"$seg\" | sed -E 's/[0-9]?>>?&?[0-9-]+//g; s/[0-9]?>>?[[:space:]]*[^[:space:]]+//g; s/<[[:space:]]*[^[:space:]]+//g')\n toks=$(echo \"$seg_clean\" | tr -s ' ' '\\n' | grep -vE '^(sudo|command|builtin|/usr/bin/env|git|rm|mv|unlink|/bin/rm|/usr/bin/rm|/bin/mv|/usr/bin/mv|find|-delete|-[a-zA-Z]+|--[a-zA-Z-]+|>|>>|<|2>|2>&1|&>)$' | grep -v '^$')\n\n for t in $toks; do\n bare=\"${t#./}\"\n case \"$bare\" in *..*) set_deny 'Blocked: path traversal (..) not allowed in destructive operations.' ;; esac\n [ \"$denied\" -eq 1 ] && break\n done\n [ \"$denied\" -eq 1 ] && break\n\n case \"$op\" in\n rm|unlink|gitrm|finddelete)\n for t in $toks; do\n bare=\"${t#./}\"\n [ \"$bare\" = \".\" ] && continue\n is_ci_archive \"$bare\" && continue\n set_deny \"Blocked: deletion outside .github/ci-archive/ is not allowed (${bare}).\"\n break\n done\n ;;\n mv|gitmv)\n # last operand is destination; ALL preceding tokens are sources.\n # each source must be a recognised CI source (or already under ci-archive).\n dst=$(echo \"$toks\" | tail -1)\n dst=\"${dst#./}\"\n is_ci_archive \"$dst\" || set_deny \"Blocked: move destination must be under .github/ci-archive/ (${dst}).\"\n if [ \"$denied\" -eq 0 ]; then\n src_count=$(echo \"$toks\" | wc -l | tr -d ' ')\n srcs=$(echo \"$toks\" | head -n $((src_count-1)))\n for src in $srcs; do\n src=\"${src#./}\"\n if is_ci_archive \"$src\"; then continue; fi\n if echo \"$src\" | grep -qE \"$allow_ci_src_regex\"; then continue; fi\n set_deny \"Blocked: only CI source files may be moved to archive (${src}).\"\n break\n done\n fi\n ;;\n esac\n\n [ \"$denied\" -eq 1 ] && break\ndone <<< \"$(echo \"$CMD\" | tr ';|&' '\\n')\"\n\nif [ \"$denied\" -eq 1 ]; then\n AC='REPOSITORY POLICY: destructive operations are restricted. Only CI-source archival into .github/ci-archive/ is allowed; standalone deletion outside archive is blocked.'\n jq -n --arg r \"$reason\" --arg ac \"$AC\" '{permissionDecision:\"deny\",permissionDecisionReason:$r,additionalContext:$ac,hookSpecificOutput:{hookEventName:\"PreToolUse\",permissionDecision:\"deny\",permissionDecisionReason:$r,additionalContext:$ac}}'\nelse\n echo '{}'\nfi"
}
],
"postToolUse": [
{
"type": "command",
"description": "Quality check + actionlint on workflow writes (CLI + VS Code)",
"matcher": "create|edit",
"timeoutSec": 60,
"bash": "if ! command -v jq >/dev/null 2>&1; then\n echo '{\"additionalContext\":\"WARNING: migration quality check skipped because jq is not installed. Install jq to enable enforcement.\",\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"WARNING: migration quality check skipped because jq is not installed.\"}}'\n exit 0\nfi\nINPUT=$(cat)\nARGS=$(echo \"$INPUT\" | jq -c 'if (.toolArgs|type)==\"string\" then (try (.toolArgs|fromjson) catch {}) elif (.toolArgs|type)==\"object\" then .toolArgs elif (.tool_input|type)==\"object\" then .tool_input else {} end' 2>/dev/null)\nFILE=$(echo \"$ARGS\" | jq -r '.filePath // .path // .file_path // empty' 2>/dev/null)\necho \"$FILE\" | grep -q '.github/workflows/' || { echo '{}'; exit 0; }\n[ -f \"$FILE\" ] || { echo '{}'; exit 0; }\n\nhas_unpinned_external() {\n awk '\n /uses:[[:space:]]*/ {\n ref=$0\n sub(/.*uses:[[:space:]]*/, \"\", ref)\n sub(/[[:space:]]*#.*/, \"\", ref)\n gsub(/[[:space:]]/, \"\", ref)\n if (ref ~ /^\\.?\\//) next\n if (ref ~ /^docker:\\/\\//) next\n if (ref !~ /@/) next\n n=split(ref,a,\"@\"); v=a[n]\n if (v !~ /^[0-9a-fA-F]{40}$/) bad=1\n }\n END { exit bad ? 0 : 1 }\n ' \"$1\"\n}\n\nW=''\nhas_unpinned_external \"$FILE\" && W=\"${W}unpinned-actions; \"\ngrep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$FILE\" 2>/dev/null && W=\"${W}placeholder-text; \"\ngrep -qE 'permissions:\\s*write-all' \"$FILE\" 2>/dev/null && W=\"${W}write-all-permissions; \"\ngrep -qE '^permissions:' \"$FILE\" 2>/dev/null || W=\"${W}missing-permissions-block; \"\n\nL=''\nif command -v actionlint >/dev/null 2>&1; then\n L=$(actionlint \"$FILE\" 2>&1 | head -5 | tr '\\n' ' ')\nfi\n\nif [ -n \"$W\" ] || [ -n \"$L\" ]; then\n MSG=\"MIGRATION QUALITY CHECK ($FILE): ${W}\"\n [ -n \"$L\" ] && MSG=\"${MSG}actionlint: ${L}\"\n jq -n --arg m \"$MSG\" '{additionalContext:$m,hookSpecificOutput:{hookEventName:\"PostToolUse\",additionalContext:$m}}'\nelse\n echo '{}'\nfi"
}
],
"agentStop": [
{
"type": "command",
"description": "Migration quality gate (CLI)",
"timeoutSec": 60,
"bash": "if ! command -v jq >/dev/null 2>&1; then\n echo '{\"decision\":\"block\",\"reason\":\"Migration quality gate unavailable: jq is not installed. Install jq and re-run.\",\"hookSpecificOutput\":{\"hookEventName\":\"AgentStop\",\"decision\":\"block\",\"reason\":\"Migration quality gate unavailable: jq is not installed.\"}}'\n exit 0\nfi\nscan_workflows() {\n CWD=\"$1\"\n LA=0\n command -v actionlint >/dev/null 2>&1 && LA=1\n ISSUES=''\n TOTAL=0; CLEAN=0; BAD=0; DETAILS=''\n for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do\n [ -f \"$f\" ] || continue\n TOTAL=$((TOTAL+1))\n FN=$(basename \"$f\")\n W=''\n awk '\n /uses:[[:space:]]*/ {\n ref=$0\n sub(/.*uses:[[:space:]]*/, \"\", ref)\n sub(/[[:space:]]*#.*/, \"\", ref)\n gsub(/[[:space:]]/, \"\", ref)\n if (ref ~ /^\\.?\\//) next\n if (ref ~ /^docker:\\/\\//) next\n if (ref !~ /@/) next\n n=split(ref,a,\"@\"); v=a[n]\n if (v !~ /^[0-9a-fA-F]{40}$/) bad=1\n }\n END { exit bad ? 0 : 1 }\n ' \"$f\" && W=\"${W}unpinned-actions \"\n grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && W=\"${W}placeholders \"\n grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && W=\"${W}write-all \"\n grep -qE '^permissions:' \"$f\" 2>/dev/null || W=\"${W}no-permissions \"\n [ \"$LA\" -eq 1 ] && ! actionlint \"$f\" >/dev/null 2>&1 && W=\"${W}actionlint-errors \"\n\n if [ -n \"$W\" ]; then\n BAD=$((BAD+1))\n ISSUES=\"${ISSUES}${FN}: ${W}; \"\n W2=$(echo \"$W\" | sed -E 's/[[:space:]]+$//' | sed 's/ /, /g')\n DETAILS=\"${DETAILS}| ${FN} | ${W2} |\\n\"\n else\n CLEAN=$((CLEAN+1))\n DETAILS=\"${DETAILS}| ${FN} | clean |\\n\"\n fi\n done\n}\n\nINPUT=$(cat)\nSID=$(echo \"$INPUT\" | jq -r '.sessionId // .session_id // \"default\"' 2>/dev/null)\nCWD=$(echo \"$INPUT\" | jq -r '.cwd // \"\"' 2>/dev/null)\n[ -z \"$CWD\" ] && CWD=\"${GITHUB_WORKSPACE:-$PWD}\"\nCF=\"/tmp/.migration-quality-gate-${SID}\"\nCOUNT=$(cat \"$CF\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT+1))\necho \"$COUNT\" > \"$CF\"\nif [ \"$COUNT\" -gt 3 ]; then\n rm -f \"$CF\"\n echo '{}'\n exit 0\nfi\n\nscan_workflows \"$CWD\"\n\nif [ -n \"$ISSUES\" ]; then\n R=\"Migration quality gate FAILED (attempt ${COUNT}/3): ${ISSUES}Fix these before completing.\"\n jq -n --arg r \"$R\" '{decision:\"block\",reason:$r,hookSpecificOutput:{hookEventName:\"AgentStop\",decision:\"block\",reason:$r}}'\nelse\n rm -f \"$CF\"\n echo '{}'\nfi"
}
],
"sessionEnd": [
{
"type": "command",
"description": "Append migration scorecard (CLI)",
"timeoutSec": 45,
"bash": "if ! command -v jq >/dev/null 2>&1; then\n echo '{}'\n exit 0\nfi\nscan_workflows() {\n CWD=\"$1\"\n LA=0\n command -v actionlint >/dev/null 2>&1 && LA=1\n ISSUES=''\n TOTAL=0; CLEAN=0; BAD=0; DETAILS=''\n for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do\n [ -f \"$f\" ] || continue\n TOTAL=$((TOTAL+1))\n FN=$(basename \"$f\")\n W=''\n awk '\n /uses:[[:space:]]*/ {\n ref=$0\n sub(/.*uses:[[:space:]]*/, \"\", ref)\n sub(/[[:space:]]*#.*/, \"\", ref)\n gsub(/[[:space:]]/, \"\", ref)\n if (ref ~ /^\\.?\\//) next\n if (ref ~ /^docker:\\/\\//) next\n if (ref !~ /@/) next\n n=split(ref,a,\"@\"); v=a[n]\n if (v !~ /^[0-9a-fA-F]{40}$/) bad=1\n }\n END { exit bad ? 0 : 1 }\n ' \"$f\" && W=\"${W}unpinned-actions \"\n grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && W=\"${W}placeholders \"\n grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && W=\"${W}write-all \"\n grep -qE '^permissions:' \"$f\" 2>/dev/null || W=\"${W}no-permissions \"\n [ \"$LA\" -eq 1 ] && ! actionlint \"$f\" >/dev/null 2>&1 && W=\"${W}actionlint-errors \"\n\n if [ -n \"$W\" ]; then\n BAD=$((BAD+1))\n ISSUES=\"${ISSUES}${FN}: ${W}; \"\n W2=$(echo \"$W\" | sed -E 's/[[:space:]]+$//' | sed 's/ /, /g')\n DETAILS=\"${DETAILS}| ${FN} | ${W2} |\\n\"\n else\n CLEAN=$((CLEAN+1))\n DETAILS=\"${DETAILS}| ${FN} | clean |\\n\"\n fi\n done\n}\n\nINPUT=$(cat)\nCWD=$(echo \"$INPUT\" | jq -r '.cwd // \"\"' 2>/dev/null)\n[ -z \"$CWD\" ] && CWD=\"${GITHUB_WORKSPACE:-$PWD}\"\nREASON=$(echo \"$INPUT\" | jq -r '.reason // \"complete\"' 2>/dev/null)\nSESSION=$(echo \"$INPUT\" | jq -r '.sessionId // .session_id // \"unknown\"' 2>/dev/null)\n\nscan_workflows \"$CWD\"\n\nmkdir -p \"$CWD/.github\"\nSC=\"$CWD/.github/MIGRATION-SCORECARD.md\"\nNOW=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n{\n echo \"## ${NOW}\"\n echo \"- session: ${SESSION}\"\n echo \"- reason: ${REASON}\"\n echo \"- workflows: total=${TOTAL}, clean=${CLEAN}, with_issues=${BAD}\"\n echo\n echo \"| workflow | status |\"\n echo \"|---|---|\"\n printf \"%b\" \"$DETAILS\"\n echo\n} >> \"$SC\"\n\nrm -f \"/tmp/.migration-quality-gate-${SESSION}\" /tmp/.migration-quality-gate\nfind /tmp -maxdepth 1 -name '.migration-quality-gate-*' -type f -mmin +240 -delete 2>/dev/null || true\necho '{}' "
}
],
"Stop": [
{
"type": "command",
"description": "Migration quality gate + scorecard append (VS Code Stop event)",
"timeoutSec": 45,
"bash": "if ! command -v jq >/dev/null 2>&1; then\n echo '{\"decision\":\"block\",\"reason\":\"Migration scorecard unavailable: jq is not installed. Install jq (brew install jq / apt install jq / choco install jq) and re-run.\",\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"decision\":\"block\",\"reason\":\"Migration scorecard unavailable: jq is not installed.\"}}'\n exit 0\nfi\nscan_workflows() {\n CWD=\"$1\"\n LA=0\n command -v actionlint >/dev/null 2>&1 && LA=1\n ISSUES=''\n TOTAL=0; CLEAN=0; BAD=0; DETAILS=''\n for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do\n [ -f \"$f\" ] || continue\n TOTAL=$((TOTAL+1))\n FN=$(basename \"$f\")\n W=''\n awk '\n /uses:[[:space:]]*/ {\n ref=$0\n sub(/.*uses:[[:space:]]*/, \"\", ref)\n sub(/[[:space:]]*#.*/, \"\", ref)\n gsub(/[[:space:]]/, \"\", ref)\n if (ref ~ /^\\.?\\//) next\n if (ref ~ /^docker:\\/\\//) next\n if (ref !~ /@/) next\n n=split(ref,a,\"@\"); v=a[n]\n if (v !~ /^[0-9a-fA-F]{40}$/) bad=1\n }\n END { exit bad ? 0 : 1 }\n ' \"$f\" && W=\"${W}unpinned-actions \"\n grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && W=\"${W}placeholders \"\n grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && W=\"${W}write-all \"\n grep -qE '^permissions:' \"$f\" 2>/dev/null || W=\"${W}no-permissions \"\n [ \"$LA\" -eq 1 ] && ! actionlint \"$f\" >/dev/null 2>&1 && W=\"${W}actionlint-errors \"\n\n if [ -n \"$W\" ]; then\n BAD=$((BAD+1))\n ISSUES=\"${ISSUES}${FN}: ${W}; \"\n W2=$(echo \"$W\" | sed -E 's/[[:space:]]+$//' | sed 's/ /, /g')\n DETAILS=\"${DETAILS}| ${FN} | ${W2} |\\n\"\n else\n CLEAN=$((CLEAN+1))\n DETAILS=\"${DETAILS}| ${FN} | clean |\\n\"\n fi\n done\n}\n\nINPUT=$(cat)\nACTIVE=$(echo \"$INPUT\" | jq -r '.stop_hook_active // false' 2>/dev/null)\n[ \"$ACTIVE\" = \"true\" ] && { echo '{}'; exit 0; }\nCWD=$(echo \"$INPUT\" | jq -r '.cwd // \"\"' 2>/dev/null)\n[ -z \"$CWD\" ] && CWD=\"${GITHUB_WORKSPACE:-$PWD}\"\nREASON=$(echo \"$INPUT\" | jq -r '.reason // \"complete\"' 2>/dev/null)\nSESSION=$(echo \"$INPUT\" | jq -r '.sessionId // .session_id // \"unknown\"' 2>/dev/null)\n\nCF=\"/tmp/.migration-quality-gate-${SESSION}\"\nCOUNT=$(cat \"$CF\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT+1))\necho \"$COUNT\" > \"$CF\"\n\nscan_workflows \"$CWD\"\n\nif [ -n \"$ISSUES\" ] && [ \"$COUNT\" -le 3 ]; then\n R=\"Migration quality gate FAILED (attempt ${COUNT}/3): ${ISSUES}Fix these before completing.\"\n jq -n --arg r \"$R\" '{decision:\"block\",reason:$r,hookSpecificOutput:{hookEventName:\"Stop\",decision:\"block\",reason:$r}}'\n exit 0\nfi\n\nmkdir -p \"$CWD/.github\"\nSC=\"$CWD/.github/MIGRATION-SCORECARD.md\"\nNOW=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n{\n echo \"## ${NOW}\"\n echo \"- session: ${SESSION}\"\n echo \"- reason: ${REASON}\"\n echo \"- workflows: total=${TOTAL}, clean=${CLEAN}, with_issues=${BAD}\"\n echo\n echo \"| workflow | status |\"\n echo \"|---|---|\"\n printf \"%b\" \"$DETAILS\"\n echo\n} >> \"$SC\"\n\nif [ \"$COUNT\" -gt 3 ]; then\n rm -f \"$CF\"\nfi\necho '{}' "
}
]
}
}