From 5bc9f036f3f77ddf4b8b56f260d1fe6bca01a056 Mon Sep 17 00:00:00 2001 From: Division 7 Date: Mon, 20 Jul 2026 12:37:43 -0700 Subject: [PATCH 1/3] fix(pkgd-action): replace Python YAML validation with Node.js for Ubuntu 24.04 Signed-off-by: Division 7 --- CHANGELOG.md | 1 + github-action/.github/workflows/ci.yml | 7 ------- github-action/.github/workflows/release.yml | 7 ------- github-action/CHANGELOG.md | 18 +++++++++++++++--- github-action/tests/validate.sh | 2 +- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b402c2..69c4e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ and this project adheres to ### Fixed + - GitHub Action CI failed on Ubuntu 24.04 runners due to PEP 668 blocking system-wide Python package installs (`externally-managed-environment`). Replaced `python3 -c "import yaml..."` YAML validation in `validate.sh` with Node.js `require('yaml')` and removed the now-unnecessary `setup-uv` + `uv pip install --system pyyaml` steps from the action's CI and release workflows - `aiohttp>=3.14.1,<4.0` constraint — the requirement was incorrectly constrained to `aiohttp<3.14` during aiohttp 3.14/`aioresponses` 0.7.9 compatibility investigation. Added a temporary patching fixture in diff --git a/github-action/.github/workflows/ci.yml b/github-action/.github/workflows/ci.yml index a91d065..69841ae 100644 --- a/github-action/.github/workflows/ci.yml +++ b/github-action/.github/workflows/ci.yml @@ -47,12 +47,5 @@ jobs: - name: Run unit tests run: npm test - - name: Install uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 - with: - enable-cache: true - - name: Install Python deps for validate.sh - run: uv pip install --system pyyaml - - name: Run validation script run: bash tests/validate.sh diff --git a/github-action/.github/workflows/release.yml b/github-action/.github/workflows/release.yml index c0b2efe..ab9fa35 100644 --- a/github-action/.github/workflows/release.yml +++ b/github-action/.github/workflows/release.yml @@ -133,13 +133,6 @@ jobs: - name: Run unit tests run: npm test - - name: Install uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 - with: - enable-cache: true - - name: Install Python deps for validate.sh - run: uv pip install --system pyyaml - - name: Run validation script run: bash tests/validate.sh diff --git a/github-action/CHANGELOG.md b/github-action/CHANGELOG.md index aed3c14..90438ab 100644 --- a/github-action/CHANGELOG.md +++ b/github-action/CHANGELOG.md @@ -8,18 +8,30 @@ and this project adheres to ## [Unreleased] +## Fixed + +- CI failed on Ubuntu 24.04 runners due to PEP 668 blocking + system-wide Python package installs (`externally-managed-environment`). + Replaced `python3 -c "import yaml..."` YAML validation in `validate.sh` with + Node.js `require('yaml')` and removed the now-unnecessary `setup-uv` + + `uv pip install --system pyyaml` steps from the action's CI and release + workflows. All tests pass. + ## [1.0.0] - 2026-07-06 ### Added -- CI workflow: new `.github/workflows/ci.yml` runs tests on every push/PR to `main` +- CI workflow: new `.github/workflows/ci.yml` runs tests on every push/PR to + `main` - Initial release of the PKG-Defender GitHub Action -- `fail-on` input to control workflow failure threshold (critical, high, medium, low, none) +- `fail-on` input to control workflow failure threshold (critical, high, medium, + low, none) - `lock-files` input to configure glob pattern for lock file discovery - `findings`, `summary`, and `exit-code` outputs for downstream workflow steps - Thin CLI wrapper that installs `pkg-defender` via pip and runs `pkgd audit` - Automatic lock file discovery for npm, PyPI, Cargo, and RubyGems ecosystems -- Smart `--fail-on-threat` flag passthrough matching pkgd's CRITICAL/HIGH threshold +- Smart `--fail-on-threat` flag passthrough matching pkgd's CRITICAL/HIGH + threshold - Graceful handling of missing lock files, empty results, and malformed output - Comprehensive test suite with 39 unit tests - Input validation with warnings for invalid `fail-on` values diff --git a/github-action/tests/validate.sh b/github-action/tests/validate.sh index 3135176..01dcb68 100755 --- a/github-action/tests/validate.sh +++ b/github-action/tests/validate.sh @@ -11,7 +11,7 @@ echo "" # Test 1: action.yml exists and is valid YAML echo "Test 1: Validating action.yml..." if [ -f "action.yml" ]; then - if python3 -c "import yaml; yaml.safe_load(open('action.yml'))" 2>/dev/null; then + if node -e "require('yaml').parse(require('fs').readFileSync('action.yml', 'utf8'))" 2>/dev/null; then echo " ✓ action.yml is valid YAML" else echo " ✗ action.yml is NOT valid YAML" From 71e31ec1baa9c70c815343d3fe664fdf1ede331d Mon Sep 17 00:00:00 2001 From: Division 7 Date: Mon, 20 Jul 2026 16:15:24 -0700 Subject: [PATCH 2/3] fix(ci): use create-pull-request for signed downstream commits Downstream repo commits were unsigned, triggering GitHub's "unsigned commit" security alerts on homebrew-pkg-defender and pkg-defender-action. Replace manual git clone/commit/push operations with `peter-evans/create-pull-request@v8` and `actions/create-github-app-token@v3` across `sync-homebrew-tap.yml`, `sync-github-action.yml`, and `release.yml`. All downstream commits are now signed by the GitHub App identity. - Removed PAT-based auth - Added GitHub App token generation - Added sign-commits: `true` on all PR creation steps - Removed stale PR cleanup steps (handled by `delete-branch: true`) - Replaced gh pr merge with `enable-pull-request-automerge` Signed-off-by: Division 7 --- .github/workflows/release.yml | 176 ++++++++------------- .github/workflows/sync-github-action.yml | 193 ++++++----------------- .github/workflows/sync-homebrew-tap.yml | 171 +++++--------------- CHANGELOG.md | 1 + 4 files changed, 151 insertions(+), 390 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1d361d..8afc4b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -618,8 +618,8 @@ jobs: # Updates the formula in divisionseven/homebrew-pkg-defender with the correct # version and SHA256 hashes for all platform binaries. Creates a PR in the tap repo. # - # Requires: HOMEBREW_TAP_PAT — a fine-grained PAT with Contents: write + Pull requests: - # write permissions on the tap repo + # Uses GitHub App token (PKG_DEFENDER_APP_ID + PKG_DEFENDER_APP_PRIVATE_KEY) + # for signed commits and cross-repo PR creation. # Skipped for pre-releases (Homebrew users expect stable versions only). update-homebrew-tap: name: Update Homebrew Tap @@ -644,24 +644,21 @@ jobs: with: python-version: "3.12" - - name: Clone tap repository - env: - HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - echo "Cloning ${TAP_REPO}..." - git clone "https://${HOMEBREW_TAP_PAT}@github.com/${TAP_REPO}.git" /tmp/tap-repo || { - echo "FAIL: Could not clone tap repo ${TAP_REPO}" - echo "" - echo "Possible causes:" - echo " 1. Repository does not exist — create it first (see Step 1)" - echo " 2. PAT is invalid or expired — generate a new one (see Step 5)" - echo " 3. PAT is missing required permissions — needs 'Contents: write' and 'Pull requests: write' on ${TAP_REPO}" - echo "" - echo "Verify at: https://github.com/${TAP_REPO}" - echo "PAT settings: https://github.com/settings/personal-access-tokens" - exit 1 - } - echo "Tap repo cloned successfully to /tmp/tap-repo" + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.PKG_DEFENDER_APP_ID }} + private-key: ${{ secrets.PKG_DEFENDER_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: homebrew-pkg-defender + + - name: Checkout tap repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: divisionseven/homebrew-pkg-defender + token: ${{ steps.app-token.outputs.token }} + path: /tmp/tap-repo - name: Download binary checksums and compute SHA256 id: sha256 @@ -792,17 +789,21 @@ jobs: echo "Version verification passed: ${ACTUAL_VERSION}" + - name: Get GitHub App User ID + id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Commit formula updates for validation env: VERSION: ${{ needs.validate.outputs.version }} run: | cd /tmp/tap-repo - git config user.name "Division 7" - git config user.email "divisionseven@users.noreply.github.com" + git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]" + git config user.email "${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com" git add "${FORMULA_PATH}" - git commit -m "temp: update pkg-defender to ${VERSION} for validation - - Co-authored-by: github-actions[bot] " + git commit -m "temp: update pkg-defender to ${VERSION} for validation" - name: Set up Homebrew PATH run: echo "/home/linuxbrew/.linuxbrew/bin" >> "$GITHUB_PATH" @@ -886,104 +887,51 @@ jobs: brew uninstall --force pkg-defender 2>/dev/null || true echo "Cleanup complete" - - name: Close stale tap PRs - env: - HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }} - GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - echo "Checking for stale tap PRs..." - echo "" - - # Find all open PRs whose head branch starts with 'update/pkg-defender-'. - STALE_PRS=$(gh pr list \ - --repo "${TAP_REPO}" \ - --state open \ - --json headRefName,url \ - --jq '.[] | select(.headRefName | startswith("update/pkg-defender-")) | .url') - - if [ -z "${STALE_PRS}" ]; then - echo "✓ No stale tap PRs found." - else - echo "Found stale PR(s):" - for pr_url in ${STALE_PRS}; do - echo " Closing: ${pr_url}" - gh pr close "${pr_url}" \ - --repo "${TAP_REPO}" \ - --comment "Superseded by newer release." - echo " ✓ Closed: ${pr_url}" - done - fi - - - name: Create pull request in tap repository - id: create-pr - env: - HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }} - GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} + # ── Undo temp commit before PR creation ───────────────────────────────── + - name: Undo temp commit run: | - VERSION="${{ needs.validate.outputs.version }}" - TAG="${{ needs.validate.outputs.tag }}" - cd /tmp/tap-repo - git config user.name "Division 7" - git config user.email "divisionseven@users.noreply.github.com" - - # Amend the temp validation commit with the proper message - git commit --amend -m "PKG-Defender ${VERSION} - - Co-authored-by: github-actions[bot] " - - BRANCH="formula/pkg-defender-${VERSION}" - git checkout -b "${BRANCH}" - git push origin "${BRANCH}" + git reset HEAD~1 - # Create PR and capture its number - # gh pr create outputs the PR URL (no --json support for create) - PR_URL=$(gh pr create \ - --repo "${TAP_REPO}" \ - --title "PKG-Defender ${VERSION}" \ - --body "## What - - Updates PKG-Defender formula version to ${TAG}. - - ## Binary Checksums + # ── Create pull request ───────────────────────────────────────────────── + - name: Create pull request + id: create-pr + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.app-token.outputs.token }} + path: /tmp/tap-repo + commit-message: "PKG-Defender ${{ needs.validate.outputs.version }}" + sign-commits: true + branch: formula/pkg-defender-${{ needs.validate.outputs.version }} + delete-branch: true + title: "PKG-Defender ${{ needs.validate.outputs.version }}" + body: | + ## Summary - | Platform | SHA256 | - |----------|--------| - | darwin-arm64 | \`${{ steps.sha256.outputs.darwin_arm64 }}\` | - | darwin-amd64 | \`${{ steps.sha256.outputs.darwin_amd64 }}\` | - | linux-amd64 | \`${{ steps.sha256.outputs.linux_amd64 }}\` | + Updates PKG-Defender formula version to ${{ needs.validate.outputs.tag }}. - ## Verification + ## Binary Checksums - - [ ] \`brew audit --new --formula pkg-defender\` passes (CI) - - [ ] \`brew install pkg-defender\` succeeds (CI) - - [ ] \`brew test pkg-defender\` passes (CI) + | Platform | SHA256 | + |----------|--------| + | darwin-arm64 | `${{ steps.sha256.outputs.darwin_arm64 }}` | + | darwin-amd64 | `${{ steps.sha256.outputs.darwin_amd64 }}` | + | linux-amd64 | `${{ steps.sha256.outputs.linux_amd64 }}` | - Auto-generated by the PKG-Defender release workflow." \ - --head "${BRANCH}" \ - --base "main") + ## Verification - # Extract PR number from the URL (e.g., https://github.com/owner/repo/pull/123) - PR_NUMBER="${PR_URL##*/}" + - [x] `brew audit --new --formula pkg-defender` passes (CI) + - [x] `brew install pkg-defender` succeeds (CI) + - [x] `brew test pkg-defender` passes (CI) - # Expose PR number to downstream steps - echo "pull-request-number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - echo "" - echo "Created PR #${PR_NUMBER}: ${PR_URL}" + Auto-generated by the PKG-Defender release workflow. - - name: Enable auto-merge for tap PR + # ── Enable auto-merge ─────────────────────────────────────────────────── + - name: Enable auto-merge if: steps.create-pr.outputs.pull-request-number != '' continue-on-error: true - env: - HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }} - GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}" - echo "Enabling auto-merge for PR #${PR_NUMBER}..." - echo "" - gh pr merge "${PR_NUMBER}" \ - --repo "${TAP_REPO}" \ - --auto --squash - echo "" - echo "✓ Auto-merge enabled for PR #${PR_NUMBER}" - echo " The PR will merge automatically when tests.yml passes." + uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + pull-request-number: ${{ steps.create-pr.outputs.pull-request-number }} + merge-method: squash diff --git a/.github/workflows/sync-github-action.yml b/.github/workflows/sync-github-action.yml index c7f9884..09d0689 100644 --- a/.github/workflows/sync-github-action.yml +++ b/.github/workflows/sync-github-action.yml @@ -6,11 +6,11 @@ # Triggers: Push to main/develop touching github-action/ files, manual dispatch, or weekly schedule. # # What it does: -# 1. Clones the subsidiary action repo (divisionseven/pkg-defender-action) +# 1. Checks out the subsidiary action repo (divisionseven/pkg-defender-action) # 2. Compares github-action/ tree against downstream using diff -rq # 3. If differences found, rsyncs the github-action/ directory into the action repo # 4. Verifies changes were applied correctly -# 5. Creates a sync PR in the action repo (closes any prior stale sync PRs) +# 5. Creates a sync PR in the action repo using peter-evans/create-pull-request # # The --delete flag in rsync is intentional — if a file is removed from # github-action/ in the monorepo, it is removed from the action repo too. @@ -57,30 +57,26 @@ jobs: fetch-depth: 1 persist-credentials: false - # ── Step 2: Clone action repo ─────────────────────────────────────────── - # Uses the ACTION_REPO_PAT for authentication. This PAT must have - # 'Contents: write' and 'Pull requests: write' on the target repo. - - name: Clone action repository - env: - ACTION_REPO_PAT: ${{ secrets.ACTION_REPO_PAT }} - run: | - echo "Cloning ${ACTION_REPO}..." - git clone "https://${ACTION_REPO_PAT}@github.com/${ACTION_REPO}.git" /tmp/action-repo || { - echo "FAIL: Could not clone action repo ${ACTION_REPO}" - echo "" - echo "Possible causes:" - echo " 1. Repository does not exist — create divisionseven/pkg-defender-action first" - echo " 2. ACTION_REPO_PAT is invalid or expired — generate a new one in GitHub settings" - echo " 3. ACTION_REPO_PAT is missing required permissions — needs 'Contents: write' and" - echo " 'Pull requests: write' on ${ACTION_REPO}" - echo "" - echo "Verify at: https://github.com/${ACTION_REPO}" - echo "PAT settings: https://github.com/settings/personal-access-tokens" - exit 1 - } - echo "Action repo cloned successfully to /tmp/action-repo" + # ── Step 2: Generate GitHub App Token ──────────────────────────────────── + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.PKG_DEFENDER_APP_ID }} + private-key: ${{ secrets.PKG_DEFENDER_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: pkg-defender-action + + # ── Step 3: Checkout action repository ─────────────────────────────────── + # Uses the GitHub App token for authentication. + - name: Checkout action repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: divisionseven/pkg-defender-action + token: ${{ steps.app-token.outputs.token }} + path: /tmp/action-repo - # ── Step 3: Compare files against downstream repo ────────────────────── + # ── Step 4: Compare files against downstream repo ────────────────────── # Uses diff -rq for content-based comparison BEFORE any files are copied. # This is a pure read-only check with no side effects. # @@ -115,7 +111,7 @@ jobs: echo "$DIFF_OUTPUT" fi - # ── Step 4: Sync files from github-action/ to action repo ────────────── + # ── Step 5: Sync files from github-action/ to action repo ────────────── # Uses rsync with --delete so files removed from the source are also # removed from the target. The trailing slash on github-action/ means # we copy the CONTENTS of the directory, not the directory itself. @@ -147,7 +143,7 @@ jobs: echo "" echo "Sync complete." - # ── Step 5: Verify changes were applied ──────────────────────────────── + # ── Step 6: Verify changes were applied ──────────────────────────────── # Safety-net verification that the rsync actually produced content changes. # A warning is emitted if rsync ran but git status shows no changes — # this can happen with permission-only differences. @@ -162,126 +158,27 @@ jobs: git status --short fi - # ── Step 6: Configure git identity ────────────────────────────────────── - # Only needed when there are changes to commit. Commits are attributed to - # the team identity "Division 7" with github-actions[bot] recorded as a - # co-author for traceability. - - name: Configure git identity - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/action-repo - git config user.name "Division 7" - git config user.email "divisionseven@users.noreply.github.com" - - # ── Step 7: Create sync branch ───────────────────────────────────────── - # Branch name includes the date and short commit SHA for traceability - # back to the triggering commit in the monorepo. - - name: Create sync branch - id: create-branch - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/action-repo - BRANCH="sync/action-$(date +%Y%m%d)-${GITHUB_SHA:0:7}" - git checkout -b "${BRANCH}" - echo "Created branch: ${BRANCH}" - echo "branch-name=${BRANCH}" >> "$GITHUB_OUTPUT" - - # ── Step 8: Stage and commit ──────────────────────────────────────────── - # Commit message references the triggering commit URL for full audit - # trail. Uses `git add -A` to handle renames and deletions correctly. - # Uses two -m arguments to produce a conventional commit with a blank - # line separating the subject from the body. - - name: Commit changes - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/action-repo - git add -A - git commit \ - -m "chore: sync github-action from pkg-defender" \ - -m "Triggered by ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} - - Co-authored-by: github-actions[bot] " - - # ── Step 9: Push sync branch ─────────────────────────────────────────── - # Pushes the sync branch to the action repo so a PR can be created. - # The remote URL already has the PAT embedded from the git clone step. - - name: Push sync branch - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/action-repo - git push origin "${{ steps.create-branch.outputs.branch-name }}" - echo "✓ Pushed branch: ${{ steps.create-branch.outputs.branch-name }}" - - # ── Step 10: Close stale sync PRs ────────────────────────────────────── - # Unconditional — always runs, even if there are no new changes. - # Closes any open PRs in the action repo whose head branch starts - # with "sync/action-" to prevent merge conflicts between overlapping - # syncs. Uses GH_TOKEN for authentication. - - name: Close stale sync PRs - env: - GH_TOKEN: ${{ secrets.ACTION_REPO_PAT }} - run: | - echo "Checking for stale action sync PRs..." - echo "" - - STALE_PRS=$(gh pr list \ - --repo "${ACTION_REPO}" \ - --state open \ - --json headRefName,url \ - --jq '.[] | select(.headRefName | startswith("sync/action-")) | .url') - - if [ -z "${STALE_PRS}" ]; then - echo "✓ No stale action sync PRs found." - else - echo "Found stale PR(s):" - for pr_url in ${STALE_PRS}; do - echo " Closing: ${pr_url}" - gh pr close "${pr_url}" \ - --repo "${ACTION_REPO}" \ - --comment "Superseded by newer sync." - echo " ✓ Closed: ${pr_url}" - done - fi - - # ── Step 11: Create pull request in action repo ──────────────────────── - # Only runs when there are actual changes to sync. Builds a summary - # of changed files from the git diff and creates a PR targeting main. + # ── Step 7: Create pull request ───────────────────────────────────────── - name: Create pull request - id: create-pr if: steps.compare.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.ACTION_REPO_PAT }} - run: | - cd /tmp/action-repo - - # Build a summary of changed files - SUMMARY=$(git diff --name-status main..."${{ steps.create-branch.outputs.branch-name }}" 2>/dev/null || echo "Unknown changes") - - # Create the PR and capture its URL - PR_URL=$(gh pr create \ - --repo "${ACTION_REPO}" \ - --title "Sync GitHub Action — $(date +%Y-%m-%d)" \ - --body "## What - - Syncs \`github-action/\` from \`divisionseven/pkg-defender\` to this action repository. - - ## Changes - - \`\`\` - ${SUMMARY} - \`\`\` - - ## Note - - This is a full directory sync. Files removed from the source will also be removed here. - - Auto-generated by the [sync-github-action](${{ github.server_url }}/${{ github.repository }}/actions/workflows/sync-github-action.yml) workflow. - " \ - --head "${{ steps.create-branch.outputs.branch-name }}" \ - --base "main") - - # Extract PR number from URL - PR_NUMBER="${PR_URL##*/}" - echo "pull-request-number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - echo "" - echo "✓ Created PR #${PR_NUMBER}: ${PR_URL}" + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.app-token.outputs.token }} + path: /tmp/action-repo + commit-message: | + chore: sync github-action from pkg-defender + + Triggered by ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + sign-commits: true + branch: sync/action + delete-branch: true + title: "Sync GitHub Action — ${{ github.run_id }}" + body: | + ## Summary + + Syncs `github-action/` source directory changes from `divisionseven/pkg-defender` to this action repository. + + > [!NOTE] + > This is a full directory sync. Files removed from the source will also be removed here. + > + > Auto-generated by the [sync-github-action](${{ github.server_url }}/${{ github.repository }}/actions/workflows/sync-github-action.yml) workflow. diff --git a/.github/workflows/sync-homebrew-tap.yml b/.github/workflows/sync-homebrew-tap.yml index 29811e1..6bd0ba7 100644 --- a/.github/workflows/sync-homebrew-tap.yml +++ b/.github/workflows/sync-homebrew-tap.yml @@ -57,28 +57,25 @@ jobs: fetch-depth: 1 persist-credentials: false - # ── 2. Clone tap repository ──────────────────────────────────────────── - - name: Clone tap repository - env: - HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - echo "Cloning ${TAP_REPO}..." - git clone "https://${HOMEBREW_TAP_PAT}@github.com/${TAP_REPO}.git" /tmp/tap-repo || { - echo "FAIL: Could not clone tap repo ${TAP_REPO}" - echo "" - echo "Possible causes:" - echo " 1. Repository does not exist — create it first" - echo " 2. PAT is invalid or expired — generate a new one" - echo " 3. PAT is missing required permissions — needs 'Contents: write' and 'Pull requests: write' on ${TAP_REPO}" - echo "" - echo "Verify at: https://github.com/${TAP_REPO}" - echo "PAT settings: https://github.com/settings/personal-access-tokens" - exit 1 - } - echo "Tap repo cloned successfully to /tmp/tap-repo" - ls -la /tmp/tap-repo/ + # ── 2. Generate GitHub App Token ───────────────────────────────────────── + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.PKG_DEFENDER_APP_ID }} + private-key: ${{ secrets.PKG_DEFENDER_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: homebrew-pkg-defender + + # ── 3. Checkout tap repository ────────────────────────────────────────── + - name: Checkout tap repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: divisionseven/homebrew-pkg-defender + token: ${{ steps.app-token.outputs.token }} + path: /tmp/tap-repo - # ── 3. Compare files against downstream repo ────────────────────────── + # ── 4. Compare files against downstream repo ────────────────────────── - name: Compare files against downstream repo id: compare run: | @@ -146,7 +143,7 @@ jobs: echo "⛁ Files to sync:$MISMATCHES" fi - # ── 4. Sync source-of-truth files (only if changes) ──────────────────── + # ── 5. Sync source-of-truth files (only if changes) ──────────────────── - name: Sync source-of-truth files if: steps.compare.outputs.has_changes == 'true' run: | @@ -165,7 +162,7 @@ jobs: echo "All source-of-truth files synced." - # ── 5. Smart-merge the formula (only if changes) ─────────────────────── + # ── 6. Smart-merge the formula (only if changes) ─────────────────────── - name: Smart-merge formula (preserve version/url/sha256) if: steps.compare.outputs.has_changes == 'true' run: | @@ -173,110 +170,28 @@ jobs: "${{ github.workspace }}/homebrew-tap/Formula/pkg-defender.rb" \ "/tmp/tap-repo/Formula/pkg-defender.rb" - # ── 6. Git config (only if changes) ──────────────────────────────────── - # Commits are attributed to the team identity "Division 7" with the - # github-actions[bot] recorded as a co-author for traceability. - - name: Git config - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/tap-repo - git config user.name "Division 7" - git config user.email "divisionseven@users.noreply.github.com" - - # ── 7. Create branch (only if changes) ───────────────────────────────── - - name: Create branch - id: create-branch - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/tap-repo - BRANCH="sync/tap-$(date +%Y%m%d)-${GITHUB_SHA:0:7}" - git checkout -b "${BRANCH}" - echo "Created branch: ${BRANCH}" - echo "branch-name=${BRANCH}" >> "$GITHUB_OUTPUT" - - # ── 8. Commit changes (only if changes) ──────────────────────────────── - - name: Commit changes - if: steps.compare.outputs.has_changes == 'true' - run: | - cd /tmp/tap-repo - git add -A - git commit -m "chore: sync homebrew-tap from pkg-defender - - Triggered by ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} - - Co-authored-by: github-actions[bot] " - - # ── 9. Push branch (only if changes) ─────────────────────────────────── - - name: Push branch - if: steps.compare.outputs.has_changes == 'true' - env: - HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - cd /tmp/tap-repo - git push origin "${{ steps.create-branch.outputs.branch-name }}" - - # ── 10. Close stale sync PRs (unconditional) ─────────────────────────── - - name: Close stale sync PRs - env: - GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - cd /tmp/tap-repo - - STALE_PRS=$(gh pr list \ - --repo "${TAP_REPO}" \ - --state open \ - --json headRefName,url \ - --jq '.[] | select(.headRefName | startswith("sync/tap-")) | .url') - - if [ -z "${STALE_PRS}" ]; then - echo "✓ No stale tap sync PRs found." - else - echo "Found stale PR(s):" - for pr_url in ${STALE_PRS}; do - echo " Closing: ${pr_url}" - gh pr close "${pr_url}" \ - --repo "${TAP_REPO}" \ - --comment "Superseded by newer sync." - echo " ✓ Closed: ${pr_url}" - done - fi - - # ── 11. Create pull request (only if changes) ────────────────────────── + # ── 7. Create pull request ────────────────────────────────────────────── - name: Create pull request - id: create-pr if: steps.compare.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} - run: | - cd /tmp/tap-repo - - BRANCH="${{ steps.create-branch.outputs.branch-name }}" - - # Build a summary of changed files - SUMMARY=$(git diff --name-status main..."${BRANCH}" 2>/dev/null || echo "Unknown changes") - - PR_URL=$(gh pr create \ - --repo "${TAP_REPO}" \ - --title "Sync Homebrew Tap — $(date +%Y-%m-%d)" \ - --body "## What - - Syncs \`homebrew-tap/\` from \`divisionseven/pkg-defender\` to this tap repository. - - ## Changes - - \`\`\` - ${SUMMARY} - \`\`\` - - ## Note - - - \`version\`, \`url\`, and \`sha256\` in \`Formula/pkg-defender.rb\` are **preserved** from the current tap state. - - All other structural changes (desc, caveats, test block, etc.) are synced from the source. - - Auto-generated by the [sync-homebrew-tap](https://github.com/${{ github.repository }}/actions/workflows/sync-homebrew-tap.yml) workflow." \ - --head "${BRANCH}" \ - --base "main") - - PR_NUMBER="${PR_URL##*/}" - echo "pull-request-number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - echo "✓ Created PR: ${PR_URL}" + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.app-token.outputs.token }} + path: /tmp/tap-repo + commit-message: | + chore: sync homebrew-tap from pkg-defender + + Triggered by ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + sign-commits: true + branch: sync/tap + delete-branch: true + title: "Sync Homebrew Tap — ${{ github.run_id }}" + body: | + ## Summary + + Syncs `homebrew-tap/` source directory changes from `divisionseven/pkg-defender` to this tap repository. + + > [!NOTE] + > - `version`, `url`, and `sha256` in `Formula/pkg-defender.rb` are **preserved** from the current tap state. + > - All other structural changes (desc, caveats, test block, etc.) are synced from the source. + > + > Auto-generated by the [sync-homebrew-tap](https://github.com/${{ github.repository }}/actions/workflows/sync-homebrew-tap.yml) workflow. diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c4e24..92f07a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ and this project adheres to ### Fixed + - Sync workflow commits to downstream repos were unsigned, triggering GitHub's "unsigned commit" security alerts on `homebrew-pkg-defender` and `pkg-defender-action`. Replaced manual `git commit`/`git push` operations with `peter-evans/create-pull-request@v8` and `actions/create-github-app-token@v3` in `sync-homebrew-tap.yml`, `sync-github-action.yml`, and `release.yml` — all downstream commits are now signed by the GitHub App identity - GitHub Action CI failed on Ubuntu 24.04 runners due to PEP 668 blocking system-wide Python package installs (`externally-managed-environment`). Replaced `python3 -c "import yaml..."` YAML validation in `validate.sh` with Node.js `require('yaml')` and removed the now-unnecessary `setup-uv` + `uv pip install --system pyyaml` steps from the action's CI and release workflows - `aiohttp>=3.14.1,<4.0` constraint — the requirement was incorrectly constrained to `aiohttp<3.14` during aiohttp 3.14/`aioresponses` From 5c1c57d8e30affee8f063941824353a8fc79e3b9 Mon Sep 17 00:00:00 2001 From: Division 7 Date: Mon, 20 Jul 2026 16:54:18 -0700 Subject: [PATCH 3/3] fix(ci): use git clone instead of actions/checkout for downstream repos Signed-off-by: Division 7 --- .github/workflows/release.yml | 11 ++++++----- .github/workflows/sync-github-action.yml | 13 ++++++------- .github/workflows/sync-homebrew-tap.yml | 12 ++++++------ CHANGELOG.md | 1 + 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8afc4b2..0d90e31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -653,12 +653,13 @@ jobs: owner: ${{ github.repository_owner }} repositories: homebrew-pkg-defender + # ── Checkout tap repository ───────────────────────────────────────────── + # Uses git clone (not actions/checkout) because actions/checkout rejects + # paths outside $GITHUB_WORKSPACE (/home/runner/work/.../pkg-defender). + # The /tmp/ paths are required for cross-repo operations. - name: Checkout tap repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - repository: divisionseven/homebrew-pkg-defender - token: ${{ steps.app-token.outputs.token }} - path: /tmp/tap-repo + run: | + git clone "https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/divisionseven/homebrew-pkg-defender.git" /tmp/tap-repo - name: Download binary checksums and compute SHA256 id: sha256 diff --git a/.github/workflows/sync-github-action.yml b/.github/workflows/sync-github-action.yml index 09d0689..8e3913d 100644 --- a/.github/workflows/sync-github-action.yml +++ b/.github/workflows/sync-github-action.yml @@ -67,14 +67,13 @@ jobs: owner: ${{ github.repository_owner }} repositories: pkg-defender-action - # ── Step 3: Checkout action repository ─────────────────────────────────── - # Uses the GitHub App token for authentication. + # ── Checkout action repository ────────────────────────────────────────── + # Uses git clone (not actions/checkout) because actions/checkout rejects + # paths outside $GITHUB_WORKSPACE (/home/runner/work/.../pkg-defender). + # The /tmp/ paths are required for cross-repo operations. - name: Checkout action repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - repository: divisionseven/pkg-defender-action - token: ${{ steps.app-token.outputs.token }} - path: /tmp/action-repo + run: | + git clone "https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/divisionseven/pkg-defender-action.git" /tmp/action-repo # ── Step 4: Compare files against downstream repo ────────────────────── # Uses diff -rq for content-based comparison BEFORE any files are copied. diff --git a/.github/workflows/sync-homebrew-tap.yml b/.github/workflows/sync-homebrew-tap.yml index 6bd0ba7..54bd3d7 100644 --- a/.github/workflows/sync-homebrew-tap.yml +++ b/.github/workflows/sync-homebrew-tap.yml @@ -67,13 +67,13 @@ jobs: owner: ${{ github.repository_owner }} repositories: homebrew-pkg-defender - # ── 3. Checkout tap repository ────────────────────────────────────────── + # ── Checkout tap repository ───────────────────────────────────────────── + # Uses git clone (not actions/checkout) because actions/checkout rejects + # paths outside $GITHUB_WORKSPACE (/home/runner/work/.../pkg-defender). + # The /tmp/ paths are required for cross-repo operations. - name: Checkout tap repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - repository: divisionseven/homebrew-pkg-defender - token: ${{ steps.app-token.outputs.token }} - path: /tmp/tap-repo + run: | + git clone "https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/divisionseven/homebrew-pkg-defender.git" /tmp/tap-repo # ── 4. Compare files against downstream repo ────────────────────────── - name: Compare files against downstream repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f07a9..6a29456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ and this project adheres to ### Fixed + - Downstream repo checkout in `release.yml`, `sync-homebrew-tap.yml`, and `sync-github-action.yml` failed because `actions/checkout` rejects `/tmp/` paths outside the workspace. Replaced `actions/checkout` with `git clone` using the GitHub App token for all downstream repo checkouts - Sync workflow commits to downstream repos were unsigned, triggering GitHub's "unsigned commit" security alerts on `homebrew-pkg-defender` and `pkg-defender-action`. Replaced manual `git commit`/`git push` operations with `peter-evans/create-pull-request@v8` and `actions/create-github-app-token@v3` in `sync-homebrew-tap.yml`, `sync-github-action.yml`, and `release.yml` — all downstream commits are now signed by the GitHub App identity - GitHub Action CI failed on Ubuntu 24.04 runners due to PEP 668 blocking system-wide Python package installs (`externally-managed-environment`). Replaced `python3 -c "import yaml..."` YAML validation in `validate.sh` with Node.js `require('yaml')` and removed the now-unnecessary `setup-uv` + `uv pip install --system pyyaml` steps from the action's CI and release workflows - `aiohttp>=3.14.1,<4.0` constraint — the requirement was