Auto-Update Upstream Checksums #3613
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto-Update Upstream Checksums | |
| on: | |
| schedule: | |
| - cron: "*/15 * * * *" # Every 15 minutes — minimizes stale-checksum window | |
| workflow_dispatch: # Manual trigger for testing | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'install.sh' | |
| - 'acfs.manifest.yaml' | |
| - 'checksums.yaml' | |
| - 'packages/manifest/**' | |
| - 'scripts/lib/**' | |
| - 'scripts/generated/**' | |
| - '.github/workflows/checksum-monitor.yml' | |
| repository_dispatch: | |
| types: [upstream-changed] # Triggered by our other repos via webhook | |
| concurrency: | |
| group: checksum-monitor | |
| cancel-in-progress: false # Let running job complete, queue new ones | |
| jobs: | |
| auto-update-checksums: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| permissions: | |
| contents: write | |
| issues: write # For creating issues on failure | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log repository dispatch payload | |
| if: github.event_name == 'repository_dispatch' | |
| env: | |
| PAYLOAD: ${{ toJson(github.event.client_payload) }} | |
| run: | | |
| echo "Repository dispatch payload:" | |
| echo "$PAYLOAD" | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install manifest generator dependencies | |
| run: bun install | |
| working-directory: packages/manifest | |
| - name: Verify generated artifact drift | |
| id: drift | |
| run: | | |
| chmod +x ./scripts/check-manifest-drift.sh | |
| echo "🔍 Verifying manifest/internal generated artifact drift..." | |
| set +e | |
| ./scripts/check-manifest-drift.sh --json > drift.json 2>drift-errors.log | |
| drift_exit=$? | |
| set -e | |
| if [[ "$drift_exit" -gt 1 ]]; then | |
| echo "❌ Drift checker failed (exit $drift_exit)" | |
| cat drift-errors.log || true | |
| exit 1 | |
| fi | |
| if ! jq empty drift.json 2>/dev/null; then | |
| echo "❌ Drift checker did not return valid JSON" | |
| cat drift.json | |
| exit 1 | |
| fi | |
| drift_detected=$(jq -r '.drift_detected' drift.json) | |
| internal_drifted=$(jq '.internal_scripts.drifted // 0' drift.json) | |
| echo "drift_detected=$drift_detected" >> "$GITHUB_OUTPUT" | |
| echo "internal_drifted=$internal_drifted" >> "$GITHUB_OUTPUT" | |
| if [[ "$drift_detected" == "true" ]]; then | |
| echo "⚠️ Generated artifact drift detected" | |
| jq -r '.reasons[] | " - \(.)"' drift.json | |
| else | |
| echo "✅ No manifest/internal drift detected" | |
| fi | |
| - name: Auto-fix generated artifact drift | |
| id: drift_fix | |
| if: steps.drift.outputs.drift_detected == 'true' | |
| run: | | |
| ./scripts/check-manifest-drift.sh --fix | |
| echo "fixed=true" >> "$GITHUB_OUTPUT" | |
| - name: Verify current checksums | |
| id: verify | |
| run: | | |
| chmod +x ./scripts/lib/security.sh | |
| echo "🔍 Verifying checksums against upstream..." | |
| # --verify returns non-zero for mismatches/errors. Capture output and classify. | |
| set +e | |
| ./scripts/lib/security.sh --verify --json > current.json 2>verify-errors.log | |
| verify_exit=$? | |
| set -e | |
| # Check if JSON is valid | |
| if ! jq empty current.json 2>/dev/null; then | |
| echo "error=invalid_json" >> $GITHUB_OUTPUT | |
| echo "❌ Failed to parse verification output" | |
| cat current.json | |
| exit 1 | |
| fi | |
| # Extract counts from JSON | |
| mismatches=$(jq '.mismatches | length' current.json) | |
| errors=$(jq '.errors | length' current.json) | |
| skipped=$(jq '.skipped | length' current.json) | |
| total_issues=$((mismatches + errors + skipped)) | |
| echo "mismatches=$mismatches" >> $GITHUB_OUTPUT | |
| echo "errors=$errors" >> $GITHUB_OUTPUT | |
| echo "skipped=$skipped" >> $GITHUB_OUTPUT | |
| echo "total_issues=$total_issues" >> $GITHUB_OUTPUT | |
| # Categorize changed tools | |
| TRUSTED_CHANGED="" | |
| EXTERNAL_CHANGED="" | |
| if [[ "$mismatches" -gt 0 ]]; then | |
| while IFS= read -r name; do | |
| url=$(jq -r --arg n "$name" '.mismatches[] | select(.name==$n) | .url // empty' current.json) | |
| if [[ "$url" == *"Dicklesworthstone"* ]]; then | |
| TRUSTED_CHANGED="${TRUSTED_CHANGED}${name}," | |
| else | |
| EXTERNAL_CHANGED="${EXTERNAL_CHANGED}${name}," | |
| fi | |
| done < <(jq -r '.mismatches[].name' current.json) | |
| fi | |
| # Remove trailing commas | |
| TRUSTED_CHANGED="${TRUSTED_CHANGED%,}" | |
| EXTERNAL_CHANGED="${EXTERNAL_CHANGED%,}" | |
| echo "trusted_changed=$TRUSTED_CHANGED" >> $GITHUB_OUTPUT | |
| echo "external_changed=$EXTERNAL_CHANGED" >> $GITHUB_OUTPUT | |
| if [[ "$errors" -gt 0 || "$skipped" -gt 0 ]]; then | |
| echo "changed=false" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "❌ Verification returned errors/skips; refusing to auto-update checksums.yaml" | |
| jq -r '.errors[] | " error: \(.name) -> \(.error)"' current.json 2>/dev/null || true | |
| jq -r '.skipped[] | " skipped: \(.name) -> \(.reason)"' current.json 2>/dev/null || true | |
| exit 1 | |
| fi | |
| if [[ "$mismatches" -gt 0 ]]; then | |
| echo "changed=true" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "📋 Changed tools:" | |
| jq -r '.mismatches[] | " - \(.name)"' current.json 2>/dev/null || true | |
| if [[ -n "$TRUSTED_CHANGED" ]]; then | |
| echo "" | |
| echo " 🏠 Trusted (Dicklesworthstone): $TRUSTED_CHANGED" | |
| fi | |
| if [[ -n "$EXTERNAL_CHANGED" ]]; then | |
| echo "" | |
| echo " 🌐 External: $EXTERNAL_CHANGED" | |
| fi | |
| else | |
| echo "changed=false" >> $GITHUB_OUTPUT | |
| if [[ "$verify_exit" -ne 0 ]]; then | |
| echo "❌ Verification failed unexpectedly with no mismatches/errors/skips" | |
| cat verify-errors.log || true | |
| exit 1 | |
| fi | |
| echo "✅ All checksums match - no update needed" | |
| fi | |
| - name: Generate updated checksums | |
| if: steps.verify.outputs.changed == 'true' | |
| run: | | |
| ./scripts/lib/security.sh --update-checksums > checksums.yaml.new | |
| mv checksums.yaml.new checksums.yaml | |
| - name: Commit and push updates | |
| if: always() && (steps.verify.outputs.changed == 'true' || steps.drift_fix.outputs.fixed == 'true') && github.ref == 'refs/heads/main' | |
| id: commit | |
| env: | |
| TRUSTED_CHANGED: ${{ steps.verify.outputs.trusted_changed || 'none' }} | |
| EXTERNAL_CHANGED: ${{ steps.verify.outputs.external_changed || 'none' }} | |
| VERIFY_CHANGED: ${{ steps.verify.outputs.changed || 'false' }} | |
| DRIFT_FIXED: ${{ steps.drift_fix.outputs.fixed || 'false' }} | |
| run: | | |
| # Stage only expected generated/security artifacts. | |
| git add checksums.yaml scripts/generated/ 2>/dev/null || true | |
| if git diff --cached --quiet; then | |
| echo "No staged checksum/generated drift changes to commit" | |
| echo "committed=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Generate commit message with details | |
| CHANGED_TOOLS=$(jq -r '.mismatches[].name' current.json 2>/dev/null | tr '\n' ', ' | sed 's/,$//') | |
| [[ -z "$CHANGED_TOOLS" ]] && CHANGED_TOOLS="none" | |
| COMMIT_SUBJECT="chore(security): auto-update checksums for ${CHANGED_TOOLS}" | |
| if [[ "$VERIFY_CHANGED" != "true" ]] && [[ "$DRIFT_FIXED" == "true" ]]; then | |
| COMMIT_SUBJECT="chore(manifest): auto-fix generated artifact drift" | |
| elif [[ "$VERIFY_CHANGED" == "true" ]] && [[ "$DRIFT_FIXED" == "true" ]]; then | |
| COMMIT_SUBJECT="chore(security): auto-update checksums + generated drift fixes" | |
| fi | |
| git commit -m "$COMMIT_SUBJECT" \ | |
| -m "Updated checksums for upstream installer scripts that have changed." \ | |
| -m "" \ | |
| -m "Changed tools: ${CHANGED_TOOLS}" \ | |
| -m "Trusted: $TRUSTED_CHANGED" \ | |
| -m "External: $EXTERNAL_CHANGED" \ | |
| -m "Drift fixed: $DRIFT_FIXED" \ | |
| -m "" \ | |
| -m "🤖 Generated by checksum-monitor workflow" | |
| # Pull any changes that happened while we were running (rebase our commit on top) | |
| git pull --rebase origin main || { | |
| echo "Rebase failed - likely a conflict. Will retry on next scheduled run." | |
| echo "committed=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| git push origin HEAD:main | |
| git push origin main:master | |
| echo "committed=true" >> $GITHUB_OUTPUT | |
| echo "✅ Successfully pushed checksum updates and mirrored main->master" | |
| - name: Create issue for external changes (security visibility) | |
| if: steps.verify.outputs.external_changed != '' && steps.commit.outputs.committed == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const external = '${{ steps.verify.outputs.external_changed }}'; | |
| const tools = external.split(',').filter(t => t); | |
| if (tools.length === 0) return; | |
| // Check for existing open issue | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'security,checksum-update' | |
| }); | |
| const existingIssue = issues.find(i => i.title.includes('External installer checksums')); | |
| // Build body with proper indentation for YAML literal block | |
| const toolsList = tools.map(t => '- `' + t + '`').join('\n'); | |
| const reviewList = tools.map(t => '- [ ] Review ' + t + ' changes').join('\n'); | |
| const body = [ | |
| '## External Installer Checksums Updated', | |
| '', | |
| 'The following **external** (non-Dicklesworthstone) installer scripts have changed:', | |
| '', | |
| toolsList, | |
| '', | |
| '### Action Required', | |
| 'These checksums were automatically updated. Please verify the upstream changes are legitimate:', | |
| '', | |
| reviewList, | |
| '', | |
| '### Why this matters', | |
| 'External installers (ohmyzsh, rustup, bun, etc.) could be compromised. While auto-updating keeps users unblocked, a quick review ensures we\'re not distributing malicious code.', | |
| '', | |
| '---', | |
| '🤖 Auto-generated by checksum-monitor workflow' | |
| ].join('\n'); | |
| if (existingIssue) { | |
| // Update existing issue | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: existingIssue.number, | |
| body: '### Additional changes detected\n\n' + body | |
| }); | |
| } else { | |
| // Create new issue | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: '🔐 External installer checksums updated - review recommended', | |
| body: body, | |
| labels: ['security', 'checksum-update'] | |
| }); | |
| } | |
| - name: Summary | |
| if: always() | |
| env: | |
| DRIFT_DETECTED: ${{ steps.drift.outputs.drift_detected || 'false' }} | |
| DRIFT_FIXED: ${{ steps.drift_fix.outputs.fixed || 'false' }} | |
| INTERNAL_DRIFTED: ${{ steps.drift.outputs.internal_drifted || 0 }} | |
| MISMATCHES: ${{ steps.verify.outputs.mismatches || 0 }} | |
| ERRORS: ${{ steps.verify.outputs.errors || 0 }} | |
| SKIPPED: ${{ steps.verify.outputs.skipped || 0 }} | |
| TRUSTED_CHANGED: ${{ steps.verify.outputs.trusted_changed || 'none' }} | |
| EXTERNAL_CHANGED: ${{ steps.verify.outputs.external_changed || 'none' }} | |
| VERIFY_CHANGED: ${{ steps.verify.outputs.changed }} | |
| VERIFY_OUTCOME: ${{ steps.verify.outcome }} | |
| COMMIT_COMMITTED: ${{ steps.commit.outputs.committed }} | |
| run: | | |
| echo "## Checksum Auto-Update Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Total Checked | $(jq '.total // 0' current.json 2>/dev/null || echo 0) |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Manifest/Internal Drift | $DRIFT_DETECTED |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Internal Drifted Files | $INTERNAL_DRIFTED |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Mismatches | $MISMATCHES |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Errors | $ERRORS |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Skipped | $SKIPPED |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Trusted Changed | $TRUSTED_CHANGED |" >> $GITHUB_STEP_SUMMARY | |
| echo "| External Changed | $EXTERNAL_CHANGED |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "$DRIFT_DETECTED" == "true" ]]; then | |
| echo "✅ **Generated artifact drift detected and auto-fixed**" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "$VERIFY_OUTCOME" != "success" ]]; then | |
| echo "❌ **Checksum verification failed; no automatic update was applied**" >> $GITHUB_STEP_SUMMARY | |
| elif [[ "$COMMIT_COMMITTED" == "true" ]] && [[ "$DRIFT_FIXED" == "true" ]] && [[ "$VERIFY_CHANGED" != "true" ]]; then | |
| echo "✅ **Generated artifact drift fixes were committed to main**" >> $GITHUB_STEP_SUMMARY | |
| elif [[ "$VERIFY_CHANGED" == "true" ]]; then | |
| if [[ "$COMMIT_COMMITTED" == "true" ]]; then | |
| echo "✅ **Checksums automatically updated and committed to main**" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ **Changes detected but commit skipped (race condition or conflict)**" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "✅ **All checksums match upstream - no action needed**" >> $GITHUB_STEP_SUMMARY | |
| fi |