Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/workflows/agent-shield-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Reusable AgentShield workflow — single source of truth for the org.
# Repo-level agent-shield.yml files call this to avoid duplicating
# the security scan and structural checks.
# Standard: https://github.com/petry-projects/.github/blob/main/standards/agent-standards.md
#
# Two-layer approach:
# 1. ecc-agentshield CLI — deep security scan (102 rules across secrets,
# permissions, hooks, MCP servers, and agent config)
# 2. Org-specific structural checks — required files, cross-references,
# SKILL.md frontmatter validation
#
# Inputs let callers tune severity, the org reference string, and the set
# of required files without forking the workflow.
name: AgentShield (Reusable)

on:
workflow_call:
inputs:
min-severity:
description: "Minimum AgentShield severity to fail on (low/medium/high/critical)"
required: false
type: string
default: "high"
agentshield-version:
description: "Pinned ecc-agentshield npm version"
required: false
type: string
default: "1.4.0"
required-files:
description: "Newline-separated list of required agent config files"
required: false
type: string
default: |
CLAUDE.md
AGENTS.md
org-standards-ref:
description: "String AGENTS.md must reference (regex, basic grep -E)"
required: false
type: string
default: 'petry-projects/\.github'

permissions:
contents: read

jobs:
agent-shield:
name: AgentShield
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

# --- Deep security scan via AgentShield CLI ---
# Uses ecc-agentshield (https://github.com/affaan-m/agentshield)
# 102 rules: secrets, permissions, hooks, MCP servers, agent config
- name: AgentShield Security Scan
env:
AS_VERSION: ${{ inputs.agentshield-version }}
AS_SEVERITY: ${{ inputs.min-severity }}
run: |
npx "ecc-agentshield@${AS_VERSION}" scan \
--path . \
--min-severity "${AS_SEVERITY}" \
--format terminal

# --- Org-specific structural checks ---
- name: Check required agent files exist
env:
REQUIRED_FILES: ${{ inputs.required-files }}
run: |
status=0
while IFS= read -r f; do
[ -z "$f" ] && continue
if [ ! -f "$f" ]; then
echo "::error::Missing required agent file: $f"
status=1
fi
done <<< "$REQUIRED_FILES"
exit $status

- name: Validate cross-references
env:
ORG_REF: ${{ inputs.org-standards-ref }}
run: |
status=0

if [ -f "CLAUDE.md" ] && \
! grep -qi 'AGENTS.md' CLAUDE.md; then
echo "::error file=CLAUDE.md::Must reference AGENTS.md"
status=1
fi

if [ -f "AGENTS.md" ] && \
! grep -qiE "$ORG_REF" AGENTS.md; then
echo "::error file=AGENTS.md::Must reference org standards ($ORG_REF)"
status=1
fi

exit $status

- name: Validate SKILL.md frontmatter
run: |
status=0

while IFS= read -r file; do
frontmatter=$(awk \
'/^---$/{n++; next} n==1{print} n>=2{exit}' \
"$file")

if [ -z "$frontmatter" ]; then
echo "::error file=$file::Missing YAML frontmatter"
status=1
continue
fi

if ! echo "$frontmatter" | grep -q '^name:'; then
echo "::error file=$file::Missing 'name' field"
status=1
fi
if ! echo "$frontmatter" | grep -q '^description:'; then
echo "::error file=$file::Missing 'description' field"
status=1
fi
done < <(find . -name 'SKILL.md' \
-not -path '*/node_modules/*' \
-not -path '*/.git/*')

if [ "$status" -eq 0 ]; then
echo "All SKILL.md frontmatter validated."
fi
exit $status
Comment on lines +100 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Frontmatter validation assumes fields at column 0.

The grep patterns ^name: and ^description: (lines 115, 119) only match fields at the start of a line. Valid YAML frontmatter could have these fields indented (e.g., nested under a parent key):

---
metadata:
  name: "My Skill"
  description: "Does things"
---

This would fail validation despite being valid YAML with the required fields present.

If indented fields should be supported, consider relaxing the patterns:

♻️ Optional: Support indented frontmatter fields
-            if ! echo "$frontmatter" | grep -q '^name:'; then
+            if ! echo "$frontmatter" | grep -qE '^\s*name:'; then
               echo "::error file=$file::Missing 'name' field"
               status=1
             fi
-            if ! echo "$frontmatter" | grep -q '^description:'; then
+            if ! echo "$frontmatter" | grep -qE '^\s*description:'; then
               echo "::error file=$file::Missing 'description' field"
               status=1
             fi

If the org standard explicitly requires top-level name: and description: fields, the current behavior is correct and this is a non-issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/agent-shield-reusable.yml around lines 100 - 130, The
frontmatter check currently uses grep patterns '^name:' and '^description:'
which only match fields at column 0; update the validation to accept optional
leading whitespace by changing those checks to search for '^[[:space:]]*name:'
and '^[[:space:]]*description:' (i.e., modify the two grep invocations that read
the frontmatter variable inside the while loop), so indented YAML keys like
"metadata: \n  name: ..." are treated as present while keeping the same error
handling and status logic.

94 changes: 94 additions & 0 deletions .github/workflows/dependabot-automerge-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Reusable Dependabot auto-merge workflow — single source of truth for the org.
# Repo-level dependabot-automerge.yml files call this to avoid duplicating
# eligibility logic and the GitHub App token dance.
# Standard: https://github.com/petry-projects/.github/blob/main/standards/dependabot-policy.md
#
# Auto-approves and enables auto-merge for Dependabot PRs that are:
# - GitHub Actions updates (patch or minor version bumps)
# - Security updates for any ecosystem (patch or minor)
# - Indirect (transitive) dependency updates
# Major version updates are always left for human review.
# Uses --auto so the merge waits for all required CI checks to pass.
#
# Safety model: application ecosystems use open-pull-requests-limit: 0 in
# dependabot.yml, so the only app-ecosystem PRs Dependabot can create are
# security updates. This workflow adds defense-in-depth by also checking
# the package ecosystem.
#
# Required org/repo secrets (passed via `secrets: inherit` from caller):
# APP_ID — GitHub App ID with contents:write and pull-requests:write
# APP_PRIVATE_KEY — GitHub App private key
name: Dependabot auto-merge (Reusable)

on:
workflow_call:
secrets:
APP_ID:
description: "GitHub App ID with contents:write and pull-requests:write"
required: true
APP_PRIVATE_KEY:
description: "GitHub App private key"
required: true

jobs:
dependabot:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
skip-commit-verification: true

- name: Determine if auto-merge eligible
id: eligible
run: |
UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}"
DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}"
ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}"

# Must be patch, minor, or indirect
if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \
"$UPDATE_TYPE" != "version-update:semver-minor" && \
"$DEP_TYPE" != "indirect" ]]; then
echo "eligible=false" >> "$GITHUB_OUTPUT"
echo "Skipping: major update requires human review"
exit 0
fi

# GitHub Actions version updates are always eligible
# App ecosystem PRs can only exist as security updates (limit: 0)
echo "eligible=true" >> "$GITHUB_OUTPUT"
echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE"

- name: Check app secrets
if: steps.eligible.outputs.eligible == 'true'
env:
APP_ID: ${{ secrets.APP_ID }}
run: |
if [[ -z "$APP_ID" ]]; then
echo "::error::APP_ID secret is missing. Set APP_ID and APP_PRIVATE_KEY in org secrets. See standards/dependabot-policy.md"
exit 1
fi

- name: Generate app token
if: steps.eligible.outputs.eligible == 'true'
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Approve and enable auto-merge
if: steps.eligible.outputs.eligible == 'true'
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
144 changes: 144 additions & 0 deletions .github/workflows/dependabot-rebase-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Reusable Dependabot update-and-merge workflow — single source of truth for the org.
# Repo-level dependabot-rebase.yml files call this to avoid duplicating
# the rebase/merge serialization logic.
# Standard: https://github.com/petry-projects/.github/blob/main/standards/dependabot-policy.md
#
# Problem: when branch protection requires branches to be up-to-date
# (strict status checks), merging one Dependabot PR makes the others fall
# behind. Dependabot does not auto-rebase PRs that are merely behind — it
# only rebases on its next scheduled run or when there are merge conflicts.
# Additionally, GitHub auto-merge (--auto) may not trigger when rulesets
# cause mergeable_state to report "blocked" even though all requirements
# are actually met.
#
# Solution: after every push to main (typically a merged PR), this workflow:
# 1. Updates behind Dependabot PRs using the merge method (not rebase)
# 2. Merges any Dependabot PR that is up-to-date, approved, and passing CI
#
# Using the app token for merges ensures the resulting push to main triggers
# this workflow again, creating a self-sustaining chain that serializes
# Dependabot PR merges one at a time.
#
# Important: never use the API update-branch endpoint with rebase method on
# Dependabot PRs — it replaces Dependabot's commit signature with GitHub's,
# which breaks dependabot/fetch-metadata verification and causes Dependabot
# to refuse future rebases on that PR. The merge method preserves the
# original commits.
#
# Note: the merge commit is authored by GitHub, not Dependabot, so the
# dependabot-automerge workflow must use skip-commit-verification: true
# in the dependabot/fetch-metadata step.
#
# Required org/repo secrets (passed via `secrets: inherit` from caller):
# APP_ID — GitHub App ID with contents:write and pull-requests:write
# APP_PRIVATE_KEY — GitHub App private key
name: Dependabot update and merge (Reusable)

on:
workflow_call:
secrets:
APP_ID:
description: "GitHub App ID with contents:write and pull-requests:write"
required: true
APP_PRIVATE_KEY:
description: "GitHub App private key"
required: true

jobs:
update-and-merge:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- name: Check app secrets
env:
APP_ID: ${{ secrets.APP_ID }}
run: |
if [[ -z "$APP_ID" ]]; then
echo "::error::APP_ID secret is missing. Set APP_ID and APP_PRIVATE_KEY in org secrets. See standards/dependabot-policy.md"
exit 1
fi

- name: Generate app token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Update and merge Dependabot PRs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
run: |
# Find open Dependabot PRs
PRS=$(gh pr list --repo "$REPO" --author "app/dependabot" \
--json number,headRefName \
--jq '.[] | "\(.number) \(.headRefName)"')

if [[ -z "$PRS" ]]; then
echo "No open Dependabot PRs"
exit 0
fi

MERGED=false

while IFS=' ' read -r PR_NUMBER HEAD_REF; do
BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \
--jq '.behind_by')

if [[ "$BEHIND" -gt 0 ]]; then
echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — merging main into branch"
gh api "repos/$REPO/pulls/$PR_NUMBER/update-branch" \
-X PUT -f update_method=merge \
--silent || echo "Warning: failed to update PR #$PR_NUMBER"
continue
fi

echo "PR #$PR_NUMBER ($HEAD_REF) is up to date — checking if merge-ready"

# Skip if we already merged one (strict mode means others are now behind)
if [[ "$MERGED" == "true" ]]; then
echo " Skipping — already merged a PR this run"
continue
fi

# Check if auto-merge is enabled (set by the automerge workflow)
AUTO_MERGE=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json autoMergeRequest --jq '.autoMergeRequest != null')

if [[ "$AUTO_MERGE" != "true" ]]; then
echo " Skipping — auto-merge not enabled"
continue
fi

# Check if all required checks pass (look at overall rollup)
CHECKS_PASS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json statusCheckRollup \
--jq '[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or . == "NEUTRAL" or . == "SKIPPED")')

CHECKS_PENDING=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json statusCheckRollup \
--jq '[.statusCheckRollup[]? | select(.name != null and .status != "COMPLETED")] | length')
Comment on lines +117 to +123
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Edge case: Empty statusCheckRollup may produce unexpected results.

When statusCheckRollup is empty or contains no items with status == "COMPLETED", the jq expression:

[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or ...)

Returns true for an empty array (vacuous truth). Combined with CHECKS_PENDING being 0 for an empty array, a PR with no status checks would be considered merge-ready.

If repos without required status checks should not auto-merge, consider adding an explicit check:

🛡️ Proposed fix to require at least one completed check
+            CHECKS_COUNT=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
+              --json statusCheckRollup \
+              --jq '[.statusCheckRollup[]? | select(.name != null)] | length')
+
+            if [[ "$CHECKS_COUNT" -eq 0 ]]; then
+              echo "  Skipping — no status checks configured"
+              continue
+            fi
+
             CHECKS_PASS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/dependabot-rebase-reusable.yml around lines 117 - 123, The
current jq logic for CHECKS_PASS and CHECKS_PENDING treats an empty
statusCheckRollup as passing (vacuous truth); update the gh pr view/jq pipeline
to compute a completed checks count (e.g., COMPLETED_COUNT from
[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") |
.conclusion] | length) and then require COMPLETED_COUNT > 0 in addition to
CHECKS_PASS being true and CHECKS_PENDING == 0 before treating the PR as
merge-ready; adjust the existing CHECKS_PASS and CHECKS_PENDING usage
accordingly so repos with no status checks won’t auto-merge.


if [[ "$CHECKS_PENDING" -gt 0 ]]; then
echo " Skipping — $CHECKS_PENDING check(s) still pending"
continue
fi

if [[ "$CHECKS_PASS" != "true" ]]; then
echo " Skipping — some checks failed"
continue
fi

echo " All checks pass — merging PR #$PR_NUMBER"
if gh api "repos/$REPO/pulls/$PR_NUMBER/merge" \
-X PUT -f merge_method=squash \
--silent; then
echo " Merged PR #$PR_NUMBER"
MERGED=true
else
echo " Warning: failed to merge PR #$PR_NUMBER"
fi
done <<< "$PRS"
Loading
Loading