diff --git a/.github/scripts/validate/detect-changes.sh b/.github/scripts/validate/detect-changes.sh index bcf2298..2e0736c 100755 --- a/.github/scripts/validate/detect-changes.sh +++ b/.github/scripts/validate/detect-changes.sh @@ -60,6 +60,8 @@ if [[ -z "$PLUGIN_LIST" ]]; then echo "close_reason=" >> "$GITHUB_OUTPUT" echo "skip_validation=false" >> "$GITHUB_OUTPUT" echo "outside_violation=true" >> "$GITHUB_OUTPUT" + echo "has_new_plugin=false" >> "$GITHUB_OUTPUT" + echo "has_updated_plugin=false" >> "$GITHUB_OUTPUT" { echo "outside_files<> "$GITHUB_OUTPUT" echo "outside_violation=false" >> "$GITHUB_OUTPUT" echo "pub_key_changed=$PUB_KEY_CHANGED" >> "$GITHUB_OUTPUT" + echo "has_new_plugin=false" >> "$GITHUB_OUTPUT" + echo "has_updated_plugin=false" >> "$GITHUB_OUTPUT" + if [[ -n "$OUTSIDE_CHANGES" ]]; then + { + echo "outside_files<> "$GITHUB_OUTPUT" + fi echo "No plugin changes detected - skipping plugin validation (author has write access)." exit 0 fi @@ -105,18 +116,22 @@ if [[ -z "$PLUGIN_LIST" ]]; then echo "close_reason=no-valid-plugins" >> "$GITHUB_OUTPUT" echo "plugin_count=0" >> "$GITHUB_OUTPUT" echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_new_plugin=false" >> "$GITHUB_OUTPUT" + echo "has_updated_plugin=false" >> "$GITHUB_OUTPUT" exit 0 fi PLUGIN_COUNT=$(echo "$PLUGIN_LIST" | wc -l | tr -d ' ') -# --- Check if any modified plugin is new (does not exist on base branch) --- +# --- Check if any modified plugin is new / updated --- HAS_NEW_PLUGIN=0 +HAS_UPDATED_PLUGIN=0 while IFS= read -r plugin; do [[ -z "$plugin" ]] && continue if ! git show "origin/${BASE_REF}:plugins/${plugin}/plugin.json" > /dev/null 2>&1; then HAS_NEW_PLUGIN=1 - break + else + HAS_UPDATED_PLUGIN=1 fi done <<< "$PLUGIN_LIST" @@ -166,7 +181,7 @@ if [[ "$CLOSE_PR" == "true" ]]; then else echo "close_reason=" >> "$GITHUB_OUTPUT" fi -if [[ $HAS_OUTSIDE_VIOLATION -eq 1 ]]; then +if [[ -n "$OUTSIDE_CHANGES" ]]; then { echo "outside_files<> "$GITHUB_OUTPUT" +echo "has_new_plugin=$( [[ $HAS_NEW_PLUGIN -eq 1 ]] && echo true || echo false )" >> "$GITHUB_OUTPUT" +echo "has_updated_plugin=$( [[ $HAS_UPDATED_PLUGIN -eq 1 ]] && echo true || echo false )" >> "$GITHUB_OUTPUT" + echo "Detected $PLUGIN_COUNT plugin(s): $PLUGIN_LIST" echo "close_pr=$CLOSE_PR" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3b82996..67f36a9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,6 +4,8 @@ name: CodeQL # which runs CodeQL as part of the plugin validation flow. on: workflow_dispatch: + push: + branches: [main] schedule: - cron: '0 6 * * 1' diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index 719c02c..2de718c 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -58,6 +58,8 @@ jobs: outside_violation: ${{ steps.detect.outputs.outside_violation }} skip_validation: ${{ steps.detect.outputs.skip_validation }} pub_key_changed: ${{ steps.detect.outputs.pub_key_changed }} + has_new_plugin: ${{ steps.detect.outputs.has_new_plugin }} + has_updated_plugin: ${{ steps.detect.outputs.has_updated_plugin }} steps: - name: Checkout base branch scripts (trusted) uses: actions/checkout@v4 @@ -102,7 +104,52 @@ jobs: "${{ github.event.pull_request.base.ref }}" # -------------------------------------------------------------------------- - # Job 3: CodeQL security and quality analysis (runs after validate-plugin) + # Job 3: Apply PR labels based on change classification + # -------------------------------------------------------------------------- + label-pr: + needs: [detect-changes] + if: always() && needs.detect-changes.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Apply labels + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HAS_NEW_PLUGIN: ${{ needs.detect-changes.outputs.has_new_plugin }} + HAS_UPDATED_PLUGIN: ${{ needs.detect-changes.outputs.has_updated_plugin }} + OUTSIDE_FILES: ${{ needs.detect-changes.outputs.outside_files }} + OUTSIDE_VIOLATION: ${{ needs.detect-changes.outputs.outside_violation }} + CLOSE_PR: ${{ needs.detect-changes.outputs.close_pr }} + run: | + apply_label() { + local label=$1 condition=$2 + if [[ "$condition" == "true" ]]; then + gh pr edit "$PR_NUMBER" --add-label "$label" || true + else + gh pr edit "$PR_NUMBER" --remove-label "$label" 2>/dev/null || true + fi + } + apply_label "New Plugin" "$HAS_NEW_PLUGIN" + apply_label "Plugin Update" "$HAS_UPDATED_PLUGIN" + # Repo Update only when there are outside files AND it's not a violation (authorized) + if [[ -n "$OUTSIDE_FILES" && "$OUTSIDE_VIOLATION" != "true" ]]; then + IS_REPO_UPDATE=true + else + IS_REPO_UPDATE=false + fi + apply_label "Repo Update" "$IS_REPO_UPDATE" + # Invalid when PR has unauthorized outside changes or unauthorized plugin modifications + if [[ "$OUTSIDE_VIOLATION" == "true" || "$CLOSE_PR" == "true" ]]; then + IS_INVALID=true + else + IS_INVALID=false + fi + apply_label "Invalid" "$IS_INVALID" + + # -------------------------------------------------------------------------- + # Job 4: CodeQL security and quality analysis (runs after validate-plugin) # Skipped automatically if any validation shard fails, saving ~15 min. # Analysis is explicitly scoped to only the changed plugin directories. # -------------------------------------------------------------------------- @@ -155,16 +202,6 @@ jobs: upload: false continue-on-error: true - - name: Upload SARIF to GitHub (always, so resolved alerts are closed) - if: always() && hashFiles('sarif-results/**') != '' - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: sarif-results - ref: refs/pull/${{ github.event.pull_request.number }}/merge - sha: ${{ steps.merge.outputs.sha }} - category: /language:python/pr-${{ github.event.pull_request.number }} - continue-on-error: true - - name: Set CodeQL status output id: status if: always() @@ -216,9 +253,9 @@ jobs: ([ (($run.tool.driver.rules // [])[] | {key: (.id // ""), value: ((.properties["security-severity"] // "0") | tostring)}), ((($run.tool.extensions // [])[].rules // [])[] | {key: (.id // ""), value: ((.properties["security-severity"] // "0") | tostring)}) ] | from_entries) as $secmap | ($run.results // [])[] | - (($r.ruleId // $r.rule.id // "") | tostring) as $rid | + ((.ruleId // .rule.id // "") | tostring) as $rid | select((($secmap[$rid] // "0") | tonumber) >= 7.0) | - " [blocking] \($rid) sec-sev=\($secmap[$rid] // \"n/a\")" ] | .[]' || true + " [blocking] \($rid) sec-sev=\($secmap[$rid] // "n/a")" ] | .[]' || true RESULT_COUNT=$((RESULT_COUNT + ${COUNT:-0})) MEDIUM_COUNT=$((MEDIUM_COUNT + ${MED:-0})) TOTAL_COUNT=$((TOTAL_COUNT + ${TOT:-0})) @@ -326,7 +363,7 @@ jobs: run: exit 1 # -------------------------------------------------------------------------- - # Job 4: Validate each plugin in parallel via matrix + # Job 5: Validate each plugin in parallel via matrix # -------------------------------------------------------------------------- validate-plugin: needs: [detect-changes] @@ -397,7 +434,7 @@ jobs: run: exit 1 # -------------------------------------------------------------------------- - # Job 5: Aggregate all fragments, post PR comment, set final status + # Job 6: Aggregate all fragments, post PR comment, set final status # -------------------------------------------------------------------------- report: needs: [detect-changes, validate-plugin, codeql-analyze] @@ -463,7 +500,7 @@ jobs: exit 1 # -------------------------------------------------------------------------- - # Job 6: Auto-close unauthorized PRs + # Job 7: Auto-close unauthorized PRs # -------------------------------------------------------------------------- close-unauthorized: needs: detect-changes @@ -493,7 +530,7 @@ jobs: "/dev/null" # -------------------------------------------------------------------------- - # Job 7: Gate - single fixed status check for branch protection rules + # Job 8: Gate - single fixed status check for branch protection rules # Reference this job by name "Plugin PR Check" in your branch protection. # Passes only when detect-changes + CodeQL + all matrix validations + report # all succeed. Fails for any error, including unauthorized PRs.