feat(state): add installation progress checklist #6
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
| # Installer Notification Receiver Workflow | ||
| # Receives repository_dispatch events from /dp project repos when installers change | ||
| # Independently verifies checksums and creates PRs with security scan results | ||
| # | ||
| # Related: bd-19y9.2.1 | ||
| name: Installer Notification Receiver | ||
| on: | ||
| repository_dispatch: | ||
| types: [installer-updated, installer-removed, installer-added] | ||
| workflow_dispatch: | ||
| inputs: | ||
| tool_name: | ||
| description: 'Tool name to manually verify' | ||
| required: true | ||
| type: string | ||
| dry_run: | ||
| description: 'Skip PR creation' | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
| concurrency: | ||
| group: installer-updates | ||
| cancel-in-progress: false # Queue updates, don't cancel | ||
| env: | ||
| CHECKSUMS_FILE: checksums.yaml | ||
| AUDIT_LOG_FILE: .github/audit/installer-updates.jsonl | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
| jobs: | ||
| validate-dispatch: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| tool_name: ${{ steps.validate.outputs.tool_name }} | ||
| event_type: ${{ steps.validate.outputs.event_type }} | ||
| new_sha256: ${{ steps.validate.outputs.new_sha256 }} | ||
| old_sha256: ${{ steps.validate.outputs.old_sha256 }} | ||
| source_repo: ${{ steps.validate.outputs.source_repo }} | ||
| source_commit: ${{ steps.validate.outputs.source_commit }} | ||
| installer_url: ${{ steps.validate.outputs.installer_url }} | ||
| is_valid: ${{ steps.validate.outputs.is_valid }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: Install yq | ||
| run: | | ||
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 | ||
| sudo chmod +x /usr/local/bin/yq | ||
| - name: Validate payload and extract fields | ||
| id: validate | ||
| run: | | ||
| set -euo pipefail | ||
| echo "::group::Payload Validation" | ||
| # Determine event source | ||
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | ||
| TOOL_NAME="${{ github.event.inputs.tool_name }}" | ||
| EVENT_TYPE="manual-verify" | ||
| NEW_SHA256="" | ||
| OLD_SHA256="" | ||
| SOURCE_REPO="manual" | ||
| SOURCE_COMMIT="manual" | ||
| else | ||
| # Extract from repository_dispatch payload | ||
| TOOL_NAME="${{ github.event.client_payload.tool }}" | ||
| EVENT_TYPE="${{ github.event.action }}" | ||
| NEW_SHA256="${{ github.event.client_payload.new_sha256 }}" | ||
| OLD_SHA256="${{ github.event.client_payload.old_sha256 }}" | ||
| SOURCE_REPO="${{ github.event.client_payload.repo }}" | ||
| SOURCE_COMMIT="${{ github.event.client_payload.commit }}" | ||
| fi | ||
| echo "Tool: $TOOL_NAME" | ||
| echo "Event: $EVENT_TYPE" | ||
| echo "Source: $SOURCE_REPO @ $SOURCE_COMMIT" | ||
| # Validate tool name format (alphanumeric, underscore, hyphen only) | ||
| if ! [[ "$TOOL_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then | ||
| echo "::error::Invalid tool name format: $TOOL_NAME" | ||
| echo "is_valid=false" >> $GITHUB_OUTPUT | ||
| exit 0 | ||
| fi | ||
| # Check if tool exists in checksums.yaml (for update/remove) | ||
| if [[ "$EVENT_TYPE" != "installer-added" ]]; then | ||
| if ! yq e ".installers.$TOOL_NAME" $CHECKSUMS_FILE | grep -q "url:"; then | ||
| echo "::error::Unknown tool: $TOOL_NAME (not in checksums.yaml)" | ||
| echo "is_valid=false" >> $GITHUB_OUTPUT | ||
| exit 0 | ||
| fi | ||
| fi | ||
| # Get URL from checksums.yaml (or from payload for new tools) | ||
| if [[ "$EVENT_TYPE" == "installer-added" ]]; then | ||
| INSTALLER_URL="${{ github.event.client_payload.url }}" | ||
| else | ||
| INSTALLER_URL=$(yq e ".installers.$TOOL_NAME.url" $CHECKSUMS_FILE) | ||
| fi | ||
| echo "URL: $INSTALLER_URL" | ||
| # Validate URL is from trusted domain | ||
| TRUSTED_DOMAINS="github.com githubusercontent.com raw.githubusercontent.com claude.ai astral.sh bun.sh setup.atuin.sh sh.rustup.rs jeffreysprompts.com" | ||
| URL_HOST=$(echo "$INSTALLER_URL" | sed -E 's|^https?://([^/]+)/.*|\1|') | ||
| IS_TRUSTED=false | ||
| for domain in $TRUSTED_DOMAINS; do | ||
| if [[ "$URL_HOST" == "$domain" ]] || [[ "$URL_HOST" == *".$domain" ]]; then | ||
| IS_TRUSTED=true | ||
| break | ||
| fi | ||
| done | ||
| if [[ "$IS_TRUSTED" != "true" ]]; then | ||
| echo "::error::Untrusted URL domain: $URL_HOST" | ||
| echo "is_valid=false" >> $GITHUB_OUTPUT | ||
| exit 0 | ||
| fi | ||
| echo "::endgroup::" | ||
| # Set outputs | ||
| echo "tool_name=$TOOL_NAME" >> $GITHUB_OUTPUT | ||
| echo "event_type=$EVENT_TYPE" >> $GITHUB_OUTPUT | ||
| echo "new_sha256=$NEW_SHA256" >> $GITHUB_OUTPUT | ||
| echo "old_sha256=$OLD_SHA256" >> $GITHUB_OUTPUT | ||
| echo "source_repo=$SOURCE_REPO" >> $GITHUB_OUTPUT | ||
| echo "source_commit=$SOURCE_COMMIT" >> $GITHUB_OUTPUT | ||
| echo "installer_url=$INSTALLER_URL" >> $GITHUB_OUTPUT | ||
| echo "is_valid=true" >> $GITHUB_OUTPUT | ||
| - name: Append to audit log | ||
| if: always() | ||
| run: | | ||
| mkdir -p .github/audit | ||
| echo '{ | ||
| "timestamp": "'$(date -Iseconds)'", | ||
| "event_type": "${{ steps.validate.outputs.event_type }}", | ||
| "tool": "${{ steps.validate.outputs.tool_name }}", | ||
| "source_repo": "${{ steps.validate.outputs.source_repo }}", | ||
| "source_commit": "${{ steps.validate.outputs.source_commit }}", | ||
| "is_valid": ${{ steps.validate.outputs.is_valid }}, | ||
| "workflow_run": "${{ github.run_id }}" | ||
| }' >> $AUDIT_LOG_FILE | ||
| verify-checksum: | ||
| needs: validate-dispatch | ||
| if: needs.validate-dispatch.outputs.is_valid == 'true' && needs.validate-dispatch.outputs.event_type != 'installer-removed' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| computed_sha256: ${{ steps.compute.outputs.sha256 }} | ||
| checksum_changed: ${{ steps.compare.outputs.changed }} | ||
| current_sha256: ${{ steps.compare.outputs.current }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install yq | ||
| run: | | ||
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 | ||
| sudo chmod +x /usr/local/bin/yq | ||
| - name: Download and compute SHA256 | ||
| id: compute | ||
| run: | | ||
| set -euo pipefail | ||
| INSTALLER_URL="${{ needs.validate-dispatch.outputs.installer_url }}" | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| echo "::group::Downloading installer from $INSTALLER_URL" | ||
| # Download with timeout and size limit (10MB max) | ||
| curl -fsSL --max-time 60 --max-filesize 10485760 \ | ||
| -o /tmp/installer.sh "$INSTALLER_URL" | ||
| # Compute SHA256 | ||
| COMPUTED_SHA256=$(sha256sum /tmp/installer.sh | cut -d' ' -f1) | ||
| echo "Computed SHA256: $COMPUTED_SHA256" | ||
| echo "::endgroup::" | ||
| echo "sha256=$COMPUTED_SHA256" >> $GITHUB_OUTPUT | ||
| - name: Compare with current checksum | ||
| id: compare | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| COMPUTED="${{ steps.compute.outputs.sha256 }}" | ||
| # Get current checksum from file | ||
| CURRENT=$(yq e ".installers.$TOOL_NAME.sha256" $CHECKSUMS_FILE 2>/dev/null || echo "none") | ||
| echo "Current: $CURRENT" | ||
| echo "Computed: $COMPUTED" | ||
| if [[ "$CURRENT" == "$COMPUTED" ]]; then | ||
| echo "Checksums match - no update needed" | ||
| echo "changed=false" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "Checksums differ - update required" | ||
| echo "changed=true" >> $GITHUB_OUTPUT | ||
| fi | ||
| echo "current=$CURRENT" >> $GITHUB_OUTPUT | ||
| security-scan: | ||
| needs: [validate-dispatch, verify-checksum] | ||
| if: needs.verify-checksum.outputs.checksum_changed == 'true' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| scan_passed: ${{ steps.scan.outputs.passed }} | ||
| scan_warnings: ${{ steps.scan.outputs.warnings }} | ||
| steps: | ||
| - name: Download installer for scanning | ||
| run: | | ||
| curl -fsSL --max-time 60 \ | ||
| -o /tmp/installer.sh "${{ needs.validate-dispatch.outputs.installer_url }}" | ||
| - name: Security scan | ||
| id: scan | ||
| run: | | ||
| set -euo pipefail | ||
| echo "::group::Security Scan Results" | ||
| WARNINGS="" | ||
| PASSED=true | ||
| # Pattern 1: Curl piped directly to bash from non-trusted URLs | ||
| if grep -E 'curl.*\|.*bash' /tmp/installer.sh | grep -vE '(github\.com|githubusercontent\.com|astral\.sh)'; then | ||
| WARNINGS="$WARNINGS\n- WARNING: curl piped to bash from untrusted URL" | ||
| fi | ||
| # Pattern 2: wget piped to shell | ||
| if grep -E 'wget.*\|.*(bash|sh)' /tmp/installer.sh; then | ||
| WARNINGS="$WARNINGS\n- WARNING: wget piped to shell" | ||
| fi | ||
| # Pattern 3: eval with user input | ||
| if grep -E 'eval.*\$' /tmp/installer.sh; then | ||
| WARNINGS="$WARNINGS\n- WARNING: eval with variable expansion detected" | ||
| fi | ||
| # Pattern 4: Downloading to /usr/bin or /usr/local/bin without verification | ||
| if grep -E '(curl|wget).*(/usr/bin|/usr/local/bin)' /tmp/installer.sh | grep -v 'sha256\|checksum'; then | ||
| WARNINGS="$WARNINGS\n- WARNING: Direct download to system bin without checksum" | ||
| fi | ||
| # Pattern 5: sudo with stdin redirect | ||
| if grep -E 'sudo.*<<<' /tmp/installer.sh; then | ||
| WARNINGS="$WARNINGS\n- WARNING: sudo with heredoc/stdin (potential injection)" | ||
| fi | ||
| # Pattern 6: Overly permissive chmod | ||
| if grep -E 'chmod.*777' /tmp/installer.sh; then | ||
| WARNINGS="$WARNINGS\n- WARNING: chmod 777 detected (overly permissive)" | ||
| fi | ||
| # Pattern 7: rm -rf with variables | ||
| if grep -E 'rm\s+-rf\s+.*\$' /tmp/installer.sh; then | ||
| WARNINGS="$WARNINGS\n- WARNING: rm -rf with variable (potential path traversal)" | ||
| fi | ||
| echo "::endgroup::" | ||
| if [[ -n "$WARNINGS" ]]; then | ||
| echo "Security warnings found:" | ||
| echo -e "$WARNINGS" | ||
| echo "warnings<<EOF" >> $GITHUB_OUTPUT | ||
| echo -e "$WARNINGS" >> $GITHUB_OUTPUT | ||
| echo "EOF" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "No security warnings" | ||
| echo "warnings=" >> $GITHUB_OUTPUT | ||
| fi | ||
| # Security scan is advisory, always pass | ||
| echo "passed=true" >> $GITHUB_OUTPUT | ||
| update-checksums: | ||
| needs: [validate-dispatch, verify-checksum, security-scan] | ||
| if: | | ||
| always() && | ||
| needs.verify-checksum.outputs.checksum_changed == 'true' && | ||
| (needs.security-scan.result == 'success' || needs.security-scan.result == 'skipped') | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| branch_name: ${{ steps.branch.outputs.name }} | ||
| pr_body: ${{ steps.pr-body.outputs.body }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: Install yq | ||
| run: | | ||
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 | ||
| sudo chmod +x /usr/local/bin/yq | ||
| - name: Create branch | ||
| id: branch | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| SHORT_SHA="${{ needs.verify-checksum.outputs.computed_sha256 }}" | ||
| SHORT_SHA=${SHORT_SHA:0:8} | ||
| BRANCH_NAME="auto/update-${TOOL_NAME}-checksum-${SHORT_SHA}" | ||
| echo "name=$BRANCH_NAME" >> $GITHUB_OUTPUT | ||
| git checkout -b "$BRANCH_NAME" | ||
| - name: Update checksums.yaml | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| NEW_SHA256="${{ needs.verify-checksum.outputs.computed_sha256 }}" | ||
| EVENT_TYPE="${{ needs.validate-dispatch.outputs.event_type }}" | ||
| if [[ "$EVENT_TYPE" == "installer-added" ]]; then | ||
| # Add new entry | ||
| INSTALLER_URL="${{ needs.validate-dispatch.outputs.installer_url }}" | ||
| yq e -i ".installers.$TOOL_NAME.url = \"$INSTALLER_URL\"" $CHECKSUMS_FILE | ||
| yq e -i ".installers.$TOOL_NAME.sha256 = \"$NEW_SHA256\"" $CHECKSUMS_FILE | ||
| else | ||
| # Update existing entry | ||
| yq e -i ".installers.$TOOL_NAME.sha256 = \"$NEW_SHA256\"" $CHECKSUMS_FILE | ||
| fi | ||
| # Update timestamp comment | ||
| sed -i "1s/.*/# checksums.yaml - Auto-updated $(date -Iseconds)/" $CHECKSUMS_FILE | ||
| - name: Run tests | ||
| run: | | ||
| # Verify YAML is valid | ||
| yq e '.' $CHECKSUMS_FILE > /dev/null | ||
| # Run project tests if available | ||
| if [[ -f "package.json" ]] && grep -q "test" package.json; then | ||
| npm test || echo "Tests failed but continuing" | ||
| fi | ||
| - name: Generate PR body | ||
| id: pr-body | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| OLD_SHA="${{ needs.verify-checksum.outputs.current_sha256 }}" | ||
| NEW_SHA="${{ needs.verify-checksum.outputs.computed_sha256 }}" | ||
| SOURCE_REPO="${{ needs.validate-dispatch.outputs.source_repo }}" | ||
| SOURCE_COMMIT="${{ needs.validate-dispatch.outputs.source_commit }}" | ||
| WARNINGS="${{ needs.security-scan.outputs.scan_warnings }}" | ||
| cat << EOF > /tmp/pr-body.md | ||
| ## Automated Checksum Update: $TOOL_NAME | ||
| This PR was automatically generated by the installer notification system. | ||
| ### Changes | ||
| - **Tool**: \`$TOOL_NAME\` | ||
| - **Old SHA256**: \`$OLD_SHA\` | ||
| - **New SHA256**: \`$NEW_SHA\` | ||
| ### Source | ||
| - **Repository**: $SOURCE_REPO | ||
| - **Commit**: $SOURCE_COMMIT | ||
| - **Upstream Link**: https://github.com/$SOURCE_REPO/commit/$SOURCE_COMMIT | ||
| ### Security Scan | ||
| EOF | ||
| if [[ -n "$WARNINGS" ]]; then | ||
| echo "**Warnings detected:**" >> /tmp/pr-body.md | ||
| echo "$WARNINGS" >> /tmp/pr-body.md | ||
| echo "" >> /tmp/pr-body.md | ||
| echo "Please review the installer changes carefully before merging." >> /tmp/pr-body.md | ||
| else | ||
| echo "No security warnings detected." >> /tmp/pr-body.md | ||
| fi | ||
| cat << 'EOF2' >> /tmp/pr-body.md | ||
| ### Verification Steps | ||
| - [x] Downloaded installer independently | ||
| - [x] Computed SHA256 locally | ||
| - [x] Ran security pattern scan | ||
| - [x] Updated checksums.yaml | ||
| ### Manual Review Checklist | ||
| - [ ] Verified upstream commit is legitimate | ||
| - [ ] Reviewed installer diff for suspicious changes | ||
| - [ ] Confirmed tool functionality still works | ||
| --- | ||
| *Generated by ACFS Installer Notification Receiver* | ||
| EOF2 | ||
| # Output as multiline | ||
| echo "body<<PREOF" >> $GITHUB_OUTPUT | ||
| cat /tmp/pr-body.md >> $GITHUB_OUTPUT | ||
| echo "PREOF" >> $GITHUB_OUTPUT | ||
| - name: Commit and push | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add $CHECKSUMS_FILE | ||
| git commit -m "chore(checksums): Update ${{ needs.validate-dispatch.outputs.tool_name }} to ${{ needs.verify-checksum.outputs.computed_sha256 }}" | ||
| git push origin "${{ steps.branch.outputs.name }}" | ||
| create-pr: | ||
| needs: [validate-dispatch, verify-checksum, security-scan, update-checksums] | ||
| if: needs.update-checksums.result == 'success' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Create Pull Request | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| BRANCH_NAME="${{ needs.update-checksums.outputs.branch_name }}" | ||
| SHORT_SHA="${{ needs.verify-checksum.outputs.computed_sha256 }}" | ||
| SHORT_SHA=${SHORT_SHA:0:8} | ||
| # Create PR | ||
| gh pr create \ | ||
| --base main \ | ||
| --head "$BRANCH_NAME" \ | ||
| --title "chore(checksums): Update $TOOL_NAME to $SHORT_SHA" \ | ||
| --body "${{ needs.update-checksums.outputs.pr_body }}" \ | ||
| --label "automated,checksum-update,needs-review" | ||
| - name: Add comment to tracking issue (optional) | ||
| continue-on-error: true | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| # Find tracking issue if exists | ||
| ISSUE=$(gh issue list --label "installer-tracking" --state open --limit 1 --json number -q '.[0].number') | ||
| if [[ -n "$ISSUE" ]]; then | ||
| gh issue comment "$ISSUE" --body "Checksum update received for **${{ needs.validate-dispatch.outputs.tool_name }}** from ${{ needs.validate-dispatch.outputs.source_repo }}" | ||
| fi | ||
| handle-removal: | ||
| needs: validate-dispatch | ||
| if: needs.validate-dispatch.outputs.is_valid == 'true' && needs.validate-dispatch.outputs.event_type == 'installer-removed' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install yq | ||
| run: | | ||
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 | ||
| sudo chmod +x /usr/local/bin/yq | ||
| - name: Remove tool from checksums.yaml | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| # Create branch | ||
| BRANCH_NAME="auto/remove-${TOOL_NAME}" | ||
| git checkout -b "$BRANCH_NAME" | ||
| # Remove entry | ||
| yq e -i "del(.installers.$TOOL_NAME)" $CHECKSUMS_FILE | ||
| # Commit and push | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add $CHECKSUMS_FILE | ||
| git commit -m "chore(checksums): Remove $TOOL_NAME" | ||
| git push origin "$BRANCH_NAME" | ||
| - name: Create removal PR | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" | ||
| SOURCE_REPO="${{ needs.validate-dispatch.outputs.source_repo }}" | ||
| gh pr create \ | ||
| --base main \ | ||
| --head "auto/remove-${TOOL_NAME}" \ | ||
| --title "chore(checksums): Remove $TOOL_NAME" \ | ||
| --body "## Tool Removal: $TOOL_NAME | ||
| This PR removes **$TOOL_NAME** from checksums.yaml. | ||
| ### Reason | ||
| The upstream repository ($SOURCE_REPO) has indicated the installer should be removed. | ||
| ### Checklist | ||
| - [ ] Confirmed tool is no longer needed | ||
| - [ ] Verified this is intentional, not an error | ||
| --- | ||
| *Generated by ACFS Installer Notification Receiver*" \ | ||
| --label "automated,checksum-removal,needs-review" | ||