Skip to content

feat: deterministic enforcement hooks for migration quality#33

Open
AlexDeMichieli wants to merge 6 commits into
mainfrom
feat/plugin-hooks
Open

feat: deterministic enforcement hooks for migration quality#33
AlexDeMichieli wants to merge 6 commits into
mainfrom
feat/plugin-hooks

Conversation

@AlexDeMichieli

@AlexDeMichieli AlexDeMichieli commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

What this brings

This PR adds deterministic enforcement hooks to the actions-migrator plugin. The hooks enforce migration quality as shell commands that execute outside the model — the agent cannot skip or ignore them — and they run identically across all three Copilot surfaces (CLI, Cloud agent, and VS Code) from a single plugin/hooks.json.

Hooks overview

Hook Event Behavior
Secret detection preToolUse Hard-denies file writes containing hardcoded secrets. Forces ${{ secrets.NAME }}.
Destructive-op guard preToolUse Hard-denies rm, mv, git rm, git mv, unlink, and find -delete outside .github/ci-archive/. Allows CI-source archival. Blocks path traversal.
Quality check + actionlint postToolUse After each workflow write, injects warnings into agent context (unpinned actions, placeholders, over-broad permissions, missing permissions, actionlint errors) on the same turn.
Quality gate agentStop (CLI) Scans all workflow files when the agent finishes a turn. Blocks completion if any workflow has issues, forcing a fix. 3-attempt safety valve prevents infinite loops.
Migration scorecard sessionEnd (CLI) / Stop (VS Code) Appends an entry to .github/MIGRATION-SCORECARD.md — an audit artifact with session ID, timestamp, completion reason, and per-file quality table. Appends rather than overwrites, so progression across passes is visible.

How enforcement works

Agent writes workflow → postToolUse injects quality warnings (same turn)
Agent finishes turn   → agentStop/Stop blocks if issues remain (forces another turn)
Agent blocked 3x      → safety valve releases
Session ends          → sessionEnd/Stop appends scorecard entry

The hooks cannot be skipped — they are shell commands run by the runtime, not advice the model may ignore. This is the difference between "please run actionlint" and "actionlint will run and block you."

Cross-surface support (CLI + Cloud agent + VS Code)

Hooks must work on every surface a migration can run on. The three surfaces send different payload schemas, which we captured directly from live hook invocations:

Copilot CLI / Cloud agent VS Code Agent Plugins
tool name field toolName tool_name
tool args toolArgs (a JSON string) tool_input (an object)
session field sessionId session_id
tool result toolResult tool_response
tool names bash, create, edit run_in_terminal, create_file, replace_string_in_file
deny output top-level permissionDecision hookSpecificOutput.permissionDecision
lifecycle (gate/scorecard) agentStop + sessionEnd Stop
matchers honored parsed but ignored (every hook runs on every tool)

A single hooks.json adapts to both:

  • Normalized input: reads .toolName // .tool_name; parses args whether .toolArgs is a JSON string (fromjson) or .tool_input is an object; normalizes command, filePath/path/file_path, and content/new_string.
  • Dual output: emits both top-level and hookSpecificOutput shapes so each surface reads the field it expects.
  • Lifecycle under both names: sessionEnd (CLI) and Stop (VS Code) run the same scorecard script; the Stop variant honors stop_hook_active to avoid infinite loops.
  • Self-guarding hooks: because VS Code ignores matchers, each preToolUse hook no-ops when its field is absent.

actionlint auto-install

The three hooks that use actionlint install it automatically if absent:

  • Linux (Cloud agent): downloads pinned v1.7.11 binary from GitHub releases (checksum-verified).
  • macOS (CLI): brew install actionlint.
  • Already installed: skips with zero overhead.

This removes the dependency on the agent remembering to install the linter — the hook handles it deterministically.

Migration scorecard

sessionEnd/Stop appends to .github/MIGRATION-SCORECARD.md after each session. Multiple passes show quality progression with per-file detail:

# Migration Scorecard

## 2026-06-18T18:14:20Z
- Session: 357db4ba-e5b8-4edd-9427-c6ae319fe269
- Reason: complete
- Workflows: 1 total, 1 clean, 0 with issues

| File | Issues |
|------|--------|
| ci.yml | clean |

Each workflow is checked for unpinned actions (@v4 instead of SHA), placeholder text (TODO/FIXME/etc.), over-broad permissions (write-all), missing permissions block, and actionlint errors. A workflow must pass all checks to count as "clean."

Token savings

Without hooks, a typical actionlint cycle costs ~3 extra agent turns per workflow file:

Turn What happens Input tokens
1 Agent decides to invoke the actionlint skill ~100k
2 Agent reads skill, installs, reads output ~105k
3 Agent runs actionlint, reads lint output, starts fixing ~105k
4 Agent finishes fixing, moves on ~105k

That's ~3 extra turns (~315k input tokens) just for linting — repeated per workflow file, and skippable since skills are advisory.

With hooks, the same cycle is:

Turn What happens Input tokens
1 Agent writes workflow. Hook auto-installs actionlint, runs it, injects results as additionalContext on the same turn. ~100k
2 Agent fixes errors. Hook re-runs actionlint, confirms clean. ~100k

That's ~100–200k saved per workflow file — no skill invocation, no manual install, no separate lint run. At scale across dozens of migrations this compounds.

Tests

A committed test suite guards against silent breakage — critical because the surfaces use different schemas and a change that breaks one would otherwise go unnoticed.

  • plugin/hooks.test.sh — 22 contract tests exercising every hook against both CLI and VS Code payloads (deny / allow / quality-context / scorecard / loop-guard).
  • .github/workflows/hooks-test.yml — runs the suite on every PR touching the hooks and validates hooks.json parses. A schema or behavior change that breaks any surface now fails the PR.
  • Negative-tested: deliberately removing the VS Code arg parsing makes the suite fail exactly the 4 VS Code destructive-guard cases and exit non-zero — proving the tests catch this class of regression.

Validation performed

Surface How Result
Contract tests bash plugin/hooks.test.sh 22/22 pass (CLI + VS Code schemas)
VS Code (live) Agent-mode chat, captured raw hook stdin PreToolUse, PostToolUse, UserPromptSubmit, Stop all fire with correct create_file / run_in_terminal payloads
CLI (end-to-end) copilot --allow-all --agent actions-migrator:jenkins-migrator rm README.md blocked; Jenkinsfile migrated to clean ci.yml (12 SHA-pinned actions + least-privilege permissions); original archived via git mv; scorecard = 1 clean, 0 with issues

Issues found and fixed during testing:

  • git mv / mv / find -delete bypass of the original rm-only guard — the guard now covers all destructive verbs while still allowing CI-source archival into .github/ci-archive/.
  • Shell redirects (2>&1, > file) caused false-positive denies — the guard now strips redirect operators before checking targets.
  • Parallel sessions shared one /tmp quality-gate counter — now keyed per sessionId.
  • Cloud agent cwd — lifecycle hooks fall back to $GITHUB_WORKSPACE then $PWD so the scorecard lands in the cloned repo, not the plugin install dir.

Adoption

Consumers enable the plugin on all three surfaces with one committed file — see consumer-template/.github/copilot/settings.json:

{ "enabledPlugins": { "actions-migrator@actions-migrations-via-copilot": true } }

This single declaration drives the CLI auto-install, Cloud agent plugin load, and VS Code workspace recommendation. No personalized paths or per-user setup required.

actionlint skill: kept here, removal tracked separately

The actionlint skill is intentionally retained in this PR. Hooks and the skill were originally complementary because earlier surfaces didn't support hooks — the skill was the only linting mechanism on Cloud agent / VS Code.

That condition is now met: this PR demonstrates hooks firing on CLI, Cloud agent, and VS Code, so the skill is no longer required for cross-surface parity. Removing it is deferred to a follow-up PR (delete plugin/skills/actionlint/SKILL.md and strip the "pair with actionlint" references from the 7 platform *-migration skills, migration-core, and the agent files). Keeping the subtractive change separate from this additive one lets each be reviewed and reverted independently, and fully realizes the token savings above once merged.

How to test it yourself

Contract tests (no Copilot session needed)

git clone https://github.com/github/actions-migrations-via-copilot.git
cd actions-migrations-via-copilot
bash plugin/hooks.test.sh        # 22/22 expected; requires bash + jq only

CLI (end-to-end)

# install the plugin from a local clone
copilot plugin install ./plugin

# in any repo with a CI source file (e.g. a Jenkinsfile):
copilot --allow-all --agent actions-migrator:jenkins-migrator -p \
  "Try to delete README.md via bash rm. Then migrate the Jenkinsfile to \
   .github/workflows/ci.yml with SHA-pinned actions and least-privilege \
   permissions, and archive the original to .github/ci-archive/ via git mv. \
   Report which steps the hook blocked. Do not create a PR."

# verify enforcement fired:
test -f README.md && echo "README intact (delete was blocked)"
grep -c '@[0-9a-f]\{40\}' .github/workflows/ci.yml   # SHA pins
cat .github/MIGRATION-SCORECARD.md                     # scorecard

VS Code Agent Plugins (preview)

// 1. VS Code user settings.json — enable plugins + point at the local clone
//    (For real adoption, users instead commit consumer-template/.github/copilot/settings.json
//     to their repo; the local path here is only for testing an un-published build.)
"chat.plugins.enabled": true,
"chat.pluginLocations": {
  "/absolute/path/to/actions-migrations-via-copilot/plugin": true
}
2. Developer: Reload Window
3. Open a repo containing a Jenkinsfile, open Copilot Chat in Agent mode, send:

   Use the jenkins-migrator agent. Try to delete README.md via the terminal,
   then migrate the Jenkinsfile to .github/workflows/ci.yml with SHA-pinned
   actions and least-privilege permissions, and archive the original to
   .github/ci-archive/ via git mv. Do not create a PR.

4. Expected: the README delete is blocked by the preToolUse hook; the workflow
   is created SHA-pinned with a permissions block; .github/MIGRATION-SCORECARD.md
   is written by the Stop hook.

To inspect raw hook execution in VS Code, open Output → "GitHub Copilot Chat Hooks" — it shows each hook firing with the stdin payload.

Cloud agent (github.com)

# in the target repo, commit the consumer template so the plugin auto-loads:
mkdir -p .github/copilot
cp /path/to/consumer-template/.github/copilot/settings.json .github/copilot/settings.json
git add .github/copilot/settings.json && git commit -m "Enable actions-migrator plugin" && git push
# then open an issue ("Migrate the Jenkinsfile to GitHub Actions") and assign Copilot;
# inspect the resulting PR for the SHA-pinned workflow and MIGRATION-SCORECARD.md.

Files changed

  • plugin/hooks.json — cross-surface enforcement hooks (secret detection, destructive guard, quality check, quality gate, scorecard) with auto-install and dual-schema input/output.
  • plugin/hooks.test.sh — 22 contract tests across CLI and VS Code schemas.
  • .github/workflows/hooks-test.yml — CI that runs the suite on every hooks change.
  • consumer-template/ — drop-in settings.json + README for enabling the plugin across surfaces.
  • plugin/README.md — hooks documentation.

@AlexDeMichieli AlexDeMichieli force-pushed the feat/plugin-hooks branch 4 times, most recently from c99e748 to e18c746 Compare June 3, 2026 15:58
@AlexDeMichieli AlexDeMichieli marked this pull request as ready for review June 4, 2026 20:50
@AlexDeMichieli AlexDeMichieli requested a review from antgrutta as a code owner June 4, 2026 20:50
Copilot AI review requested due to automatic review settings June 4, 2026 20:50
@AlexDeMichieli AlexDeMichieli requested a review from ssulei7 as a code owner June 4, 2026 20:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a plugin/hooks.json hook pack for the Copilot CLI plugin to deterministically enforce migration-quality constraints (block/deny unsafe tool calls, inject workflow quality feedback, and produce an audit scorecard), and documents the new enforcement model in plugin/README.md.

Changes:

  • Adds deterministic enforcement hooks (preToolUse, postToolUse, agentStop, sessionEnd) in plugin/hooks.json.
  • Implements workflow “quality” detection (unpinned actions, placeholders, write-all, missing permissions, actionlint) and a blocking quality gate with a 3-attempt safety valve.
  • Documents hook behavior, rationale, and how to enable/disable hooks in plugin/README.md.
Show a summary per file
File Description
plugin/hooks.json Adds 5 hooks to block hardcoded secrets and unsafe deletions, lint/check workflows, enforce a blocking quality gate, and append a migration scorecard.
plugin/README.md Documents the hooks, their lifecycle events, and how to verify/disable them.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 7

Comment thread plugin/hooks.json Outdated
"description": "Block hardcoded secrets in file writes",
"matcher": "create|edit",
"timeoutSec": 10,
"bash": "INPUT=$(cat); ARGS=$(echo \"$INPUT\" | jq -c '.toolArgs' 2>/dev/null); HAS_SECRET=$(echo \"$ARGS\" | grep -ciE '(password|secret|token|api[_-]?key)\\s*[:=]' 2>/dev/null || true); HAS_EXPR=$(echo \"$ARGS\" | grep -cF '${' 2>/dev/null || true); if [ \"${HAS_SECRET:-0}\" -gt 0 ] && [ \"${HAS_EXPR:-0}\" -eq 0 ]; then echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked: hardcoded secret detected. Use GitHub Secrets (${{ secrets.NAME }}) instead.\"}'; else echo '{}'; fi"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Secret detection now: (1) extracts only the content/new_string field from toolArgs, (2) checks each line individually — a line must match a secret keyword + [:=] + 8+ chars of value AND not contain \${ on that same line, (3) secrets: inherit no longer triggers because inherit is <8 chars and there's no [:=] value pattern.

Comment thread plugin/hooks.json Outdated
"description": "Guard against file deletion outside ci-archive",
"matcher": "bash",
"timeoutSec": 10,
"bash": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.toolArgs.command // .toolArgs // empty' 2>/dev/null); if echo \"$CMD\" | grep -qE 'rm\\s+(-[rfi]+\\s+)*' 2>/dev/null; then if ! echo \"$CMD\" | grep -q '.github/ci-archive' 2>/dev/null; then echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked: file deletion only allowed inside .github/ci-archive/. Move source CI files there before removing.\"}'; exit 0; fi; fi; echo '{}'"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The rm guard now: (1) checks for ... path traversal BEFORE checking ci-archive paths — any target containing .. is denied immediately, (2) uses a case statement with glob patterns (*/.github/ci-archive/* and *.github/ci-archive/*) for literal matching instead of regex, so xgithub/ci-archive no longer slips through.

Comment thread plugin/hooks.json Outdated
"description": "Quality check and actionlint on workflow files after write",
"matcher": "create|edit",
"timeoutSec": 60,
"bash": "INPUT=$(cat); ARGS=$(echo \"$INPUT\" | jq -c '.toolArgs' 2>/dev/null); FILE=$(echo \"$ARGS\" | jq -r '.file_path // .path // empty' 2>/dev/null); echo \"$FILE\" | grep -q '.github/workflows/' || exit 0; [ -f \"$FILE\" ] || exit 0; if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; W=''; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$FILE\" 2>/dev/null && W=\"${W}- Unpinned actions: use full SHA commit refs\\n\"; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$FILE\" 2>/dev/null && W=\"${W}- Placeholder text found: replace before merging\\n\"; grep -qE 'permissions:\\s*write-all' \"$FILE\" 2>/dev/null && W=\"${W}- Over-broad permissions: replace write-all with least-privilege\\n\"; grep -qE '^permissions:' \"$FILE\" 2>/dev/null || W=\"${W}- Missing top-level permissions block\\n\"; L=''; command -v actionlint >/dev/null 2>&1 && L=$(actionlint \"$FILE\" 2>&1 | head -5); if [ -n \"$W\" ] || [ -n \"$L\" ]; then MSG=\"MIGRATION QUALITY CHECK ($FILE):\\n${W}\"; [ -n \"$L\" ] && MSG=\"${MSG}actionlint errors:\\n${L}\\n\"; MSG=\"${MSG}Fix these issues now.\"; ESCAPED=$(printf '%s' \"$MSG\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); printf '{\"additionalContext\":\"%s\"}' \"$ESCAPED\"; fi"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All 3 hooks now download to a temp file and verify SHA256 (900919a8...) before extracting. Checksum mismatch aborts install and surfaces an explicit warning. Also fixed the PR description — removed the 'checksum-verified upstream' claim and replaced with accurate description.

Comment thread plugin/hooks.json Outdated
"type": "command",
"description": "Migration quality gate — block completion if workflows have issues",
"timeoutSec": 60,
"bash": "INPUT=$(cat); CWD=$(echo \"$INPUT\" | jq -r '.cwd // \".\"' 2>/dev/null); COUNTER_FILE='/tmp/.migration-quality-gate'; COUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0); COUNT=$((COUNT + 1)); echo \"$COUNT\" > \"$COUNTER_FILE\"; if [ \"$COUNT\" -gt 3 ]; then rm -f \"$COUNTER_FILE\"; echo '{}'; exit 0; fi; if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; ISSUES=''; for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do [ -f \"$f\" ] || continue; FN=$(basename \"$f\"); W=''; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$f\" 2>/dev/null && W=\"${W}unpinned-actions \"; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && W=\"${W}placeholders \"; grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && W=\"${W}write-all \"; command -v actionlint >/dev/null 2>&1 && ! actionlint \"$f\" >/dev/null 2>&1 && W=\"${W}actionlint-errors \"; [ -n \"$W\" ] && ISSUES=\"${ISSUES}${FN}: ${W}; \"; done; if [ -n \"$ISSUES\" ]; then ESCAPED=$(printf '%s' \"$ISSUES\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); printf '{\"decision\":\"block\",\"reason\":\"Migration quality gate FAILED (attempt %d/3). Fix these workflow issues:\\n%s\"}' \"$COUNT\" \"$ESCAPED\"; else rm -f \"$COUNTER_FILE\"; echo '{}'; fi"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. agentStop now includes grep -qE '^permissions:' || W='no-permissions ' in its per-file scan, consistent with postToolUse.

Comment thread plugin/hooks.json Outdated
"type": "command",
"description": "Generate final migration scorecard",
"timeoutSec": 45,
"bash": "INPUT=$(cat); CWD=$(echo \"$INPUT\" | jq -r '.cwd // \".\"' 2>/dev/null); REASON=$(echo \"$INPUT\" | jq -r '.reason // \"unknown\"' 2>/dev/null); SESSION=$(echo \"$INPUT\" | jq -r '.sessionId // \"unknown\"' 2>/dev/null); if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; TOTAL=0; CLEAN=0; BAD=0; for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do [ -f \"$f\" ] || continue; TOTAL=$((TOTAL + 1)); HAS=0; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$f\" 2>/dev/null && HAS=1; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && HAS=1; grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && HAS=1; command -v actionlint >/dev/null 2>&1 && ! actionlint \"$f\" >/dev/null 2>&1 && HAS=1; [ \"$HAS\" -eq 0 ] && CLEAN=$((CLEAN + 1)) || BAD=$((BAD + 1)); done; rm -f /tmp/.migration-quality-gate; SC=\"$CWD/.github/MIGRATION-SCORECARD.md\"; [ -f \"$SC\" ] || printf '# Migration Scorecard\\n' > \"$SC\" 2>/dev/null; printf '\\n## %s\\n- Session: %s\\n- Reason: %s\\n- Workflows: %d total, %d clean, %d with issues\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$SESSION\" \"$REASON\" \"$TOTAL\" \"$CLEAN\" \"$BAD\" >> \"$SC\" 2>/dev/null; echo '{}'"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. sessionEnd scorecard now includes grep -qE '^permissions:' || HAS=1 so missing-permissions counts as an issue. Consistent with all other hooks.

Comment thread plugin/README.md Outdated
| File deletion guard | `preToolUse` | `bash` | Hard-denies `rm` operations outside `.github/ci-archive/`. Prevents accidental deletion of application source code. |
| Quality check + actionlint | `postToolUse` | `create\|edit` | After any workflow file write, injects `additionalContext` with: unpinned actions (tag vs SHA), placeholder text (TODO/FIXME), over-broad permissions (`write-all`), missing permissions block, and actionlint errors. The agent sees these on the same turn. |
| **Quality gate** | `agentStop` | — | Scans ALL workflow files when the agent finishes a turn. If any have issues, returns `decision: "block"` forcing the agent to take another turn to fix them. Safety valve releases after 3 attempts to prevent infinite loops. |
| **Migration scorecard** | `sessionEnd` | — | Generates `.github/MIGRATION-SCORECARD.md` with session ID, timestamp, completion reason, and workflow counts (total / clean / with-issues). Audit artifact for migration quality tracking. |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Description changed to 'Appends an entry to' and hook description changed to 'Append migration scorecard entry'.

Comment thread plugin/hooks.json Outdated
"description": "Quality check and actionlint on workflow files after write",
"matcher": "create|edit",
"timeoutSec": 60,
"bash": "INPUT=$(cat); ARGS=$(echo \"$INPUT\" | jq -c '.toolArgs' 2>/dev/null); FILE=$(echo \"$ARGS\" | jq -r '.file_path // .path // empty' 2>/dev/null); echo \"$FILE\" | grep -q '.github/workflows/' || exit 0; [ -f \"$FILE\" ] || exit 0; if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; W=''; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$FILE\" 2>/dev/null && W=\"${W}- Unpinned actions: use full SHA commit refs\\n\"; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$FILE\" 2>/dev/null && W=\"${W}- Placeholder text found: replace before merging\\n\"; grep -qE 'permissions:\\s*write-all' \"$FILE\" 2>/dev/null && W=\"${W}- Over-broad permissions: replace write-all with least-privilege\\n\"; grep -qE '^permissions:' \"$FILE\" 2>/dev/null || W=\"${W}- Missing top-level permissions block\\n\"; L=''; command -v actionlint >/dev/null 2>&1 && L=$(actionlint \"$FILE\" 2>&1 | head -5); if [ -n \"$W\" ] || [ -n \"$L\" ]; then MSG=\"MIGRATION QUALITY CHECK ($FILE):\\n${W}\"; [ -n \"$L\" ] && MSG=\"${MSG}actionlint errors:\\n${L}\\n\"; MSG=\"${MSG}Fix these issues now.\"; ESCAPED=$(printf '%s' \"$MSG\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); printf '{\"additionalContext\":\"%s\"}' \"$ESCAPED\"; fi"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All 3 hooks now: (1) on checksum mismatch, abort install and inject explicit warning via additionalContext, (2) after install attempt, re-check command -v actionlint — if still missing, inject 'actionlint not available (install failed)' warning, (3) agentStop adds 'actionlint-unavailable' to the issues list so it blocks completion.

@antgrutta

Copy link
Copy Markdown
Collaborator

@AlexDeMichieli, please set up a meeting with @ssulei7 and myself for a quick demo. This is good stuff and we're excited to rubber duck a couple things with you.

GitHub Advanced Security started work on behalf of AlexDeMichieli June 11, 2026 19:55 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 11, 2026 19:56
GitHub Advanced Security started work on behalf of AlexDeMichieli June 11, 2026 19:59 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 11, 2026 20:01
… actionlint

Rebuild hooks.json with correct Copilot hooks API:
- preToolUse (matcher: create|edit): secret detection with permissionDecision deny
- preToolUse (matcher: bash): rm guard blocks deletion outside ci-archive
- postToolUse (matcher: create|edit): quality check + actionlint per workflow file
- agentStop: quality gate blocks agent completion until workflows pass (3-attempt safety valve)
- sessionEnd: generates MIGRATION-SCORECARD.md with session stats

Key changes from previous version:
- Use permissionDecision/permissionDecisionReason (not decision/reason)
- Add matcher filtering (no more shell-level tool name checks)
- agentStop replaces postToolUse-only approach — actually blocks completion
- sessionEnd provides audit artifact for migration quality tracking
- actionlint runs in 3 hooks: postToolUse, agentStop, sessionEnd
- Test harness with 21 passing tests included
GitHub Advanced Security started work on behalf of AlexDeMichieli June 11, 2026 20:09 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 11, 2026 20:11
Convert declarative Jenkinsfile with Build, Test, and Deploy stages
to a GitHub Actions CI workflow with pinned action SHAs and
least-privilege permissions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AlexDeMichieli

Copy link
Copy Markdown
Collaborator Author

🚀 Jenkins to GitHub Actions Migration Report

📊 Migration Overview

Metric Before (Jenkins) After (GitHub Actions)
Pipeline Files 1 file 1 workflow
Pipeline Stages 3 stages 3 jobs
Pipeline Steps 3 steps 3 steps
Shared Libraries 0 libraries N/A
Credentials 0 credentials 0 secrets/variables

🔄 Conversion Diagram

graph LR
    A[Jenkins Pipeline] --> B[GitHub Actions Workflow]

    subgraph "Jenkins Structure"
        D1[Stage: Build]
        D2[Stage: Test]
        D3[Stage: Deploy]
    end

    subgraph "GitHub Actions Structure"
        G1[Job: build]
        G2[Job: test]
        G3[Job: deploy]
    end

    D1 --> G1
    D2 --> G2
    D3 --> G3
Loading

🔧 Key Transformations

Stage and Step Conversions

  • agent anyruns-on: ubuntu-latest
  • Jenkins sequential stages → GitHub Actions jobs with needs: dependencies
  • sh 'npm ci'run: npm ci
  • Added actions/checkout (not implicit in GitHub Actions unlike Jenkins SCM checkout)
  • Added actions/setup-node with npm caching for faster builds

Trigger Mapping

  • Jenkins pipeline (typically triggered by SCM polling or webhooks) → on: push and on: pull_request on main branch

✅ Validation Results

Linting Results

$ actionlint .github/workflows/ci.yml
(no output — zero errors)

Manual Verification Checklist

  • YAML syntax validated
  • All actions properly versioned and pinned to SHAs
  • Job dependencies verified (build → test → deploy)
  • Environment variables migrated (none required)
  • Triggers match original behavior
  • Least-privilege permissions applied

🔐 Security Improvements

  • Implemented least-privilege permissions: contents: read
  • All actions pinned to commit SHAs to prevent supply-chain attacks
  • Only verified marketplace actions used (actions/checkout, actions/setup-node)

🔗 Variable and Secret Requirements

Required GitHub Secrets

None required for this pipeline.

Required GitHub Variables

None required for this pipeline.

🎯 Next Steps

  1. Test the workflow by pushing to a feature branch
  2. Adjust Node.js version if your project requires a different version than 20
  3. Enhance the deploy job with actual deployment steps and environment protection rules
  4. Add branch protection rules to require CI to pass before merging

📁 Original Jenkins Files

The original Jenkins pipeline file has been archived:

📚 Migration Notes

  • The Jenkins pipeline used agent any which maps to ubuntu-latest as the default GitHub-hosted runner.
  • Each stage was converted to a separate job with sequential dependencies to preserve the original execution order.
  • actions/setup-node with npm caching was added to optimize install times since Jenkins environments typically have Node.js pre-installed globally.

Migration completed by GitHub Copilot Jenkins Migration Agent

GitHub Advanced Security started work on behalf of AlexDeMichieli June 11, 2026 20:14 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 11, 2026 20:15
…nter

Plugin manifest:
- plugin/plugin.json: declare hooks field, bump to 1.4.0
- .github/plugin/marketplace.json: sync to 1.4.0

Hook fixes:
- preToolUse rm guard: extend to rm, mv, unlink, find -delete, git rm, git mv;
  strip shell redirect operators (2>&1, >/tmp/x, etc.) before tokenizing targets
  so legitimate commands with redirects are not denied (regression).
  Allowlist: targets inside .github/ci-archive/ OR CI source files at repo root
  (Jenkinsfile, .travis.yml, .gitlab-ci.yml, .drone.yml, bitbucket-pipelines.yml,
  azure-pipelines.yml, bamboo-specs/*, .circleci/*).
- agentStop quality-gate: per-session counter (${sessionId}) instead of a
  global /tmp file (parallel sessions no longer collide).
- agentStop & sessionEnd: fall back to $GITHUB_WORKSPACE then $PWD when input
  cwd is missing/'/root' (CCA sandbox).
- sessionEnd: prune stale per-session counters > 60 min as garbage collection.

Consumer adoption:
- consumer-template/.github/copilot/settings.json with enabledPlugins entry
- consumer-template/README.md explaining surfaces (CLI, CCA, VS Code Agent
  Plugins preview).

Closes Sully + Anthony feedback (2026-06-11): toolArgs parsing, scorecard
per-file detail, custom-agent hook firing, plus newly found:
- destructive-op bypass via git mv / mv / find -delete
- false-positive denies on commands with shell redirects
- shared /tmp counter across parallel sessions
- agent narrating fake delete after hook denial

Tested:
- 13 unit tests against the rm guard (all pass)
- End-to-end CLI run with --agent actions-migrator:jenkins-migrator
  on alexdemichieli-migrations/jenkins-migration-test:
  * README protected (hook denied bash rm)
  * Jenkinsfile migrated to clean .github/workflows/ci.yml (10 SHA pins, perms)
  * Original archived via git mv into .github/ci-archive/
  * Scorecard shows: 1 total, 1 clean, 0 with issues
GitHub Advanced Security started work on behalf of AlexDeMichieli June 16, 2026 20:45 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 16, 2026 20:48
…ract tests

Problem
  Hooks only understood the Copilot CLI / Cloud agent payload schema. In VS
  Code Agent Plugins (preview) the same hooks ran but read empty fields, so
  enforcement silently no-opped (audit logged tool:"null"; the README 'block'
  users saw was VS Code's own terminal safety, not our hook).

Root cause (captured from live payloads on both surfaces)
  CLI:     { toolName, toolArgs:<json string>, sessionId, toolResult }
  VS Code: { tool_name, tool_input:<object>, session_id, tool_response }
  - field names differ (camelCase vs snake_case)
  - args differ: CLI sends a JSON *string*; VS Code sends an *object*
  - tool names differ: bash/create/edit vs run_in_terminal/create_file/...
  - deny output differs: top-level permissionDecision vs hookSpecificOutput
  - lifecycle events differ: CLI agentStop+sessionEnd vs VS Code Stop
  - VS Code ignores matchers (every preToolUse hook runs on every tool)

Fix (plugin/hooks.json) — one file, self-adapting on all surfaces
  - Normalize input: read .toolName // .tool_name; parse args whether
    .toolArgs is a string (fromjson) or .tool_input is an object.
  - Normalize fields: command, filePath//path//file_path, content//new_string.
  - Emit BOTH output shapes (top-level + hookSpecificOutput) so each surface
    finds the field it expects.
  - Register lifecycle under both names: sessionEnd (CLI) and Stop (VS Code)
    run the same scorecard script; Stop honors stop_hook_active to avoid loops.
  - Each preToolUse hook self-guards (no-op when its field is absent) since
    VS Code ignores matchers.

Tests (NEW — guards against silent breakage)
  - plugin/hooks.test.sh: 22 contract tests exercising every hook against BOTH
    CLI and VS Code payloads (deny/allow/context/scorecard/loop-guard).
  - .github/workflows/hooks-test.yml: runs the suite on every PR touching the
    hooks; validates hooks.json parses; fails the PR on regression.
  - Negative-tested: sabotaging the VS Code arg parsing makes the suite fail
    exactly the 4 VS Code destructive-guard cases and exit non-zero.

Validation performed
  - 22/22 contract tests pass (CLI + VS Code schemas).
  - VS Code live capture: PreToolUse/PostToolUse/UserPromptSubmit/Stop all fire
    with correct payloads (create_file + run_in_terminal).
  - CLI end-to-end (--agent actions-migrator:jenkins-migrator): README delete
    blocked; Jenkinsfile migrated to clean ci.yml (12 SHA pins + permissions);
    archived via git mv; scorecard = 1 clean, 0 issues.
GitHub Advanced Security started work on behalf of AlexDeMichieli June 18, 2026 18:15 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 18, 2026 18:18
Commit 5aa2045 accidentally committed a test Jenkins migration into the plugin
repository. These are not part of the plugin and should not ship:
- .github/ci-archive/Jenkinsfile
- .github/ci-archive/MIGRATION-README.md
- .github/workflows/ci.yml (migrated-from-Jenkins test output)

The migration was a test of the hooks, not a deliverable. Removing keeps the PR
scoped to the hooks, tests, and consumer template.
GitHub Advanced Security started work on behalf of AlexDeMichieli June 18, 2026 18:21 View session
Adds a 'Testing the hooks' subsection covering:
- how to run the suite (bash plugin/hooks.test.sh)
- what the 22 contract tests check across CLI + VS Code schemas
- requirements (bash + jq only; no actionlint/network/curl/brew needed)
- self-contained guarantees (no working-tree writes, cleans temp files)
- CI wiring (.github/workflows/hooks-test.yml runs on hook changes)

Also updates the hooks table to reflect cross-surface support (CLI sessionEnd
+ VS Code Stop) so contributors know the same hooks.json serves all surfaces.
GitHub Advanced Security started work on behalf of AlexDeMichieli June 18, 2026 18:23 View session
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 18, 2026 18:24
GitHub Advanced Security finished work on behalf of AlexDeMichieli June 18, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants