Skip to content
Merged
24 changes: 21 additions & 3 deletions .github/scripts/validate/detect-changes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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<<OUTSIDE_EOF"
echo "$OUTSIDE_CHANGES"
Expand All @@ -80,6 +82,15 @@ if [[ -z "$PLUGIN_LIST" ]]; then
echo "skip_validation=true" >> "$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<<OUTSIDE_EOF"
echo "$OUTSIDE_CHANGES"
echo "OUTSIDE_EOF"
} >> "$GITHUB_OUTPUT"
fi
echo "No plugin changes detected - skipping plugin validation (author has write access)."
exit 0
fi
Expand All @@ -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"

Expand Down Expand Up @@ -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<<OUTSIDE_EOF"
echo "$OUTSIDE_CHANGES"
Expand All @@ -183,5 +198,8 @@ if [[ "$(has_write_access "$PR_AUTHOR")" -eq 1 ]]; then
fi
echo "pub_key_changed=$PUB_KEY_CHANGED" >> "$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"
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
71 changes: 54 additions & 17 deletions .github/workflows/validate-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}))
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -463,7 +500,7 @@ jobs:
exit 1

# --------------------------------------------------------------------------
# Job 6: Auto-close unauthorized PRs
# Job 7: Auto-close unauthorized PRs
# --------------------------------------------------------------------------
close-unauthorized:
needs: detect-changes
Expand Down Expand Up @@ -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.
Expand Down
Loading