-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workflows): centralize standards via reusable workflows #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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 }} |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edge case: Empty When [.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or ...)Returns 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 |
||
|
|
||
| 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" | ||
There was a problem hiding this comment.
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):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 the org standard explicitly requires top-level
name:anddescription:fields, the current behavior is correct and this is a non-issue.🤖 Prompt for AI Agents