diff --git a/.forgejo/workflows/docker-rebuild.yml b/.forgejo/workflows/docker-rebuild.yml deleted file mode 100644 index 34cc3d6..0000000 --- a/.forgejo/workflows/docker-rebuild.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Rebuilds and re-pushes the `latest` image without a version bump so newly -# *fixed* Alpine / ffmpeg / Go patches land between tagged releases. Versioned -# tags are immutable and never touched here. Runs weekly and on demand. -name: Docker rebuild - -on: - schedule: - # Mondays 04:17 UTC (off the hour to avoid the scheduler rush) - - cron: "17 4 * * 1" - workflow_dispatch: - -jobs: - rebuild: - runs-on: docker - container: - image: docker.io/library/docker:27-cli - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install build deps - run: apk add --no-cache curl git bash - - - name: Install buildx - run: | - mkdir -p ~/.docker/cli-plugins - curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 \ - -o ~/.docker/cli-plugins/docker-buildx - chmod +x ~/.docker/cli-plugins/docker-buildx - - - name: Set up qemu - run: docker run --rm --privileged tonistiigi/binfmt --install all - - # Stamp the binary with the most recent release tag (not "dev"). - - name: Resolve version - id: ver - run: | - v=$(git describe --tags --abbrev=0 2>/dev/null || echo dev) - echo "version=$v" >> "$GITHUB_OUTPUT" - - - name: Login to Docker Hub - env: - DH_USER: ${{ secrets.DOCKERHUB_USERNAME }} - DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: echo "$DH_TOKEN" | docker login -u "$DH_USER" --password-stdin - - - name: Build + push (refresh latest) - env: - VERSION: ${{ steps.ver.outputs.version }} - run: | - docker buildx create --name builder --use --driver docker-container - # Refresh the floating tag only — never overwrite a versioned release. - # Force a fresh base pull so apk upgrade picks up new patches. - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg "VERSION=$VERSION" \ - --tag "torrentclaw/unarr:latest" \ - --no-cache \ - --push \ - . diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index 3c5a5cc..0000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - workflow_dispatch: - -permissions: - contents: write - -jobs: - release: - runs-on: docker - container: - image: docker.io/library/golang:1.25 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install build deps (bash, curl, jq, ffmpeg fetch deps) - run: | - apt-get update - apt-get install -y --no-install-recommends bash curl ca-certificates jq xz-utils unzip - - - name: Install goreleaser - run: | - curl -sSfL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz \ - | tar -xz -C /usr/local/bin goreleaser - - - name: Run goreleaser - env: - # Forgejo runner injects GITHUB_TOKEN — but goreleaser uses it to talk to - # the *Forgejo* API thanks to the gitea_urls override in .goreleaser.yml. - GITEA_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 }} - run: goreleaser release --clean - - - name: Sign checksums.txt with ed25519 - if: ${{ vars.RELEASE_SIGNING_PUBKEY != '' && secrets.RELEASE_SIGNING_KEY != '' }} - env: - RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} - RELEASE_TAG: ${{ github.ref_name }} - FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Tailscale IP — domain-agnostic; the runner shares the dokploy-network with - # forgejo (hostname `forgejo`), so the in-cluster hostname is fastest, but the - # Tailscale IP is the documented fallback. - FORGEJO_API: http://forgejo:3000/api/v1 - REPO: deivid/unarr - run: | - set -euo pipefail - go run ./scripts/sign-checksums \ - -key "$RELEASE_SIGNING_KEY" \ - -in dist/checksums.txt \ - -out dist/checksums.txt.sig - - # Find the release ID for this tag, then upload the sig as an asset. - rel_id=$(curl -sSf "$FORGEJO_API/repos/$REPO/releases/tags/$RELEASE_TAG" \ - -H "Authorization: token $FORGEJO_TOKEN" | jq -r '.id') - curl -sSf -X POST \ - "$FORGEJO_API/repos/$REPO/releases/$rel_id/assets?name=checksums.txt.sig" \ - -H "Authorization: token $FORGEJO_TOKEN" \ - -F "attachment=@dist/checksums.txt.sig" - - docker: - needs: release - runs-on: docker - container: - # Docker-in-Docker capable image — buildx + qemu pre-installed. - image: docker.io/library/docker:27-cli - steps: - - uses: actions/checkout@v4 - - - name: Install buildx - run: | - apk add --no-cache curl - mkdir -p ~/.docker/cli-plugins - curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 \ - -o ~/.docker/cli-plugins/docker-buildx - chmod +x ~/.docker/cli-plugins/docker-buildx - - - name: Login to Docker Hub - env: - DH_USER: ${{ secrets.DOCKERHUB_USERNAME }} - DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: echo "$DH_TOKEN" | docker login -u "$DH_USER" --password-stdin - - - name: Set up qemu - run: docker run --rm --privileged tonistiigi/binfmt --install all - - - name: Build + push multi-arch image - env: - VERSION: ${{ github.ref_name }} - run: | - set -euo pipefail - VERSION_SEMVER="${VERSION#v}" - MAJOR_MINOR="${VERSION_SEMVER%.*}" - docker buildx create --name builder --use --driver docker-container - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg "VERSION=$VERSION" \ - --tag "torrentclaw/unarr:$VERSION_SEMVER" \ - --tag "torrentclaw/unarr:$MAJOR_MINOR" \ - --tag "torrentclaw/unarr:latest" \ - --push \ - . diff --git a/.forgejo/workflows/ci.yml b/.github/workflows/ci.yml similarity index 61% rename from .forgejo/workflows/ci.yml rename to .github/workflows/ci.yml index 82ee799..7dabcc4 100644 --- a/.forgejo/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,26 +12,35 @@ permissions: jobs: test: name: Test - runs-on: docker - container: - image: docker.io/library/golang:1.25 + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ["1.25"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go-version }} - name: Run tests run: go test -v -race -count=1 ./... build: name: Build - runs-on: docker - container: - image: docker.io/library/golang:1.25 + runs-on: ubuntu-latest strategy: matrix: goos: [linux, darwin, windows] goarch: [amd64, arm64] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" - name: Build env: @@ -41,30 +50,30 @@ jobs: lint: name: Lint - runs-on: docker - container: - image: docker.io/library/golang:1.25 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install golangci-lint - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v2.11.4/install.sh \ - | sh -s -- -b /usr/local/bin v2.11.4 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" - name: Run golangci-lint - run: golangci-lint run ./... + uses: golangci/golangci-lint-action@v9 + with: + version: v2.11.4 coverage: name: Coverage - runs-on: docker - container: - image: docker.io/library/golang:1.25 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install python3 - run: apt-get update && apt-get install -y --no-install-recommends python3 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" - name: Run tests with coverage (all packages) run: | @@ -93,13 +102,24 @@ jobs: print('OK: Coverage meets minimum threshold') " + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v6 + with: + files: ./coverage.out + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + vet: name: Vet - runs-on: docker - container: - image: docker.io/library/golang:1.25 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" - name: Run go vet run: go vet ./... diff --git a/.github/workflows/docker-rebuild.yml b/.github/workflows/docker-rebuild.yml new file mode 100644 index 0000000..c1634f1 --- /dev/null +++ b/.github/workflows/docker-rebuild.yml @@ -0,0 +1,52 @@ +# Rebuilds and re-pushes the `latest` image without a version bump so newly +# *fixed* Alpine / ffmpeg / Go patches land between tagged releases. Versioned +# tags are immutable and never touched here. Runs weekly and on demand. +name: Docker rebuild + +on: + schedule: + # Mondays 04:17 UTC (off the hour to avoid the scheduler rush) + - cron: "17 4 * * 1" + workflow_dispatch: + +jobs: + rebuild: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # Stamp the binary with the most recent release tag (not "dev"). + - name: Resolve version + id: ver + run: echo "version=$(git describe --tags --abbrev=0 2>/dev/null || echo dev)" >> "$GITHUB_OUTPUT" + + - 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 + # Refresh the floating tag only — never overwrite a versioned release. + tags: torrentclaw/unarr:latest + build-args: | + VERSION=${{ steps.ver.outputs.version }} + # Force a fresh base pull so apk upgrade picks up new patches. + no-cache: true + + - 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 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..d0c683d --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,52 @@ +name: Deploy install scripts to Pages + +on: + push: + branches: [main] + paths: + - install.sh + - install.ps1 + - CNAME + - .nojekyll + - .github/workflows/pages.yml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + - name: Stage install scripts + run: | + mkdir -p _site + cp install.sh install.ps1 _site/ + [ -f CNAME ] && cp CNAME _site/ + touch _site/.nojekyll + # Also index page (humans landing) + cat > _site/index.html <<'HTML' + +
Linux/macOS: curl -fsSL https://unarr.torrentclaw.com/install.sh | sh + Windows: irm https://unarr.torrentclaw.com/install.ps1 | iex+
Source: github.com/torrentclaw/unarr
+ + HTML + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dcb49ce --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,210 @@ +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" diff --git a/.goreleaser.yml b/.goreleaser.yml index 099f55f..26ce802 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -59,18 +59,6 @@ changelog: - "^test:" - "^chore:" -# Self-hosted Forgejo at git.torrentclaw.com. goreleaser detects GITEA_TOKEN + -# these URLs and publishes the release there instead of GitHub. Reachable via -# `forgejo` hostname inside the dokploy-network (the runner shares it); for -# local goreleaser runs outside the network, override via env GITEA_API_URL. -release: - gitea_urls: - api: http://forgejo:3000/api/v1 - download: https://git.torrentclaw.com - skip_tls_verify: false - draft: false - prerelease: auto - # Homebrew tap — requires PAT with repo scope (not GITHUB_TOKEN) # Enable when torrentclaw/homebrew-tap PAT is configured as HOMEBREW_TAP_TOKEN # brews: diff --git a/CHANGELOG.md b/CHANGELOG.md index c8681bf..3d75ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,21 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.9.13] - 2026-05-27 - -### Added - -- **Session-ready webhook** (`/api/internal/agent/session-ready`). Daemon - watches every new HLSSession's segment counter and, the moment seg-0 + - init.mp4 land on disk, POSTs the sessionId to the server. The web side - flips `streaming_session.ready_at = NOW()`, which its new SSE endpoint - pushes to subscribed players so the "Preparando…" UI flips to - "Stream listo" without waiting for the player's HEAD-probe retry loop - to discover it. Cache-HIT sessions fire the webhook immediately on - StartHLSSession return. -- `engine.HLSSession.ReadyCount()` + `FromCache()` accessors so the - ready-watcher goroutine doesn't reach into private state. - ## [0.9.12] - 2026-05-27 ### Added diff --git a/internal/agent/client.go b/internal/agent/client.go index e7f2c37..e60b0a4 100644 --- a/internal/agent/client.go +++ b/internal/agent/client.go @@ -109,27 +109,6 @@ func (c *Client) ReportUpgradeResult(ctx context.Context, agentID string, succes return nil } -// MarkSessionReady signals the server that the first HLS segment + init.mp4 -// landed on disk for the given session. The web side flips -// streaming_session.ready_at = NOW(), which its SSE endpoint emits to -// subscribed players so the "Preparando…" UI ends without polling HEAD -// on /hls/