diff --git a/.github/workflows/build-sign-release.yml b/.github/workflows/build-sign-release.yml new file mode 100644 index 0000000..6b176f3 --- /dev/null +++ b/.github/workflows/build-sign-release.yml @@ -0,0 +1,133 @@ +name: 📖 Build, Sign & Release PDF + +on: + workflow_dispatch: + inputs: + build_mode: + description: 'PDF build mode' + required: true + default: 'both' + type: choice + options: + - light + - dark + - both + push: + branches: + - main + paths: + - "docs/**" + - "mkdocs.yml" + - "scripts/**" + - ".github/workflows/**" + +permissions: + contents: write + id-token: write + +jobs: + build-sign-release: + name: Build, Sign & Release PDFs + runs-on: ubuntu-latest + steps: + - name: 🛠️ Checkout + uses: actions/checkout@v4 + + - name: 🐍 Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: 📦 Install MkDocs Material + run: pip install mkdocs-material + + - name: Setup Chrome + uses: browser-actions/setup-chrome@v2 + with: + chrome-version: 120 + install-dependencies: true + install-chromedriver: true + + - name: 🔑 Install GPG tools + run: | + sudo apt-get update + sudo apt-get install gnupg + + - name: 🖨️ Build PDFs + env: + CI: true + run: python scripts/build_guide_pdf.py --${{ inputs.build_mode || 'both' }} + + - name: 🔒 Sign SHA256 hash file with GPG + env: + GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + cd ${{ github.workspace }} + + # Import GPG key + export GPG_TTY=$(tty) + echo "$GPG_KEY" | gpg --batch --import 2>/dev/null || true + + # Create combined hash file with all PDFs + sha256sum export/thgtoa.pdf > export/checksums.sha256 + sha256sum export/thgtoa-dark.pdf >> export/checksums.sha256 + + # Sign the checksum file + gpg --batch --yes --armor --detach-sign --output export/checksums.sha256.sig export/checksums.sha256 2>/dev/null || true + + - name: 🔒 Sign PDF files with GPG + env: + GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + cd ${{ github.workspace }} + + # Import GPG key (if not already imported) + export GPG_TTY=$(tty) + echo "$GPG_KEY" | gpg --batch --import 2>/dev/null || true + + # Sign the PDFs with detach signatures + gpg --batch --yes --armor --detach-sign --output export/thgtoa.pdf.sig export/thgtoa.pdf 2>/dev/null || true + gpg --batch --yes --armor --detach-sign --output export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf 2>/dev/null || true + + - name: 🦠 Upload PDFs to VirusTotal + uses: crazy-max/ghaction-virustotal@v5 + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + files: | + ./export/thgtoa.pdf + ./export/thgtoa-dark.pdf + + - name: 📊 Extract VT scan results + id: vt-scan + run: | + echo "status=completed" >> $GITHUB_OUTPUT + + - name: 🔗 Generate VT report links + run: | + # Create a markdown file with VT scan results and links + cat > export/virus-total-results.md << EOF + ## VirusTotal Scan Results + + **Scan Date:** \$(date -u +"%Y-%m-%d %H:%M UTC") + + ### thgtoa.pdf (Light Mode) + - **VT Report:** https://www.virustotal.com/gui/file/\$(sha256sum export/thgtoa.pdf | cut -d' ' -f1) + + ### thgtoa-dark.pdf (Dark Mode) + - **VT Report:** https://www.virustotal.com/gui/file/\$(sha256sum export/thgtoa-dark.pdf | cut -d' ' -f1) + + --- + *Scan performed automatically by GitHub Actions* + EOF + + - name: 📤 Upload export directory as artifact + uses: actions/upload-artifact@v4 + with: + name: pdf-export-${{ inputs.build_mode || 'both' }} + path: | + export/ + if-no-files-found: error + retention-days: 90 + compression-level: 0 diff --git a/scripts/pdf_release.sh b/scripts/pdf_release.sh index 2d47a30..f5fc452 100644 --- a/scripts/pdf_release.sh +++ b/scripts/pdf_release.sh @@ -57,7 +57,7 @@ create_hash_file() { local pdf_path="$1" local base_name=$(basename "$pdf_path") local hash_file="${EXPORT_DIR}/${base_name}.sha256" - + (cd "$EXPORT_DIR" && sha256sum "$base_name") > "$hash_file" echo "Created: $hash_file" } @@ -65,39 +65,39 @@ create_hash_file() { scan_with_virustotal() { local pdf_path="$1" local base_name=$(basename "$pdf_path") - + if [[ -z "$VT_API_KEY" ]]; then echo "Warning: VT_API_KEY not provided, skipping VirusTotal scan for $base_name" return 1 fi - + echo "Scanning $base_name with VirusTotal..." - + local upload_response=$(curl -s -X POST \ -H "x-apikey: $VT_API_KEY" \ -F "file=@$pdf_path" \ https://www.virustotal.com/api/v3/files) - + if [[ $? -ne 0 ]]; then echo "Error uploading $base_name to VirusTotal" return 1 fi - + local file_id=$(echo "$upload_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['id'])" 2>/dev/null || echo "") - + if [[ -z "$file_id" ]]; then echo "Error: Could not extract file ID from VirusTotal response for $base_name" echo "Response: $upload_response" return 1 fi - + local vt_url="https://www.virustotal.com/gui/file/$file_id" echo "$vt_url" } scan_all_pdfs() { local results_file="$EXPORT_DIR/virus-total-results.md" - + cat > "$results_file" << 'HEADER' ## VirusTotal Scan Results @@ -105,9 +105,9 @@ scan_all_pdfs() { --- HEADER - + sed -i "s/TIMESTAMP/$(date -u +"%Y-%m-%d %H:%M UTC")/" "$results_file" - + local pdf_files=() if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then pdf_files+=("$EXPORT_DIR/thgtoa.pdf") @@ -115,16 +115,16 @@ HEADER if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then pdf_files+=("$EXPORT_DIR/thgtoa-dark.pdf") fi - + for pdf in "${pdf_files[@]}"; do if [[ -f "$pdf" ]]; then local base_name=$(basename "$pdf") local hash=$(generate_hash "$pdf") - + echo "" >> "$results_file" echo "### $base_name" >> "$results_file" echo "- **SHA256 Hash:** \`$hash\`" >> "$results_file" - + if [[ -n "$VT_API_KEY" ]]; then local vt_url=$(scan_with_virustotal "$pdf") if [[ $? -eq 0 && -n "$vt_url" ]]; then @@ -135,37 +135,37 @@ HEADER else echo "- **VirusTotal Report:** VT_API_KEY not configured, scan skipped" >> "$results_file" fi - + create_hash_file "$pdf" else echo "Warning: $pdf does not exist, skipping..." fi done - + cat >> "$results_file" << 'FOOTER' --- *Scan performed automatically by GitHub Actions* FOOTER - + echo "VirusTotal results saved to: $results_file" } update_release() { local tag="${1:-}" local release_notes="$EXPORT_DIR/release-notes.md" - + if [[ "$RELEASE_MODE" == "tag" && -z "$tag" ]]; then tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") fi - + if [[ -z "$tag" ]]; then echo "Warning: No release tag found, skipping release update." return 1 fi - + echo "Updating release for tag: $tag" - + cat > "$release_notes" << EOF # Release Notes - $tag @@ -174,7 +174,7 @@ update_release() { ## PDF Files EOF - + if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then local hash=$(generate_hash "$EXPORT_DIR/thgtoa.pdf") echo "- **thgtoa.pdf (Light Mode)**" >> "$release_notes" @@ -183,7 +183,7 @@ EOF echo " - Signature: \`thgtoa.pdf.sig\` (GPG signed)" >> "$release_notes" fi fi - + if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then local hash=$(generate_hash "$EXPORT_DIR/thgtoa-dark.pdf") echo "- **thgtoa-dark.pdf (Dark Mode)**" >> "$release_notes" @@ -192,19 +192,19 @@ EOF echo " - Signature: \`thgtoa-dark.pdf.sig\` (GPG signed)" >> "$release_notes" fi fi - + echo "" >> "$release_notes" echo "---" >> "$release_notes" - + if [[ -f "$EXPORT_DIR/virus-total-results.md" ]]; then echo "## VirusTotal Scan Results" >> "$release_notes" echo "" >> "$release_notes" cat "$EXPORT_DIR/virus-total-results.md" >> "$release_notes" echo "" >> "$release_notes" fi - + local files_to_upload="" - + if [[ -f "$EXPORT_DIR/thgtoa.pdf" ]]; then files_to_upload+="$EXPORT_DIR/thgtoa.pdf " fi @@ -217,9 +217,9 @@ EOF if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sig" ]]; then files_to_upload+="$EXPORT_DIR/thgtoa-dark.pdf.sig " fi - + local combined_hash_file="$EXPORT_DIR/sha256sum-combined.txt" - + if [[ -f "$EXPORT_DIR/thgtoa.pdf.sha256" ]]; then cat "$EXPORT_DIR/thgtoa.pdf.sha256" >> "$combined_hash_file" 2>/dev/null || true fi @@ -227,9 +227,9 @@ EOF echo "" >> "$combined_hash_file" cat "$EXPORT_DIR/thgtoa-dark.pdf.sha256" >> "$combined_hash_file" fi - + files_to_upload+="$combined_hash_file " - + if [[ -n "${GPG_PRIVATE_KEY:-}" && -n "${GPG_PASSPHRASE:-}" ]]; then echo "$GPG_PRIVATE_KEY" | gpg --batch --import 2>/dev/null || true gpg --batch --yes --armor --detach-sign --output "$combined_hash_file.sig" "$combined_hash_file" 2>/dev/null || true @@ -237,17 +237,17 @@ EOF files_to_upload+="$combined_hash_file.sig " fi fi - + if command -v gh &> /dev/null && [[ -n "$GITHUB_TOKEN" ]]; then echo "Uploading release with GitHub CLI..." - + local release_exists=$(gh release view "$tag" 2>/dev/null && echo "yes" || echo "no") - + if [[ "$release_exists" == "yes" ]]; then gh release edit "$tag" --notes-file "$release_notes" 2>/dev/null || { echo "Warning: Failed to update release notes" } - + for file in $files_to_upload; do if [[ -f "$file" ]]; then local file_name=$(basename "$file") @@ -265,32 +265,32 @@ EOF return 1 } fi - + else if [[ -n "$GITHUB_TOKEN" && -n "$tag" ]]; then echo "Using GitHub API to upload release..." - + local repo="${GITHUB_REPOSITORY:-}" local api_url="https://api.github.com/repos/$repo/releases" - + local existing_release=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "$api_url/tags/$tag") - + if [[ $(echo "$existing_release" | grep -c '"id":') -gt 0 ]]; then echo "Release already exists, updating..." local release_id=$(echo "$existing_release" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "") - + curl -X PATCH \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "$api_url/$release_id" \ -d "{\"body\":\"$(cat "$release_notes")\"}" 2>/dev/null || true - + for file in $files_to_upload; do if [[ -f "$file" ]]; then local file_name=$(basename "$file") local mime_type=$(file --mime-type -b "$file") - + curl -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Content-Type: $mime_type" \ @@ -302,20 +302,20 @@ EOF done else echo "Creating new release..." - + local create_response=$(curl -s -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "$api_url" \ -d "{\"tag_name\":\"$tag\",\"name\":\"Release $tag\",\"body\":\"$(cat "$release_notes")\"}") - + local release_id=$(echo "$create_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "") - + for file in $files_to_upload; do if [[ -f "$file" ]]; then local file_name=$(basename "$file") local mime_type=$(file --mime-type -b "$file") - + curl -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Content-Type: $mime_type" \ @@ -331,7 +331,7 @@ EOF return 1 fi fi - + echo "Release update complete!" }