name: Release on: push: tags: - "v*" permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-go@v6 with: go-version-file: go.mod - uses: goreleaser/goreleaser-action@v6 with: version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} # Empty when RELEASE_SIGNING_PUBKEY variable is unset — goreleaser # accepts it and the resulting binary disables signature checks # (back-compat: pre-signing releases continue to update). Set # RELEASE_SIGNING_PUBKEY (variable) + RELEASE_SIGNING_KEY (secret) # to turn verification on. RELEASE_SIGNING_PUBKEY: ${{ vars.RELEASE_SIGNING_PUBKEY }} - name: Sign checksums.txt with ed25519 # Reference secrets.X directly — step-level env defined in this same # step is unreliable to read from this step's own if: expression. if: ${{ vars.RELEASE_SIGNING_PUBKEY != '' && secrets.RELEASE_SIGNING_KEY != '' }} env: RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} RELEASE_TAG: ${{ github.ref_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail go run ./scripts/sign-checksums \ -key "$RELEASE_SIGNING_KEY" \ -in dist/checksums.txt \ -out dist/checksums.txt.sig gh release upload "$RELEASE_TAG" dist/checksums.txt.sig --clobber docker: needs: release runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: torrentclaw/unarr tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest - uses: docker/setup-qemu-action@v4 - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/build-push-action@v7 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | VERSION=${{ github.ref_name }} # CVE gate. Fails the release on FIXABLE critical/high only — unfixed # upstream ffmpeg codec CVEs are accepted (see SECURITY.md), so the # codec noise does not block. Runs post-push (image already published); # a failure here flags that a fixable CVE slipped through. - name: Scan image for fixable CVEs (gate) uses: docker/scout-action@v1 with: command: cves image: torrentclaw/unarr:latest only-severities: critical,high only-fixed: true exit-code: true # Sync the Docker Hub repo description from DOCKERHUB.md. Non-fatal: a # description-API auth hiccup must not undo a successful image push. - name: Update Docker Hub description uses: peter-evans/dockerhub-description@v4 continue-on-error: true with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: torrentclaw/unarr readme-filepath: ./DOCKERHUB.md short-description: "unarr — the single binary that replaces your *arr stack" virustotal: needs: release runs-on: ubuntu-latest if: vars.VT_ENABLED == 'true' steps: - name: Get release tag id: tag run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Download release assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | mkdir -p assets gh release download "${{ steps.tag.outputs.tag }}" \ --repo "${{ github.repository }}" \ --dir assets \ --pattern '*.tar.gz' \ --pattern '*.zip' \ --pattern 'checksums.txt' - name: Scan assets with VirusTotal env: VT_API_KEY: ${{ secrets.VT_API_KEY }} run: | mkdir -p results for file in assets/*; do filename=$(basename "$file") echo "Uploading $filename to VirusTotal..." response=$(curl -s --request POST \ --url https://www.virustotal.com/api/v3/files \ --header "x-apikey: $VT_API_KEY" \ --form "file=@$file") analysis_id=$(echo "$response" | jq -r '.data.id // empty') if [ -z "$analysis_id" ]; then echo "::warning::Failed to upload $filename: $response" continue fi echo "$filename=$analysis_id" >> results/scans.txt echo " Analysis ID: $analysis_id" # Rate limit: VT free tier allows 4 req/min sleep 16 done - name: Wait for analysis completion env: VT_API_KEY: ${{ secrets.VT_API_KEY }} run: | echo "Waiting 60s for VirusTotal analysis to complete..." sleep 60 vt_report="## 🛡️ VirusTotal Scan Results\n\n" vt_report+="| File | Result | Link |\n" vt_report+="|------|--------|------|\n" while IFS='=' read -r filename analysis_id; do result=$(curl -s --request GET \ --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \ --header "x-apikey: $VT_API_KEY") malicious=$(echo "$result" | jq -r '.data.attributes.stats.malicious // 0') undetected=$(echo "$result" | jq -r '.data.attributes.stats.undetected // 0') sha256=$(echo "$result" | jq -r '.meta.file_info.sha256 // empty') if [ "$malicious" = "0" ]; then status="✅ Clean ($undetected engines)" else status="⚠️ $malicious detections" fi link="https://www.virustotal.com/gui/file/$sha256" vt_report+="| \`$filename\` | $status | [View]($link) |\n" sleep 16 done < results/scans.txt echo -e "$vt_report" > results/report.md cat results/report.md - name: Append scan results to release notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | current_body=$(gh release view "${{ steps.tag.outputs.tag }}" \ --repo "${{ github.repository }}" \ --json body --jq '.body') new_body="${current_body} $(cat results/report.md)" gh release edit "${{ steps.tag.outputs.tag }}" \ --repo "${{ github.repository }}" \ --notes "$new_body"