Release #5
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: Release | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read # jobs that need more declare it at job level | |
| jobs: | |
| build: | |
| name: ${{ matrix.asset_name }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # macOS Apple Silicon (native) | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| asset_name: plainapp-cli-macos-arm64 | |
| binary: plainapp-cli | |
| # macOS Intel (cross-compiled from Apple Silicon runner) | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| asset_name: plainapp-cli-macos-x86_64 | |
| binary: plainapp-cli | |
| # Linux x86_64 — fully static musl binary (no libc dependency) | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| asset_name: plainapp-cli-linux-x86_64 | |
| binary: plainapp-cli | |
| # Windows x86_64 | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| asset_name: plainapp-cli-windows-x86_64.exe | |
| binary: plainapp-cli.exe | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Install musl-tools (Linux static build) | |
| if: contains(matrix.target, 'musl') | |
| run: sudo apt-get install -y musl-tools | |
| - name: Cache Cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry/index | |
| ~/.cargo/registry/cache | |
| ~/.cargo/git/db | |
| key: ${{ matrix.target }}-cargo-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: ${{ matrix.target }}-cargo- | |
| - name: Build release binary | |
| run: cargo build --release --target ${{ matrix.target }} | |
| - name: Stage artifact | |
| shell: bash | |
| run: | | |
| cp target/${{ matrix.target }}/release/${{ matrix.binary }} ${{ matrix.asset_name }} | |
| - name: Upload artifact (for manual inspection) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.asset_name }} | |
| path: ${{ matrix.asset_name }} | |
| if-no-files-found: error | |
| # Collect all matrix binaries and compute hashes for SLSA provenance. | |
| hashes: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| hashes: ${{ steps.hash.outputs.hashes }} | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Compute SHA-256 hashes | |
| id: hash | |
| run: | | |
| echo "hashes=$(sha256sum artifacts/* | base64 -w0)" >> "$GITHUB_OUTPUT" | |
| # SLSA Level 3 provenance covers all release binaries. | |
| provenance: | |
| needs: hashes | |
| permissions: | |
| actions: read | |
| id-token: write | |
| contents: write | |
| uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 | |
| with: | |
| base64-subjects: ${{ needs.hashes.outputs.hashes }} | |
| upload-assets: false | |
| # Scans all four binaries concurrently to minimise wall-clock time. | |
| virustotal: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| arm64_report: ${{ steps.scan.outputs.arm64_report }} | |
| arm64_status: ${{ steps.scan.outputs.arm64_status }} | |
| x86_64_mac_report: ${{ steps.scan.outputs.x86_64_mac_report }} | |
| x86_64_mac_status: ${{ steps.scan.outputs.x86_64_mac_status }} | |
| linux_report: ${{ steps.scan.outputs.linux_report }} | |
| linux_status: ${{ steps.scan.outputs.linux_status }} | |
| windows_report: ${{ steps.scan.outputs.windows_report }} | |
| windows_status: ${{ steps.scan.outputs.windows_status }} | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Scan with VirusTotal | |
| id: scan | |
| env: | |
| VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} | |
| run: | | |
| upload_and_poll() { | |
| local file="$1" outprefix="$2" | |
| local sha256 size_bytes upload_url analysis_id last_response poll_status | |
| local malicious suspicious undetected harmless total detected result_status | |
| sha256=$(sha256sum "$file" | awk '{print $1}') | |
| size_bytes=$(stat -c%s "$file") | |
| upload_url="https://www.virustotal.com/api/v3/files" | |
| if [ "$size_bytes" -gt 33554432 ]; then | |
| upload_url=$(curl -sS --request GET \ | |
| --url https://www.virustotal.com/api/v3/files/upload_url \ | |
| --header "x-apikey: $VT_API_KEY" | jq -r '.data') | |
| if [ -z "$upload_url" ] || [ "$upload_url" = "null" ]; then | |
| printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url" | |
| printf '%s' "⬜ N/A" > "${outprefix}.status" | |
| return | |
| fi | |
| fi | |
| analysis_id=$(curl -sS --request POST \ | |
| --url "$upload_url" \ | |
| --header "x-apikey: $VT_API_KEY" \ | |
| --form "file=@$file" | jq -r '.data.id') | |
| if [ -z "$analysis_id" ] || [ "$analysis_id" = "null" ]; then | |
| printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url" | |
| printf '%s' "⬜ N/A" > "${outprefix}.status" | |
| return | |
| fi | |
| last_response="" | |
| for i in $(seq 1 30); do | |
| sleep 20 | |
| last_response=$(curl -sS \ | |
| --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \ | |
| --header "x-apikey: $VT_API_KEY") | |
| poll_status=$(echo "$last_response" | jq -r '.data.attributes.status') | |
| [ "$poll_status" = "completed" ] && break | |
| done | |
| malicious=$(echo "$last_response" | jq -r '.data.attributes.stats.malicious // 0') | |
| suspicious=$(echo "$last_response" | jq -r '.data.attributes.stats.suspicious // 0') | |
| undetected=$(echo "$last_response" | jq -r '.data.attributes.stats.undetected // 0') | |
| harmless=$(echo "$last_response" | jq -r '.data.attributes.stats.harmless // 0') | |
| total=$((malicious + suspicious + undetected + harmless)) | |
| detected=$((malicious + suspicious)) | |
| [ "$detected" -eq 0 ] \ | |
| && result_status="✅ ${detected}/${total} Clean" \ | |
| || result_status="⚠️ ${detected}/${total} Detected" | |
| printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url" | |
| printf '%s' "$result_status" > "${outprefix}.status" | |
| } | |
| upload_and_poll "artifacts/plainapp-cli-macos-arm64" /tmp/vt_arm64 & | |
| upload_and_poll "artifacts/plainapp-cli-macos-x86_64" /tmp/vt_x86_64_mac & | |
| upload_and_poll "artifacts/plainapp-cli-linux-x86_64" /tmp/vt_linux & | |
| upload_and_poll "artifacts/plainapp-cli-windows-x86_64.exe" /tmp/vt_windows & | |
| wait | |
| { | |
| echo "arm64_report=$(cat /tmp/vt_arm64.url)" | |
| echo "arm64_status=$(cat /tmp/vt_arm64.status)" | |
| echo "x86_64_mac_report=$(cat /tmp/vt_x86_64_mac.url)" | |
| echo "x86_64_mac_status=$(cat /tmp/vt_x86_64_mac.status)" | |
| echo "linux_report=$(cat /tmp/vt_linux.url)" | |
| echo "linux_status=$(cat /tmp/vt_linux.status)" | |
| echo "windows_report=$(cat /tmp/vt_windows.url)" | |
| echo "windows_status=$(cat /tmp/vt_windows.status)" | |
| } >> $GITHUB_OUTPUT | |
| release: | |
| name: Create GitHub Release | |
| needs: [provenance, virustotal] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Read version from Cargo.toml | |
| id: meta | |
| run: | | |
| VER=$(grep '^version' Cargo.toml | head -1 | awk -F '"' '{print $2}') | |
| echo "version=$VER" >> "$GITHUB_OUTPUT" | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ needs.provenance.outputs.provenance-name }} | |
| - name: Compute SHA-256 checksums | |
| run: | | |
| sha256sum artifacts/* ${{ needs.provenance.outputs.provenance-name }} > checksums.txt | |
| cat checksums.txt | |
| - name: Publish GitHub Release (draft) | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: v${{ steps.meta.outputs.version }} | |
| name: plainapp-cli v${{ steps.meta.outputs.version }} | |
| body: | | |
| ## Install | |
| **macOS / Linux — one-liner:** | |
| ```bash | |
| bash <(curl -fsSL https://raw.githubusercontent.com/plainhub/plainapp-cli/main/install.sh) | |
| ``` | |
| **Manual download** — pick the binary for your platform below, then: | |
| ```bash | |
| chmod +x plainapp-cli-* | |
| sudo mv plainapp-cli-* /usr/local/bin/plainapp-cli | |
| ``` | |
| ## Security | |
| ### VirusTotal Scan | |
| | File | Status | Scan Report | | |
| |------|--------|-------------| | |
| | `plainapp-cli-macos-arm64` | ${{ needs.virustotal.outputs.arm64_status }} | [View Report](${{ needs.virustotal.outputs.arm64_report }}) | | |
| | `plainapp-cli-macos-x86_64` | ${{ needs.virustotal.outputs.x86_64_mac_status }} | [View Report](${{ needs.virustotal.outputs.x86_64_mac_report }}) | | |
| | `plainapp-cli-linux-x86_64` | ${{ needs.virustotal.outputs.linux_status }} | [View Report](${{ needs.virustotal.outputs.linux_report }}) | | |
| | `plainapp-cli-windows-x86_64.exe` | ${{ needs.virustotal.outputs.windows_status }} | [View Report](${{ needs.virustotal.outputs.windows_report }}) | | |
| ### SLSA Provenance (Level 3) | |
| The `.intoto.jsonl` file is a signed SLSA provenance document covering all release binaries. | |
| Verify with [slsa-verifier](https://github.com/slsa-framework/slsa-verifier): | |
| ```sh | |
| slsa-verifier verify-artifact plainapp-cli-linux-x86_64 \ | |
| --provenance-path ${{ needs.provenance.outputs.provenance-name }} \ | |
| --source-uri github.com/${{ github.repository }} | |
| ``` | |
| ## Checksums (SHA-256) | |
| See `checksums.txt` attached below. | |
| draft: true | |
| prerelease: false | |
| artifacts: "artifacts/*,${{ needs.provenance.outputs.provenance-name }},checksums.txt" |