Skip to content

Auto-Update Upstream Checksums #3613

Auto-Update Upstream Checksums

Auto-Update Upstream Checksums #3613

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