Skip to content

chore: standardise, Justfile, (rename,, fix, parse, errors,, remove, … #9

chore: standardise, Justfile, (rename,, fix, parse, errors,, remove, …

chore: standardise, Justfile, (rename,, fix, parse, errors,, remove, … #9

# SPDX-License-Identifier: PMPL-1.0-or-later
# Static Analysis Gate — Required by branch protection rules.
# Runs panic-attack and hypatia, deposits findings for gitbot-fleet learning.
name: Static Analysis Gate
on:
pull_request:
branches: ['**']
push:
branches: [main, master]
permissions:
contents: read
jobs:
# ---------------------------------------------------------------------------
# Job 1: panic-attack assail
# ---------------------------------------------------------------------------
panic-attack-assail:
name: panic-attack assail
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install panic-attack (if available)
id: install
run: |
# Try to fetch the latest release binary from the org
PA_URL="https://github.com/hyperpolymath/panic-attack/releases/latest/download/panic-attack-linux-x86_64"
if curl -fsSL --head "$PA_URL" >/dev/null 2>&1; then
curl -fsSL -o /usr/local/bin/panic-attack "$PA_URL"
chmod +x /usr/local/bin/panic-attack
echo "installed=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::panic-attack binary not available — skipping assail"
echo "installed=false" >> "$GITHUB_OUTPUT"
fi
- name: Run panic-attack assail
id: assail
if: steps.install.outputs.installed == 'true'
run: |
set +e
panic-attack assail --format json . > panic-attack-findings.json 2>&1
PA_EXIT=$?
set -e
if [ ! -s panic-attack-findings.json ]; then
echo "[]" > panic-attack-findings.json
fi
# Parse finding counts
TOTAL=$(jq '. | length' panic-attack-findings.json 2>/dev/null || echo 0)
CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
HIGH=$(jq '[.[] | select(.severity == "high")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
LOW=$(jq '[.[] | select(.severity == "low")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
echo "total=$TOTAL" >> "$GITHUB_OUTPUT"
echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT"
echo "high=$HIGH" >> "$GITHUB_OUTPUT"
echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT"
echo "low=$LOW" >> "$GITHUB_OUTPUT"
echo "exit_code=$PA_EXIT" >> "$GITHUB_OUTPUT"
- name: Emit check annotations
if: steps.install.outputs.installed == 'true'
run: |
# Convert JSON findings into GitHub Actions annotations
jq -r '.[] | select(.file != null) |
if .severity == "critical" then
"::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
elif .severity == "high" then
"::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
else
"::warning file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
end
' panic-attack-findings.json || true
- name: Write step summary
if: steps.install.outputs.installed == 'true'
run: |
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
## panic-attack assail Results
| Severity | Count |
|----------|-------|
| Critical | ${{ steps.assail.outputs.critical }} |
| High | ${{ steps.assail.outputs.high }} |
| Medium | ${{ steps.assail.outputs.medium }} |
| Low | ${{ steps.assail.outputs.low }} |
| **Total**| ${{ steps.assail.outputs.total }} |
EOF
- name: Create stub findings (when panic-attack unavailable)
if: steps.install.outputs.installed != 'true'
run: |
echo "[]" > panic-attack-findings.json
echo "## panic-attack assail" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY"
- name: Upload panic-attack findings
uses: actions/upload-artifact@65c79d7f54e76e4e3c7a8f34db0f4ac8b515c478 # v4
with:
name: panic-attack-findings
path: panic-attack-findings.json
retention-days: 90
- name: Fail on critical findings
if: steps.install.outputs.installed == 'true' && steps.assail.outputs.critical > 0
run: |
echo "::error::panic-attack found ${{ steps.assail.outputs.critical }} critical issue(s) — blocking merge"
exit 1
# ---------------------------------------------------------------------------
# Job 2: hypatia-scan
# ---------------------------------------------------------------------------
hypatia-scan:
name: Hypatia neurosymbolic scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Elixir for Hypatia scanner
id: beam
continue-on-error: true
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.18.2
with:
elixir-version: '1.19.4'
otp-version: '28.3'
- name: Clone and build Hypatia
id: build
continue-on-error: true
run: |
git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" 2>/dev/null || true
if [ -f "$HOME/hypatia/mix.exs" ]; then
cd "$HOME/hypatia"
if [ ! -f hypatia-v2 ]; then
mix deps.get
mix escript.build
mv hypatia hypatia-v2
fi
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::Hypatia scanner not available — skipping scan"
echo "ready=false" >> "$GITHUB_OUTPUT"
fi
- name: Run Hypatia scan
id: scan
if: steps.build.outputs.ready == 'true'
run: |
set +e
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1
HYP_EXIT=$?
set -e
if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then
echo "[]" > hypatia-findings.json
fi
TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json 2>/dev/null || echo 0)
HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json 2>/dev/null || echo 0)
MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json 2>/dev/null || echo 0)
LOW=$(jq '[.[] | select(.severity == "low")] | length' hypatia-findings.json 2>/dev/null || echo 0)
echo "total=$TOTAL" >> "$GITHUB_OUTPUT"
echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT"
echo "high=$HIGH" >> "$GITHUB_OUTPUT"
echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT"
echo "low=$LOW" >> "$GITHUB_OUTPUT"
- name: Emit check annotations
if: steps.build.outputs.ready == 'true'
run: |
jq -r '.[] | select(.file != null) |
if .severity == "critical" then
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
elif .severity == "high" then
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
else
"::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
end
' hypatia-findings.json || true
- name: Write step summary
if: steps.build.outputs.ready == 'true'
run: |
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
## Hypatia Scan Results
| Severity | Count |
|----------|-------|
| Critical | ${{ steps.scan.outputs.critical }} |
| High | ${{ steps.scan.outputs.high }} |
| Medium | ${{ steps.scan.outputs.medium }} |
| Low | ${{ steps.scan.outputs.low }} |
| **Total**| ${{ steps.scan.outputs.total }} |
EOF
- name: Create stub findings (when Hypatia unavailable)
if: steps.build.outputs.ready != 'true'
run: |
echo "[]" > hypatia-findings.json
echo "## Hypatia Scan" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY"
- name: Upload hypatia findings
uses: actions/upload-artifact@65c79d7f54e76e4e3c7a8f34db0f4ac8b515c478 # v4
with:
name: hypatia-findings
path: hypatia-findings.json
retention-days: 90
- name: Fail on critical security findings
if: steps.build.outputs.ready == 'true' && steps.scan.outputs.critical > 0
run: |
echo "::error::Hypatia found ${{ steps.scan.outputs.critical }} critical security issue(s) — blocking merge"
exit 1
# ---------------------------------------------------------------------------
# Job 3: deposit-findings (combines + archives for gitbot-fleet)
# ---------------------------------------------------------------------------
deposit-findings:
name: Deposit findings for gitbot-fleet
runs-on: ubuntu-latest
needs: [panic-attack-assail, hypatia-scan]
if: always()
steps:
- name: Download panic-attack findings
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
name: panic-attack-findings
path: findings/
- name: Download hypatia findings
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
name: hypatia-findings
path: findings/
- name: Combine findings into unified report
id: combine
run: |
PA_FILE="findings/panic-attack-findings.json"
HYP_FILE="findings/hypatia-findings.json"
# Ensure both files exist and are valid JSON arrays
for f in "$PA_FILE" "$HYP_FILE"; do
if [ ! -s "$f" ] || ! jq empty "$f" 2>/dev/null; then
echo "[]" > "$f"
fi
done
# Tag each finding with its source scanner
jq '[.[] | . + {"scanner": "panic-attack"}]' "$PA_FILE" > /tmp/pa-tagged.json
jq '[.[] | . + {"scanner": "hypatia"}]' "$HYP_FILE" > /tmp/hyp-tagged.json
# Build unified report envelope
jq -n \
--arg repo "${{ github.repository }}" \
--arg sha "${{ github.sha }}" \
--arg ref "${{ github.ref }}" \
--arg run_id "${{ github.run_id }}" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--slurpfile pa /tmp/pa-tagged.json \
--slurpfile hyp /tmp/hyp-tagged.json \
'{
schema_version: "1.0.0",
repository: $repo,
commit_sha: $sha,
ref: $ref,
run_id: $run_id,
timestamp: $ts,
findings: ($pa[0] + $hyp[0])
}' > findings/unified-findings.json
TOTAL=$(jq '.findings | length' findings/unified-findings.json)
CRITICAL=$(jq '[.findings[] | select(.severity == "critical")] | length' findings/unified-findings.json)
HIGH=$(jq '[.findings[] | select(.severity == "high")] | length' findings/unified-findings.json)
MEDIUM=$(jq '[.findings[] | select(.severity == "medium")] | length' findings/unified-findings.json)
LOW=$(jq '[.findings[] | select(.severity == "low")] | length' findings/unified-findings.json)
echo "total=$TOTAL" >> "$GITHUB_OUTPUT"
echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT"
echo "high=$HIGH" >> "$GITHUB_OUTPUT"
echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT"
echo "low=$LOW" >> "$GITHUB_OUTPUT"
- name: Upload unified findings (fleet scanner picks these up)
uses: actions/upload-artifact@65c79d7f54e76e4e3c7a8f34db0f4ac8b515c478 # v4
with:
name: unified-findings
path: findings/unified-findings.json
retention-days: 90
- name: Write deposit summary
run: |
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
## Unified Findings Deposit
**Repository:** ${{ github.repository }}
**Commit:** \`${{ github.sha }}\`
**Deposited at:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")
| Severity | Count |
|----------|-------|
| Critical | ${{ steps.combine.outputs.critical }} |
| High | ${{ steps.combine.outputs.high }} |
| Medium | ${{ steps.combine.outputs.medium }} |
| Low | ${{ steps.combine.outputs.low }} |
| **Total**| ${{ steps.combine.outputs.total }} |
Findings saved as \`unified-findings\` artifact.
The gitbot-fleet scanner will ingest these on its next pass.
EOF