Auto-Update Upstream Checksums #643
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: "0 */2 * * *" # Every 2 hours (more frequent for faster updates) | |
| workflow_dispatch: # Manual trigger for testing | |
| push: | |
| paths: | |
| - 'scripts/lib/security.sh' # Re-run if security.sh changes | |
| 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 | |
| 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' | |
| run: | | |
| echo "Repository dispatch payload:" | |
| echo '${{ toJson(github.event.client_payload) }}' | |
| - name: Verify current checksums | |
| id: verify | |
| run: | | |
| chmod +x ./scripts/lib/security.sh | |
| echo "🔍 Verifying checksums against upstream..." | |
| # Note: --verify returns exit 1 when mismatches found, which is expected | |
| # We capture the JSON output regardless of exit code | |
| # stderr is separate to avoid corrupting JSON with any warning messages | |
| ./scripts/lib/security.sh --verify --json > current.json 2>verify-errors.log || true | |
| # 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) | |
| total_issues=$((mismatches + errors)) | |
| echo "mismatches=$mismatches" >> $GITHUB_OUTPUT | |
| echo "errors=$errors" >> $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 [[ "$total_issues" -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 | |
| 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: steps.verify.outputs.changed == 'true' | |
| id: commit | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Check if checksums.yaml actually changed (handles race condition) | |
| if git diff --quiet checksums.yaml; then | |
| echo "checksums.yaml unchanged (possibly already updated by another run)" | |
| 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/,$//') | |
| git add checksums.yaml | |
| git commit -m "chore(security): auto-update checksums for ${CHANGED_TOOLS}" \ | |
| -m "Updated checksums for upstream installer scripts that have changed." \ | |
| -m "" \ | |
| -m "Changed tools: ${CHANGED_TOOLS}" \ | |
| -m "Trusted: ${{ steps.verify.outputs.trusted_changed || 'none' }}" \ | |
| -m "External: ${{ steps.verify.outputs.external_changed || 'none' }}" \ | |
| -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 | |
| echo "committed=true" >> $GITHUB_OUTPUT | |
| echo "✅ Successfully pushed checksum updates" | |
| - 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() | |
| 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 "| Mismatches | ${{ steps.verify.outputs.mismatches || 0 }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Errors | ${{ steps.verify.outputs.errors || 0 }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Trusted Changed | ${{ steps.verify.outputs.trusted_changed || 'none' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| External Changed | ${{ steps.verify.outputs.external_changed || 'none' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ steps.verify.outputs.changed }}" == "true" ]]; then | |
| if [[ "${{ steps.commit.outputs.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 |