From 729d28fc0d5a950bed1da463ece62508a0dfdce3 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 26 Aug 2026 22:52:16 -0700 Subject: [PATCH 01/41] Replace Makefile releases with GoReleaser Pro. Nightly pushes to unstable publish Hub/GHCR :unstable without moving latest; tags still cut GitHub releases, AUR, brew, and signed packages. Co-authored-by: Cursor --- .github/workflows/release.yml | 343 ++++++------------------ .gitignore | 1 + .goreleaser.yaml | 437 +++++++++++++++++++++++++++++++ Makefile | 422 ----------------------------- init/archlinux/PKGBUILD.template | 70 ----- init/archlinux/README.md | 2 - init/archlinux/SRCINFO.template | 25 -- init/archlinux/aur-deploy.sh | 48 ---- init/docker/Dockerfile | 62 +---- init/docker/hooks/build | 50 ---- init/docker/hooks/pre_build | 6 - init/docker/hooks/push | 2 - init/docker/makedocker.sh | 19 -- init/macos/Info.plist.tmpl | 40 +++ init/macos/makedmg.sh | 41 --- init/macos/notarize.json | 7 - init/macos/notarize.sh | 80 ------ init/macos/sign.json | 6 - init/windows/signexe.sh | 19 +- scripts/unstable_upload.sh | 30 +++ settings.sh | 33 --- 21 files changed, 598 insertions(+), 1145 deletions(-) create mode 100644 .goreleaser.yaml delete mode 100644 Makefile delete mode 100644 init/archlinux/PKGBUILD.template delete mode 100644 init/archlinux/README.md delete mode 100644 init/archlinux/SRCINFO.template delete mode 100644 init/archlinux/aur-deploy.sh delete mode 100755 init/docker/hooks/build delete mode 100644 init/docker/hooks/pre_build delete mode 100644 init/docker/hooks/push delete mode 100755 init/docker/makedocker.sh create mode 100644 init/macos/Info.plist.tmpl delete mode 100755 init/macos/makedmg.sh delete mode 100644 init/macos/notarize.json delete mode 100755 init/macos/notarize.sh delete mode 100644 init/macos/sign.json create mode 100755 scripts/unstable_upload.sh delete mode 100644 settings.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73d87324..aabda7b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,290 +5,99 @@ on: - unstable tags: - v* - pull_request: - branches: - - main permissions: - contents: read + contents: write + packages: write jobs: - macapp-test: - # description: "Builds and signs a macOS app then packages it in a notarized DMG." - if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/unstable' - name: Make macOS DMG - runs-on: macos-latest - steps: - - uses: actions/checkout@v7 - with: - # we need the whole thing so we can count commits. - fetch-depth: '0' - - uses: actions/setup-go@v7 - with: - go-version-file: 'go.mod' - - name: go-generate - run: go generate ./... - - name: make-signdmg - env: - APPLE_SIGNING_KEY: ${{ secrets.APPLE_SIGNING_KEY }} - AC_USERNAME: ${{ secrets.AC_USERNAME }} - AC_PASSWORD: ${{ secrets.AC_PASSWORD }} - AC_PROVIDER: ${{ secrets.AC_PROVIDER }} - id: release - run: | - brew install Bearer/tap/gon - make signdmg - - name: upload artifacts - uses: actions/upload-artifact@v7 - with: - name: dmg-release - path: release - - release-test: - # description: "Builds all the application client binaries and packages for a release." - if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/unstable' || github.ref == 'refs/heads/main' - outputs: - version: ${{ steps.release.outputs.version }} - name: Make Release Assets + release: runs-on: ubuntu-latest permissions: - contents: read + contents: write + packages: write id-token: write env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} # Map first: secrets cannot be referenced in steps.if. CODESIGN_URL: ${{ secrets.CODESIGN_URL }} steps: - - uses: actions/checkout@v7 - with: - # we need the whole thing so we can count commits. - fetch-depth: '0' - - uses: actions/setup-go@v7 - with: - go-version-file: 'go.mod' - - name: make-release - id: release - env: - # Job-level CODESIGN_URL is only for the codesign step's if. - # Make must not see it: signexe.sh would fail before the Action installs the CLI. - CODESIGN_URL: "" - run: | - sudo apt install -y rpm fakeroot zip debsigs gnupg jq libarchive-tools - sudo gem install --no-document fpm - echo "${GPG_SIGNING_KEY}" | gpg --import - - make release WINDOWS_ZIP=0 - source settings.sh ; echo "version=${VERSION}-${ITERATION}" >> $GITHUB_OUTPUT - - uses: golift/codesign@v1 - if: env.CODESIGN_URL != '' - with: - files: unpackerr.amd64.exe - url: ${{ secrets.CODESIGN_URL }} - client-cert: ${{ secrets.CODESIGN_CLIENT_CERT }} - client-key: ${{ secrets.CODESIGN_CLIENT_KEY }} - name: Unpackerr - website: https://unpackerr.zip - - name: Zip Windows exe - run: | - make windows_zip - rm -f release/checksums.sha256.txt - openssl dgst -r -sha256 release/* | sed 's#release/##' | tee release/checksums.sha256.txt - - name: upload artifacts - uses: actions/upload-artifact@v7 - with: - name: release - path: release - - deploy-unstable-unstable: - # description: "Uploads pre-built binaries to unstable.golift.io." - if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/unstable' - strategy: - matrix: - files: [release, dmg-release] - needs: - - release-test - - macapp-test - name: Deploy Unstable.app - runs-on: ubuntu-latest - steps: - - name: "Download files: ${{ matrix.files }}" - uses: actions/download-artifact@v8 - with: - name: ${{ matrix.files }} - - name: Upload files to unstable.golift.io - run: >- - set -euo pipefail; - for file in *.{zip,dmg,gz}; do - [ -f "$file" ] || continue; - echo "Uploading: ${file}"; - curl -sS --fail-with-body --retry 5 --retry-all-errors --retry-delay 2 - -H "X-API-KEY: ${{ secrets.UNSTABLE_UPLOAD_KEY }}" - "https://unstable.golift.io/upload.php?folder=unpackerr" -F "file=@${file}"; - versionfile="${{needs.release-test.outputs.version}};filename=${file}.txt;type=text/plain"; - curl -sS --fail-with-body --retry 5 --retry-all-errors --retry-delay 2 - -H "X-API-KEY: ${{ secrets.UNSTABLE_UPLOAD_KEY }}" - "https://unstable.golift.io/upload.php?folder=unpackerr" -F "file=${versionfile}"; - done - - deploy-unstable-packagecloud: - # description: "Uploads pre-built RPM and DEB packages to packagecloud.io/golift" - if: github.ref == 'refs/heads/unstable' - needs: release-test - name: Deploy Unstable PackageCloud - runs-on: ubuntu-latest - steps: - - name: Download release files - uses: actions/download-artifact@v8 - with: - name: release - - uses: golift/upload-packagecloud@v1 - with: - userrepo: golift/unstable - apitoken: ${{ secrets.PACKAGECLOUD_TOKEN }} - packages: . - rpmdists: el/6 - debdists: ubuntu/focal - - deploy-packagecloud: - # description: "Uploads pre-built RPM and DEB packages to packagecloud.io/golift" - if: startsWith(github.ref, 'refs/tags/v') - needs: release-test - name: Deploy Release PackageCloud - runs-on: ubuntu-latest - steps: - - name: Download release files - uses: actions/download-artifact@v8 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - name: release - - uses: golift/upload-packagecloud@v1 - with: - userrepo: golift/pkgs - apitoken: ${{ secrets.PACKAGECLOUD_TOKEN }} - packages: . - rpmdists: el/6 - debdists: ubuntu/focal - - deploy-github-release: - # description: uploads all the built release assets to the GitHub Release. - if: startsWith(github.ref, 'refs/tags/v') - permissions: - contents: write - needs: [release-test, macapp-test] - strategy: - matrix: - files: [release, dmg-release] - name: Deploy GitHub Release - runs-on: ubuntu-latest - steps: - - name: Download ${{ matrix.files }} Files - uses: actions/download-artifact@v8 - with: - name: ${{ matrix.files }} - - name: Publish ${{ matrix.files }} artifacts to github - uses: softprops/action-gh-release@v3 - with: - files: | - *.rpm - *.deb - *.txz - *.zip - *.dmg - *.gz - *.txt - *.zst - *.sig - - archlinux-aur: - # description: creates and uploads and aur file to the golift/aur repo for arch linux. - needs: deploy-github-release - name: Deploy ArchLinux AUR - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: '0' - - name: Deploy Arch AUR - run: bash init/archlinux/aur-deploy.sh - env: - DEPLOY_KEY: ${{ secrets.AUR_DEPLOY_KEY }} - - docker-ghcr-build-push: - if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/unstable' - name: Deploy GHCR Container - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v7 - + fetch-depth: 0 + - run: git fetch --force --tags + - name: Install package tooling + run: sudo apt-get update && sudo apt-get install -y rpm genisoimage - name: Set up QEMU uses: docker/setup-qemu-action@v4 with: - platforms: 'arm64' - - # Workaround: https://github.com/docker/build-push-action/issues/461 - - name: Setup Docker buildx + platforms: arm64,arm + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ghcr.io - if: github.event_name != 'pull_request' + - name: Log into GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v6 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern=v{{version}} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=ref,enable=true,event=branch - - - name: Set docker build-args - run: >- - git fetch --force --tags; - source ./settings.sh; - for item in DATE COMMIT VERSION ITERATION BRANCH LICENSE DESC VENDOR MAINT SOURCE_URL; do - eval echo "settings.sh build-arg: ${item}=\$${item}"; - eval echo "${item}=\$${item}" >> $GITHUB_ENV; - done - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v7 + - name: Log into Docker Hub + uses: docker/login-action@v4 with: - platforms: |- - linux/amd64 - linux/arm64 - context: . - file: init/docker/Dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - BUILD_DATE=${{ env.DATE }} - COMMIT=${{ env.COMMIT }} - VERSION=${{ env.VERSION }} - ITERATION=${{ env.ITERATION }} - BRANCH=${{ env.BRANCH }} - LICENSE=${{ env.LICENSE }} - DESC=${{ env.DESC }} - VENDOR=${{ env.VENDOR }} - AUTHOR=${{ env.MAINT }} - SOURCE_URL=${{ env.SOURCE_URL }} + username: golift + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} + - name: Create GPG_SIGNING_KEY file + run: echo '${{ secrets.GPG_SIGNING_KEY }}' > /tmp/key.gpg + - name: Save iteration in Revision + run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + - name: Install golift codesign + env: + GOBIN: ${{ runner.temp }}/codesign-bin + run: | + set -euo pipefail + mkdir -p "${GOBIN}" + go install golift.io/codesign/cmd/codesign@v1.0.3 + echo "${GOBIN}" >> "${GITHUB_PATH}" + echo "CODESIGN_BIN=${GOBIN}/codesign" >> "${GITHUB_ENV}" + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser-pro + version: "~> v2" + args: ${{ github.ref == 'refs/heads/unstable' && 'release --nightly --clean --timeout 60m' || 'release --clean --timeout 60m' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + USER: github-actions + GPG_SIGNING_KEY: /tmp/key.gpg + GPG_SIGNING_KEY_ID: ${{ steps.import_gpg.outputs.keyid }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + CODESIGN_URL: ${{ secrets.CODESIGN_URL }} + CODESIGN_CLIENT_CERT: ${{ secrets.CODESIGN_CLIENT_CERT }} + CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} + CODESIGN_NAME: Unpackerr + CODESIGN_WEBSITE: https://unpackerr.zip + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} + AUR_DEPLOY_KEY: ${{ secrets.AUR_DEPLOY_KEY }} + - uses: golift/upload-packagecloud@eb0067369b59340b04dce218010960ebe74720b2 # v1.1.1 + with: + userrepo: ${{ github.ref == 'refs/heads/unstable' && 'golift/unstable' || 'golift/pkgs' }} + apitoken: ${{ secrets.PACKAGECLOUD_TOKEN }} + packages: dist/ + rpmdists: el/6 + debdists: ubuntu/focal + - name: Upload to unstable.golift.io + env: + UNSTABLE_UPLOAD_KEY: ${{ secrets.UNSTABLE_UPLOAD_KEY }} + VERSION: ${{ github.ref_name }} + run: bash scripts/unstable_upload.sh dist diff --git a/.gitignore b/.gitignore index 5eb8438c..bd1beb1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /unpackerr +/dist/ # JetBrains IDEs: GoLand, IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm .idea/* diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..e35e3187 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,437 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json +version: 2 +pro: true + +project_name: unpackerr + +# Nightly (push to unstable): version is for packages/ldflags; Docker tag stays `unstable`. +nightly: + version_template: "{{ incpatch .Version }}-unstable" + +before: + hooks: + - go mod tidy + - go generate ./... + - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}-{{.Env.REVISION}}" --date "{{.Date}}" examples/MANUAL.md + - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}-{{.Env.REVISION}}" --date "{{.Date}}" README.md + - gzip -9nf examples/MANUAL + - mv -f examples/MANUAL.gz unpackerr.1.gz + - go run github.com/akavel/rsrc@latest -arch amd64 -ico init/windows/application.ico -manifest init/windows/manifest.xml -o rsrc_windows_amd64.syso + +builds: + - id: unpackerr + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm + - arm64 + - '386' + goarm: + - '6' + - '7' + flags: + - -trimpath + tags: + - osusergo + - netgo + ldflags: + - -s -w + - -X "golift.io/version.Version={{.Version}}" + - -X "golift.io/version.BuildDate={{.Date}}" + - -X "golift.io/version.BuildUser={{.Env.USER}}" + - -X "golift.io/version.Revision={{.Env.REVISION}}" + - -X "golift.io/version.Branch={{.ShortCommit}} [{{.Branch}}]" + - id: unpackerr-darwin + env: + - CGO_ENABLED=0 + - CGO_LDFLAGS=-mmacosx-version-min=10.8 + - CGO_CFLAGS=-mmacosx-version-min=10.8 + goos: + - darwin + goarch: + - amd64 + - arm64 + flags: + - -trimpath + tags: + - osusergo + - netgo + ldflags: + - -s -w + - -X "golift.io/version.Version={{.Version}}" + - -X "golift.io/version.BuildDate={{.Date}}" + - -X "golift.io/version.BuildUser={{.Env.USER}}" + - -X "golift.io/version.Revision={{.Env.REVISION}}" + - -X "golift.io/version.Branch={{.ShortCommit}} [{{.Branch}}]" + - id: unpackerr-freebsd + env: + - CGO_ENABLED=0 + binary: usr/local/bin/unpackerr + goos: + - freebsd + goarch: + - amd64 + - arm + - arm64 + - '386' + goarm: + - '6' + - '7' + flags: + - -trimpath + tags: + - osusergo + - netgo + ldflags: + - -s -w + - -X "golift.io/version.Version={{.Version}}" + - -X "golift.io/version.BuildDate={{.Date}}" + - -X "golift.io/version.BuildUser={{.Env.USER}}" + - -X "golift.io/version.Revision={{.Env.REVISION}}" + - -X "golift.io/version.Branch={{.ShortCommit}} [{{.Branch}}]" + - id: unpackerr-windows + env: + - CGO_ENABLED=0 + goos: + - windows + goarch: + - amd64 + flags: + - -trimpath + tags: + - osusergo + - netgo + ldflags: + - -s -w + - -H=windowsgui + - -X "golift.io/version.Version={{.Version}}" + - -X "golift.io/version.BuildDate={{.Date}}" + - -X "golift.io/version.BuildUser={{.Env.USER}}" + - -X "golift.io/version.Revision={{.Env.REVISION}}" + - -X "golift.io/version.Branch={{.ShortCommit}} [{{.Branch}}]" + hooks: + post: + - cmd: bash init/windows/signexe.sh "{{ .Path }}" + +universal_binaries: + - id: unpackerr-darwin + ids: [unpackerr-darwin] + replace: true + name_template: unpackerr + +# Sign + notarize darwin (quill; works on Linux CI). +# Enabled only when MACOS_SIGN_P12 is set so local snapshots still work. +notarize: + macos: + - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' + ids: + - unpackerr-darwin + sign: + certificate: "{{ .Env.MACOS_SIGN_P12 }}" + password: "{{ .Env.MACOS_SIGN_PASSWORD }}" + notarize: + issuer_id: "{{ .Env.MACOS_NOTARY_ISSUER_ID }}" + key_id: "{{ .Env.MACOS_NOTARY_KEY_ID }}" + key: "{{ .Env.MACOS_NOTARY_KEY }}" + wait: true + timeout: 20m + +app_bundles: + - id: unpackerr-app + name: Unpackerr + ids: [unpackerr-darwin] + icon: init/macos/Unpackerr.app/Contents/Resources/Unpackerr.icns + bundle: io.golift.unpackerr + extra_files: + - src: init/macos/Unpackerr.app/Contents/Resources/terminal-notifier.app + dst: Contents/Resources/terminal-notifier.app + templated_extra_files: + - src: init/macos/Info.plist.tmpl + dst: Contents/Info.plist + +dmg: + - id: unpackerr-dmg + name: Unpackerr + ids: [unpackerr-app] + use: appbundle + extra_files: + - src: init/macos/background.png + dst: background.png + +source: + enabled: true + name_template: '{{ .ProjectName }}-{{ .Version }}' + prefix_template: '{{ .ProjectName }}-{{ .Version }}/' + +archives: + - id: default + ids: [unpackerr, unpackerr-darwin, unpackerr-windows] + formats: [tar.gz] + wrap_in_directory: true + files: + - LICENSE + - README.md + - src: examples/unpackerr.conf.example + dst: unpackerr.conf.example + - src: unpackerr.1.gz + dst: unpackerr.1.gz + - src: examples/MANUAL.html + dst: unpackerr-manual.html + info: + mode: 0644 + format_overrides: + - goos: windows + formats: [zip] + - id: freebsd + ids: [unpackerr-freebsd] + formats: [tar.xz] + wrap_in_directory: false + files: + - src: examples/unpackerr.conf.example + dst: usr/local/etc/unpackerr/unpackerr.conf.example + strip_parent: true + - src: init/bsd/freebsd.rc.d + dst: usr/local/etc/rc.d/unpackerr + strip_parent: true + info: + mode: 0755 + - src: unpackerr.1.gz + dst: usr/local/share/man/man1/unpackerr.1.gz + strip_parent: true + - src: LICENSE + dst: usr/local/share/doc/unpackerr/LICENSE + strip_parent: true + +nfpms: + - id: unpackerr-packages + ids: [unpackerr] + vendor: Go Lift + homepage: https://unpackerr.zip + maintainer: David Newhall II + description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. + license: MIT + formats: + - deb + - rpm + - archlinux + bindir: /usr/bin + version_metadata: git + section: default + priority: extra + provides: + - unpackerr + rpm: + signature: + key_file: "{{ .Env.GPG_SIGNING_KEY }}" + deb: + signature: + key_file: "{{ .Env.GPG_SIGNING_KEY }}" + type: origin + scripts: + preinstall: init/systemd/before-install.sh + postinstall: init/systemd/after-install.sh + preremove: init/systemd/before-remove.sh + contents: + - dst: /etc/unpackerr + type: dir + file_info: + mode: 0755 + - src: examples/unpackerr.conf.example + dst: /etc/unpackerr/unpackerr.conf + type: config|noreplace + file_info: + mode: 0644 + - src: examples/unpackerr.conf.example + dst: /etc/unpackerr/unpackerr.conf.example + type: config + file_info: + mode: 0644 + - src: unpackerr.1.gz + dst: /usr/share/man/man1/unpackerr.1.gz + file_info: + mode: 0644 + - src: LICENSE + dst: /usr/share/doc/unpackerr/LICENSE + file_info: + mode: 0644 + - src: examples/MANUAL.html + dst: /usr/share/doc/unpackerr/unpackerr-manual.html + file_info: + mode: 0644 + - src: init/systemd/unpackerr.service + dst: /usr/lib/systemd/system/unpackerr.service + type: config + file_info: + mode: 0644 + +dockers_v2: + - id: unpackerr-release + disable: "{{ .IsNightly }}" + dockerfile: init/docker/Dockerfile + ids: [unpackerr] + images: + - ghcr.io/unpackerr/unpackerr + - docker.io/golift/unpackerr + tags: + - latest + - "{{ .Version }}" + - "v{{ .Version }}" + - "{{ .Major }}.{{ .Minor }}" + - "{{ .Major }}" + platforms: + - linux/amd64 + - linux/arm64 + - linux/arm/v7 + labels: + org.opencontainers.image.created: "{{ .Date }}" + org.opencontainers.image.title: Unpackerr + org.opencontainers.image.revision: "{{ .FullCommit }}" + org.opencontainers.image.version: "{{ .Version }}" + org.opencontainers.image.documentation: https://unpackerr.zip/docs/install/docker + org.opencontainers.image.description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. + org.opencontainers.image.url: https://unpackerr.zip + org.opencontainers.image.source: "{{ .GitURL }}" + org.opencontainers.image.vendor: Go Lift + org.opencontainers.image.licenses: MIT + - id: unpackerr-nightly + disable: "{{ not .IsNightly }}" + dockerfile: init/docker/Dockerfile + ids: [unpackerr] + images: + - ghcr.io/unpackerr/unpackerr + - docker.io/golift/unpackerr + tags: + - unstable + platforms: + - linux/amd64 + - linux/arm64 + - linux/arm/v7 + labels: + org.opencontainers.image.created: "{{ .Date }}" + org.opencontainers.image.title: Unpackerr + org.opencontainers.image.revision: "{{ .FullCommit }}" + org.opencontainers.image.version: "{{ .Version }}" + org.opencontainers.image.documentation: https://unpackerr.zip/docs/install/docker + org.opencontainers.image.description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. + org.opencontainers.image.url: https://unpackerr.zip + org.opencontainers.image.source: "{{ .GitURL }}" + org.opencontainers.image.vendor: Go Lift + org.opencontainers.image.licenses: MIT + +aur_sources: + - name: unpackerr + disable: "{{ .IsNightly }}" + homepage: https://unpackerr.zip + description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. + license: MIT + git_url: ssh://aur@aur.archlinux.org/unpackerr.git + private_key: "{{ .Env.AUR_DEPLOY_KEY }}" + commit_author: + name: goreleaserbot + email: bot@goreleaser.com + commit_msg_template: "Update unpackerr to {{ .Tag }}" + maintainers: + - David Newhall II + - Donald Webster + provides: + - unpackerr + makedepends: + - go + - gzip + optdepends: + - 'transmission-cli: torrent downloader (CLI and daemon)' + - 'transmission-gtk: torrent downloader (GTK+)' + - 'transmission-qt: torrent downloader (Qt)' + - 'deluge: torrent downloader' + - 'rtorrent: torrent downloader' + backup: + - etc/unpackerr/unpackerr.conf + arches: + - x86_64 + - aarch64 + - arm + - armv6h + - armv7h + - i686 + prepare: | + cd "${pkgname}-${pkgver}" + mkdir -p build + build: | + cd "${pkgname}-${pkgver}" + export GOFLAGS="-buildmode=pie -trimpath -modcacherw" + go build -o unpackerr -ldflags "-w -s -X golift.io/version.Version=${pkgver} -X golift.io/version.Revision=${pkgrel}" . + go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "$(date -u +%Y-%m-%d)" examples/MANUAL.md + gzip -9nf examples/MANUAL + mv examples/MANUAL.gz unpackerr.1.gz + package: | + cd "${pkgname}-${pkgver}" + install -d -m 755 "${pkgdir}/usr/share/licenses/${pkgname}" "${pkgdir}/usr/share/doc/${pkgname}" "${pkgdir}/etc/${pkgname}" + install -D -m 755 unpackerr "${pkgdir}/usr/bin/unpackerr" + install -D -m 644 examples/unpackerr.conf.example "${pkgdir}/etc/unpackerr/unpackerr.conf" + install -D -m 644 examples/unpackerr.conf.example "${pkgdir}/etc/unpackerr/unpackerr.conf.example" + install -D -m 644 LICENSE "${pkgdir}/usr/share/licenses/unpackerr/LICENSE" + install -D -m 644 unpackerr.1.gz "${pkgdir}/usr/share/man/man1/unpackerr.1.gz" + install -D -m 644 init/systemd/unpackerr.service "${pkgdir}/usr/lib/systemd/system/unpackerr.service" + echo 'u unpackerr - "unpackerr daemon"' > unpackerr.sysusers + install -D -m 644 unpackerr.sysusers "${pkgdir}/usr/lib/sysusers.d/unpackerr.conf" + +brews: + - name: unpackerr + ids: [default] + skip_upload: "{{ .IsNightly }}" + repository: + owner: golift + name: homebrew-mugs + branch: master + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + commit_author: + name: goreleaserbot + email: bot@goreleaser.com + commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" + directory: Formula + homepage: https://unpackerr.zip + description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. + license: MIT + url_template: "https://github.com/Unpackerr/unpackerr/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + test: assert_match "#{name} v#{version}", shell_output("#{bin}/#{name} -v 2>&1", 2) + extra_install: | + (etc/"unpackerr").mkpath + conf_example = etc/"unpackerr/unpackerr.conf.example" + FileUtils.cp "unpackerr.conf.example", conf_example + FileUtils.cp "unpackerr.conf.example", etc/"unpackerr/unpackerr.conf" unless (etc/"unpackerr/unpackerr.conf").exist? + (var/"log").mkpath + touch var/"log/unpackerr.log" + service: | + run [opt_bin/"unpackerr", "--config", etc/"unpackerr/unpackerr.conf"] + keep_alive true + log_path var/"log/unpackerr.log" + error_log_path var/"log/unpackerr.log" + caveats: | + Edit the config file at #{etc}/unpackerr/unpackerr.conf then start unpackerr with + brew services start unpackerr ~ log file: #{var}/log/unpackerr.log + The manual explains the config file options: man unpackerr + install: | + bin.install "unpackerr" + man1.install "unpackerr.1.gz" + +checksum: + name_template: checksums.sha256.txt + algorithm: sha256 + +signs: + - artifacts: checksum + +release: + disable: "{{ .IsNightly }}" + name_template: "v{{ .Version }}" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^ci:" diff --git a/Makefile b/Makefile deleted file mode 100644 index d71a5d98..00000000 --- a/Makefile +++ /dev/null @@ -1,422 +0,0 @@ -# This Makefile is written as generic as possible. -# Setting the variables in settings.sh and creating the paths in the repo makes this work. -# See more: https://github.com/golift/application-builder - -# Suck in our application information. -IGNORED:=$(shell bash -c "source settings.sh ; env | grep -v BASH_FUNC | sed 's/=/:=/;s/^/export /' > /tmp/.metadata.make") - -BUILD_FLAGS=-tags osusergo,netgo -GOFLAGS=-trimpath -mod=readonly -modcacherw - -# Preserve the passed-in version & iteration (homebrew). -_VERSION:=$(VERSION) -_ITERATION:=$(ITERATION) -include /tmp/.metadata.make - -# Travis CI passes the version in. Local builds get it from the current git tag. -ifneq ($(_VERSION),) -VERSION:=$(_VERSION) -ITERATION:=$(_ITERATION) -endif - -# rpm is wierd and changes - to _ in versions. -RPMVERSION:=$(shell echo $(VERSION) | tr -- - _) - -define PACKAGE_ARGS ---before-install init/systemd/before-install.sh \ ---after-install init/systemd/after-install.sh \ ---before-remove init/systemd/before-remove.sh \ ---name unpackerr \ ---deb-no-default-config-files \ ---rpm-os linux \ ---iteration $(ITERATION) \ ---license $(LICENSE) \ ---url $(SOURCE_URL) \ ---maintainer "$(MAINT)" \ ---vendor "$(VENDOR)" \ ---description "$(DESC)" \ ---config-files "/etc/unpackerr/unpackerr.conf" \ ---freebsd-origin "$(SOURCE_URL)" -endef - - -VERSION_LDFLAGS:= -X \"golift.io/version.Branch=$(BRANCH) ($(COMMIT))\" \ - -X \"golift.io/version.BuildDate=$(DATE)\" \ - -X \"golift.io/version.BuildUser=$(shell whoami)\" \ - -X \"golift.io/version.Revision=$(ITERATION)\" \ - -X \"golift.io/version.Version=$(VERSION)\" - -WINDOWS_LDFLAGS:= -H=windowsgui - -# CI sets WINDOWS_ZIP=0 so golift/codesign@v1 can Authenticode-sign the exe -# before this zip loop deletes it. Local `make release` still zips. -WINDOWS_ZIP ?= 1 - -# Makefile targets follow. - -all: clean build - -#################### -##### Releases ##### -#################### - -# Prepare a release. -release: clean linux_packages freebsd_packages windows - # Preparing a release! - mkdir -p $@ - mv unpackerr.*.linux unpackerr.*.freebsd $@/ - gzip -9r $@/ -ifneq ($(WINDOWS_ZIP),0) - for i in unpackerr*.exe ; do zip -9qj $@/$$i.zip $$i examples/*.example *.html; rm -f $$i;done -endif - mv *.rpm *.deb *.txz *.zst *.sig $@/ - # Generating File Hashes - openssl dgst -r -sha256 $@/* | sed 's#release/##' | tee $@/checksums.sha256.txt - -# requires a mac. -signdmg: Unpackerr.app - bash init/macos/makedmg.sh - -# Delete all build assets. -clean: - rm -f unpackerr unpackerr.*.{macos,freebsd,linux,exe}{,.gz,.zip} unpackerr.1{,.gz} unpackerr.rb - rm -f unpackerr{_,-}*.{deb,rpm,txz,zst,sig} v*.tar.gz.sha256 examples/MANUAL .metadata.make rsrc_*.syso - rm -f cmd/unpackerr/README{,.html} README{,.html} ./unpackerr_manual.html rsrc.syso Unpackerr.*.app.zip - rm -f PKGBUILD - rm -rf package_build_* release Unpackerr.*.app Unpackerr.app - -#################### -##### Sidecars ##### -#################### - -# Build a man page from a markdown file using md2roff. -# This also turns the repo readme into an html file. -# md2roff is needed to build the man file and html pages from the READMEs. -man: unpackerr.1.gz -unpackerr.1.gz: - # Building man page. Build dependency first: md2roff - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version $(VERSION) --date "$(DATE)" examples/MANUAL.md - gzip -9nc examples/MANUAL > $@ - mv examples/MANUAL.html unpackerr_manual.html - -# TODO: provide a template that adds the date to the built html file. -readme: README.html -README.html: - # This turns README.md into README.html - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version $(VERSION) --date "$(DATE)" README.md - -rsrc: rsrc.syso -rsrc.syso: init/windows/application.ico init/windows/manifest.xml - go run github.com/akavel/rsrc@latest -arch amd64 -ico init/windows/application.ico -manifest init/windows/manifest.xml - -generate: examples/unpackerr.conf.example -examples/unpackerr.conf.example: init/config/* - find pkg -name .DS\* -delete - go generate ./... - -#################### -##### Binaries ##### -#################### - -build: unpackerr -unpackerr: generate main.go - go build $(BUILD_FLAGS) -o unpackerr -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -linux: unpackerr.amd64.linux -unpackerr.amd64.linux: generate main.go - # Building linux 64-bit x86 binary. - GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -linux386: unpackerr.386.linux -unpackerr.386.linux: generate main.go - # Building linux 32-bit x86 binary. - GOOS=linux GOARCH=386 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -arm: arm64 armhf - -arm64: unpackerr.arm64.linux -unpackerr.arm64.linux: generate main.go - # Building linux 64-bit ARM binary. - GOOS=linux GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -armhf: unpackerr.arm.linux -unpackerr.arm.linux: generate main.go - # Building linux 32-bit ARM binary. - GOOS=linux GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -macos: unpackerr.universal.macos -unpackerr.universal.macos: unpackerr.amd64.macos unpackerr.arm64.macos - # Building darwin 64-bit universal binary. - lipo -create -output $@ unpackerr.amd64.macos unpackerr.arm64.macos -unpackerr.amd64.macos: generate main.go - # Building darwin 64-bit x86 binary. - GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CGO_LDFLAGS=-mmacosx-version-min=10.8 CGO_CFLAGS=-mmacosx-version-min=10.8 go build $(BUILD_FLAGS) -o $@ -ldflags "-v -w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " -unpackerr.arm64.macos: generate main.go - # Building darwin 64-bit arm binary. - GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CGO_LDFLAGS=-mmacosx-version-min=10.8 CGO_CFLAGS=-mmacosx-version-min=10.8 go build $(BUILD_FLAGS) -o $@ -ldflags "-v -w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - - -freebsd: unpackerr.amd64.freebsd -unpackerr.amd64.freebsd: generate main.go - GOOS=freebsd GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -freebsd386: unpackerr.i386.freebsd -unpackerr.i386.freebsd: generate main.go - GOOS=freebsd GOARCH=386 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -freebsdarm: unpackerr.armhf.freebsd -unpackerr.armhf.freebsd: generate main.go - GOOS=freebsd GOARCH=arm go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) " - -exe: unpackerr.amd64.exe -windows: unpackerr.amd64.exe -unpackerr.amd64.exe: generate rsrc.syso main.go - # Building windows 64-bit x86 binary. - GOOS=windows GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ -ldflags "-w -s $(VERSION_LDFLAGS) $(EXTRA_LDFLAGS) $(WINDOWS_LDFLAGS)" - bash init/windows/signexe.sh $@ - -# Zip Windows exe after Authenticode (used by CI when WINDOWS_ZIP=0). -windows_zip: - mkdir -p release - for i in unpackerr*.exe ; do \ - [ -f $$i ] || continue; \ - zip -9qj release/$$i.zip $$i examples/*.example *.html; \ - rm -f $$i; \ - done - -#################### -##### Packages ##### -#################### - -linux_packages: rpm deb zst rpm386 deb386 debarm rpmarm zstarm debarmhf rpmarmhf zstarmhf - -freebsd_packages: freebsd_pkg freebsd386_pkg freebsdarm_pkg - -macapp: Unpackerr.app -Unpackerr.app: unpackerr.universal.macos - cp -rp init/macos/Unpackerr.app Unpackerr.app - mkdir -p Unpackerr.app/Contents/MacOS - cp unpackerr.universal.macos Unpackerr.app/Contents/MacOS/Unpackerr - sed -i '' -e "s/{{VERSION}}/$(VERSION)/g" Unpackerr.app/Contents/Info.plist - -rpm: unpackerr-$(RPMVERSION)-$(ITERATION).x86_64.rpm -unpackerr-$(RPMVERSION)-$(ITERATION).x86_64.rpm: package_build_linux_rpm check_fpm - @echo "Building 'rpm' package for unpackerr version '$(RPMVERSION)-$(ITERATION)'." - fpm -s dir -t rpm $(PACKAGE_ARGS) -a x86_64 -v $(RPMVERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || rpmsign --key-id=$(SIGNING_KEY) --resign $@ - -deb: unpackerr_$(VERSION)-$(ITERATION)_amd64.deb -unpackerr_$(VERSION)-$(ITERATION)_amd64.deb: package_build_linux_deb check_fpm - @echo "Building 'deb' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t deb $(PACKAGE_ARGS) -a amd64 -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || debsigs --default-key="$(SIGNING_KEY)" --sign=origin $@ - -rpm386: unpackerr-$(RPMVERSION)-$(ITERATION).i386.rpm -unpackerr-$(RPMVERSION)-$(ITERATION).i386.rpm: package_build_linux_386_rpm check_fpm - @echo "Building 32-bit 'rpm' package for unpackerr version '$(RPMVERSION)-$(ITERATION)'." - fpm -s dir -t rpm $(PACKAGE_ARGS) -a i386 -v $(RPMVERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || rpmsign --key-id=$(SIGNING_KEY) --resign $@ - -deb386: unpackerr_$(VERSION)-$(ITERATION)_i386.deb -unpackerr_$(VERSION)-$(ITERATION)_i386.deb: package_build_linux_386_deb check_fpm - @echo "Building 32-bit 'deb' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t deb $(PACKAGE_ARGS) -a i386 -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || debsigs --default-key="$(SIGNING_KEY)" --sign=origin $@ - -rpmarm: unpackerr-$(RPMVERSION)-$(ITERATION).aarch64.rpm -unpackerr-$(RPMVERSION)-$(ITERATION).aarch64.rpm: package_build_linux_arm64_rpm check_fpm - @echo "Building 64-bit ARM8 'rpm' package for unpackerr version '$(RPMVERSION)-$(ITERATION)'." - fpm -s dir -t rpm $(PACKAGE_ARGS) -a arm64 -v $(RPMVERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || rpmsign --key-id=$(SIGNING_KEY) --resign $@ - -debarm: unpackerr_$(VERSION)-$(ITERATION)_arm64.deb -unpackerr_$(VERSION)-$(ITERATION)_arm64.deb: package_build_linux_arm64_deb check_fpm - @echo "Building 64-bit ARM8 'deb' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t deb $(PACKAGE_ARGS) -a arm64 -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || debsigs --default-key="$(SIGNING_KEY)" --sign=origin $@ - -rpmarmhf: unpackerr-$(RPMVERSION)-$(ITERATION).armhf.rpm -unpackerr-$(RPMVERSION)-$(ITERATION).armhf.rpm: package_build_linux_armhf_rpm check_fpm - @echo "Building 32-bit ARM6/7 HF 'rpm' package for unpackerr version '$(RPMVERSION)-$(ITERATION)'." - fpm -s dir -t rpm $(PACKAGE_ARGS) -a armhf -v $(RPMVERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || rpmsign --key-id=$(SIGNING_KEY) --resign $@ - -debarmhf: unpackerr_$(VERSION)-$(ITERATION)_armhf.deb -unpackerr_$(VERSION)-$(ITERATION)_armhf.deb: package_build_linux_armhf_deb check_fpm - @echo "Building 32-bit ARM6/7 HF 'deb' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t deb $(PACKAGE_ARGS) -a armhf -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) - [ "$(SIGNING_KEY)" = "" ] || debsigs --default-key="$(SIGNING_KEY)" --sign=origin $@ - -freebsd_pkg: unpackerr-$(VERSION)_$(ITERATION).amd64.txz unpackerr-$(VERSION)_$(ITERATION).amd64.txz.sig -unpackerr-$(VERSION)_$(ITERATION).amd64.txz: package_build_freebsd check_fpm - @echo "Building 'freebsd pkg' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t freebsd $(PACKAGE_ARGS) -a amd64 -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) -unpackerr-$(VERSION)_$(ITERATION).amd64.txz.sig: unpackerr-$(VERSION)_$(ITERATION).amd64.txz - [ "$(SIGNING_KEY)" = "" ] || gpg --local-user "$(SIGNING_KEY)" --output $@ --detach-sig $< - -freebsd386_pkg: unpackerr-$(VERSION)_$(ITERATION).i386.txz unpackerr-$(VERSION)_$(ITERATION).i386.txz.sig -unpackerr-$(VERSION)_$(ITERATION).i386.txz: package_build_freebsd_386 check_fpm - @echo "Building 32-bit 'freebsd pkg' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t freebsd $(PACKAGE_ARGS) -a 386 -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) -unpackerr-$(VERSION)_$(ITERATION).i386.txz.sig: unpackerr-$(VERSION)_$(ITERATION).i386.txz - [ "$(SIGNING_KEY)" = "" ] || gpg --local-user "$(SIGNING_KEY)" --output $@ --detach-sig $< - -freebsdarm_pkg: unpackerr-$(VERSION)_$(ITERATION).armhf.txz unpackerr-$(VERSION)_$(ITERATION).armhf.txz.sig -unpackerr-$(VERSION)_$(ITERATION).armhf.txz: package_build_freebsd_arm check_fpm - @echo "Building 32-bit ARM6/7 HF 'freebsd pkg' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t freebsd $(PACKAGE_ARGS) -a arm -v $(VERSION) -p $@ -C $< $(EXTRA_FPM_FLAGS) -unpackerr-$(VERSION)_$(ITERATION).armhf.txz.sig: unpackerr-$(VERSION)_$(ITERATION).armhf.txz - [ "$(SIGNING_KEY)" = "" ] || gpg --local-user "$(SIGNING_KEY)" --output $@ --detach-sig $< - -zst: unpackerr-$(VERSION)-$(ITERATION)-x86_64.pkg.tar.zst unpackerr-$(VERSION)-$(ITERATION)-x86_64.pkg.tar.zst.sig -unpackerr-$(VERSION)-$(ITERATION)-x86_64.pkg.tar.zst: package_build_linux_zst check_fpm - @echo "Building 'pacman' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t pacman $(PACKAGE_ARGS) -a x86_64 -v $(VERSION) -C $< $(EXTRA_FPM_FLAGS) -unpackerr-$(VERSION)-$(ITERATION)-x86_64.pkg.tar.zst.sig: unpackerr-$(VERSION)-$(ITERATION)-x86_64.pkg.tar.zst - [ "$(SIGNING_KEY)" = "" ] || gpg --local-user "$(SIGNING_KEY)" --output $@ --detach-sig $< - -zstarm: unpackerr-$(VERSION)-$(ITERATION)-aarch64.pkg.tar.zst unpackerr-$(VERSION)-$(ITERATION)-aarch64.pkg.tar.zst.sig -unpackerr-$(VERSION)-$(ITERATION)-aarch64.pkg.tar.zst: package_build_linux_aarch64_zst check_fpm - @echo "Building 64-bit ARM8 'pacman' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t pacman $(PACKAGE_ARGS) -a aarch64 -v $(VERSION) -C $< $(EXTRA_FPM_FLAGS) -unpackerr-$(VERSION)-$(ITERATION)-aarch64.pkg.tar.zst.sig: unpackerr-$(VERSION)-$(ITERATION)-aarch64.pkg.tar.zst - [ "$(SIGNING_KEY)" = "" ] || gpg --local-user "$(SIGNING_KEY)" --output $@ --detach-sig $< - -zstarmhf: unpackerr-$(VERSION)-$(ITERATION)-arm7hf.pkg.tar.zst unpackerr-$(VERSION)-$(ITERATION)-arm7hf.pkg.tar.zst.sig -unpackerr-$(VERSION)-$(ITERATION)-arm7hf.pkg.tar.zst: package_build_linux_armhf_zst check_fpm - @echo "Building 32-bit ARM6/7 HF 'pacman' package for unpackerr version '$(VERSION)-$(ITERATION)'." - fpm -s dir -t pacman $(PACKAGE_ARGS) -a armhf -v $(VERSION) -C $< $(EXTRA_FPM_FLAGS) -unpackerr-$(VERSION)-$(ITERATION)-arm7hf.pkg.tar.zst.sig: unpackerr-$(VERSION)-$(ITERATION)-arm7hf.pkg.tar.zst - [ "$(SIGNING_KEY)" = "" ] || gpg --local-user "$(SIGNING_KEY)" --output $@ --detach-sig $< - -# Build an environment that can be packaged for linux. -package_build_linux_rpm: generate readme man linux - # Building package environment for linux. - mkdir -p $@/usr/bin $@/etc/unpackerr $@/usr/share/man/man1 $@/usr/share/doc/unpackerr $@/usr/lib/unpackerr - # Copying the binary, config file, unit file, and man page into the env. - cp unpackerr.amd64.linux $@/usr/bin/unpackerr - cp *.1.gz $@/usr/share/man/man1 - cp examples/unpackerr.conf.example $@/etc/unpackerr/ - cp examples/unpackerr.conf.example $@/etc/unpackerr/unpackerr.conf - cp LICENSE *.html examples/*?.?* $@/usr/share/doc/unpackerr/ - mkdir -p $@/lib/systemd/system - cp init/systemd/unpackerr.service $@/lib/systemd/system/ - [ ! -d "init/linux/rpm" ] || cp -r init/linux/rpm/* $@ - -# Build an environment that can be packaged for linux. -package_build_linux_deb: generate readme man linux - # Building package environment for linux. - mkdir -p $@/usr/bin $@/etc/unpackerr $@/usr/share/man/man1 $@/usr/share/doc/unpackerr $@/usr/lib/unpackerr - # Copying the binary, config file, unit file, and man page into the env. - cp unpackerr.amd64.linux $@/usr/bin/unpackerr - cp *.1.gz $@/usr/share/man/man1 - cp examples/unpackerr.conf.example $@/etc/unpackerr/ - cp examples/unpackerr.conf.example $@/etc/unpackerr/unpackerr.conf - cp LICENSE *.html examples/*?.?* $@/usr/share/doc/unpackerr/ - mkdir -p $@/lib/systemd/system - cp init/systemd/unpackerr.service $@/lib/systemd/system/ - [ ! -d "init/linux/deb" ] || cp -r init/linux/deb/* $@ - -# Build an environment that can be packaged for arch linux. -package_build_linux_zst: generate readme man linux - # Building package environment for linux. - mkdir -p $@/usr/bin $@/etc/unpackerr $@/usr/share/man/man1 \ - $@/usr/share/licenses/unpackerr $@/usr/share/doc/unpackerr $@/var/log/unpackerr - # Copying the binary, config file, unit file, and man page into the env. - cp unpackerr.amd64.linux $@/usr/bin/unpackerr - cp *.1.gz $@/usr/share/man/man1 - cp examples/unpackerr.conf.example $@/etc/unpackerr/ - cp examples/unpackerr.conf.example $@/etc/unpackerr/unpackerr.conf - cp LICENSE $@/usr/share/licenses/unpackerr - cp *.html examples/*?.?* $@/usr/share/doc/unpackerr/ - mkdir -p $@/usr/lib/systemd/system $@/usr/lib/sysusers.d - echo "u unpackerr - \"unpackerr daemon\"" > $@/usr/lib/sysusers.d/unpackerr.conf - chmod 775 $@/var/log/unpackerr $@/usr/share/doc/unpackerr $@/etc/unpackerr - cp init/systemd/unpackerr.service $@/usr/lib/systemd/system/ - [ ! -d "init/linux/zst" ] || cp -r init/linux/zst/* $@ - -package_build_linux_386_deb: package_build_linux_deb linux386 - mkdir -p $@ - cp -r $ /dev/null || (echo "FPM missing. Install FPM: https://fpm.readthedocs.io/en/latest/installing.html" && false) - -################## -##### Extras ##### -################## - -# Run code tests and lint. -test: lint - # Testing. - go test -race -covermode=atomic ./... -lint: generate - # Checking lint. - golangci-lint version - GOOS=linux golangci-lint run - GOOS=freebsd golangci-lint run - GOOS=windows golangci-lint run - -################## -##### Docker ##### -################## - -docker: - init/docker/makedocker.sh diff --git a/init/archlinux/PKGBUILD.template b/init/archlinux/PKGBUILD.template deleted file mode 100644 index 2dfc7a55..00000000 --- a/init/archlinux/PKGBUILD.template +++ /dev/null @@ -1,70 +0,0 @@ -# Maintainer: David Newhall II -# Maintainer: Donald Webster - -pkgname='unpackerr' -pkgver={{VERSION}} -pkgrel={{Iter}} -pkgdesc='{{Desc}}' -arch=('x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64' 'i686' 'pentium4') -url='https://unpackerr.zip' -license=('MIT') -makedepends=('go' 'gzip') -optdepends=( - 'transmission-cli: torrent downloader (CLI and daemon)' - 'transmission-gtk: torrent downloader (GTK+)' - 'transmission-qt: torrent downloader (Qt)' - 'deluge: torrent downloader' - 'rtorrent: torrent downloader' -) - -source=("${pkgname}-${pkgver}.tar.gz::https://github.com/Unpackerr/unpackerr/archive/v${pkgver}.tar.gz") -sha512sums=('{{SHA}}') - -backup=("etc/${pkgname}/${pkgname}.conf") - -prepare(){ - cd "$pkgname-$pkgver" - mkdir -p build/ -} - -build() { - cd "$pkgname-$pkgver" - - export GOFLAGS="-buildmode=pie -trimpath -modcacherw" - LDFLAGS="-w -s -X 'golift.io/version.Branch=main (${sha512sums[0]:0:11})' \ - -X golift.io/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:00Z) \ - -X golift.io/version.BuildUser=$(whoami || echo unknown) \ - -X golift.io/version.Revision=${pkgrel} \ - -X golift.io/version.Version=${pkgver}" - - go build -o unpackerr -ldflags "$LDFLAGS" . - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "${DATE}" README.md - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "${DATE}" examples/MANUAL.md - gzip -9 examples/MANUAL - mv examples/MANUAL.gz "${pkgname}.1.gz" -} - -package() { - cd "${srcdir}/${pkgname}-${pkgver}" - - # Directories. - install -d -m 775 "${pkgdir}/usr/share/"{licenses,doc}"/${pkgname}" "${pkgdir}/etc/${pkgname}" - - # Install the binary - install -D -m 755 "${pkgname}" "${pkgdir}/usr/bin/${pkgname}" - - # Install configuration file(s). - install -D -m 644 "examples/${pkgname}.conf.example" "${pkgdir}/etc/${pkgname}/${pkgname}.conf" - install -D -m 644 "examples/${pkgname}.conf.example" "${pkgdir}/etc/${pkgname}/${pkgname}.conf.example" - - # License, documentation, manual. - install -D -m 644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" - install -D -m 644 ./*.html examples/* "${pkgdir}/usr/share/doc/${pkgname}/" - install -D -m 644 "${pkgname}.1.gz" "${pkgdir}/usr/share/man/man1/${pkgname}.1.gz" - - # Install the systemd service unit and system user account. - install -D -m 644 "init/systemd/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service" - echo "u ${pkgname} - \"${pkgname} daemon\"" > "${pkgname}.sysusers" - install -D -m 644 "${pkgname}.sysusers" "${pkgdir}/usr/lib/sysusers.d/${pkgname}.conf" - rm "${pkgname}.sysusers" -} diff --git a/init/archlinux/README.md b/init/archlinux/README.md deleted file mode 100644 index 14b0580d..00000000 --- a/init/archlinux/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This template file is built by GitHub Actions and uploaded to the -[AUR repo](https://aur.archlinux.org/packages/unpackerr). diff --git a/init/archlinux/SRCINFO.template b/init/archlinux/SRCINFO.template deleted file mode 100644 index 0808a96f..00000000 --- a/init/archlinux/SRCINFO.template +++ /dev/null @@ -1,25 +0,0 @@ -pkgbase = unpackerr - pkgdesc = {{Desc}} - pkgver = {{VERSION}} - pkgrel = 1 - url = https://unpackerr.zip - arch = x86_64 - arch = arm - arch = armv6h - arch = armv7h - arch = aarch64 - arch = i686 - arch = pentium4 - license = MIT - makedepends = go - makedepends = gzip - optdepends = transmission-cli: torrent downloader (CLI and daemon) - optdepends = transmission-gtk: torrent downloader (GTK+) - optdepends = transmission-qt: torrent downloader (Qt) - optdepends = deluge: torrent downloader - optdepends = rtorrent: torrent downloader - backup = etc/unpackerr/unpackerr.conf - source = unpackerr-{{VERSION}}.tar.gz::{{SOURCE_PATH}} - sha512sums = {{SHA}} - -pkgname = unpackerr diff --git a/init/archlinux/aur-deploy.sh b/init/archlinux/aur-deploy.sh deleted file mode 100644 index 270ac77b..00000000 --- a/init/archlinux/aur-deploy.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -x - -# Deploys a new aur PKGBUILD file to an arch linux aur github repo. -# Run by GitHub Actions when a new release is created on GitHub. - -source settings.sh - -SOURCE_PATH="https://github.com/Unpackerr/unpackerr/archive/v${VERSION}.tar.gz" -echo "==> Using URL: $SOURCE_PATH" -SHA=$(curl -sL "$SOURCE_PATH" | sha512sum | awk '{print $1}') - -push_it() { - pushd release_repo - git add . - git config --global user.email "unpackerr@github.releaser" - git config --global user.name "unpackerr-github-releaser" - git commit -m "Update unpackerr on Release: v${VERSION}-${ITERATION}" - git push - popd - rm -rf release_repo -} - -set -e - -mkdir -p "${HOME}/.ssh" -KEY_FILE=$(mktemp -u "${HOME}/.ssh/aur-deploy-key.XXXXX") -echo "${DEPLOY_KEY}" > "${KEY_FILE}" -chmod 600 "${KEY_FILE}" -# Configure ssh to use this secret. -export GIT_SSH_COMMAND="ssh -i ${KEY_FILE} -o 'StrictHostKeyChecking no'" - -rm -rf release_repo -git clone aur@aur.archlinux.org:unpackerr.git release_repo - -sed -e "s/{{VERSION}}/${VERSION}/g" \ - -e "s/{{Iter}}/${ITERATION}/g" \ - -e "s/{{SHA}}/${SHA}/g" \ - -e "s/{{Desc}}/${DESC}/g" \ - init/archlinux/PKGBUILD.template | tee release_repo/PKGBUILD - -sed -e "s/{{VERSION}}/${VERSION}/g" \ - -e "s/{{Iter}}/${ITERATION}/g" \ - -e "s/{{SHA}}/${SHA}/g" \ - -e "s/{{Desc}}/${DESC}/g" \ - -e "s%{{SOURCE_PATH}}%${SOURCE_PATH}%g" \ - init/archlinux/SRCINFO.template | tee release_repo/.SRCINFO - -[ "$1" != "" ] || push_it diff --git a/init/docker/Dockerfile b/init/docker/Dockerfile index 048117e1..eddf92da 100644 --- a/init/docker/Dockerfile +++ b/init/docker/Dockerfile @@ -1,62 +1,10 @@ -FROM golang:1-alpine AS builder +FROM alpine:3.22 -WORKDIR /src -COPY main.go settings.sh go.mod go.sum ./ -RUN go mod download -COPY pkg pkg -COPY examples examples -RUN go generate ./... +RUN apk add --no-cache ca-certificates openssl tzdata -ARG TARGETOS -ARG TARGETARCH -ARG BUILD_DATE -ARG BRANCH -ARG VERSION -ARG ITERATION -ARG COMMIT - -ENV GOFLAGS="-trimpath -mod=readonly -modcacherw" -ENV GOOS=${TARGETOS} -ENV GOARCH=${TARGETARCH} - -RUN go build -o /tmp/unpackerr -tags osusergo,netgo \ - -ldflags "-w -s -X \"golift.io/version.Branch=${BRANCH} (${COMMIT})\" \ - -X \"golift.io/version.BuildDate=${BUILD_DATE}\" \ - -X \"golift.io/version.BuildUser=docker\" \ - -X \"golift.io/version.Revision=${ITERATION}\" \ - -X \"golift.io/version.Version=${VERSION}\"" - -FROM alpine - -ARG TARGETOS -ARG TARGETARCH -ARG BUILD_DATE -ARG COMMIT -ARG VERSION -ARG ITERATION -ARG LICENSE=MIT -ARG SOURCE_URL=http://github.com/golift/application-builder -ARG DESC=application-builder -ARG VENDOR=golift -ARG AUTHOR=golift -# Build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/master/annotations.md -LABEL org.opencontainers.image.created="${BUILD_DATE}" \ - org.opencontainers.image.title="Unpackerr" \ - org.opencontainers.image.documentation="https://unpackerr.zip" \ - org.opencontainers.image.description="${DESC}" \ - org.opencontainers.image.url="${SOURCE_URL}" \ - org.opencontainers.image.revision="${COMMIT}" \ - org.opencontainers.image.source="${SOURCE_URL}" \ - org.opencontainers.image.vendor="${VENDOR}" \ - org.opencontainers.image.authors="${AUTHOR}" \ - org.opencontainers.image.architecture="${TARGETOS} ${TARGETARCH}" \ - org.opencontainers.image.licenses="${LICENSE}" \ - org.opencontainers.image.version="${VERSION}-${ITERATION}" - -COPY --from=builder /tmp/unpackerr /unpackerr -# Make sure we have an ssl cert chain and timezone data. -RUN apk add --no-cache openssl tzdata +ARG TARGETPLATFORM +COPY ${TARGETPLATFORM}/unpackerr /unpackerr ENV TZ=UTC -ENTRYPOINT [ "/unpackerr" ] +ENTRYPOINT ["/unpackerr"] diff --git a/init/docker/hooks/build b/init/docker/hooks/build deleted file mode 100755 index d449bec7..00000000 --- a/init/docker/hooks/build +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash - -# This file is designed to run as build hook for Automated Builds on Docker.com. -# This always run local to the Dockerfile folder, so the path is ../.. -pushd ../.. -source settings.sh - -docker buildx inspect - -if ! (docker buildx inspect | grep -Eq 'docker-container'); then - echo "Creating docker builder instance" - docker buildx create --use -fi - -read -r -d '' BUILD_ARGS << EOF ---build-arg "BUILD_DATE=${DATE}" \ ---build-arg "BUILD_FLAGS=${BUILD_FLAGS}" \ ---build-arg "COMMIT=${COMMIT}" \ ---build-arg "BRANCH=${BRANCH}" \ ---build-arg "VERSION=${VERSION}" \ ---build-arg "ITERATION=${ITERATION}" \ ---build-arg "LICENSE=${LICENSE}" \ ---build-arg "DESC=${DESC}" \ ---build-arg "VENDOR=${VENDOR}" \ ---build-arg "AUTHOR=${MAINT}" \ ---build-arg "SOURCE_URL=${SOURCE_URL}" \ ---file ${DOCKERFILE_PATH} . -EOF - -echo "Build Args: ${BUILD_ARGS}" -eval "docker buildx build --load --tag current:amd64 --platform linux/amd64 $BUILD_ARGS" -eval "docker buildx build --load --tag current:arm64 --platform linux/arm64/v8 $BUILD_ARGS" -eval "docker buildx build --load --tag current:arm --platform linux/arm $BUILD_ARGS" -echo "Done Building." - -TAGS="$SOURCE_BRANCH" -if [ "v$VERSION" = "$SOURCE_BRANCH" ]; then - TAGS="$VERSION" - - echo $SOURCE_BRANCH | grep -q -- - - if [ "$?" = "1" ]; then - # tag does not contain a dash, so assume it's a prod tag. - TAGS="$TAGS latest unstable $(echo $VERSION | cut -d. -f1,2) $(echo $VERSION | cut -d. -f1)" - fi -fi - -for tag in $TAGS; do - echo "Pushing tag: $DOCKER_REPO:$tag" - eval "docker buildx build --push --tag $DOCKER_REPO:$tag --platform linux/amd64,linux/arm64/v8,linux/arm $BUILD_ARGS" -done diff --git a/init/docker/hooks/pre_build b/init/docker/hooks/pre_build deleted file mode 100644 index 35f818b3..00000000 --- a/init/docker/hooks/pre_build +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -# This upgrades the docker client on the Docker Cloud server to a version -# that contains the `docker manifest` command, so we can build with QEMU. - -docker run --rm --privileged multiarch/qemu-user-static --reset -p yes diff --git a/init/docker/hooks/push b/init/docker/hooks/push deleted file mode 100644 index 497e4c09..00000000 --- a/init/docker/hooks/push +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -#empty \ No newline at end of file diff --git a/init/docker/makedocker.sh b/init/docker/makedocker.sh deleted file mode 100755 index 914343b9..00000000 --- a/init/docker/makedocker.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# This is invoked by the Makefile to create a simple docker image ready to go. - -source settings.sh - -docker buildx build --load --pull --tag unpackerr \ - --platform linux/amd64 \ - --build-arg "BUILD_DATE=${DATE}" \ - --build-arg "COMMIT=${COMMIT}" \ - --build-arg "BRANCH=${BRANCH}" \ - --build-arg "VERSION=${VERSION}" \ - --build-arg "ITERATION=${ITERATION}" \ - --build-arg "LICENSE=${LICENSE}" \ - --build-arg "DESC=${DESC}" \ - --build-arg "VENDOR=${VENDOR}" \ - --build-arg "AUTHOR=${MAINT}" \ - --build-arg "SOURCE_URL=${SOURCE_URL}" \ - --file init/docker/Dockerfile . \ No newline at end of file diff --git a/init/macos/Info.plist.tmpl b/init/macos/Info.plist.tmpl new file mode 100644 index 00000000..6eb0b8c1 --- /dev/null +++ b/init/macos/Info.plist.tmpl @@ -0,0 +1,40 @@ + + + + + CFBundleName + Unpackerr + CFBundleIdentifier + io.golift.unpackerr + CFBundleInfoDictionaryVersion + 6.0 + CFBundleVersion + {{ .Version }} + CFBundleShortVersionString + {{ .Version }} + CFBundleExecutable + {{ .BinaryName }} + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSPrincipalClass + NSApplication + LSUIElement + + CFBundleIconFile + Unpackerr.icns + CFBundleDisplayName + Unpackerr + NSHumanReadableCopyright + © Go Lift (https://golift.io) + LSEnvironment + + USEGUI + true + + NSHighResolutionCapable + + + diff --git a/init/macos/makedmg.sh b/init/macos/makedmg.sh deleted file mode 100755 index 7a8bb90f..00000000 --- a/init/macos/makedmg.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -# This file builds a standard DMG installer for macOS. -# This only works on macOS. -########################################### - -set -e -o pipefail - -# If we are running in GH Actions, make a new keychain and import the certificate. -if [ -n "$APPLE_SIGNING_KEY" ]; then - KEYCHAIN="ios-build.keychain" - - echo "==> Creating new keychain: $KEYCHAIN" - security create-keychain -p secret $KEYCHAIN - - echo "==> Importing certificate into ${KEYCHAIN}" - echo "${APPLE_SIGNING_KEY}" | base64 -d | \ - security import /dev/stdin -P "" -f pkcs12 -k $KEYCHAIN -T /usr/bin/codesign - - echo "==> Unlocking keychain ${KEYCHAIN}" - security unlock-keychain -p secret $KEYCHAIN - - echo "==> Increase keychain unlock timeout to 1 hour." - security set-keychain-settings -lut 3600 $KEYCHAIN - - security set-key-partition-list -S apple-tool:,apple: -s -k secret $KEYCHAIN - - echo "==> Add keychain to keychain-list" - security list-keychains -s $KEYCHAIN -fi - -echo "==> Signing App." -gon init/macos/sign.json - -# Creating non-notarized DMG. -mkdir -p release -hdiutil create release/Unpackerr.dmg -srcfolder Unpackerr.app -ov - -echo "==> Notarizing DMG." -gon init/macos/notarize.json - -echo "==> Finished." diff --git a/init/macos/notarize.json b/init/macos/notarize.json deleted file mode 100644 index 7e268fd6..00000000 --- a/init/macos/notarize.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "notarize": [{ - "path": "release/Unpackerr.dmg", - "bundle_id": "io.golift.unpackerr", - "staple": true - }] -} \ No newline at end of file diff --git a/init/macos/notarize.sh b/init/macos/notarize.sh deleted file mode 100755 index 02c6bff2..00000000 --- a/init/macos/notarize.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash - -## This doesn't work in CI/CD because of the AppleScript. -# Must be run on a real Mac with a real apple ID and password. -# AC_PASSWORD and AC_USERNAME variables are required to be set. -# The only part that doesn't work in CI/CD currently is the applescript. - -# Download latest release. -echo "==> Getting latest release." -URL=$(curl -s https://api.github.com/repos/Unpackerr/unpackerr/releases/latest | \ - jq -r '.assets[] | select(.name == "Unpackerr.dmg") | .browser_download_url') -echo "==> Downloading: $URL" -curl -sSLo /tmp/Unpackerr.dmg "$URL" -echo "==> Mounting Unpackerr.dmg to /Volumes/UnpackerrRelease" -hdiutil attach -readonly -mountpoint /Volumes/UnpackerrRelease /tmp/Unpackerr.dmg - -# Create r/w image with latest release app as source. -echo "==> Creating intermediate image: pack.temp.dmg." -rm -f pack.temp.dmg -hdiutil create -srcfolder "/Volumes/UnpackerrRelease/Unpackerr.app" -volname "Unpackerr" -fs HFS+ \ - -fsargs "-c c=64,a=16,e=16" -format UDRW -size 200000k pack.temp.dmg - -echo "==> Unmounting /Volumes/UnpackerrRelease and /Volumes/UnpackerrIntermediate (may not be mounted)." -hdiutil detach "/Volumes/UnpackerrIntermediate" -hdiutil detach "/Volumes/UnpackerrRelease" -sleep 1 - -echo "==> Mounting pack.temp.dmg to /Volumes/UnpackerrIntermediate" -hdiutil attach -mountpoint /Volumes/UnpackerrIntermediate -readwrite -noverify -noautoopen "pack.temp.dmg" | \ - egrep '^/dev/' | sed 1q | awk '{print $1}' - -# Create content. -sleep 1 -echo "==> Copying background image." -mkdir "/Volumes/UnpackerrIntermediate/.background" -cp -r init/macos/background.png "/Volumes/UnpackerrIntermediate/.background/Unpackerr.png" - -echo "==> Running AppleScript to build custom DMG." -echo ' - tell application "Finder" - tell disk "'UnpackerrIntermediate'" - open - set current view of container window to icon view - set toolbar visible of container window to false - set statusbar visible of container window to false - set the bounds of container window to {400, 100, 1320, 600} - set theViewOptions to the icon view options of container window - set arrangement of theViewOptions to not arranged - set icon size of theViewOptions to 256 - set background picture of theViewOptions to file ".background:'Unpackerr.png'" - make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"} - set position of item "'Unpackerr.app'" of container window to {0, 0} - set position of item "Applications" of container window to {600, 0} - update without registering applications - delay 1 - close - end tell - end tell -' | osascript - -sleep 1 -# Finalize. -echo "==> Finalizing DMG." -chmod -Rf go-w /Volumes/UnpackerrIntermediate - -sleep 1 -echo "==> Unmounting /Volumes/UnpackerrIntermediate." -hdiutil detach /Volumes/UnpackerrIntermediate - -sleep 1 -echo "==> Converting DMG to compressed read only." -mkdir -p release -rm -f "release/Unpackerr.dmg" -hdiutil convert "pack.temp.dmg" -format UDZO -imagekey zlib-level=9 -o "release/Unpackerr.dmg" -rm -f pack.temp.dmg - -echo "==> Notarizing DMG." -gon init/macos/notarize.json -echo "==> Finished!" -ls -l release/Unpackerr.dmg \ No newline at end of file diff --git a/init/macos/sign.json b/init/macos/sign.json deleted file mode 100644 index 4c9435d5..00000000 --- a/init/macos/sign.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "source": ["Unpackerr.app"], - "bundle_id": "io.golift.unpackerr", - "sign": { "application_identity" : "1AE9653F18114B23927AF7FA6624EF74F95A4544" } -} - diff --git a/init/windows/signexe.sh b/init/windows/signexe.sh index 894a2ad8..44a7ad0a 100755 --- a/init/windows/signexe.sh +++ b/init/windows/signexe.sh @@ -3,24 +3,27 @@ set -e -o pipefail # Authenticode-sign a Windows PE via golift/codesign (YubiKey-backed signerd). -# GitHub Actions uses golift/codesign@v1 after `make release WINDOWS_ZIP=0`. -# This script is for local `make windows` when CODESIGN_URL is set (SSH tunnel -# or a configured CLI). +# GoReleaser calls this from builds.hooks.post on windows binaries, after the +# CLI is installed into a temp GOBIN (never /usr/bin/codesign). # -# On macOS, never call /usr/bin/codesign (Apple's tool). Prefer CODESIGN_BIN, -# "$(go env GOPATH)/bin/codesign", then any other `codesign` on PATH. +# Skip when CODESIGN_URL is unset so local snapshots still work. +# Prefer CODESIGN_BIN, then GOBIN/codesign, then GOPATH/bin, then PATH +# entries that are not Apple's /usr/bin/codesign. function pick_codesign() { if [ -n "${CODESIGN_BIN:-}" ]; then echo "${CODESIGN_BIN}" return fi + if [ -n "${GOBIN:-}" ] && [ -x "${GOBIN}/codesign" ]; then + echo "${GOBIN}/codesign" + return + fi gopath="$(go env GOPATH 2>/dev/null || true)" if [ -n "${gopath}" ] && [ -x "${gopath}/bin/codesign" ]; then echo "${gopath}/bin/codesign" return fi - # Apple ships /usr/bin/codesign. Skip it; any other PATH hit is the CLI. while IFS= read -r p; do case "$p" in /usr/bin/codesign|/bin/codesign) continue ;; @@ -38,10 +41,6 @@ function sign() { fi bin="$(pick_codesign)" || { - if [ -n "${GITHUB_ACTIONS:-}" ]; then - echo "Skipped signing ${FILE} (codesign CLI not on PATH; Action signs later) .." >&2 - exit 0 - fi echo "CODESIGN_URL is set but golift codesign CLI not found (set CODESIGN_BIN)" >&2 exit 1 } diff --git a/scripts/unstable_upload.sh b/scripts/unstable_upload.sh new file mode 100755 index 00000000..609e8c3e --- /dev/null +++ b/scripts/unstable_upload.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Upload zip/dmg/gz artifacts to unstable.golift.io (same as the old Actions loop). +set -euo pipefail + +dir="${1:-dist}" +if [ -z "${UNSTABLE_UPLOAD_KEY:-}" ]; then + echo "UNSTABLE_UPLOAD_KEY unset; skipping unstable.golift.io upload" >&2 + exit 0 +fi + +shopt -s nullglob +files=("${dir}"/*.zip "${dir}"/*.dmg "${dir}"/*.gz) +if [ ${#files[@]} -eq 0 ]; then + echo "no zip/dmg/gz artifacts in ${dir}" >&2 + exit 0 +fi + +version="${VERSION:-unknown}" +for file in "${files[@]}"; do + [ -f "$file" ] || continue + name="$(basename "$file")" + echo "Uploading ${name}" + curl -sS --fail-with-body --retry 5 --retry-all-errors --retry-delay 2 \ + -H "X-API-KEY: ${UNSTABLE_UPLOAD_KEY}" \ + "https://unstable.golift.io/upload.php?folder=unpackerr" -F "file=@${file}" + curl -sS --fail-with-body --retry 5 --retry-all-errors --retry-delay 2 \ + -H "X-API-KEY: ${UNSTABLE_UPLOAD_KEY}" \ + "https://unstable.golift.io/upload.php?folder=unpackerr" \ + -F "file=${version};filename=${name}.txt;type=text/plain" +done diff --git a/settings.sh b/settings.sh deleted file mode 100644 index 65c6fc21..00000000 --- a/settings.sh +++ /dev/null @@ -1,33 +0,0 @@ - -MAINT="David Newhall II " -DESC="Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them." -LICENSE="MIT" -# Used for source links in package metadata and docker labels. -SOURCE_URL="https://github.com/Unpackerr/unpackerr" -VENDOR="Go Lift " -export MAINT DESC LICENSE SOURCE_URL VENDOR - -DATE="$(date -u +%Y-%m-%dT%H:%M:00Z)" -VERSION=$(git tag --sort version:refname | tail -n1 | tr -d v) -[ "$VERSION" != "" ] || VERSION=development -# This produces a 0 in some environments (like Homebrew), but it's only used for packages. -ITERATION=$(git rev-list --count --all || echo 0) -COMMIT="$(git rev-parse --short HEAD || echo 0)" -GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD || echo unknown)" -BRANCH="${GIT_BRANCH:-${GITHUB_REF_NAME}}" -export DATE VERSION ITERATION COMMIT BRANCH - -### Optional ### - -# Import this signing key only if it's in the keyring. -if gpg --list-keys 2>/dev/null | grep -q B93DD66EF98E54E2EAE025BA0166AD34ABC5A57C && debsigs -h > /dev/null; then - export SIGNING_KEY=B93DD66EF98E54E2EAE025BA0166AD34ABC5A57C -fi - -# Make sure Docker builds work locally. -# These do not affect automated builds, just allow the docker build scripts to run from a local clone. -[ -n "$SOURCE_BRANCH" ] || export SOURCE_BRANCH=$BRANCH -[ -n "$DOCKER_TAG" ] || export DOCKER_TAG=$(echo $SOURCE_BRANCH | sed 's/^v*\([0-9].*\)/\1/') -[ -n "$DOCKER_REPO" ] || export DOCKER_REPO="golift/unpackerr" -[ -n "$IMAGE_NAME" ] || export IMAGE_NAME="${DOCKER_REPO}:${DOCKER_TAG}" -[ -n "$DOCKERFILE_PATH" ] || export DOCKERFILE_PATH="init/docker/Dockerfile" From 12ed6047c0501849537cc97582e86c6c9422f615 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 05:59:53 +0000 Subject: [PATCH 02/41] Skip Docker Hub login when DOCKERHUB_PASSWORD is unset. The release job failed at docker/login-action with "Password required" because secrets.DOCKERHUB_PASSWORD is empty in this repository. Map the secret for step ifs, skip Hub login and Hub image publish when it is blank, and keep GHCR plus the rest of GoReleaser running. Co-authored-by: Andreas Echavez --- .github/workflows/release.yml | 8 +++++++ .goreleaser.yaml | 45 +++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aabda7b6..5a4d6638 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ jobs: env: # Map first: secrets cannot be referenced in steps.if. CODESIGN_URL: ${{ secrets.CODESIGN_URL }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -38,10 +39,17 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log into Docker Hub + if: env.DOCKERHUB_PASSWORD != '' uses: docker/login-action@v4 with: username: golift password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Enable Docker Hub publish + if: env.DOCKERHUB_PASSWORD != '' + run: echo DOCKERHUB_PUBLISH=1 >> "$GITHUB_ENV" + - name: Skip Docker Hub + if: env.DOCKERHUB_PASSWORD == '' + run: echo '::warning::DOCKERHUB_PASSWORD is empty; skipping Docker Hub login and Hub image publish. Grant the org secret to this public repository (or add a repo secret) to resume golift/unpackerr pushes.' - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e35e3187..7d46d664 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -274,18 +274,17 @@ dockers_v2: ids: [unpackerr] images: - ghcr.io/unpackerr/unpackerr - - docker.io/golift/unpackerr tags: - latest - "{{ .Version }}" - "v{{ .Version }}" - "{{ .Major }}.{{ .Minor }}" - "{{ .Major }}" - platforms: + platforms: &docker_platforms - linux/amd64 - linux/arm64 - linux/arm/v7 - labels: + labels: &docker_labels org.opencontainers.image.created: "{{ .Date }}" org.opencontainers.image.title: Unpackerr org.opencontainers.image.revision: "{{ .FullCommit }}" @@ -296,30 +295,40 @@ dockers_v2: org.opencontainers.image.source: "{{ .GitURL }}" org.opencontainers.image.vendor: Go Lift org.opencontainers.image.licenses: MIT + - id: unpackerr-release-hub + disable: '{{ or .IsNightly (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' + dockerfile: init/docker/Dockerfile + ids: [unpackerr] + images: + - docker.io/golift/unpackerr + tags: + - latest + - "{{ .Version }}" + - "v{{ .Version }}" + - "{{ .Major }}.{{ .Minor }}" + - "{{ .Major }}" + platforms: *docker_platforms + labels: *docker_labels - id: unpackerr-nightly disable: "{{ not .IsNightly }}" dockerfile: init/docker/Dockerfile ids: [unpackerr] images: - ghcr.io/unpackerr/unpackerr + tags: + - unstable + platforms: *docker_platforms + labels: *docker_labels + - id: unpackerr-nightly-hub + disable: '{{ or (not .IsNightly) (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' + dockerfile: init/docker/Dockerfile + ids: [unpackerr] + images: - docker.io/golift/unpackerr tags: - unstable - platforms: - - linux/amd64 - - linux/arm64 - - linux/arm/v7 - labels: - org.opencontainers.image.created: "{{ .Date }}" - org.opencontainers.image.title: Unpackerr - org.opencontainers.image.revision: "{{ .FullCommit }}" - org.opencontainers.image.version: "{{ .Version }}" - org.opencontainers.image.documentation: https://unpackerr.zip/docs/install/docker - org.opencontainers.image.description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. - org.opencontainers.image.url: https://unpackerr.zip - org.opencontainers.image.source: "{{ .GitURL }}" - org.opencontainers.image.vendor: Go Lift - org.opencontainers.image.licenses: MIT + platforms: *docker_platforms + labels: *docker_labels aur_sources: - name: unpackerr From cac74136b6c9fa1605808cb5efd8e82545d188fb Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 26 Aug 2026 23:11:58 -0700 Subject: [PATCH 03/41] Split Docker channels: nightly from main, unstable stays manual. GoReleaser --nightly now means "not a GitHub release." A daily cron (and manual dispatch on main) publishes Hub/GHCR :nightly from main; pushing the unstable branch still publishes :unstable only. Co-authored-by: Cursor --- .../scripts}/unstable_upload.sh | 0 .github/workflows/release.yml | 38 ++++++++++++++++-- .goreleaser.yaml | 39 +++++++++++++++---- 3 files changed, 66 insertions(+), 11 deletions(-) rename {scripts => .github/scripts}/unstable_upload.sh (100%) diff --git a/scripts/unstable_upload.sh b/.github/scripts/unstable_upload.sh similarity index 100% rename from scripts/unstable_upload.sh rename to .github/scripts/unstable_upload.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a4d6638..ca9f8fa8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,9 @@ name: build-and-release on: + # Calendar nightly is Docker :nightly from main. It does not touch the unstable branch. + schedule: + - cron: "27 12 * * *" + workflow_dispatch: push: branches: - unstable @@ -23,7 +27,32 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + # Scheduled runs always build main, even if GitHub starts the workflow from another ref. + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} - run: git fetch --force --tags + - name: Choose release channel + id: channel + run: | + set -euo pipefail + if [[ "${GITHUB_EVENT_NAME}" == schedule ]]; then + channel=nightly + args='release --nightly --clean --timeout 60m' + elif [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch && "${GITHUB_REF}" == refs/heads/main ]]; then + channel=nightly + args='release --nightly --clean --timeout 60m' + elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + channel=release + args='release --clean --timeout 60m' + elif [[ "${GITHUB_REF}" == refs/heads/unstable ]]; then + channel=unstable + args='release --nightly --clean --timeout 60m' + else + echo "refusing to release from ${GITHUB_REF} (${GITHUB_EVENT_NAME})" >&2 + exit 1 + fi + echo "channel=${channel}" >> "${GITHUB_OUTPUT}" + echo "CHANNEL=${channel}" >> "${GITHUB_ENV}" + echo "args=${args}" >> "${GITHUB_OUTPUT}" - name: Install package tooling run: sudo apt-get update && sudo apt-get install -y rpm genisoimage - name: Set up QEMU @@ -76,8 +105,9 @@ jobs: with: distribution: goreleaser-pro version: "~> v2" - args: ${{ github.ref == 'refs/heads/unstable' && 'release --nightly --clean --timeout 60m' || 'release --clean --timeout 60m' }} + args: ${{ steps.channel.outputs.args }} env: + CHANNEL: ${{ steps.channel.outputs.channel }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} @@ -98,14 +128,16 @@ jobs: PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} AUR_DEPLOY_KEY: ${{ secrets.AUR_DEPLOY_KEY }} - uses: golift/upload-packagecloud@eb0067369b59340b04dce218010960ebe74720b2 # v1.1.1 + if: env.CHANNEL != 'nightly' with: - userrepo: ${{ github.ref == 'refs/heads/unstable' && 'golift/unstable' || 'golift/pkgs' }} + userrepo: ${{ env.CHANNEL == 'release' && 'golift/pkgs' || 'golift/unstable' }} apitoken: ${{ secrets.PACKAGECLOUD_TOKEN }} packages: dist/ rpmdists: el/6 debdists: ubuntu/focal - name: Upload to unstable.golift.io + if: env.CHANNEL == 'unstable' env: UNSTABLE_UPLOAD_KEY: ${{ secrets.UNSTABLE_UPLOAD_KEY }} VERSION: ${{ github.ref_name }} - run: bash scripts/unstable_upload.sh dist + run: bash .github/scripts/unstable_upload.sh dist diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7d46d664..255f130a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -4,9 +4,12 @@ pro: true project_name: unpackerr -# Nightly (push to unstable): version is for packages/ldflags; Docker tag stays `unstable`. +# GoReleaser --nightly is "not a tagged GitHub release". CHANNEL picks the Docker tag: +# unstable = push to the unstable branch (manual) +# nightly = daily cron from main +# release = v* tags nightly: - version_template: "{{ incpatch .Version }}-unstable" + version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}" before: hooks: @@ -269,7 +272,7 @@ nfpms: dockers_v2: - id: unpackerr-release - disable: "{{ .IsNightly }}" + disable: '{{ ne .Env.CHANNEL "release" }}' dockerfile: init/docker/Dockerfile ids: [unpackerr] images: @@ -296,7 +299,7 @@ dockers_v2: org.opencontainers.image.vendor: Go Lift org.opencontainers.image.licenses: MIT - id: unpackerr-release-hub - disable: '{{ or .IsNightly (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' + disable: '{{ or (ne .Env.CHANNEL "release") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile ids: [unpackerr] images: @@ -309,8 +312,8 @@ dockers_v2: - "{{ .Major }}" platforms: *docker_platforms labels: *docker_labels - - id: unpackerr-nightly - disable: "{{ not .IsNightly }}" + - id: unpackerr-unstable + disable: '{{ ne .Env.CHANNEL "unstable" }}' dockerfile: init/docker/Dockerfile ids: [unpackerr] images: @@ -319,8 +322,8 @@ dockers_v2: - unstable platforms: *docker_platforms labels: *docker_labels - - id: unpackerr-nightly-hub - disable: '{{ or (not .IsNightly) (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' + - id: unpackerr-unstable-hub + disable: '{{ or (ne .Env.CHANNEL "unstable") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile ids: [unpackerr] images: @@ -329,6 +332,26 @@ dockers_v2: - unstable platforms: *docker_platforms labels: *docker_labels + - id: unpackerr-nightly + disable: '{{ ne .Env.CHANNEL "nightly" }}' + dockerfile: init/docker/Dockerfile + ids: [unpackerr] + images: + - ghcr.io/unpackerr/unpackerr + tags: + - nightly + platforms: *docker_platforms + labels: *docker_labels + - id: unpackerr-nightly-hub + disable: '{{ or (ne .Env.CHANNEL "nightly") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' + dockerfile: init/docker/Dockerfile + ids: [unpackerr] + images: + - docker.io/golift/unpackerr + tags: + - nightly + platforms: *docker_platforms + labels: *docker_labels aur_sources: - name: unpackerr From 2ad8e64adee71f0ab2839b3037727129bd760f09 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 26 Aug 2026 23:18:43 -0700 Subject: [PATCH 04/41] Fix DMG extra_files for GoReleaser 2.18. v2.18 expects glob/name_template on dmg extra_files, not archive-style src/dst. Co-authored-by: Cursor --- .goreleaser.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 255f130a..622399d6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -161,8 +161,8 @@ dmg: ids: [unpackerr-app] use: appbundle extra_files: - - src: init/macos/background.png - dst: background.png + - glob: init/macos/background.png + name_template: background.png source: enabled: true From 5e1a83f30a446f22bc2491c6ba38a74f3d31454c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 06:34:18 +0000 Subject: [PATCH 05/41] Fix Darwin GoReleaser builds: enable CGO for energye/systray. The unpackerr-darwin target used CGO_ENABLED=0, so GitHub's Linux release job omitted energye/systray's Objective-C Cocoa backend and failed with undefined nativeLoop/setInternalLoop/quit while compiling darwin/amd64. Restore CGO_ENABLED=1 (same as the old Makefile) and install osxcross from goreleaser-cross so ubuntu-latest can compile it. Co-authored-by: Andreas Echavez --- .github/workflows/release.yml | 16 +++++++++++++++- .goreleaser.yaml | 9 ++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca9f8fa8..d91af1f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: echo "CHANNEL=${channel}" >> "${GITHUB_ENV}" echo "args=${args}" >> "${GITHUB_OUTPUT}" - name: Install package tooling - run: sudo apt-get update && sudo apt-get install -y rpm genisoimage + run: sudo apt-get update && sudo apt-get install -y rpm genisoimage clang llvm - name: Set up QEMU uses: docker/setup-qemu-action@v4 with: @@ -101,6 +101,19 @@ jobs: go install golift.io/codesign/cmd/codesign@v1.0.3 echo "${GOBIN}" >> "${GITHUB_PATH}" echo "CODESIGN_BIN=${GOBIN}/codesign" >> "${GITHUB_ENV}" + # energye/systray needs CGO on Darwin. Copy osxcross from goreleaser-cross + # so ubuntu-latest can compile the Cocoa backend (o64-clang / oa64-clang). + - name: Install Darwin CGO toolchain + run: | + set -euo pipefail + docker pull ghcr.io/goreleaser/goreleaser-cross:v1.27.0-1 + cid="$(docker create ghcr.io/goreleaser/goreleaser-cross:v1.27.0-1)" + sudo docker cp "${cid}:/usr/local/osxcross" /usr/local/osxcross + docker rm "${cid}" + test -x /usr/local/osxcross/bin/o64-clang + test -x /usr/local/osxcross/bin/oa64-clang + echo /usr/local/osxcross/bin >> "${GITHUB_PATH}" + echo OSX_CROSS=1 >> "${GITHUB_ENV}" - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro @@ -108,6 +121,7 @@ jobs: args: ${{ steps.channel.outputs.args }} env: CHANNEL: ${{ steps.channel.outputs.channel }} + OSX_CROSS: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 622399d6..33c46ba1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -47,11 +47,18 @@ builds: - -X "golift.io/version.BuildUser={{.Env.USER}}" - -X "golift.io/version.Revision={{.Env.REVISION}}" - -X "golift.io/version.Branch={{.ShortCommit}} [{{.Branch}}]" + # energye/systray's Darwin backend is Objective-C (Cocoa). CGO_ENABLED=0 + # drops those symbols (undefined: nativeLoop, setInternalLoop, ...). + # Linux CI sets OSX_CROSS and uses osxcross (o64-clang / oa64-clang); + # local macOS snapshots keep the host clang. - id: unpackerr-darwin env: - - CGO_ENABLED=0 + - CGO_ENABLED=1 - CGO_LDFLAGS=-mmacosx-version-min=10.8 - CGO_CFLAGS=-mmacosx-version-min=10.8 + - MACOSX_DEPLOYMENT_TARGET=10.8 + - 'CC={{ if isEnvSet "OSX_CROSS" }}{{ if eq .Arch "amd64" }}o64-clang{{ else }}oa64-clang{{ end }}{{ else }}clang{{ end }}' + - 'CXX={{ if isEnvSet "OSX_CROSS" }}{{ if eq .Arch "amd64" }}o64-clang++{{ else }}oa64-clang++{{ end }}{{ else }}clang++{{ end }}' goos: - darwin goarch: From 12e53eec1ae45572fb835e831344f4648becc350 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 00:53:28 -0700 Subject: [PATCH 06/41] Match Darwin min to Go 1.27 and skip Docker SBOM attestations. Go 1.27 emits macOS 13 objects, so 10.8 only produced ld warnings. Repo-scoped Hub tokens cannot pull docker/buildkit-syft-scanner, which dockers_v2 SBOM attestations require. Co-authored-by: Cursor --- .goreleaser.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 33c46ba1..bbaa9f26 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -54,9 +54,10 @@ builds: - id: unpackerr-darwin env: - CGO_ENABLED=1 - - CGO_LDFLAGS=-mmacosx-version-min=10.8 - - CGO_CFLAGS=-mmacosx-version-min=10.8 - - MACOSX_DEPLOYMENT_TARGET=10.8 + # Go 1.27's darwin objects are macOS 13; a lower min just makes ld warn. + - CGO_LDFLAGS=-mmacosx-version-min=13.0 + - CGO_CFLAGS=-mmacosx-version-min=13.0 + - MACOSX_DEPLOYMENT_TARGET=13.0 - 'CC={{ if isEnvSet "OSX_CROSS" }}{{ if eq .Arch "amd64" }}o64-clang{{ else }}oa64-clang{{ end }}{{ else }}clang{{ end }}' - 'CXX={{ if isEnvSet "OSX_CROSS" }}{{ if eq .Arch "amd64" }}o64-clang++{{ else }}oa64-clang++{{ end }}{{ else }}clang++{{ end }}' goos: @@ -305,6 +306,8 @@ dockers_v2: org.opencontainers.image.source: "{{ .GitURL }}" org.opencontainers.image.vendor: Go Lift org.opencontainers.image.licenses: MIT + # Repo-scoped Hub PATs cannot pull docker/buildkit-syft-scanner. + sbom: false - id: unpackerr-release-hub disable: '{{ or (ne .Env.CHANNEL "release") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile @@ -319,6 +322,7 @@ dockers_v2: - "{{ .Major }}" platforms: *docker_platforms labels: *docker_labels + sbom: false - id: unpackerr-unstable disable: '{{ ne .Env.CHANNEL "unstable" }}' dockerfile: init/docker/Dockerfile @@ -329,6 +333,7 @@ dockers_v2: - unstable platforms: *docker_platforms labels: *docker_labels + sbom: false - id: unpackerr-unstable-hub disable: '{{ or (ne .Env.CHANNEL "unstable") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile @@ -339,6 +344,7 @@ dockers_v2: - unstable platforms: *docker_platforms labels: *docker_labels + sbom: false - id: unpackerr-nightly disable: '{{ ne .Env.CHANNEL "nightly" }}' dockerfile: init/docker/Dockerfile @@ -349,6 +355,7 @@ dockers_v2: - nightly platforms: *docker_platforms labels: *docker_labels + sbom: false - id: unpackerr-nightly-hub disable: '{{ or (ne .Env.CHANNEL "nightly") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile @@ -359,6 +366,7 @@ dockers_v2: - nightly platforms: *docker_platforms labels: *docker_labels + sbom: false aur_sources: - name: unpackerr From a24cca7e526976f28062caf8ced369293bb366da Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 01:02:57 -0700 Subject: [PATCH 07/41] Publish Homebrew as a cask, matching motifini. Cask service: is Automator, not brew services, and casks have no etc helper. Flatten archives so the cask finds the binary, install a LaunchAgent in postflight, and restore Docker SBOM now that the Hub token can pull the scanner. Co-authored-by: Cursor --- .goreleaser.yaml | 95 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index bbaa9f26..d35e6c6d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -181,7 +181,8 @@ archives: - id: default ids: [unpackerr, unpackerr-darwin, unpackerr-windows] formats: [tar.gz] - wrap_in_directory: true + # Flat so the Homebrew cask can find binary/conf/manpage without a rename. + wrap_in_directory: false files: - LICENSE - README.md @@ -306,8 +307,6 @@ dockers_v2: org.opencontainers.image.source: "{{ .GitURL }}" org.opencontainers.image.vendor: Go Lift org.opencontainers.image.licenses: MIT - # Repo-scoped Hub PATs cannot pull docker/buildkit-syft-scanner. - sbom: false - id: unpackerr-release-hub disable: '{{ or (ne .Env.CHANNEL "release") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile @@ -322,7 +321,6 @@ dockers_v2: - "{{ .Major }}" platforms: *docker_platforms labels: *docker_labels - sbom: false - id: unpackerr-unstable disable: '{{ ne .Env.CHANNEL "unstable" }}' dockerfile: init/docker/Dockerfile @@ -333,7 +331,6 @@ dockers_v2: - unstable platforms: *docker_platforms labels: *docker_labels - sbom: false - id: unpackerr-unstable-hub disable: '{{ or (ne .Env.CHANNEL "unstable") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile @@ -344,7 +341,6 @@ dockers_v2: - unstable platforms: *docker_platforms labels: *docker_labels - sbom: false - id: unpackerr-nightly disable: '{{ ne .Env.CHANNEL "nightly" }}' dockerfile: init/docker/Dockerfile @@ -355,7 +351,6 @@ dockers_v2: - nightly platforms: *docker_platforms labels: *docker_labels - sbom: false - id: unpackerr-nightly-hub disable: '{{ or (ne .Env.CHANNEL "nightly") (not (isEnvSet "DOCKERHUB_PUBLISH")) }}' dockerfile: init/docker/Dockerfile @@ -366,7 +361,6 @@ dockers_v2: - nightly platforms: *docker_platforms labels: *docker_labels - sbom: false aur_sources: - name: unpackerr @@ -425,9 +419,13 @@ aur_sources: echo 'u unpackerr - "unpackerr daemon"' > unpackerr.sysusers install -D -m 644 unpackerr.sysusers "${pkgdir}/usr/lib/sysusers.d/unpackerr.conf" -brews: +homebrew_casks: - name: unpackerr ids: [default] + binaries: + - unpackerr + manpages: + - unpackerr.1.gz skip_upload: "{{ .IsNightly }}" repository: owner: golift @@ -437,32 +435,69 @@ brews: commit_author: name: goreleaserbot email: bot@goreleaser.com - commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" - directory: Formula + commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}" + directory: Casks homepage: https://unpackerr.zip description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. license: MIT - url_template: "https://github.com/Unpackerr/unpackerr/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - test: assert_match "#{name} v#{version}", shell_output("#{bin}/#{name} -v 2>&1", 2) - extra_install: | - (etc/"unpackerr").mkpath - conf_example = etc/"unpackerr/unpackerr.conf.example" - FileUtils.cp "unpackerr.conf.example", conf_example - FileUtils.cp "unpackerr.conf.example", etc/"unpackerr/unpackerr.conf" unless (etc/"unpackerr/unpackerr.conf").exist? - (var/"log").mkpath - touch var/"log/unpackerr.log" - service: | - run [opt_bin/"unpackerr", "--config", etc/"unpackerr/unpackerr.conf"] - keep_alive true - log_path var/"log/unpackerr.log" - error_log_path var/"log/unpackerr.log" + url: + template: "https://github.com/Unpackerr/unpackerr/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + verified: github.com/Unpackerr/unpackerr + # Do not set `service:` — cask `service` is ~/Library/Services (Automator), not LaunchAgents. + # Install a LaunchAgent in postflight; brew services is formula-only. + hooks: + post: + install: | + conf_dir = HOMEBREW_PREFIX/"etc/unpackerr" + conf_dir.mkpath + FileUtils.cp "#{staged_path}/unpackerr.conf.example", conf_dir/"unpackerr.conf.example" + had_conf = (conf_dir/"unpackerr.conf").exist? + FileUtils.cp "#{staged_path}/unpackerr.conf.example", conf_dir/"unpackerr.conf" unless had_conf + (HOMEBREW_PREFIX/"var/log").mkpath + label = "io.golift.unpackerr" + agent = Pathname.new(Dir.home)/"Library/LaunchAgents/#{label}.plist" + agent.dirname.mkpath + binary = HOMEBREW_PREFIX/"bin/unpackerr" + conf = conf_dir/"unpackerr.conf" + log = HOMEBREW_PREFIX/"var/log/unpackerr.log" + agent.write <<~XML + + + + + Label#{label} + ProgramArguments + + #{binary} + --config + #{conf} + + RunAtLoad + KeepAlive + WorkingDirectory#{HOMEBREW_PREFIX} + StandardOutPath#{log} + StandardErrorPath#{log} + + + XML + if had_conf + uid = Process.uid + system "/bin/launchctl", "bootout", "gui/#{uid}/#{label}" + system "/bin/launchctl", "bootstrap", "gui/#{uid}", agent.to_s + end + uninstall: | + label = "io.golift.unpackerr" + agent = Pathname.new(Dir.home)/"Library/LaunchAgents/#{label}.plist" + system "/bin/launchctl", "bootout", "gui/#{Process.uid}/#{label}" + agent.delete if agent.exist? caveats: | - Edit the config file at #{etc}/unpackerr/unpackerr.conf then start unpackerr with - brew services start unpackerr ~ log file: #{var}/log/unpackerr.log + First install: edit #{HOMEBREW_PREFIX}/etc/unpackerr/unpackerr.conf then: + launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.golift.unpackerr.plist + Upgrades restart the LaunchAgent when that config already exists. + Restart: launchctl kickstart -k gui/$(id -u)/io.golift.unpackerr + Stop: launchctl bootout gui/$(id -u)/io.golift.unpackerr + (brew services does not manage casks.) The manual explains the config file options: man unpackerr - install: | - bin.install "unpackerr" - man1.install "unpackerr.1.gz" checksum: name_template: checksums.sha256.txt From 1989e4045ef6ad0f863d84ec724157a3cf68bf9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 08:08:45 +0000 Subject: [PATCH 08/41] Fix packagecloud upload: drop duplicate armhf debs and unique nightly versions. nFPM maps both GOARM 6 and 7 to Debian armhf, so golift/upload-packagecloud rejected the second file with "filename has already been taken". Skip GOARM=7 nfpms (keep GOARM=6, matching the old Makefile) and include REVISION in the nightly version so later unstable pushes are not the same Debian filename. Co-authored-by: Andreas Echavez --- .goreleaser.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d35e6c6d..8e07c5ef 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,7 +9,10 @@ project_name: unpackerr # nightly = daily cron from main # release = v* tags nightly: - version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}" + # Include REVISION (git rev-list count) so packagecloud accepts every + # unstable/nightly push. Without it the Debian filename is reused + # (e.g. unpackerr_0.15.3~unstable+git_amd64.deb) and the upload fails. + version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}.{{ .Env.REVISION }}" before: hooks: @@ -220,6 +223,11 @@ archives: nfpms: - id: unpackerr-packages ids: [unpackerr] + # nFPM maps both GOARM 6 and 7 to Debian armhf. Uploading both to + # packagecloud fails: "filename has already been taken". Keep GOARM=6 + # only, matching the old Makefile armhf package (archives/docker still + # build both). + if: '{{ not (and (eq .Arch "arm") (eq .Arm "7")) }}' vendor: Go Lift homepage: https://unpackerr.zip maintainer: David Newhall II From 09efe7d3e6cac95f6b9840ff44f7dd042b0bf515 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 01:16:08 -0700 Subject: [PATCH 09/41] Package linux armv7 only, and put REVISION on nFPM packages. Keep one armhf for Packagecloud (GOARM=7, matching Docker) instead of skipping v7. Use nFPM release as the old fpm iteration so every upload has a unique Debian/RPM filename. Co-authored-by: Cursor --- .goreleaser.yaml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8e07c5ef..04dffe72 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,10 +9,7 @@ project_name: unpackerr # nightly = daily cron from main # release = v* tags nightly: - # Include REVISION (git rev-list count) so packagecloud accepts every - # unstable/nightly push. Without it the Debian filename is reused - # (e.g. unpackerr_0.15.3~unstable+git_amd64.deb) and the upload fails. - version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}.{{ .Env.REVISION }}" + version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}" before: hooks: @@ -35,8 +32,9 @@ builds: - arm - arm64 - '386' + # One linux ARM (v7). nFPM maps GOARM 6 and 7 both to Debian armhf, so + # packaging both collides on packagecloud. Docker is linux/arm/v7. goarm: - - '6' - '7' flags: - -trimpath @@ -223,11 +221,6 @@ archives: nfpms: - id: unpackerr-packages ids: [unpackerr] - # nFPM maps both GOARM 6 and 7 to Debian armhf. Uploading both to - # packagecloud fails: "filename has already been taken". Keep GOARM=6 - # only, matching the old Makefile armhf package (archives/docker still - # build both). - if: '{{ not (and (eq .Arch "arm") (eq .Arm "7")) }}' vendor: Go Lift homepage: https://unpackerr.zip maintainer: David Newhall II @@ -238,6 +231,9 @@ nfpms: - rpm - archlinux bindir: /usr/bin + # Same as old fpm --iteration: Packagecloud uniqueness is the Debian/RPM + # filename (e.g. unpackerr_0.15.3~unstable+git-1024_amd64.deb). + release: '{{ with .Env.REVISION }}{{ . }}{{ else }}0{{ end }}' version_metadata: git section: default priority: extra From 2d5c8586076d3f5dcac8996832d02f860b9eed51 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 08:29:46 +0000 Subject: [PATCH 10/41] Fix packagecloud Debian parse error from untemplated nfpms.release. GoReleaser passes nfpms.release through to nFPM without applying Go templates, so '{{ with .Env.REVISION }}...' was written into the Debian Version and PackageCloud rejected the .deb as unreadable. Put REVISION on nightly.version_template instead, which is templated, and keep a single linux GOARM=7 armhf package. Co-authored-by: Andreas Echavez --- .goreleaser.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 04dffe72..a4d6f72b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,7 +9,11 @@ project_name: unpackerr # nightly = daily cron from main # release = v* tags nightly: - version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}" + # Include REVISION (git rev-list count) so packagecloud accepts every + # unstable/nightly push. nfpms.release is not templated by GoReleaser + # (passed through as-is, so "{{ .Env.REVISION }}" becomes an invalid + # Debian Version). Uniqueness has to live on this template. + version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}.{{ .Env.REVISION }}" before: hooks: @@ -231,9 +235,6 @@ nfpms: - rpm - archlinux bindir: /usr/bin - # Same as old fpm --iteration: Packagecloud uniqueness is the Debian/RPM - # filename (e.g. unpackerr_0.15.3~unstable+git-1024_amd64.deb). - release: '{{ with .Env.REVISION }}{{ . }}{{ else }}0{{ end }}' version_metadata: git section: default priority: extra From 49ae94149f8bd597c5928da89b25c088e55125fd Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 08:47:46 -0700 Subject: [PATCH 11/41] Drop the channel name from nightly package versions. Unstable debs already go to golift/unstable, so encoding CHANNEL in the Debian version only added noise. Keep uniqueness with REVISION on the templated nightly.version_template. Co-authored-by: Cursor --- .goreleaser.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a4d6f72b..824e09bf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,11 +9,11 @@ project_name: unpackerr # nightly = daily cron from main # release = v* tags nightly: - # Include REVISION (git rev-list count) so packagecloud accepts every - # unstable/nightly push. nfpms.release is not templated by GoReleaser - # (passed through as-is, so "{{ .Env.REVISION }}" becomes an invalid - # Debian Version). Uniqueness has to live on this template. - version_template: "{{ incpatch .Version }}-{{ .Env.CHANNEL }}.{{ .Env.REVISION }}" + # REVISION (git rev-list count) makes every --nightly cut unique for + # packagecloud. nfpms.release is not templated by GoReleaser. CHANNEL is + # only the Docker tag / repo (golift/unstable); it does not belong in the + # package version. + version_template: "{{ incpatch .Version }}-{{ .Env.REVISION }}" before: hooks: From fba938b4f35506af2f814a874d1c5f1109ebf3b1 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 09:02:55 -0700 Subject: [PATCH 12/41] Pass REVISION into GoReleaser the same way CHANNEL is passed. nightly.version_template reads .Env.REVISION; the action env map did not include it, so uniqueness never reached nFPM. Co-authored-by: Cursor --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d91af1f7..3c39c3a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,6 +121,9 @@ jobs: args: ${{ steps.channel.outputs.args }} env: CHANNEL: ${{ steps.channel.outputs.channel }} + # GITHUB_ENV alone is not enough: goreleaser-action only forwards + # this map, and nightly.version_template needs REVISION. + REVISION: ${{ env.REVISION }} OSX_CROSS: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} From f1f4be4c8aee54ee2b8c1b3244c5b829a545f600 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 10:03:59 -0700 Subject: [PATCH 13/41] Show the energye tray menu on click. getlantern attached the menu to the status item; energye only fires click callbacks. Without ShowMenu, the macOS menu-bar icon (and Windows left-click) does nothing. Co-authored-by: Cursor --- pkg/unpackerr/tray.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/unpackerr/tray.go b/pkg/unpackerr/tray.go index 8a74f725..db1106c5 100644 --- a/pkg/unpackerr/tray.go +++ b/pkg/unpackerr/tray.go @@ -31,6 +31,17 @@ func (u *Unpackerr) startTray() { systray.Run(u.readyTray, u.exitTray) } +// showTrayMenu pops the tray menu. energye/systray does not attach the menu to +// the status item the way getlantern did, so a click does nothing unless we +// call ShowMenu. Windows left-click has the same requirement. +func showTrayMenu(menu systray.IMenu) { + if menu == nil { + return + } + + _ = menu.ShowMenu() +} + func (u *Unpackerr) exitTray() { u.Stop() // stop and wait for extractions. // because systray wants to control the exit code? no.. @@ -41,6 +52,8 @@ func (u *Unpackerr) exitTray() { func (u *Unpackerr) readyTray() { systray.SetTemplateIcon(bindata.SystrayIcon, bindata.SystrayIcon) systray.SetTooltip("Unpackerr" + " v" + version.Version) + systray.SetOnClick(showTrayMenu) + systray.SetOnRClick(showTrayMenu) u.makeChannels() u.menu["info"].Disable() From 2ab0fe169b739ffc99d9f63898d4a7944127cd00 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 10:15:36 -0700 Subject: [PATCH 14/41] fix menu --- init/macos/Unpackerr.app/Contents/Info.plist | 2 +- pkg/unpackerr/tray.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/init/macos/Unpackerr.app/Contents/Info.plist b/init/macos/Unpackerr.app/Contents/Info.plist index b7ab2178..d1192939 100644 --- a/init/macos/Unpackerr.app/Contents/Info.plist +++ b/init/macos/Unpackerr.app/Contents/Info.plist @@ -34,7 +34,7 @@ Unpackerr NSHumanReadableCopyright - © 2023 Go Lift (https://golift.io) + © 2026 Go Lift (https://golift.io) LSEnvironment diff --git a/pkg/unpackerr/tray.go b/pkg/unpackerr/tray.go index 8a74f725..db1106c5 100644 --- a/pkg/unpackerr/tray.go +++ b/pkg/unpackerr/tray.go @@ -31,6 +31,17 @@ func (u *Unpackerr) startTray() { systray.Run(u.readyTray, u.exitTray) } +// showTrayMenu pops the tray menu. energye/systray does not attach the menu to +// the status item the way getlantern did, so a click does nothing unless we +// call ShowMenu. Windows left-click has the same requirement. +func showTrayMenu(menu systray.IMenu) { + if menu == nil { + return + } + + _ = menu.ShowMenu() +} + func (u *Unpackerr) exitTray() { u.Stop() // stop and wait for extractions. // because systray wants to control the exit code? no.. @@ -41,6 +52,8 @@ func (u *Unpackerr) exitTray() { func (u *Unpackerr) readyTray() { systray.SetTemplateIcon(bindata.SystrayIcon, bindata.SystrayIcon) systray.SetTooltip("Unpackerr" + " v" + version.Version) + systray.SetOnClick(showTrayMenu) + systray.SetOnRClick(showTrayMenu) u.makeChannels() u.menu["info"].Disable() From bf85345ba057c0c266db76a733aaba51fcd5e693 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 13:56:39 -0700 Subject: [PATCH 15/41] Notarize the macOS DMG on a macOS runner and publish stable names to unstable.golift.io. Quill cannot notarize an .app/DMG from Linux, and versioned GoReleaser archives break auto-update URLs. Co-authored-by: Cursor --- .github/scripts/macos_keychain.sh | 61 +++++++ .github/scripts/unstable_upload.sh | 183 +++++++++++++++++-- .github/workflows/release.yml | 276 +++++++++++++++++++++-------- .goreleaser.yaml | 25 +-- init/macos/Info.plist.tmpl | 6 +- init/macos/entitlements.plist | 16 ++ 6 files changed, 468 insertions(+), 99 deletions(-) create mode 100644 .github/scripts/macos_keychain.sh create mode 100644 init/macos/entitlements.plist diff --git a/.github/scripts/macos_keychain.sh b/.github/scripts/macos_keychain.sh new file mode 100644 index 00000000..447a0ed1 --- /dev/null +++ b/.github/scripts/macos_keychain.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Import Developer ID + App Store Connect API key into a temporary keychain +# for GoReleaser Pro macos_native (codesign + notarytool). +set -euo pipefail + +if [ -z "${MACOS_SIGN_P12:-}" ] || [ -z "${MACOS_SIGN_PASSWORD:-}" ]; then + echo "MACOS_SIGN_P12 / MACOS_SIGN_PASSWORD unset" >&2 + exit 1 +fi +if [ -z "${MACOS_NOTARY_KEY:-}" ] || [ -z "${MACOS_NOTARY_KEY_ID:-}" ] || [ -z "${MACOS_NOTARY_ISSUER_ID:-}" ]; then + echo "MACOS_NOTARY_KEY / MACOS_NOTARY_KEY_ID / MACOS_NOTARY_ISSUER_ID unset" >&2 + exit 1 +fi + +tmp="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +cert="${tmp}/unpackerr.p12" +key="${tmp}/unpackerr.p8" +keychain="${tmp}/unpackerr.keychain-db" +profile="${MACOS_NOTARY_PROFILE_NAME:-unpackerr}" +password="${KEYCHAIN_PASSWORD:-$(openssl rand -base64 32)}" + +printf '%s' "${MACOS_SIGN_P12}" | tr -d '\n' | openssl base64 -d -out "${cert}" +printf '%s' "${MACOS_NOTARY_KEY}" | tr -d '\n' | openssl base64 -d -out "${key}" +chmod 600 "${cert}" "${key}" +if [ ! -s "${cert}" ] || [ ! -s "${key}" ]; then + echo "decoded P12 or notary .p8 is empty (secrets must be base64)" >&2 + exit 1 +fi + +security delete-keychain "${keychain}" 2>/dev/null || true +security create-keychain -p "${password}" "${keychain}" +security set-keychain-settings -lut 21600 "${keychain}" +security unlock-keychain -p "${password}" "${keychain}" +security import "${cert}" -P "${MACOS_SIGN_PASSWORD}" -A -t cert -f pkcs12 -k "${keychain}" +security set-key-partition-list -S apple-tool:,apple: -k "${password}" "${keychain}" +security list-keychain -d user -s "${keychain}" + +identity="${MACOS_SIGN_IDENTITY:-}" +if [ -z "${identity}" ]; then + identity="$(security find-identity -v -p codesigning "${keychain}" | awk -F '"' '/Developer ID Application/{print $2; exit}')" +fi +if [ -z "${identity}" ]; then + echo "no Developer ID Application identity in ${keychain}" >&2 + security find-identity -v -p codesigning "${keychain}" >&2 || true + exit 1 +fi + +xcrun notarytool store-credentials "${profile}" \ + --key "${key}" \ + --key-id "${MACOS_NOTARY_KEY_ID}" \ + --issuer "${MACOS_NOTARY_ISSUER_ID}" \ + --keychain "${keychain}" + +{ + echo "KEYCHAIN_PATH=${keychain}" + echo "KEYCHAIN_PASSWORD=${password}" + echo "MACOS_SIGN_IDENTITY=${identity}" + echo "MACOS_NOTARY_PROFILE_NAME=${profile}" +} >> "${GITHUB_ENV}" + +echo "keychain ready: ${identity}" diff --git a/.github/scripts/unstable_upload.sh b/.github/scripts/unstable_upload.sh index 609e8c3e..54b78ab8 100755 --- a/.github/scripts/unstable_upload.sh +++ b/.github/scripts/unstable_upload.sh @@ -1,30 +1,185 @@ #!/usr/bin/env bash -# Upload zip/dmg/gz artifacts to unstable.golift.io (same as the old Actions loop). +# Publish auto-update artifacts to unstable.golift.io with stable names. +# +# GoReleaser writes versioned archives (unpackerr_0.15.3-1037_linux_amd64.tar.gz). +# Auto-update URLs cannot include that version; it lives in a sibling .txt. +# The payload is a gzipped (or zipped) *binary*, matching the old Makefile +# `gzip -9r` / `zip … $exe` layout — not a tar.gz (gunzip of a tar is a tar). +# +# Unpackerr.dmg +# unpackerr.amd64.exe.zip +# unpackerr.{amd64,386,arm,arm64}.linux.gz +# unpackerr.{amd64,i386,armhf,arm64}.freebsd.gz +# +# Sidecar: plain VERSION-REVISION (same as the pre-GoReleaser workflow). set -euo pipefail dir="${1:-dist}" -if [ -z "${UNSTABLE_UPLOAD_KEY:-}" ]; then - echo "UNSTABLE_UPLOAD_KEY unset; skipping unstable.golift.io upload" >&2 - exit 0 +artifacts="${dir}/artifacts.json" +metadata="${dir}/metadata.json" + +if [ ! -f "${artifacts}" ]; then + echo "missing ${artifacts}; GoReleaser did not produce artifacts.json" >&2 + exit 1 +fi +if ! command -v jq >/dev/null; then + echo "jq is required to read ${artifacts}" >&2 + exit 1 +fi + +version="${VERSION:-}" +if [ -z "${version}" ] && [ -f "${metadata}" ]; then + version="$(jq -r '.version // empty' "${metadata}")" +fi +if [ -z "${version}" ] || [ "${version}" = "unknown" ] || [ "${version}" = "unstable" ]; then + echo "refusing to upload with VERSION=${version:-} (need dist/metadata.json or VERSION=0.15.3-1234)" >&2 + exit 1 +fi + +stage="${UNSTABLE_STAGE_DIR:-}" +owned_stage=0 +if [ -z "${stage}" ]; then + stage="$(mktemp -d "${TMPDIR:-/tmp}/unpackerr-unstable.XXXXXX")" + owned_stage=1 +fi +if [ "${owned_stage}" -eq 1 ]; then + trap 'rm -rf "${stage}"' EXIT +fi +mkdir -p "${stage}" + +resolve_path() { + local p=$1 + if [ -f "${p}" ]; then + printf '%s' "${p}" + return + fi + if [ -f "${dir}/${p}" ]; then + printf '%s' "${dir}/${p}" + return + fi + echo "artifact missing: ${p}" >&2 + return 1 +} + +# Historical names: linux uses GOARCH; freebsd 386/arm used i386/armhf. +dest_name() { + local os=$1 arch=$2 + case "${os}" in + windows) + printf 'unpackerr.%s.exe.zip' "${arch}" + ;; + linux) + printf 'unpackerr.%s.linux.gz' "${arch}" + ;; + freebsd) + case "${arch}" in + 386) printf 'unpackerr.i386.freebsd.gz' ;; + arm) printf 'unpackerr.armhf.freebsd.gz' ;; + *) printf 'unpackerr.%s.freebsd.gz' "${arch}" ;; + esac + ;; + *) + return 1 + ;; + esac +} + +zip_exe() { + local src=$1 dest=$2 + python3 - "${src}" "${dest}" <<'PY' +import sys, zipfile +src, dest = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(dest, "w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.write(src, arcname="unpackerr.exe") +PY +} + +# Prefer GOARM 7 over 6 when both exist (same dest name). +while IFS=$'\t' read -r os arch goarm path; do + [ -n "${path}" ] || continue + if [ "${arch}" = arm ] && [ "${goarm}" = 6 ]; then + continue + fi + src="$(resolve_path "${path}")" || exit 1 + dest="$(dest_name "${os}" "${arch}")" || continue + out="${stage}/${dest}" + case "${os}" in + windows) + zip_exe "${src}" "${out}" "${out}" &2 +else + echo "Unpackerr.dmg missing from ${dir}; darwin split/notarize did not produce a DMG" >&2 + exit 1 fi shopt -s nullglob -files=("${dir}"/*.zip "${dir}"/*.dmg "${dir}"/*.gz) -if [ ${#files[@]} -eq 0 ]; then - echo "no zip/dmg/gz artifacts in ${dir}" >&2 - exit 0 +staged=("${stage}"/*) +shopt -u nullglob +if [ ${#staged[@]} -eq 0 ]; then + echo "no unstable artifacts staged from ${artifacts}" >&2 + exit 1 fi -version="${VERSION:-unknown}" -for file in "${files[@]}"; do - [ -f "$file" ] || continue - name="$(basename "$file")" - echo "Uploading ${name}" +upload() { + local file=$1 + local name + name="$(basename "${file}")" + echo "Uploading ${name} (${version})" curl -sS --fail-with-body --retry 5 --retry-all-errors --retry-delay 2 \ -H "X-API-KEY: ${UNSTABLE_UPLOAD_KEY}" \ - "https://unstable.golift.io/upload.php?folder=unpackerr" -F "file=@${file}" + "https://unstable.golift.io/upload.php?folder=unpackerr" \ + -F "file=@${file};filename=${name}" curl -sS --fail-with-body --retry 5 --retry-all-errors --retry-delay 2 \ -H "X-API-KEY: ${UNSTABLE_UPLOAD_KEY}" \ "https://unstable.golift.io/upload.php?folder=unpackerr" \ -F "file=${version};filename=${name}.txt;type=text/plain" +} + +if [ -z "${UNSTABLE_UPLOAD_KEY:-}" ]; then + echo "UNSTABLE_UPLOAD_KEY unset; staged without uploading:" >&2 + ls -l "${stage}" + exit 0 +fi + +for file in "${staged[@]}"; do + [ -f "${file}" ] || continue + upload "${file}" done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c39c3a4..c5f0e610 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,73 +12,66 @@ on: permissions: contents: write packages: write + +# Darwin App/DMG notarization needs xcrun/codesign (macOS). Linux/Windows/FreeBSD +# split on ubuntu; merge publishes Docker, GitHub, Homebrew, AUR, packagecloud. jobs: - release: + channel: runs-on: ubuntu-latest - permissions: - contents: write - packages: write - id-token: write - env: - # Map first: secrets cannot be referenced in steps.if. - CODESIGN_URL: ${{ secrets.CODESIGN_URL }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + outputs: + channel: ${{ steps.channel.outputs.channel }} + extra: ${{ steps.channel.outputs.extra }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - # Scheduled runs always build main, even if GitHub starts the workflow from another ref. - ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} - - run: git fetch --force --tags - name: Choose release channel id: channel run: | set -euo pipefail + extra= if [[ "${GITHUB_EVENT_NAME}" == schedule ]]; then channel=nightly - args='release --nightly --clean --timeout 60m' + extra=--nightly elif [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch && "${GITHUB_REF}" == refs/heads/main ]]; then channel=nightly - args='release --nightly --clean --timeout 60m' + extra=--nightly elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then channel=release - args='release --clean --timeout 60m' elif [[ "${GITHUB_REF}" == refs/heads/unstable ]]; then channel=unstable - args='release --nightly --clean --timeout 60m' + extra=--nightly else echo "refusing to release from ${GITHUB_REF} (${GITHUB_EVENT_NAME})" >&2 exit 1 fi echo "channel=${channel}" >> "${GITHUB_OUTPUT}" - echo "CHANNEL=${channel}" >> "${GITHUB_ENV}" - echo "args=${args}" >> "${GITHUB_OUTPUT}" - - name: Install package tooling - run: sudo apt-get update && sudo apt-get install -y rpm genisoimage clang llvm - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - with: - platforms: arm64,arm - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - name: Log into GHCR - uses: docker/login-action@v4 + echo "extra=${extra}" >> "${GITHUB_OUTPUT}" + echo "CHANNEL=${channel} extra=${extra}" + + split: + needs: channel + strategy: + fail-fast: true + matrix: + goos: [linux, windows, freebsd] + runs-on: ubuntu-latest + permissions: + contents: read + env: + CODESIGN_URL: ${{ secrets.CODESIGN_URL }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Log into Docker Hub - if: env.DOCKERHUB_PASSWORD != '' - uses: docker/login-action@v4 + fetch-depth: 0 + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} + - run: git fetch --force --tags + - name: Save iteration in Revision + run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - username: golift - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Enable Docker Hub publish - if: env.DOCKERHUB_PASSWORD != '' - run: echo DOCKERHUB_PUBLISH=1 >> "$GITHUB_ENV" - - name: Skip Docker Hub - if: env.DOCKERHUB_PASSWORD == '' - run: echo '::warning::DOCKERHUB_PASSWORD is empty; skipping Docker Hub login and Hub image publish. Grant the org secret to this public repository (or add a repo secret) to resume golift/unpackerr pushes.' + go-version-file: go.mod + cache: true + - name: Install package tooling + if: matrix.goos == 'linux' + run: sudo apt-get update && sudo apt-get install -y rpm - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 @@ -86,13 +79,8 @@ jobs: gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - name: Create GPG_SIGNING_KEY file run: echo '${{ secrets.GPG_SIGNING_KEY }}' > /tmp/key.gpg - - name: Save iteration in Revision - run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - cache: true - name: Install golift codesign + if: matrix.goos == 'windows' && env.CODESIGN_URL != '' env: GOBIN: ${{ runner.temp }}/codesign-bin run: | @@ -101,33 +89,18 @@ jobs: go install golift.io/codesign/cmd/codesign@v1.0.3 echo "${GOBIN}" >> "${GITHUB_PATH}" echo "CODESIGN_BIN=${GOBIN}/codesign" >> "${GITHUB_ENV}" - # energye/systray needs CGO on Darwin. Copy osxcross from goreleaser-cross - # so ubuntu-latest can compile the Cocoa backend (o64-clang / oa64-clang). - - name: Install Darwin CGO toolchain - run: | - set -euo pipefail - docker pull ghcr.io/goreleaser/goreleaser-cross:v1.27.0-1 - cid="$(docker create ghcr.io/goreleaser/goreleaser-cross:v1.27.0-1)" - sudo docker cp "${cid}:/usr/local/osxcross" /usr/local/osxcross - docker rm "${cid}" - test -x /usr/local/osxcross/bin/o64-clang - test -x /usr/local/osxcross/bin/oa64-clang - echo /usr/local/osxcross/bin >> "${GITHUB_PATH}" - echo OSX_CROSS=1 >> "${GITHUB_ENV}" - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro version: "~> v2" - args: ${{ steps.channel.outputs.args }} + args: release --clean --split --timeout 60m ${{ needs.channel.outputs.extra }} env: - CHANNEL: ${{ steps.channel.outputs.channel }} - # GITHUB_ENV alone is not enough: goreleaser-action only forwards - # this map, and nightly.version_template needs REVISION. + # GGOOS filters targets without leaking GOOS into before-hook `go run`. + GGOOS: ${{ matrix.goos }} + CHANNEL: ${{ needs.channel.outputs.channel }} REVISION: ${{ env.REVISION }} - OSX_CROSS: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} - HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} USER: github-actions GPG_SIGNING_KEY: /tmp/key.gpg GPG_SIGNING_KEY_ID: ${{ steps.import_gpg.outputs.keyid }} @@ -137,13 +110,174 @@ jobs: CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} CODESIGN_NAME: Unpackerr CODESIGN_WEBSITE: https://unpackerr.zip + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.goos }} + path: dist/${{ matrix.goos }} + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + + split-darwin: + needs: channel + # Nightly is Docker-only; skip Apple notarization. + if: needs.channel.outputs.channel != 'nightly' + runs-on: macos-latest + timeout-minutes: 90 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} + - run: git fetch --force --tags + - name: Save iteration in Revision + run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + - name: Import signing certificate and notary credentials + env: MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + run: bash .github/scripts/macos_keychain.sh + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser-pro + version: "~> v2" + args: release --clean --split --timeout 60m ${{ needs.channel.outputs.extra }} + env: + GGOOS: darwin + CHANNEL: ${{ needs.channel.outputs.channel }} + REVISION: ${{ env.REVISION }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} + USER: github-actions + KEYCHAIN_PATH: ${{ env.KEYCHAIN_PATH }} + MACOS_SIGN_IDENTITY: ${{ env.MACOS_SIGN_IDENTITY }} + MACOS_NOTARY_PROFILE_NAME: ${{ env.MACOS_NOTARY_PROFILE_NAME }} + - name: Staple notarized DMG + run: | + set -euo pipefail + found=0 + while IFS= read -r dmg; do + found=1 + echo "stapling ${dmg}" + xcrun stapler staple "${dmg}" + done < <(find dist/darwin -type f -name '*.dmg' | sort) + if [ "${found}" -eq 0 ]; then + echo "no DMG under dist/darwin" >&2 + find dist/darwin -type f >&2 || true + exit 1 + fi + - uses: actions/upload-artifact@v4 + with: + name: dist-darwin + path: dist/darwin + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + + release: + needs: [channel, split, split-darwin] + if: >- + always() && + !cancelled() && + needs.channel.result == 'success' && + needs.split.result == 'success' && + (needs.split-darwin.result == 'success' || needs.split-darwin.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + contents: write + packages: write + id-token: write + env: + CHANNEL: ${{ needs.channel.outputs.channel }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} + - run: git fetch --force --tags + - name: Save iteration in Revision + run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + with: + platforms: arm64,arm + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Log into GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Log into Docker Hub + if: env.DOCKERHUB_PASSWORD != '' + uses: docker/login-action@v4 + with: + username: golift + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Enable Docker Hub publish + if: env.DOCKERHUB_PASSWORD != '' + run: echo DOCKERHUB_PUBLISH=1 >> "$GITHUB_ENV" + - name: Skip Docker Hub + if: env.DOCKERHUB_PASSWORD == '' + run: echo '::warning::DOCKERHUB_PASSWORD is empty; skipping Docker Hub login and Hub image publish. Grant the org secret to this public repository (or add a repo secret) to resume golift/unpackerr pushes.' + - uses: actions/download-artifact@v4 + with: + name: dist-linux + path: dist/linux + - uses: actions/download-artifact@v4 + with: + name: dist-windows + path: dist/windows + - uses: actions/download-artifact@v4 + with: + name: dist-freebsd + path: dist/freebsd + - uses: actions/download-artifact@v4 + if: needs.split-darwin.result == 'success' + with: + name: dist-darwin + path: dist/darwin + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser-pro + version: "~> v2" + args: continue --merge --timeout 60m + env: + CHANNEL: ${{ needs.channel.outputs.channel }} + REVISION: ${{ env.REVISION }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + USER: github-actions PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} AUR_DEPLOY_KEY: ${{ secrets.AUR_DEPLOY_KEY }} + - name: Flatten Linux packages for packagecloud + if: env.CHANNEL != 'nightly' + run: | + set -euo pipefail + find dist -type f \( -name '*.deb' -o -name '*.rpm' \) -print0 | + while IFS= read -r -d '' f; do + base="$(basename "$f")" + if [ ! -e "dist/${base}" ]; then + cp "$f" "dist/${base}" + fi + done - uses: golift/upload-packagecloud@eb0067369b59340b04dce218010960ebe74720b2 # v1.1.1 if: env.CHANNEL != 'nightly' with: @@ -156,5 +290,5 @@ jobs: if: env.CHANNEL == 'unstable' env: UNSTABLE_UPLOAD_KEY: ${{ secrets.UNSTABLE_UPLOAD_KEY }} - VERSION: ${{ github.ref_name }} + CHANNEL: unstable run: bash .github/scripts/unstable_upload.sh dist diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 824e09bf..2020c24f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -54,8 +54,8 @@ builds: - -X "golift.io/version.Branch={{.ShortCommit}} [{{.Branch}}]" # energye/systray's Darwin backend is Objective-C (Cocoa). CGO_ENABLED=0 # drops those symbols (undefined: nativeLoop, setInternalLoop, ...). - # Linux CI sets OSX_CROSS and uses osxcross (o64-clang / oa64-clang); - # local macOS snapshots keep the host clang. + # CI builds this on macos-latest (host clang). OSX_CROSS is only for a + # local Linux snapshot with osxcross (o64-clang / oa64-clang). - id: unpackerr-darwin env: - CGO_ENABLED=1 @@ -138,22 +138,23 @@ universal_binaries: replace: true name_template: unpackerr -# Sign + notarize darwin (quill; works on Linux CI). -# Enabled only when MACOS_SIGN_P12 is set so local snapshots still work. +# Quill (Linux) can only sign/notarize the raw binary. A Gatekeeper-valid +# .app inside a DMG has to be codesign'd + notarytool'd on macOS. +# Enabled when the Darwin split job imported the P12 into a keychain. notarize: - macos: - - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' + macos_native: + - enabled: '{{ isEnvSet "KEYCHAIN_PATH" }}' ids: - unpackerr-darwin + use: dmg sign: - certificate: "{{ .Env.MACOS_SIGN_P12 }}" - password: "{{ .Env.MACOS_SIGN_PASSWORD }}" + keychain: "{{ .Env.KEYCHAIN_PATH }}" + identity: "{{ .Env.MACOS_SIGN_IDENTITY }}" + options: [runtime] + entitlements: init/macos/entitlements.plist notarize: - issuer_id: "{{ .Env.MACOS_NOTARY_ISSUER_ID }}" - key_id: "{{ .Env.MACOS_NOTARY_KEY_ID }}" - key: "{{ .Env.MACOS_NOTARY_KEY }}" + profile_name: "{{ .Env.MACOS_NOTARY_PROFILE_NAME }}" wait: true - timeout: 20m app_bundles: - id: unpackerr-app diff --git a/init/macos/Info.plist.tmpl b/init/macos/Info.plist.tmpl index 6eb0b8c1..c452115a 100644 --- a/init/macos/Info.plist.tmpl +++ b/init/macos/Info.plist.tmpl @@ -9,9 +9,11 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleVersion - {{ .Version }} + {{ .Env.REVISION }} CFBundleShortVersionString - {{ .Version }} + {{ trimSuffix (printf "-%s" .Env.REVISION) .Version }} + LSMinimumSystemVersion + 13.0 CFBundleExecutable {{ .BinaryName }} NSAppTransportSecurity diff --git a/init/macos/entitlements.plist b/init/macos/entitlements.plist new file mode 100644 index 00000000..f5fea3fc --- /dev/null +++ b/init/macos/entitlements.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + From 961f7b0516c7a874b0d41b9449b61f4678c9b5b3 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 14:10:11 -0700 Subject: [PATCH 16/41] Fix release split: OIDC for Windows codesign, skip empty Apple secrets. signerd 401'd because the split job had no id-token; Darwin died on an empty repo MACOS_SIGN_PASSWORD that shadowed the org secret. Co-authored-by: Cursor --- .github/scripts/macos_keychain.sh | 8 ++++---- .github/workflows/release.yml | 33 ++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/scripts/macos_keychain.sh b/.github/scripts/macos_keychain.sh index 447a0ed1..3fb20f7f 100644 --- a/.github/scripts/macos_keychain.sh +++ b/.github/scripts/macos_keychain.sh @@ -4,12 +4,12 @@ set -euo pipefail if [ -z "${MACOS_SIGN_P12:-}" ] || [ -z "${MACOS_SIGN_PASSWORD:-}" ]; then - echo "MACOS_SIGN_P12 / MACOS_SIGN_PASSWORD unset" >&2 - exit 1 + echo "MACOS_SIGN_P12 / MACOS_SIGN_PASSWORD unset; skipping keychain import" >&2 + exit 0 fi if [ -z "${MACOS_NOTARY_KEY:-}" ] || [ -z "${MACOS_NOTARY_KEY_ID:-}" ] || [ -z "${MACOS_NOTARY_ISSUER_ID:-}" ]; then - echo "MACOS_NOTARY_KEY / MACOS_NOTARY_KEY_ID / MACOS_NOTARY_ISSUER_ID unset" >&2 - exit 1 + echo "MACOS_NOTARY_KEY / MACOS_NOTARY_KEY_ID / MACOS_NOTARY_ISSUER_ID unset; skipping keychain import" >&2 + exit 0 fi tmp="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5f0e610..8bd293a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,12 +49,15 @@ jobs: split: needs: channel strategy: - fail-fast: true + fail-fast: false matrix: goos: [linux, windows, freebsd] runs-on: ubuntu-latest permissions: contents: read + # golift/codesign fetches a GitHub OIDC token (audience = CODESIGN_URL). + # Without this, signerd returns 401 unauthorized. + id-token: write env: CODESIGN_URL: ${{ secrets.CODESIGN_URL }} steps: @@ -126,6 +129,13 @@ jobs: timeout-minutes: 90 permissions: contents: read + env: + # Map first: secrets cannot be referenced in steps.if. + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -139,13 +149,21 @@ jobs: go-version-file: go.mod cache: true - name: Import signing certificate and notary credentials - env: - MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} - MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} - MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} - MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} - MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + if: >- + env.MACOS_SIGN_P12 != '' && + env.MACOS_SIGN_PASSWORD != '' && + env.MACOS_NOTARY_KEY != '' && + env.MACOS_NOTARY_KEY_ID != '' && + env.MACOS_NOTARY_ISSUER_ID != '' run: bash .github/scripts/macos_keychain.sh + - name: Skip Apple signing + if: >- + env.MACOS_SIGN_P12 == '' || + env.MACOS_SIGN_PASSWORD == '' || + env.MACOS_NOTARY_KEY == '' || + env.MACOS_NOTARY_KEY_ID == '' || + env.MACOS_NOTARY_ISSUER_ID == '' + run: echo '::warning::macOS signing/notary secrets incomplete; building an unsigned DMG. Grant MACOS_SIGN_P12, MACOS_SIGN_PASSWORD, and MACOS_NOTARY_* to this repository.' - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro @@ -162,6 +180,7 @@ jobs: MACOS_SIGN_IDENTITY: ${{ env.MACOS_SIGN_IDENTITY }} MACOS_NOTARY_PROFILE_NAME: ${{ env.MACOS_NOTARY_PROFILE_NAME }} - name: Staple notarized DMG + if: env.KEYCHAIN_PATH != '' run: | set -euo pipefail found=0 From 3bb73019ecb915e06776453452deef5adcc3fd4a Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 16:58:35 -0700 Subject: [PATCH 17/41] Decode Apple P12/p8 secrets as PEM or long-line base64 on macOS. LibreSSL openssl base64 -d without -A writes empty files for GitHub's single-line secrets, which aborted notarization. Co-authored-by: Cursor --- .github/scripts/macos_keychain.sh | 50 ++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/scripts/macos_keychain.sh b/.github/scripts/macos_keychain.sh index 3fb20f7f..0e97d4da 100644 --- a/.github/scripts/macos_keychain.sh +++ b/.github/scripts/macos_keychain.sh @@ -4,12 +4,12 @@ set -euo pipefail if [ -z "${MACOS_SIGN_P12:-}" ] || [ -z "${MACOS_SIGN_PASSWORD:-}" ]; then - echo "MACOS_SIGN_P12 / MACOS_SIGN_PASSWORD unset; skipping keychain import" >&2 - exit 0 + echo "MACOS_SIGN_P12 / MACOS_SIGN_PASSWORD unset" >&2 + exit 1 fi if [ -z "${MACOS_NOTARY_KEY:-}" ] || [ -z "${MACOS_NOTARY_KEY_ID:-}" ] || [ -z "${MACOS_NOTARY_ISSUER_ID:-}" ]; then - echo "MACOS_NOTARY_KEY / MACOS_NOTARY_KEY_ID / MACOS_NOTARY_ISSUER_ID unset; skipping keychain import" >&2 - exit 0 + echo "MACOS_NOTARY_KEY / MACOS_NOTARY_KEY_ID / MACOS_NOTARY_ISSUER_ID unset" >&2 + exit 1 fi tmp="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" @@ -19,13 +19,43 @@ keychain="${tmp}/unpackerr.keychain-db" profile="${MACOS_NOTARY_PROFILE_NAME:-unpackerr}" password="${KEYCHAIN_PASSWORD:-$(openssl rand -base64 32)}" -printf '%s' "${MACOS_SIGN_P12}" | tr -d '\n' | openssl base64 -d -out "${cert}" -printf '%s' "${MACOS_NOTARY_KEY}" | tr -d '\n' | openssl base64 -d -out "${key}" +# Quill accepted "path or base64". GitHub secrets are either PEM text or +# one-line base64. macOS openssl base64 -d without -A yields empty files +# for long lines, and PEM is not base64 at all. +write_secret() { + local dest=$1 envname=$2 + python3 - "${dest}" "${envname}" <<'PY' +import base64, os, pathlib, sys + +dest, envname = sys.argv[1], sys.argv[2] +raw = os.environ.get(envname, "") +if not raw.strip(): + sys.stderr.write(f"{envname} empty\n") + sys.exit(1) +s = raw.strip().replace("\r", "") +if "BEGIN " in s: + data = (s if s.endswith("\n") else s + "\n").encode() +else: + compact = "".join(s.split()) + compact += "=" * ((4 - len(compact) % 4) % 4) + try: + data = base64.b64decode(compact) + except Exception as exc: + sys.stderr.write(f"{envname} base64 decode failed: {exc}\n") + sys.exit(1) +if not data: + sys.stderr.write(f"{envname} decoded to empty ({len(raw)} input chars)\n") + sys.exit(1) +pathlib.Path(dest).write_bytes(data) +print(f"{envname}: {len(raw)} chars -> {len(data)} bytes") +PY +} + +write_secret "${cert}" MACOS_SIGN_P12 +write_secret "${key}" MACOS_NOTARY_KEY chmod 600 "${cert}" "${key}" -if [ ! -s "${cert}" ] || [ ! -s "${key}" ]; then - echo "decoded P12 or notary .p8 is empty (secrets must be base64)" >&2 - exit 1 -fi +echo "p12 $(wc -c < "${cert}" | tr -d ' ') bytes ($(file -b "${cert}"))" +echo "p8 $(wc -c < "${key}" | tr -d ' ') bytes ($(file -b "${key}"))" security delete-keychain "${keychain}" 2>/dev/null || true security create-keychain -p "${password}" "${keychain}" From 39452a46d0ec99d7b6e4cfc04715f1dd5847f34d Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 17:05:29 -0700 Subject: [PATCH 18/41] Use GoReleaser .RawVersion in Info.plist instead of sprig trimSuffix. The app bundle template has no trimSuffix, which aborted the Darwin split before notarization. Co-authored-by: Cursor --- .github/scripts/macos_keychain.sh | 1 - init/macos/Info.plist.tmpl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/scripts/macos_keychain.sh b/.github/scripts/macos_keychain.sh index 0e97d4da..c22e6eed 100644 --- a/.github/scripts/macos_keychain.sh +++ b/.github/scripts/macos_keychain.sh @@ -83,7 +83,6 @@ xcrun notarytool store-credentials "${profile}" \ { echo "KEYCHAIN_PATH=${keychain}" - echo "KEYCHAIN_PASSWORD=${password}" echo "MACOS_SIGN_IDENTITY=${identity}" echo "MACOS_NOTARY_PROFILE_NAME=${profile}" } >> "${GITHUB_ENV}" diff --git a/init/macos/Info.plist.tmpl b/init/macos/Info.plist.tmpl index c452115a..3a7e853c 100644 --- a/init/macos/Info.plist.tmpl +++ b/init/macos/Info.plist.tmpl @@ -11,7 +11,7 @@ CFBundleVersion {{ .Env.REVISION }} CFBundleShortVersionString - {{ trimSuffix (printf "-%s" .Env.REVISION) .Version }} + {{ .RawVersion }} LSMinimumSystemVersion 13.0 CFBundleExecutable From 6661645bfa5f1aa1d4925c07bb29b8b7d23c9160 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 17:36:45 -0700 Subject: [PATCH 19/41] Point macos_native ids at the app bundle and DMG. GoReleaser skipped codesign and notarytool because those pipes match Extra.ID on Unpackerr.app / Unpackerr.dmg, not the Darwin build id. Co-authored-by: Cursor --- .github/workflows/release.yml | 24 +++++++++++++++++++++++- .goreleaser.yaml | 5 ++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bd293a7..8fce1f29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -183,11 +183,33 @@ jobs: if: env.KEYCHAIN_PATH != '' run: | set -euo pipefail + app="$(find dist/darwin -type d -name 'Unpackerr.app' | head -n 1 || true)" + if [ -z "${app}" ]; then + echo "Unpackerr.app missing under dist/darwin" >&2 + find dist/darwin -type f >&2 || true + exit 1 + fi + if ! codesign -dv --verbose=2 "${app}" 2>&1 | tee /tmp/codesign-app.txt | grep -q "Developer ID Application"; then + echo "Unpackerr.app is not Developer ID signed; macos_native skipped or failed" >&2 + cat /tmp/codesign-app.txt >&2 + exit 1 + fi found=0 while IFS= read -r dmg; do found=1 echo "stapling ${dmg}" - xcrun stapler staple "${dmg}" + ok=0 + for attempt in 1 2 3 4 5; do + if xcrun stapler staple "${dmg}"; then + ok=1 + break + fi + echo "stapler attempt ${attempt} failed; waiting for Apple ticket" + sleep 20 + done + if [ "${ok}" -ne 1 ]; then + exit 1 + fi done < <(find dist/darwin -type f -name '*.dmg' | sort) if [ "${found}" -eq 0 ]; then echo "no DMG under dist/darwin" >&2 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2020c24f..00b5a7a8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -144,8 +144,11 @@ universal_binaries: notarize: macos_native: - enabled: '{{ isEnvSet "KEYCHAIN_PATH" }}' + # These pipes match Extra.ID on the .app / .dmg, not the Darwin build id. + # unpackerr-darwin made both skip: "no app bundles found" / "no artifacts found". ids: - - unpackerr-darwin + - unpackerr-app + - unpackerr-dmg use: dmg sign: keychain: "{{ .Env.KEYCHAIN_PATH }}" From 55e6bae44f6eaf47e9feb67d150c31d02f5507e2 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 17:53:31 -0700 Subject: [PATCH 20/41] Fail Darwin closed on missing Apple secrets and pin publisher Actions. Staple was a pipefail false fail after notarization succeeded; dump codesign then grep. Document the split/merge channels in workflows/README.md. Co-authored-by: Cursor --- .github/scripts/macos_staple.sh | 68 ++++++++++++++++++++++++++ .github/workflows/README.md | 87 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 85 +++++++------------------------- 3 files changed, 173 insertions(+), 67 deletions(-) create mode 100644 .github/scripts/macos_staple.sh create mode 100644 .github/workflows/README.md diff --git a/.github/scripts/macos_staple.sh b/.github/scripts/macos_staple.sh new file mode 100644 index 00000000..ce86ddc2 --- /dev/null +++ b/.github/scripts/macos_staple.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Confirm Developer ID on Unpackerr.app, then staple the notarized DMG. +# codesign -dv writes to stderr; grep -q in a pipefail pipeline is a false fail +# (tee gets SIGPIPE after the first match). +set -euo pipefail + +root="${1:-dist/darwin}" +dump="${RUNNER_TEMP:-/tmp}/codesign-app.txt" + +app="${root}/apps/unpackerr-app_darwinall/Unpackerr.app" +if [ ! -d "${app}" ]; then + app="" + while IFS= read -r p; do + case "${p}" in + */dmg/*) continue ;; + esac + app="${p}" + break + done < <(find "${root}" -type d -name 'Unpackerr.app') +fi +if [ ! -d "${app}" ]; then + while IFS= read -r p; do + app="${p}" + break + done < <(find "${root}" -type d -name 'Unpackerr.app') +fi +if [ ! -d "${app}" ]; then + echo "Unpackerr.app missing under ${root}" >&2 + find "${root}" -type f >&2 || true + exit 1 +fi + +codesign -dv --verbose=2 "${app}" >"${dump}" 2>&1 || true +if ! grep -F "Developer ID Application" "${dump}" >/dev/null; then + echo "Unpackerr.app is not Developer ID signed; macos_native skipped or failed" >&2 + cat "${dump}" >&2 + exit 1 +fi +echo "signed ${app}" +cat "${dump}" + +found=0 +while IFS= read -r dmg; do + [ -n "${dmg}" ] || continue + found=1 + echo "stapling ${dmg}" + ok=0 + for attempt in 1 2 3 4 5; do + if xcrun stapler staple "${dmg}"; then + ok=1 + break + fi + if [ "${attempt}" -eq 5 ]; then + break + fi + echo "stapler attempt ${attempt} failed; waiting for Apple ticket" + sleep 20 + done + if [ "${ok}" -ne 1 ]; then + exit 1 + fi +done < <(find "${root}" -type f -name '*.dmg' | sort) + +if [ "${found}" -eq 0 ]; then + echo "no DMG under ${root}" >&2 + find "${root}" -type f >&2 || true + exit 1 +fi diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..3721e040 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,87 @@ +# GitHub Actions + +Two workflows. `test-and-lint` (`codetests.yml`) runs tests and golangci-lint on push and `pull_request_target`. `build-and-release` (`release.yml`) is the only publisher. + +## Channels + +`release.yml` maps the GitHub event to a `CHANNEL` env that `.goreleaser.yaml` reads (`dockers_v2.disable`, packagecloud repo, unstable upload). `--nightly` is a GoReleaser flag: it bumps the version and turns off GitHub Releases / brew / AUR. + +| Trigger | CHANNEL | GoReleaser extra | What it publishes | +|---|---|---|---| +| Push tag `v*` | `release` | (none) | GitHub Release, Docker `:latest` + version tags, Homebrew cask, AUR, packagecloud `golift/pkgs` | +| Push branch `unstable` | `unstable` | `--nightly` | Docker `:unstable`, packagecloud `golift/unstable`, [unstable.golift.io](https://unstable.golift.io/?dir=unpackerr) | +| Cron `27 12 * * *` UTC, or `workflow_dispatch` on `main` | `nightly` | `--nightly` | Docker `:nightly` only | + +`unstable` is a **manual publish branch**. Recut it by pushing the commit you want: + +```bash +git push unpackerr ci/goreleaser-pro:unstable +``` + +Do not fast-forward `unstable` from `main` in CI. Calendar nightly builds `main` and does not touch the git `unstable` branch. + +Nightly skips the Darwin job. Apple notarization is slow and unused for a Docker-only cut. + +## Split, then merge + +GoReleaser Pro `--split` / `--continue --merge` builds each GOOS in its own job, then one merge job publishes. That exists because: + +- Darwin needs **CGO** (`energye/systray` Cocoa) and **native** `codesign` / `notarytool`. Quill on Linux can sign a naked binary; a `.app` inside a DMG is rejected by Gatekeeper unless the bundle is signed on macOS. +- Windows Authenticode talks to house **signerd** (`golift.io/codesign`) and needs `id-token: write` for GitHub OIDC. That is ubuntu, not macOS. +- Linux nFPM (deb/rpm) needs `rpm` + GPG. FreeBSD is just archives. + +So: + +1. **channel** — compute `CHANNEL` + extra args. Nothing else. +2. **split** (ubuntu, matrix `linux` / `windows` / `freebsd`) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. +3. **split-darwin** (macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. +4. **release** — download `dist-*` artifacts, `continue --merge`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. + +`REVISION` is `git rev-list --count --all`. It must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). Nightly/unstable versions are `{{ incpatch .Version }}-{{ .Env.REVISION }}` (example `0.15.3-1045`). nFPM `release` is not templated; uniqueness is that version string. Do not put `CHANNEL` in the package version. + +## Darwin signing + +`.github/scripts/macos_keychain.sh` is **required** on `split-darwin`. Missing `MACOS_SIGN_*` / `MACOS_NOTARY_*` fails the job; there is no unsigned-DMG fallback. + +`notarize.macos_native.ids` must be the **app bundle** and **DMG** ids (`unpackerr-app`, `unpackerr-dmg`), not the Darwin build id. Those pipes match Extra.ID on the `.app` / `.dmg`. After GoReleaser, `.github/scripts/macos_staple.sh` checks Developer ID on `Unpackerr.app` and staples the DMG (CloudKit can lag a bit after `notarytool` says Accepted). + +## Merge destinations + +- **Docker** — always `ghcr.io/unpackerr/unpackerr`. Hub `docker.io/golift/unpackerr` only when `DOCKERHUB_PASSWORD` is set (`DOCKERHUB_PUBLISH=1`). Platforms: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. +- **GitHub Release** — tagged `v*` only (`release.disable: "{{ .IsNightly }}"`). +- **Homebrew** — `homebrew_casks` → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly`. Do not delete `Formula/unpackerr.rb` until a tagged release has produced a cask. +- **AUR** — `aur_sources` over SSH. Skip on `--nightly`. +- **packagecloud** — `golift/pkgs` vs `golift/unstable`. Skip when `CHANNEL=nightly`. +- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1045`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh`. Upload overwrites by name; leftover versioned files on the site need a manual wipe. + +| Stable name | Payload | +|---|---| +| `Unpackerr.dmg` | notarized universal DMG | +| `unpackerr.amd64.exe.zip` | Windows exe zip | +| `unpackerr.{amd64,386,arm,arm64}.linux.gz` | gzipped binary | +| `unpackerr.{amd64,i386,armhf,arm64}.freebsd.gz` | gzipped binary | + +Linux nFPM arches are amd64, arm64, i386, armv7 (one `armhf`). Darwin min macOS 13. Windows is `-H=windowsgui`. + +## Secrets + +Set on the `Unpackerr/unpackerr` repo (or org, granted to this public repo): + +| Secret | Used by | +|---|---| +| `GORELEASER_PRO_KEY` | every goreleaser-action | +| `GPG_SIGNING_KEY` | Linux nFPM signatures | +| `MACOS_SIGN_P12`, `MACOS_SIGN_PASSWORD` | Developer ID `.p12` (PEM or long-line base64) | +| `MACOS_NOTARY_KEY`, `MACOS_NOTARY_KEY_ID`, `MACOS_NOTARY_ISSUER_ID` | App Store Connect `.p8` | +| `CODESIGN_URL`, `CODESIGN_CLIENT_CERT`, `CODESIGN_CLIENT_KEY` | Windows Authenticode (OIDC + mTLS) | +| `DOCKERHUB_PASSWORD` | Hub login; absence skips Hub only | +| `HOMEBREW_TAP_GITHUB_TOKEN` | `golift/homebrew-mugs` | +| `PACKAGECLOUD_TOKEN` | `golift/pkgs` / `golift/unstable` | +| `AUR_DEPLOY_KEY` | AUR `unpackerr` | +| `UNSTABLE_UPLOAD_KEY` | unstable.golift.io | + +`GITHUB_TOKEN` is the default Actions token (GHCR + GitHub Releases). + +## Action pins + +`release.yml` pins `owner/repo@ # vX.Y.Z`. Floating major tags (`@v4`) are not used there. `codetests.yml` still uses major tags; that is independent of the publisher. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8fce1f29..ad71749f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ permissions: # Darwin App/DMG notarization needs xcrun/codesign (macOS). Linux/Windows/FreeBSD # split on ubuntu; merge publishes Docker, GitHub, Homebrew, AUR, packagecloud. +# See README.md in this folder. jobs: channel: runs-on: ubuntu-latest @@ -113,7 +114,7 @@ jobs: CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} CODESIGN_NAME: Unpackerr CODESIGN_WEBSITE: https://unpackerr.zip - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-${{ matrix.goos }} path: dist/${{ matrix.goos }} @@ -129,13 +130,6 @@ jobs: timeout-minutes: 90 permissions: contents: read - env: - # Map first: secrets cannot be referenced in steps.if. - MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} - MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} - MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} - MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} - MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -149,21 +143,13 @@ jobs: go-version-file: go.mod cache: true - name: Import signing certificate and notary credentials - if: >- - env.MACOS_SIGN_P12 != '' && - env.MACOS_SIGN_PASSWORD != '' && - env.MACOS_NOTARY_KEY != '' && - env.MACOS_NOTARY_KEY_ID != '' && - env.MACOS_NOTARY_ISSUER_ID != '' + env: + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} run: bash .github/scripts/macos_keychain.sh - - name: Skip Apple signing - if: >- - env.MACOS_SIGN_P12 == '' || - env.MACOS_SIGN_PASSWORD == '' || - env.MACOS_NOTARY_KEY == '' || - env.MACOS_NOTARY_KEY_ID == '' || - env.MACOS_NOTARY_ISSUER_ID == '' - run: echo '::warning::macOS signing/notary secrets incomplete; building an unsigned DMG. Grant MACOS_SIGN_P12, MACOS_SIGN_PASSWORD, and MACOS_NOTARY_* to this repository.' - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro @@ -180,43 +166,8 @@ jobs: MACOS_SIGN_IDENTITY: ${{ env.MACOS_SIGN_IDENTITY }} MACOS_NOTARY_PROFILE_NAME: ${{ env.MACOS_NOTARY_PROFILE_NAME }} - name: Staple notarized DMG - if: env.KEYCHAIN_PATH != '' - run: | - set -euo pipefail - app="$(find dist/darwin -type d -name 'Unpackerr.app' | head -n 1 || true)" - if [ -z "${app}" ]; then - echo "Unpackerr.app missing under dist/darwin" >&2 - find dist/darwin -type f >&2 || true - exit 1 - fi - if ! codesign -dv --verbose=2 "${app}" 2>&1 | tee /tmp/codesign-app.txt | grep -q "Developer ID Application"; then - echo "Unpackerr.app is not Developer ID signed; macos_native skipped or failed" >&2 - cat /tmp/codesign-app.txt >&2 - exit 1 - fi - found=0 - while IFS= read -r dmg; do - found=1 - echo "stapling ${dmg}" - ok=0 - for attempt in 1 2 3 4 5; do - if xcrun stapler staple "${dmg}"; then - ok=1 - break - fi - echo "stapler attempt ${attempt} failed; waiting for Apple ticket" - sleep 20 - done - if [ "${ok}" -ne 1 ]; then - exit 1 - fi - done < <(find dist/darwin -type f -name '*.dmg' | sort) - if [ "${found}" -eq 0 ]; then - echo "no DMG under dist/darwin" >&2 - find dist/darwin -type f >&2 || true - exit 1 - fi - - uses: actions/upload-artifact@v4 + run: bash .github/scripts/macos_staple.sh dist/darwin + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-darwin path: dist/darwin @@ -254,20 +205,20 @@ jobs: go-version-file: go.mod cache: true - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 with: platforms: arm64,arm - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log into GHCR - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log into Docker Hub if: env.DOCKERHUB_PASSWORD != '' - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: golift password: ${{ secrets.DOCKERHUB_PASSWORD }} @@ -277,19 +228,19 @@ jobs: - name: Skip Docker Hub if: env.DOCKERHUB_PASSWORD == '' run: echo '::warning::DOCKERHUB_PASSWORD is empty; skipping Docker Hub login and Hub image publish. Grant the org secret to this public repository (or add a repo secret) to resume golift/unpackerr pushes.' - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-linux path: dist/linux - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-windows path: dist/windows - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-freebsd path: dist/freebsd - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 if: needs.split-darwin.result == 'success' with: name: dist-darwin From 4686163fa75fc1142900b274e630d7ca61d25bfa Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 18:04:35 -0700 Subject: [PATCH 21/41] Name split jobs Build: OS and keep the workflows README current. Display names were split (linux) vs split-darwin; job ids stay split/split-darwin for needs. Co-authored-by: Cursor --- .github/workflows/README.md | 12 ++++++------ .github/workflows/release.yml | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 3721e040..6dfa04c2 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -33,15 +33,15 @@ GoReleaser Pro `--split` / `--continue --merge` builds each GOOS in its own job, So: 1. **channel** — compute `CHANNEL` + extra args. Nothing else. -2. **split** (ubuntu, matrix `linux` / `windows` / `freebsd`) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. -3. **split-darwin** (macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. +2. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. +3. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. 4. **release** — download `dist-*` artifacts, `continue --merge`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. `REVISION` is `git rev-list --count --all`. It must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). Nightly/unstable versions are `{{ incpatch .Version }}-{{ .Env.REVISION }}` (example `0.15.3-1045`). nFPM `release` is not templated; uniqueness is that version string. Do not put `CHANNEL` in the package version. ## Darwin signing -`.github/scripts/macos_keychain.sh` is **required** on `split-darwin`. Missing `MACOS_SIGN_*` / `MACOS_NOTARY_*` fails the job; there is no unsigned-DMG fallback. +`.github/scripts/macos_keychain.sh` is **required** on Build: darwin. Missing `MACOS_SIGN_*` / `MACOS_NOTARY_*` fails the job; there is no unsigned-DMG fallback. `notarize.macos_native.ids` must be the **app bundle** and **DMG** ids (`unpackerr-app`, `unpackerr-dmg`), not the Darwin build id. Those pipes match Extra.ID on the `.app` / `.dmg`. After GoReleaser, `.github/scripts/macos_staple.sh` checks Developer ID on `Unpackerr.app` and staples the DMG (CloudKit can lag a bit after `notarytool` says Accepted). @@ -49,10 +49,10 @@ So: - **Docker** — always `ghcr.io/unpackerr/unpackerr`. Hub `docker.io/golift/unpackerr` only when `DOCKERHUB_PASSWORD` is set (`DOCKERHUB_PUBLISH=1`). Platforms: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. - **GitHub Release** — tagged `v*` only (`release.disable: "{{ .IsNightly }}"`). -- **Homebrew** — `homebrew_casks` → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly`. Do not delete `Formula/unpackerr.rb` until a tagged release has produced a cask. +- **Homebrew** — `homebrew_casks` → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly`. - **AUR** — `aur_sources` over SSH. Skip on `--nightly`. - **packagecloud** — `golift/pkgs` vs `golift/unstable`. Skip when `CHANNEL=nightly`. -- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1045`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh`. Upload overwrites by name; leftover versioned files on the site need a manual wipe. +- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1045`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh`. Upload overwrites by name. | Stable name | Payload | |---|---| @@ -84,4 +84,4 @@ Set on the `Unpackerr/unpackerr` repo (or org, granted to this public repo): ## Action pins -`release.yml` pins `owner/repo@ # vX.Y.Z`. Floating major tags (`@v4`) are not used there. `codetests.yml` still uses major tags; that is independent of the publisher. +`release.yml` pins `owner/repo@ # vX.Y.Z`. Floating major tags (`@v4`) are not used there. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ad71749f..104f3f98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,7 @@ jobs: echo "CHANNEL=${channel} extra=${extra}" split: + name: "Build: ${{ matrix.goos }}" needs: channel strategy: fail-fast: false @@ -123,6 +124,7 @@ jobs: retention-days: 1 split-darwin: + name: "Build: darwin" needs: channel # Nightly is Docker-only; skip Apple notarization. if: needs.channel.outputs.channel != 'nightly' From 400562064b9a51c987d4bc634c26337c4c37eaf9 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 18:07:59 -0700 Subject: [PATCH 22/41] Upload Darwin dist as a single tar instead of the .app tree. upload-artifact on macos-latest was hanging for many minutes on Unpackerr.app plus the leftover hdiutil clone. Co-authored-by: Cursor --- .github/workflows/README.md | 2 +- .github/workflows/release.yml | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 6dfa04c2..c65374d3 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -43,7 +43,7 @@ So: `.github/scripts/macos_keychain.sh` is **required** on Build: darwin. Missing `MACOS_SIGN_*` / `MACOS_NOTARY_*` fails the job; there is no unsigned-DMG fallback. -`notarize.macos_native.ids` must be the **app bundle** and **DMG** ids (`unpackerr-app`, `unpackerr-dmg`), not the Darwin build id. Those pipes match Extra.ID on the `.app` / `.dmg`. After GoReleaser, `.github/scripts/macos_staple.sh` checks Developer ID on `Unpackerr.app` and staples the DMG (CloudKit can lag a bit after `notarytool` says Accepted). +`notarize.macos_native.ids` must be the **app bundle** and **DMG** ids (`unpackerr-app`, `unpackerr-dmg`), not the Darwin build id. Those pipes match Extra.ID on the `.app` / `.dmg`. After GoReleaser, `.github/scripts/macos_staple.sh` checks Developer ID on `Unpackerr.app` and staples the DMG (CloudKit can lag a bit after `notarytool` says Accepted). The Darwin `dist/` is packed into one tar before `upload-artifact`; uploading the `.app` tree on macos-latest hangs. ## Merge destinations diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 104f3f98..cc8559b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -120,6 +120,7 @@ jobs: name: dist-${{ matrix.goos }} path: dist/${{ matrix.goos }} include-hidden-files: true + compression-level: 0 if-no-files-found: error retention-days: 1 @@ -169,13 +170,24 @@ jobs: MACOS_NOTARY_PROFILE_NAME: ${{ env.MACOS_NOTARY_PROFILE_NAME }} - name: Staple notarized DMG run: bash .github/scripts/macos_staple.sh dist/darwin + - name: Pack Darwin dist + run: | + set -euo pipefail + # hdiutil leaves a writable clone of Unpackerr.app next to the DMG. + # upload-artifact on macos hangs for many minutes on that file tree. + find dist/darwin/dmg -mindepth 2 -maxdepth 2 -type d -name 'Unpackerr' -exec rm -rf {} + + du -sh dist/darwin dist/darwin/* 2>/dev/null || true + tar -C dist/darwin -cf dist-darwin.tar . + ls -lh dist-darwin.tar - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-darwin - path: dist/darwin - include-hidden-files: true + path: dist-darwin.tar + compression-level: 0 if-no-files-found: error retention-days: 1 + env: + ACTIONS_ARTIFACT_UPLOAD_CONCURRENCY: "5" release: needs: [channel, split, split-darwin] @@ -246,7 +258,14 @@ jobs: if: needs.split-darwin.result == 'success' with: name: dist-darwin - path: dist/darwin + path: dist + - name: Unpack Darwin dist + if: needs.split-darwin.result == 'success' + run: | + set -euo pipefail + mkdir -p dist/darwin + tar -xf dist/dist-darwin.tar -C dist/darwin + rm -f dist/dist-darwin.tar - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro From 4219e23e90a96ad12e4179a4e87359cfe754ee4e Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 18:13:11 -0700 Subject: [PATCH 23/41] Import GPG on the merge job so checksum signatures have a secret key. nFPM already signed packages during the Linux split; continue --merge signs checksums.sha256.txt in a fresh runner. Co-authored-by: Cursor --- .github/workflows/README.md | 2 +- .github/workflows/release.yml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c65374d3..bc098377 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -35,7 +35,7 @@ So: 1. **channel** — compute `CHANNEL` + extra args. Nothing else. 2. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. 3. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. -4. **release** — download `dist-*` artifacts, `continue --merge`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. +4. **release** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. `REVISION` is `git rev-list --count --all`. It must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). Nightly/unstable versions are `{{ incpatch .Version }}-{{ .Env.REVISION }}` (example `0.15.3-1045`). nFPM `release` is not templated; uniqueness is that version string. Do not put `CHANNEL` in the package version. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc8559b8..f74cd018 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -218,6 +218,11 @@ jobs: with: go-version-file: go.mod cache: true + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - name: Set up QEMU uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 with: @@ -280,6 +285,7 @@ jobs: USER: github-actions PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} AUR_DEPLOY_KEY: ${{ secrets.AUR_DEPLOY_KEY }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} - name: Flatten Linux packages for packagecloud if: env.CHANNEL != 'nightly' run: | From c15182b5f6d1ac1a551ed8356e480bd8ca8a7095 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 18:20:09 -0700 Subject: [PATCH 24/41] Read split dist/\$GOOS/artifacts.json when uploading unstable files. continue --merge never writes dist/artifacts.json; each split job already did. Co-authored-by: Cursor --- .github/scripts/unstable_upload.sh | 35 +++++++++++++++++++++++++----- .github/workflows/README.md | 2 +- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/scripts/unstable_upload.sh b/.github/scripts/unstable_upload.sh index 54b78ab8..3caacac1 100755 --- a/.github/scripts/unstable_upload.sh +++ b/.github/scripts/unstable_upload.sh @@ -14,17 +14,38 @@ # Sidecar: plain VERSION-REVISION (same as the pre-GoReleaser workflow). set -euo pipefail +if ! command -v jq >/dev/null; then + echo "jq is required to read artifacts.json" >&2 + exit 1 +fi + dir="${1:-dist}" artifacts="${dir}/artifacts.json" metadata="${dir}/metadata.json" +combined="" +# Split/merge writes dist/$GOOS/artifacts.json. A single-job release writes dist/artifacts.json. if [ ! -f "${artifacts}" ]; then - echo "missing ${artifacts}; GoReleaser did not produce artifacts.json" >&2 - exit 1 + shopt -s nullglob + parts=("${dir}"/*/artifacts.json) + shopt -u nullglob + if [ ${#parts[@]} -eq 0 ]; then + echo "missing ${artifacts} and ${dir}/*/artifacts.json" >&2 + find "${dir}" -name artifacts.json -o -name metadata.json >&2 || true + exit 1 + fi + combined="$(mktemp "${TMPDIR:-/tmp}/unpackerr-artifacts.XXXXXX")" + jq -s 'add' "${parts[@]}" > "${combined}" + artifacts="${combined}" + echo "merged ${#parts[@]} split artifacts.json files" fi -if ! command -v jq >/dev/null; then - echo "jq is required to read ${artifacts}" >&2 - exit 1 +if [ ! -f "${metadata}" ]; then + shopt -s nullglob + metas=("${dir}"/*/metadata.json) + shopt -u nullglob + if [ ${#metas[@]} -gt 0 ]; then + metadata="${metas[0]}" + fi fi version="${VERSION:-}" @@ -43,7 +64,9 @@ if [ -z "${stage}" ]; then owned_stage=1 fi if [ "${owned_stage}" -eq 1 ]; then - trap 'rm -rf "${stage}"' EXIT + trap 'rm -rf "${stage}"; [ -n "${combined}" ] && rm -f "${combined}"' EXIT +elif [ -n "${combined}" ]; then + trap 'rm -f "${combined}"' EXIT fi mkdir -p "${stage}" diff --git a/.github/workflows/README.md b/.github/workflows/README.md index bc098377..cbdb3a1e 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -52,7 +52,7 @@ So: - **Homebrew** — `homebrew_casks` → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly`. - **AUR** — `aur_sources` over SSH. Skip on `--nightly`. - **packagecloud** — `golift/pkgs` vs `golift/unstable`. Skip when `CHANNEL=nightly`. -- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1045`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh`. Upload overwrites by name. +- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1045`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh` (reads `dist/$GOOS/artifacts.json` after split/merge). Upload overwrites by name. | Stable name | Payload | |---|---| From 7f8a348a0a9b483988dc877735dc524550eab5fa Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 18:28:20 -0700 Subject: [PATCH 25/41] Show the git revision in the merge job name (release 1054). Job names cannot read GITHUB_ENV from later steps, so channel now outputs REVISION. Co-authored-by: Cursor --- .github/workflows/README.md | 4 ++-- .github/workflows/release.yml | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index cbdb3a1e..451f0e29 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -32,10 +32,10 @@ GoReleaser Pro `--split` / `--continue --merge` builds each GOOS in its own job, So: -1. **channel** — compute `CHANNEL` + extra args. Nothing else. +1. **channel** — compute `CHANNEL`, extra args, and `REVISION` (`git rev-list --count --all`). 2. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. 3. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. -4. **release** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. +4. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. `REVISION` is `git rev-list --count --all`. It must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). Nightly/unstable versions are `{{ incpatch .Version }}-{{ .Env.REVISION }}` (example `0.15.3-1045`). nFPM `release` is not templated; uniqueness is that version string. Do not put `CHANNEL` in the package version. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f74cd018..9c54ff59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,15 @@ jobs: outputs: channel: ${{ steps.channel.outputs.channel }} extra: ${{ steps.channel.outputs.extra }} + revision: ${{ steps.revision.outputs.revision }} steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} + - name: Save iteration in Revision + id: revision + run: echo "revision=$(git rev-list --count --all || echo 0)" >> "${GITHUB_OUTPUT}" - name: Choose release channel id: channel run: | @@ -190,6 +198,7 @@ jobs: ACTIONS_ARTIFACT_UPLOAD_CONCURRENCY: "5" release: + name: "release ${{ needs.channel.outputs.revision }}" needs: [channel, split, split-darwin] if: >- always() && From b784e22f2885ccabc071a99d7e612b6da9c4ddee Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 18:32:19 -0700 Subject: [PATCH 26/41] Point the app bundle at GoReleaser's icon.icns and stop shipping a dead DMG background. Finder ignored Unpackerr.icns because the file is named icon.icns; extra_files cannot set the Finder window background. Co-authored-by: Cursor --- .goreleaser.yaml | 3 --- init/macos/Info.plist.tmpl | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 00b5a7a8..0d5c6fe7 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -177,9 +177,6 @@ dmg: name: Unpackerr ids: [unpackerr-app] use: appbundle - extra_files: - - glob: init/macos/background.png - name_template: background.png source: enabled: true diff --git a/init/macos/Info.plist.tmpl b/init/macos/Info.plist.tmpl index 3a7e853c..ff65d4e7 100644 --- a/init/macos/Info.plist.tmpl +++ b/init/macos/Info.plist.tmpl @@ -26,7 +26,7 @@ LSUIElement CFBundleIconFile - Unpackerr.icns + icon.icns CFBundleDisplayName Unpackerr NSHumanReadableCopyright From 0b4e3c57031bee1b796e9a340e7dc2d0a024d756 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 19:14:40 -0700 Subject: [PATCH 27/41] Ship real FreeBSD pkgng .txz packages and fail closed when publish secrets are missing. Keep a single channel REVISION ({{.Version}} already includes it on --nightly), drop nFPM +git metadata, and name Windows GitHub assets *.exe.zip. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.py | 314 +++++++++++++++++++++++++++++ .github/scripts/require_secrets.sh | 53 +++++ .github/scripts/unstable_upload.sh | 4 + .github/workflows/README.md | 31 +-- .github/workflows/release.yml | 68 +++++-- .gitignore | 1 + .goreleaser.yaml | 47 ++--- init/macos/entitlements.plist | 4 - init/windows/signexe.sh | 8 +- 9 files changed, 460 insertions(+), 70 deletions(-) create mode 100755 .github/scripts/freebsd_txz.py create mode 100755 .github/scripts/require_secrets.sh diff --git a/.github/scripts/freebsd_txz.py b/.github/scripts/freebsd_txz.py new file mode 100755 index 00000000..ef1584c7 --- /dev/null +++ b/.github/scripts/freebsd_txz.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Build FreeBSD pkgng .txz packages from GoReleaser freebsd binaries. + +nFPM has no freebsd target. The old Makefile used fpm -t freebsd, which wrote +pkgng +MANIFEST / +COMPACT_MANIFEST and a xz-compressed tar (a real .txz, not +a renamed tar.xz). install.sh and pkg/update/check.go still look for assets +ending in amd64.txz / i386.txz / armhf.txz / arm64.txz. + +This runs after `goreleaser --split` on the freebsd job and appends Archive +entries to dist/freebsd/artifacts.json so merge uploads them. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +from pathlib import Path + +DESC = "Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them." +MAINTAINER = "David Newhall II " +ORIGIN = "https://github.com/Unpackerr/unpackerr" +WWW = "https://unpackerr.zip" +OSVERSION = "13" + +# Filename arch (install.sh / check.go) and pkgng ABI arch. +ARCH_MAP = { + "amd64": ("amd64", "amd64"), + "386": ("i386", "i386"), + "arm": ("armhf", "arm"), + "arm64": ("arm64", "arm64"), +} + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def load_version(dist: Path, override: str) -> str: + if override: + return override + meta = dist / "metadata.json" + if not meta.is_file(): + sys.exit(f"missing {meta} (and no --version)") + data = json.loads(meta.read_text()) + version = data.get("version") or "" + if not version or version in {"unknown", "unstable"}: + sys.exit(f"refusing version {version!r} from {meta}") + return version + + +def freebsd_binaries(artifacts: list[dict]) -> list[dict]: + found: list[dict] = [] + for art in artifacts: + if art.get("type") != "Binary" or art.get("goos") != "freebsd": + continue + if str(art.get("goarch") or "") == "arm" and str(art.get("goarm") or "") == "6": + continue + found.append(art) + if not found: + sys.exit("no freebsd Binary artifacts (skipping GOARM 6)") + return found + + +def resolve_binary(dist: Path, path: str) -> Path: + p = Path(path) + if p.is_file(): + return p + cand = dist / path + if cand.is_file(): + return cand + sys.exit(f"freebsd binary missing: {path}") + + +def stage_files(repo: Path, binary: Path, staging: Path) -> list[str]: + """Populate a pkgng staging dir. Returns tar member names (no leading /).""" + files: list[tuple[str, Path, int]] = [] + + def put(rel: str, src: Path, mode: int) -> None: + dest = staging / rel + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) + dest.chmod(mode) + files.append((rel, dest, mode)) + + bin_mode = 0o755 + data_mode = 0o644 + rc_mode = 0o755 + + put("usr/local/bin/unpackerr", binary, bin_mode) + put("usr/local/etc/rc.d/unpackerr", repo / "init/bsd/freebsd.rc.d", rc_mode) + conf = repo / "examples/unpackerr.conf.example" + put("usr/local/etc/unpackerr/unpackerr.conf", conf, data_mode) + put("usr/local/etc/unpackerr/unpackerr.conf.example", conf, data_mode) + + man = repo / "unpackerr.1.gz" + if not man.is_file(): + sys.exit(f"missing {man} (goreleaser before-hook should gzip the man page)") + put("usr/local/share/man/man1/unpackerr.1.gz", man, data_mode) + put("usr/local/share/doc/unpackerr/LICENSE", repo / "LICENSE", data_mode) + + docs = [ + (repo / "examples/MANUAL.md", "usr/local/share/doc/unpackerr/MANUAL.md"), + (repo / "examples/MANUAL.html", "usr/local/share/doc/unpackerr/unpackerr_manual.html"), + (repo / "examples/docker-compose.yml", "usr/local/share/doc/unpackerr/docker-compose.yml"), + (repo / "examples/unpackerr.conf.example", "usr/local/share/doc/unpackerr/unpackerr.conf.example"), + (repo / "README.html", "usr/local/share/doc/unpackerr/README.html"), + ] + for src, rel in docs: + if src.is_file(): + put(rel, src, data_mode) + + return [rel for rel, _, _ in files] + + +def write_manifests( + staging: Path, + *, + version: str, + abi_arch: str, + members: list[str], + scripts: dict[str, str], +) -> None: + checksums = {} + for rel in members: + checksums["/" + rel] = sha256(staging / rel) + + pkgdata = { + "arch": f"FreeBSD:{OSVERSION}:{abi_arch}", + "name": "unpackerr", + "version": version, + "comment": DESC, + "desc": DESC, + "origin": ORIGIN, + "maintainer": MAINTAINER, + "www": WWW, + "prefix": "/", + } + compact = staging / "+COMPACT_MANIFEST" + compact.write_text(json.dumps(pkgdata, separators=(",", ":")) + "\n", encoding="utf-8") + pkgdata["files"] = checksums + pkgdata["scripts"] = scripts + manifest = staging / "+MANIFEST" + manifest.write_text(json.dumps(pkgdata, separators=(",", ":")) + "\n", encoding="utf-8") + + +def load_scripts(repo: Path) -> dict[str, str]: + mapping = { + "pre-install": repo / "init/systemd/before-install.sh", + "post-install": repo / "init/systemd/after-install.sh", + "pre-deinstall": repo / "init/systemd/before-remove.sh", + } + out = {} + for name, path in mapping.items(): + if not path.is_file(): + sys.exit(f"missing pkg script {path}") + out[name] = path.read_text(encoding="utf-8") + return out + + +def gnu_tar() -> str | None: + for name in ("tar", "gtar", "gnutar"): + path = shutil.which(name) + if not path: + continue + try: + proc = subprocess.run([path, "--version"], capture_output=True, text=True, check=False) + except OSError: + continue + if "GNU tar" in (proc.stdout + proc.stderr): + return path + return None + + +def pack_txz(staging: Path, members: list[str], dest: Path) -> None: + dest.parent.mkdir(parents=True, exist_ok=True) + file_list = [ "+COMPACT_MANIFEST", "+MANIFEST", *members ] + tar = gnu_tar() + if tar: + # Match fpm: no ./ prefix; leading / on payload files; +MANIFEST at top. + list_file = staging / ".file_list" + list_file.write_text("\n".join(file_list) + "\n", encoding="utf-8") + subprocess.run( + [ + tar, + "-Jcf", + str(dest), + "-C", + str(staging), + "--files-from", + str(list_file), + "--transform", + r"s|^\([^+]\)|/\1|", + ], + check=True, + ) + list_file.unlink(missing_ok=True) + return + + with tarfile.open(dest, "w:xz", format=tarfile.GNU_FORMAT, dereference=True) as tf: + for name in file_list: + path = staging / name + info = tf.gettarinfo(str(path), arcname=name) + info.uname = "root" + info.gname = "wheel" + info.uid = 0 + info.gid = 0 + if not name.startswith("+"): + info.name = "/" + name + with path.open("rb") as fh: + tf.addfile(info, fh) + + +def build_one( + *, + repo: Path, + dist: Path, + binary: Path, + version: str, + goarch: str, + scripts: dict[str, str], +) -> Path: + if goarch not in ARCH_MAP: + sys.exit(f"unsupported freebsd goarch {goarch}") + pkgarch, abi_arch = ARCH_MAP[goarch] + dest = dist / f"unpackerr-{version}.{pkgarch}.txz" + with tempfile.TemporaryDirectory(prefix="unpackerr-txz-") as tmp: + staging = Path(tmp) + members = stage_files(repo, binary, staging) + write_manifests(staging, version=version, abi_arch=abi_arch, members=members, scripts=scripts) + pack_txz(staging, members, dest) + dest.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + print(f"wrote {dest.name} from {binary} ({dest.stat().st_size} bytes)", file=sys.stderr) + return dest + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("dist", type=Path, help="dist/freebsd (split output)") + parser.add_argument("--repo", type=Path, default=repo_root()) + parser.add_argument("--version", default="") + args = parser.parse_args() + dist = args.dist.resolve() + repo = args.repo.resolve() + artifacts_path = dist / "artifacts.json" + if not artifacts_path.is_file(): + sys.exit(f"missing {artifacts_path}") + + artifacts = json.loads(artifacts_path.read_text()) + if not isinstance(artifacts, list): + sys.exit(f"{artifacts_path} is not a JSON array") + version = load_version(dist, args.version) + scripts = load_scripts(repo) + built: list[tuple[dict, Path]] = [] + seen_arch: set[str] = set() + for art in freebsd_binaries(artifacts): + goarch = str(art.get("goarch") or "") + if goarch in seen_arch: + continue + seen_arch.add(goarch) + binary = resolve_binary(dist, str(art.get("path") or "")) + dest = build_one( + repo=repo, + dist=dist, + binary=binary, + version=version, + goarch=goarch, + scripts=scripts, + ) + built.append((art, dest)) + + expected = {"amd64", "386", "arm", "arm64"} + if not expected.issubset(seen_arch): + sys.exit(f"freebsd txz missing arches {sorted(expected - seen_arch)}; built {sorted(seen_arch)}") + + for art, dest in built: + extra = dict(art.get("extra") or {}) + extra["ID"] = "freebsd-pkg" + extra["Format"] = "txz" + artifacts.append( + { + "name": dest.name, + "path": dest.name, + "goos": "freebsd", + "goarch": art.get("goarch"), + "goarm": art.get("goarm"), + "goamd64": art.get("goamd64"), + "type": "Archive", + "extra": extra, + } + ) + artifacts_path.write_text(json.dumps(artifacts, indent=2) + "\n", encoding="utf-8") + print(f"appended {len(built)} txz archives to {artifacts_path}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as exc: + sys.exit(f"tar failed: {exc}") diff --git a/.github/scripts/require_secrets.sh b/.github/scripts/require_secrets.sh new file mode 100755 index 00000000..cf39acd8 --- /dev/null +++ b/.github/scripts/require_secrets.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Fail closed when a publishing secret is empty. GitHub maps missing secrets +# to "" so `if: secrets.FOO != ''` is not a substitute. +set -euo pipefail + +CHANNEL="${CHANNEL:-}" +if [ -z "${CHANNEL}" ]; then + echo "CHANNEL is empty" >&2 + exit 1 +fi + +missing=0 +need() { + local name=$1 + local val=${!name-} + if [ -z "${val}" ]; then + echo "missing secret: ${name}" >&2 + missing=1 + fi +} + +need GORELEASER_PRO_KEY +need GPG_SIGNING_KEY +need DOCKERHUB_PASSWORD +need CODESIGN_URL +need CODESIGN_CLIENT_CERT +need CODESIGN_CLIENT_KEY + +# Nightly is Docker-only (no Darwin, no GitHub Release, no brew/AUR/packagecloud). +if [ "${CHANNEL}" != nightly ]; then + need MACOS_SIGN_P12 + need MACOS_SIGN_PASSWORD + need MACOS_NOTARY_KEY + need MACOS_NOTARY_KEY_ID + need MACOS_NOTARY_ISSUER_ID + need PACKAGECLOUD_TOKEN +fi + +if [ "${CHANNEL}" = release ]; then + need HOMEBREW_TAP_GITHUB_TOKEN + need AUR_DEPLOY_KEY +fi + +if [ "${CHANNEL}" = unstable ]; then + need UNSTABLE_UPLOAD_KEY +fi + +if [ "${missing}" -ne 0 ]; then + echo "refusing to publish CHANNEL=${CHANNEL} with empty secrets" >&2 + exit 1 +fi + +echo "CHANNEL=${CHANNEL}: required secrets are present" diff --git a/.github/scripts/unstable_upload.sh b/.github/scripts/unstable_upload.sh index 3caacac1..fd0634cb 100755 --- a/.github/scripts/unstable_upload.sh +++ b/.github/scripts/unstable_upload.sh @@ -197,6 +197,10 @@ upload() { } if [ -z "${UNSTABLE_UPLOAD_KEY:-}" ]; then + if [ -n "${GITHUB_ACTIONS:-}" ]; then + echo "UNSTABLE_UPLOAD_KEY unset; refusing to skip unstable.golift.io upload in CI" >&2 + exit 1 + fi echo "UNSTABLE_UPLOAD_KEY unset; staged without uploading:" >&2 ls -l "${stage}" exit 0 diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 451f0e29..ecd3b05e 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -28,16 +28,17 @@ GoReleaser Pro `--split` / `--continue --merge` builds each GOOS in its own job, - Darwin needs **CGO** (`energye/systray` Cocoa) and **native** `codesign` / `notarytool`. Quill on Linux can sign a naked binary; a `.app` inside a DMG is rejected by Gatekeeper unless the bundle is signed on macOS. - Windows Authenticode talks to house **signerd** (`golift.io/codesign`) and needs `id-token: write` for GitHub OIDC. That is ubuntu, not macOS. -- Linux nFPM (deb/rpm) needs `rpm` + GPG. FreeBSD is just archives. +- Linux nFPM (deb/rpm) needs `rpm` + GPG. FreeBSD pkgng `.txz` is built after `--split` by `.github/scripts/freebsd_txz.py` (nFPM has no freebsd target). So: -1. **channel** — compute `CHANNEL`, extra args, and `REVISION` (`git rev-list --count --all`). -2. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. -3. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. -4. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. +1. **channel** — compute `CHANNEL`, extra args, and `REVISION` (`git rev-list --count --all`). This is the only place the count is taken; later jobs pass `needs.channel.outputs.revision`. +2. **require secrets** — fail closed if any signing/upload/push secret needed for that channel is empty. Missing secrets used to skip Docker Hub, Windows Authenticode, or unstable.golift.io and still go green. +3. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. FreeBSD then runs `freebsd_txz.py`. +4. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. +5. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. -`REVISION` is `git rev-list --count --all`. It must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). Nightly/unstable versions are `{{ incpatch .Version }}-{{ .Env.REVISION }}` (example `0.15.3-1045`). nFPM `release` is not templated; uniqueness is that version string. Do not put `CHANNEL` in the package version. +`REVISION` must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). On `--nightly`, `nightly.version_template` already bakes it into `{{ .Version }}` (example `0.15.3-1056`), so man-page hooks use `{{ .Version }}` only — do not append `REVISION` again. The env var is still required: that template *creates* `.Version`, and tagged builds keep `.Version` as the semver while ldflags `Revision` / Darwin `CFBundleVersion` still need the count. nFPM `release` is not templated; uniqueness is the version string. Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata` (that produced `0.15.3~1056+git` on Debian). ## Darwin signing @@ -47,12 +48,12 @@ So: ## Merge destinations -- **Docker** — always `ghcr.io/unpackerr/unpackerr`. Hub `docker.io/golift/unpackerr` only when `DOCKERHUB_PASSWORD` is set (`DOCKERHUB_PUBLISH=1`). Platforms: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. -- **GitHub Release** — tagged `v*` only (`release.disable: "{{ .IsNightly }}"`). +- **Docker** — always `ghcr.io/unpackerr/unpackerr` and Hub `docker.io/golift/unpackerr` (`DOCKERHUB_PUBLISH=1`). Empty `DOCKERHUB_PASSWORD` fails the merge job. Platforms: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. +- **GitHub Release** — tagged `v*` only (`release.disable: "{{ .IsNightly }}"`). Windows assets are `unpackerr.amd64.exe.zip`. FreeBSD assets are pkgng `unpackerr-.{amd64,i386,armhf,arm64}.txz`. - **Homebrew** — `homebrew_casks` → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly`. - **AUR** — `aur_sources` over SSH. Skip on `--nightly`. - **packagecloud** — `golift/pkgs` vs `golift/unstable`. Skip when `CHANNEL=nightly`. -- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1045`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh` (reads `dist/$GOOS/artifacts.json` after split/merge). Upload overwrites by name. +- **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1056`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh` (reads `dist/$GOOS/artifacts.json` after split/merge). Upload overwrites by name. Empty `UNSTABLE_UPLOAD_KEY` fails in GitHub Actions. | Stable name | Payload | |---|---| @@ -61,11 +62,11 @@ So: | `unpackerr.{amd64,386,arm,arm64}.linux.gz` | gzipped binary | | `unpackerr.{amd64,i386,armhf,arm64}.freebsd.gz` | gzipped binary | -Linux nFPM arches are amd64, arm64, i386, armv7 (one `armhf`). Darwin min macOS 13. Windows is `-H=windowsgui`. +Linux nFPM arches are amd64, arm64, i386, armv7 (one `armhf`). Darwin min macOS 13. Windows is `-H=windowsgui`. Empty `CODESIGN_URL` fails in GitHub Actions (local snapshots still skip). ## Secrets -Set on the `Unpackerr/unpackerr` repo (or org, granted to this public repo): +Set on the `Unpackerr/unpackerr` repo (or org, granted to this public repo). `.github/scripts/require_secrets.sh` runs before any build job and **fails the workflow** if a secret required for that `CHANNEL` is empty. Nightly does not require Apple / Homebrew / AUR / packagecloud / unstable-upload secrets (those destinations are skipped). | Secret | Used by | |---|---| @@ -74,11 +75,11 @@ Set on the `Unpackerr/unpackerr` repo (or org, granted to this public repo): | `MACOS_SIGN_P12`, `MACOS_SIGN_PASSWORD` | Developer ID `.p12` (PEM or long-line base64) | | `MACOS_NOTARY_KEY`, `MACOS_NOTARY_KEY_ID`, `MACOS_NOTARY_ISSUER_ID` | App Store Connect `.p8` | | `CODESIGN_URL`, `CODESIGN_CLIENT_CERT`, `CODESIGN_CLIENT_KEY` | Windows Authenticode (OIDC + mTLS) | -| `DOCKERHUB_PASSWORD` | Hub login; absence skips Hub only | -| `HOMEBREW_TAP_GITHUB_TOKEN` | `golift/homebrew-mugs` | +| `DOCKERHUB_PASSWORD` | Hub login (required) | +| `HOMEBREW_TAP_GITHUB_TOKEN` | `golift/homebrew-mugs` (release channel) | | `PACKAGECLOUD_TOKEN` | `golift/pkgs` / `golift/unstable` | -| `AUR_DEPLOY_KEY` | AUR `unpackerr` | -| `UNSTABLE_UPLOAD_KEY` | unstable.golift.io | +| `AUR_DEPLOY_KEY` | AUR `unpackerr` (release channel) | +| `UNSTABLE_UPLOAD_KEY` | unstable.golift.io (unstable channel) | `GITHUB_TOKEN` is the default Actions token (GHCR + GitHub Releases). diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c54ff59..6f0b7d36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} - - name: Save iteration in Revision + - name: Count git revisions id: revision run: echo "revision=$(git rev-list --count --all || echo 0)" >> "${GITHUB_OUTPUT}" - name: Choose release channel @@ -55,9 +55,38 @@ jobs: echo "extra=${extra}" >> "${GITHUB_OUTPUT}" echo "CHANNEL=${channel} extra=${extra}" + require-secrets: + name: require secrets + needs: channel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} + - name: Fail if publishing secrets are missing + env: + CHANNEL: ${{ needs.channel.outputs.channel }} + GORELEASER_PRO_KEY: ${{ secrets.GORELEASER_PRO_KEY }} + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + CODESIGN_URL: ${{ secrets.CODESIGN_URL }} + CODESIGN_CLIENT_CERT: ${{ secrets.CODESIGN_CLIENT_CERT }} + CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + AUR_DEPLOY_KEY: ${{ secrets.AUR_DEPLOY_KEY }} + UNSTABLE_UPLOAD_KEY: ${{ secrets.UNSTABLE_UPLOAD_KEY }} + run: bash .github/scripts/require_secrets.sh + split: name: "Build: ${{ matrix.goos }}" - needs: channel + needs: [channel, require-secrets] strategy: fail-fast: false matrix: @@ -76,8 +105,6 @@ jobs: fetch-depth: 0 ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} - run: git fetch --force --tags - - name: Save iteration in Revision - run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod @@ -93,7 +120,7 @@ jobs: - name: Create GPG_SIGNING_KEY file run: echo '${{ secrets.GPG_SIGNING_KEY }}' > /tmp/key.gpg - name: Install golift codesign - if: matrix.goos == 'windows' && env.CODESIGN_URL != '' + if: matrix.goos == 'windows' env: GOBIN: ${{ runner.temp }}/codesign-bin run: | @@ -111,7 +138,7 @@ jobs: # GGOOS filters targets without leaking GOOS into before-hook `go run`. GGOOS: ${{ matrix.goos }} CHANNEL: ${{ needs.channel.outputs.channel }} - REVISION: ${{ env.REVISION }} + REVISION: ${{ needs.channel.outputs.revision }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} USER: github-actions @@ -123,6 +150,9 @@ jobs: CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} CODESIGN_NAME: Unpackerr CODESIGN_WEBSITE: https://unpackerr.zip + - name: Build FreeBSD pkgng txz + if: matrix.goos == 'freebsd' + run: python3 .github/scripts/freebsd_txz.py dist/freebsd - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-${{ matrix.goos }} @@ -134,7 +164,7 @@ jobs: split-darwin: name: "Build: darwin" - needs: channel + needs: [channel, require-secrets] # Nightly is Docker-only; skip Apple notarization. if: needs.channel.outputs.channel != 'nightly' runs-on: macos-latest @@ -147,8 +177,6 @@ jobs: fetch-depth: 0 ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} - run: git fetch --force --tags - - name: Save iteration in Revision - run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod @@ -169,7 +197,7 @@ jobs: env: GGOOS: darwin CHANNEL: ${{ needs.channel.outputs.channel }} - REVISION: ${{ env.REVISION }} + REVISION: ${{ needs.channel.outputs.revision }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} USER: github-actions @@ -199,11 +227,12 @@ jobs: release: name: "release ${{ needs.channel.outputs.revision }}" - needs: [channel, split, split-darwin] + needs: [channel, require-secrets, split, split-darwin] if: >- always() && !cancelled() && needs.channel.result == 'success' && + needs.require-secrets.result == 'success' && needs.split.result == 'success' && (needs.split-darwin.result == 'success' || needs.split-darwin.result == 'skipped') runs-on: ubuntu-latest @@ -214,15 +243,12 @@ jobs: id-token: write env: CHANNEL: ${{ needs.channel.outputs.channel }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} - run: git fetch --force --tags - - name: Save iteration in Revision - run: echo REVISION=$(git rev-list --count --all || echo 0) >> "$GITHUB_ENV" - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod @@ -245,17 +271,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log into Docker Hub - if: env.DOCKERHUB_PASSWORD != '' uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: golift password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Enable Docker Hub publish - if: env.DOCKERHUB_PASSWORD != '' run: echo DOCKERHUB_PUBLISH=1 >> "$GITHUB_ENV" - - name: Skip Docker Hub - if: env.DOCKERHUB_PASSWORD == '' - run: echo '::warning::DOCKERHUB_PASSWORD is empty; skipping Docker Hub login and Hub image publish. Grant the org secret to this public repository (or add a repo secret) to resume golift/unpackerr pushes.' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-linux @@ -268,6 +289,13 @@ jobs: with: name: dist-freebsd path: dist/freebsd + - name: Verify FreeBSD txz packages + run: | + set -euo pipefail + ls -l dist/freebsd/unpackerr-*.txz + for arch in amd64 i386 armhf arm64; do + ls dist/freebsd/unpackerr-*."${arch}".txz + done - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 if: needs.split-darwin.result == 'success' with: @@ -287,7 +315,7 @@ jobs: args: continue --merge --timeout 60m env: CHANNEL: ${{ needs.channel.outputs.channel }} - REVISION: ${{ env.REVISION }} + REVISION: ${{ needs.channel.outputs.revision }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index bd1beb1d..69e086db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /unpackerr /dist/ +__pycache__/ # JetBrains IDEs: GoLand, IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm .idea/* diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 0d5c6fe7..8d1e0b62 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,18 +9,19 @@ project_name: unpackerr # nightly = daily cron from main # release = v* tags nightly: - # REVISION (git rev-list count) makes every --nightly cut unique for - # packagecloud. nfpms.release is not templated by GoReleaser. CHANNEL is - # only the Docker tag / repo (golift/unstable); it does not belong in the - # package version. + # This is why REVISION is still computed (once, in the channel job) even + # though {{ .Version }} already includes it on --nightly: this template is + # what *creates* that version. Tagged builds keep .Version as the semver + # tag; ldflags Revision and Darwin CFBundleVersion still need the count. + # nfpms.release is not templated. Do not put CHANNEL in the package version. version_template: "{{ incpatch .Version }}-{{ .Env.REVISION }}" before: hooks: - go mod tidy - go generate ./... - - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}-{{.Env.REVISION}}" --date "{{.Date}}" examples/MANUAL.md - - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}-{{.Env.REVISION}}" --date "{{.Date}}" README.md + - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}" --date "{{.Date}}" examples/MANUAL.md + - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}" --date "{{.Date}}" README.md - gzip -9nf examples/MANUAL - mv -f examples/MANUAL.gz unpackerr.1.gz - go run github.com/akavel/rsrc@latest -arch amd64 -ico init/windows/application.ico -manifest init/windows/manifest.xml -o rsrc_windows_amd64.syso @@ -85,7 +86,6 @@ builds: - id: unpackerr-freebsd env: - CGO_ENABLED=0 - binary: usr/local/bin/unpackerr goos: - freebsd goarch: @@ -93,8 +93,8 @@ builds: - arm - arm64 - '386' + # One ARM (v7). Dual GOARM would collide on unpackerr-*.armhf.txz. goarm: - - '6' - '7' flags: - -trimpath @@ -185,7 +185,7 @@ source: archives: - id: default - ids: [unpackerr, unpackerr-darwin, unpackerr-windows] + ids: [unpackerr, unpackerr-darwin] formats: [tar.gz] # Flat so the Homebrew cask can find binary/conf/manpage without a rename. wrap_in_directory: false @@ -200,28 +200,16 @@ archives: dst: unpackerr-manual.html info: mode: 0644 - format_overrides: - - goos: windows - formats: [zip] - - id: freebsd - ids: [unpackerr-freebsd] - formats: [tar.xz] + # pkg/update/check.go only matches assets ending in .exe.zip. + - id: windows + ids: [unpackerr-windows] + formats: [zip] + name_template: '{{ .ProjectName }}.{{ .Arch }}.exe' wrap_in_directory: false files: - - src: examples/unpackerr.conf.example - dst: usr/local/etc/unpackerr/unpackerr.conf.example - strip_parent: true - - src: init/bsd/freebsd.rc.d - dst: usr/local/etc/rc.d/unpackerr - strip_parent: true - info: - mode: 0755 - - src: unpackerr.1.gz - dst: usr/local/share/man/man1/unpackerr.1.gz - strip_parent: true - - src: LICENSE - dst: usr/local/share/doc/unpackerr/LICENSE - strip_parent: true + - LICENSE + # Real pkgng .txz is built after --split by .github/scripts/freebsd_txz.py. + # nFPM has no freebsd target; a tar.xz is not `pkg install`-able. nfpms: - id: unpackerr-packages @@ -236,7 +224,6 @@ nfpms: - rpm - archlinux bindir: /usr/bin - version_metadata: git section: default priority: extra provides: diff --git a/init/macos/entitlements.plist b/init/macos/entitlements.plist index f5fea3fc..4ad44e40 100644 --- a/init/macos/entitlements.plist +++ b/init/macos/entitlements.plist @@ -2,10 +2,6 @@ - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-jit - com.apple.security.network.client com.apple.security.network.server diff --git a/init/windows/signexe.sh b/init/windows/signexe.sh index 44a7ad0a..8a355c0f 100755 --- a/init/windows/signexe.sh +++ b/init/windows/signexe.sh @@ -6,7 +6,9 @@ set -e -o pipefail # GoReleaser calls this from builds.hooks.post on windows binaries, after the # CLI is installed into a temp GOBIN (never /usr/bin/codesign). # -# Skip when CODESIGN_URL is unset so local snapshots still work. +# Local snapshots skip when CODESIGN_URL is unset. GitHub Actions must fail +# closed — the release contract is an Authenticode-signed Windows binary. +# # Prefer CODESIGN_BIN, then GOBIN/codesign, then GOPATH/bin, then PATH # entries that are not Apple's /usr/bin/codesign. @@ -36,6 +38,10 @@ function pick_codesign() { function sign() { if [ -z "${CODESIGN_URL:-}" ]; then + if [ -n "${GITHUB_ACTIONS:-}" ]; then + echo "CODESIGN_URL unset; refusing to ship an unsigned Windows binary in CI" >&2 + exit 1 + fi echo "Skipped signing ${FILE} (CODESIGN_URL unset) .." >&2 exit 0 fi From 32098a26a7162eb170b378a7917a85b6cb1d9183 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 21:09:32 -0700 Subject: [PATCH 28/41] Advertise FreeBSD packages as ABI FreeBSD:*: so 14/15 will pkg add them. Pinning 13 (fpm's old default) is rejected on current hosts; a wildcard OS version is what pkg add accepted in jordansissel/fpm#2064. Keep real CPU names, mark the rc conf as config, and stamp archives as root. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.py | 23 ++++++++++++++++++----- .github/scripts/require_secrets.sh | 2 ++ .goreleaser.yaml | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/scripts/freebsd_txz.py b/.github/scripts/freebsd_txz.py index ef1584c7..0dc1cf24 100755 --- a/.github/scripts/freebsd_txz.py +++ b/.github/scripts/freebsd_txz.py @@ -26,14 +26,20 @@ MAINTAINER = "David Newhall II " ORIGIN = "https://github.com/Unpackerr/unpackerr" WWW = "https://unpackerr.zip" -OSVERSION = "13" +# pkg ABI is os:version:arch. Version * matches any FreeBSD major; pkg add +# accepts it (jordansissel/fpm#2064). CPU stays real so amd64 is not +# installable on aarch64. FreeBSD 13 is EOL; do not pin 13. +OSVERSION = "*" +CONFIG_FILES = { + "usr/local/etc/unpackerr/unpackerr.conf", +} -# Filename arch (install.sh / check.go) and pkgng ABI arch. +# Filename arch (install.sh / check.go) and pkgng ABI arch (uname -p). ARCH_MAP = { "amd64": ("amd64", "amd64"), "386": ("i386", "i386"), - "arm": ("armhf", "arm"), - "arm64": ("arm64", "arm64"), + "arm": ("armhf", "armv7"), + "arm64": ("arm64", "aarch64"), } @@ -136,7 +142,11 @@ def write_manifests( ) -> None: checksums = {} for rel in members: - checksums["/" + rel] = sha256(staging / rel) + digest = sha256(staging / rel) + if rel in CONFIG_FILES: + checksums["/" + rel] = {"sum": digest, "config": True} + else: + checksums["/" + rel] = digest pkgdata = { "arch": f"FreeBSD:{OSVERSION}:{abi_arch}", @@ -196,6 +206,9 @@ def pack_txz(staging: Path, members: list[str], dest: Path) -> None: subprocess.run( [ tar, + "--owner=0", + "--group=0", + "--numeric-owner", "-Jcf", str(dest), "-C", diff --git a/.github/scripts/require_secrets.sh b/.github/scripts/require_secrets.sh index cf39acd8..361ea9fd 100755 --- a/.github/scripts/require_secrets.sh +++ b/.github/scripts/require_secrets.sh @@ -21,6 +21,8 @@ need() { need GORELEASER_PRO_KEY need GPG_SIGNING_KEY +# Hub is part of every channel (GHCR + golift/unpackerr). Absence used to +# skip login and still go green; that is no longer allowed. need DOCKERHUB_PASSWORD need CODESIGN_URL need CODESIGN_CLIENT_CERT diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8d1e0b62..7be37ba3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -396,7 +396,7 @@ aur_sources: build: | cd "${pkgname}-${pkgver}" export GOFLAGS="-buildmode=pie -trimpath -modcacherw" - go build -o unpackerr -ldflags "-w -s -X golift.io/version.Version=${pkgver} -X golift.io/version.Revision=${pkgrel}" . + go build -o unpackerr -ldflags "-w -s -X golift.io/version.Version=${pkgver} -X golift.io/version.Revision=${pkgrel} -X \"golift.io/version.Branch=${pkgver} [aur]\"" . go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "$(date -u +%Y-%m-%d)" examples/MANUAL.md gzip -9nf examples/MANUAL mv examples/MANUAL.gz unpackerr.1.gz From 26fa0d486abf6d4402d257029a269cda29e8d657 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 21:20:00 -0700 Subject: [PATCH 29/41] Set BuildDate and BuildUser on the AUR source build, matching the old PKGBUILD. Co-authored-by: Cursor --- .goreleaser.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7be37ba3..d8203cb3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -396,7 +396,12 @@ aur_sources: build: | cd "${pkgname}-${pkgver}" export GOFLAGS="-buildmode=pie -trimpath -modcacherw" - go build -o unpackerr -ldflags "-w -s -X golift.io/version.Version=${pkgver} -X golift.io/version.Revision=${pkgrel} -X \"golift.io/version.Branch=${pkgver} [aur]\"" . + LDFLAGS="-w -s -X golift.io/version.Version=${pkgver} \ + -X golift.io/version.Revision=${pkgrel} \ + -X golift.io/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:00Z) \ + -X golift.io/version.BuildUser=$(whoami || echo unknown) \ + -X \"golift.io/version.Branch=${pkgver} [aur]\"" + go build -o unpackerr -ldflags "${LDFLAGS}" . go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "$(date -u +%Y-%m-%d)" examples/MANUAL.md gzip -9nf examples/MANUAL mv examples/MANUAL.gz unpackerr.1.gz From 8b1dd02a944df12847331461f7f9a70354991e01 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 21:34:04 -0700 Subject: [PATCH 30/41] Put pkg config files in the top-level manifest array and restore Windows/AUR extras. The Windows zip only had LICENSE; tagged zips used to ship the conf example and HTML manuals. Debs already had unpackerr-manual.html; add README.html and docker-compose next to it like the old FPM doc glob. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.py | 9 +++++---- .goreleaser.yaml | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/scripts/freebsd_txz.py b/.github/scripts/freebsd_txz.py index 0dc1cf24..95be9d50 100755 --- a/.github/scripts/freebsd_txz.py +++ b/.github/scripts/freebsd_txz.py @@ -141,12 +141,11 @@ def write_manifests( scripts: dict[str, str], ) -> None: checksums = {} + config = [] for rel in members: - digest = sha256(staging / rel) + checksums["/" + rel] = sha256(staging / rel) if rel in CONFIG_FILES: - checksums["/" + rel] = {"sum": digest, "config": True} - else: - checksums["/" + rel] = digest + config.append("/" + rel) pkgdata = { "arch": f"FreeBSD:{OSVERSION}:{abi_arch}", @@ -163,6 +162,8 @@ def write_manifests( compact.write_text(json.dumps(pkgdata, separators=(",", ":")) + "\n", encoding="utf-8") pkgdata["files"] = checksums pkgdata["scripts"] = scripts + # pkg reads a top-level config array, not per-file attributes. + pkgdata["config"] = config manifest = staging / "+MANIFEST" manifest.write_text(json.dumps(pkgdata, separators=(",", ":")) + "\n", encoding="utf-8") diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d8203cb3..4387a5ea 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -208,6 +208,11 @@ archives: wrap_in_directory: false files: - LICENSE + - src: examples/unpackerr.conf.example + dst: unpackerr.conf.example + - src: examples/MANUAL.html + dst: unpackerr-manual.html + - README.html # Real pkgng .txz is built after --split by .github/scripts/freebsd_txz.py. # nFPM has no freebsd target; a tar.xz is not `pkg install`-able. @@ -266,6 +271,18 @@ nfpms: dst: /usr/share/doc/unpackerr/unpackerr-manual.html file_info: mode: 0644 + - src: README.html + dst: /usr/share/doc/unpackerr/README.html + file_info: + mode: 0644 + - src: examples/docker-compose.yml + dst: /usr/share/doc/unpackerr/docker-compose.yml + file_info: + mode: 0644 + - src: examples/unpackerr.conf.example + dst: /usr/share/doc/unpackerr/unpackerr.conf.example + file_info: + mode: 0644 - src: init/systemd/unpackerr.service dst: /usr/lib/systemd/system/unpackerr.service type: config @@ -390,6 +407,7 @@ aur_sources: - armv6h - armv7h - i686 + - pentium4 prepare: | cd "${pkgname}-${pkgver}" mkdir -p build @@ -412,6 +430,7 @@ aur_sources: install -D -m 644 examples/unpackerr.conf.example "${pkgdir}/etc/unpackerr/unpackerr.conf" install -D -m 644 examples/unpackerr.conf.example "${pkgdir}/etc/unpackerr/unpackerr.conf.example" install -D -m 644 LICENSE "${pkgdir}/usr/share/licenses/unpackerr/LICENSE" + install -D -m 644 examples/MANUAL.html "${pkgdir}/usr/share/doc/${pkgname}/unpackerr-manual.html" install -D -m 644 unpackerr.1.gz "${pkgdir}/usr/share/man/man1/unpackerr.1.gz" install -D -m 644 init/systemd/unpackerr.service "${pkgdir}/usr/lib/systemd/system/unpackerr.service" echo 'u unpackerr - "unpackerr daemon"' > unpackerr.sysusers From 3df342520df1c1e78f2ae7285853e36169bc1ed2 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 21:35:11 -0700 Subject: [PATCH 31/41] Put README.html and docker-compose in the Linux tarball and AUR too. Debs already ship those under /usr/share/doc; the GitHub tar.gz and AUR PKGBUILD were still the short set. Co-authored-by: Cursor --- .goreleaser.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4387a5ea..59befab9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -192,6 +192,7 @@ archives: files: - LICENSE - README.md + - README.html - src: examples/unpackerr.conf.example dst: unpackerr.conf.example - src: unpackerr.1.gz @@ -200,6 +201,8 @@ archives: dst: unpackerr-manual.html info: mode: 0644 + - src: examples/docker-compose.yml + dst: docker-compose.yml # pkg/update/check.go only matches assets ending in .exe.zip. - id: windows ids: [unpackerr-windows] @@ -421,6 +424,7 @@ aur_sources: -X \"golift.io/version.Branch=${pkgver} [aur]\"" go build -o unpackerr -ldflags "${LDFLAGS}" . go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "$(date -u +%Y-%m-%d)" examples/MANUAL.md + go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "${pkgver}" --date "$(date -u +%Y-%m-%d)" README.md gzip -9nf examples/MANUAL mv examples/MANUAL.gz unpackerr.1.gz package: | @@ -431,6 +435,9 @@ aur_sources: install -D -m 644 examples/unpackerr.conf.example "${pkgdir}/etc/unpackerr/unpackerr.conf.example" install -D -m 644 LICENSE "${pkgdir}/usr/share/licenses/unpackerr/LICENSE" install -D -m 644 examples/MANUAL.html "${pkgdir}/usr/share/doc/${pkgname}/unpackerr-manual.html" + install -D -m 644 README.html "${pkgdir}/usr/share/doc/${pkgname}/README.html" + install -D -m 644 examples/docker-compose.yml "${pkgdir}/usr/share/doc/${pkgname}/docker-compose.yml" + install -D -m 644 examples/unpackerr.conf.example "${pkgdir}/usr/share/doc/${pkgname}/unpackerr.conf.example" install -D -m 644 unpackerr.1.gz "${pkgdir}/usr/share/man/man1/unpackerr.1.gz" install -D -m 644 init/systemd/unpackerr.service "${pkgdir}/usr/lib/systemd/system/unpackerr.service" echo 'u unpackerr - "unpackerr daemon"' > unpackerr.sysusers From 3539a7a2f62186e802309f995bc898ce14f69193 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 21:49:54 -0700 Subject: [PATCH 32/41] Build FreeBSD .txz with fpm instead of a custom packer. nFPM still has no freebsd target, so keep a thin wrapper after --split. fpm writes the same pkgng layout as v0.15.2; the script only stages files, passes --freebsd-osversion *, and fixes ARM ABI names fpm gets wrong. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.py | 328 --------------------------------- .github/scripts/freebsd_txz.sh | 180 ++++++++++++++++++ .github/workflows/README.md | 4 +- .github/workflows/release.yml | 5 +- .goreleaser.yaml | 4 +- 5 files changed, 188 insertions(+), 333 deletions(-) delete mode 100755 .github/scripts/freebsd_txz.py create mode 100755 .github/scripts/freebsd_txz.sh diff --git a/.github/scripts/freebsd_txz.py b/.github/scripts/freebsd_txz.py deleted file mode 100755 index 95be9d50..00000000 --- a/.github/scripts/freebsd_txz.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 -"""Build FreeBSD pkgng .txz packages from GoReleaser freebsd binaries. - -nFPM has no freebsd target. The old Makefile used fpm -t freebsd, which wrote -pkgng +MANIFEST / +COMPACT_MANIFEST and a xz-compressed tar (a real .txz, not -a renamed tar.xz). install.sh and pkg/update/check.go still look for assets -ending in amd64.txz / i386.txz / armhf.txz / arm64.txz. - -This runs after `goreleaser --split` on the freebsd job and appends Archive -entries to dist/freebsd/artifacts.json so merge uploads them. -""" -from __future__ import annotations - -import argparse -import hashlib -import json -import shutil -import stat -import subprocess -import sys -import tarfile -import tempfile -from pathlib import Path - -DESC = "Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them." -MAINTAINER = "David Newhall II " -ORIGIN = "https://github.com/Unpackerr/unpackerr" -WWW = "https://unpackerr.zip" -# pkg ABI is os:version:arch. Version * matches any FreeBSD major; pkg add -# accepts it (jordansissel/fpm#2064). CPU stays real so amd64 is not -# installable on aarch64. FreeBSD 13 is EOL; do not pin 13. -OSVERSION = "*" -CONFIG_FILES = { - "usr/local/etc/unpackerr/unpackerr.conf", -} - -# Filename arch (install.sh / check.go) and pkgng ABI arch (uname -p). -ARCH_MAP = { - "amd64": ("amd64", "amd64"), - "386": ("i386", "i386"), - "arm": ("armhf", "armv7"), - "arm64": ("arm64", "aarch64"), -} - - -def sha256(path: Path) -> str: - h = hashlib.sha256() - with path.open("rb") as fh: - for chunk in iter(lambda: fh.read(1024 * 1024), b""): - h.update(chunk) - return h.hexdigest() - - -def repo_root() -> Path: - return Path(__file__).resolve().parents[2] - - -def load_version(dist: Path, override: str) -> str: - if override: - return override - meta = dist / "metadata.json" - if not meta.is_file(): - sys.exit(f"missing {meta} (and no --version)") - data = json.loads(meta.read_text()) - version = data.get("version") or "" - if not version or version in {"unknown", "unstable"}: - sys.exit(f"refusing version {version!r} from {meta}") - return version - - -def freebsd_binaries(artifacts: list[dict]) -> list[dict]: - found: list[dict] = [] - for art in artifacts: - if art.get("type") != "Binary" or art.get("goos") != "freebsd": - continue - if str(art.get("goarch") or "") == "arm" and str(art.get("goarm") or "") == "6": - continue - found.append(art) - if not found: - sys.exit("no freebsd Binary artifacts (skipping GOARM 6)") - return found - - -def resolve_binary(dist: Path, path: str) -> Path: - p = Path(path) - if p.is_file(): - return p - cand = dist / path - if cand.is_file(): - return cand - sys.exit(f"freebsd binary missing: {path}") - - -def stage_files(repo: Path, binary: Path, staging: Path) -> list[str]: - """Populate a pkgng staging dir. Returns tar member names (no leading /).""" - files: list[tuple[str, Path, int]] = [] - - def put(rel: str, src: Path, mode: int) -> None: - dest = staging / rel - dest.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(src, dest) - dest.chmod(mode) - files.append((rel, dest, mode)) - - bin_mode = 0o755 - data_mode = 0o644 - rc_mode = 0o755 - - put("usr/local/bin/unpackerr", binary, bin_mode) - put("usr/local/etc/rc.d/unpackerr", repo / "init/bsd/freebsd.rc.d", rc_mode) - conf = repo / "examples/unpackerr.conf.example" - put("usr/local/etc/unpackerr/unpackerr.conf", conf, data_mode) - put("usr/local/etc/unpackerr/unpackerr.conf.example", conf, data_mode) - - man = repo / "unpackerr.1.gz" - if not man.is_file(): - sys.exit(f"missing {man} (goreleaser before-hook should gzip the man page)") - put("usr/local/share/man/man1/unpackerr.1.gz", man, data_mode) - put("usr/local/share/doc/unpackerr/LICENSE", repo / "LICENSE", data_mode) - - docs = [ - (repo / "examples/MANUAL.md", "usr/local/share/doc/unpackerr/MANUAL.md"), - (repo / "examples/MANUAL.html", "usr/local/share/doc/unpackerr/unpackerr_manual.html"), - (repo / "examples/docker-compose.yml", "usr/local/share/doc/unpackerr/docker-compose.yml"), - (repo / "examples/unpackerr.conf.example", "usr/local/share/doc/unpackerr/unpackerr.conf.example"), - (repo / "README.html", "usr/local/share/doc/unpackerr/README.html"), - ] - for src, rel in docs: - if src.is_file(): - put(rel, src, data_mode) - - return [rel for rel, _, _ in files] - - -def write_manifests( - staging: Path, - *, - version: str, - abi_arch: str, - members: list[str], - scripts: dict[str, str], -) -> None: - checksums = {} - config = [] - for rel in members: - checksums["/" + rel] = sha256(staging / rel) - if rel in CONFIG_FILES: - config.append("/" + rel) - - pkgdata = { - "arch": f"FreeBSD:{OSVERSION}:{abi_arch}", - "name": "unpackerr", - "version": version, - "comment": DESC, - "desc": DESC, - "origin": ORIGIN, - "maintainer": MAINTAINER, - "www": WWW, - "prefix": "/", - } - compact = staging / "+COMPACT_MANIFEST" - compact.write_text(json.dumps(pkgdata, separators=(",", ":")) + "\n", encoding="utf-8") - pkgdata["files"] = checksums - pkgdata["scripts"] = scripts - # pkg reads a top-level config array, not per-file attributes. - pkgdata["config"] = config - manifest = staging / "+MANIFEST" - manifest.write_text(json.dumps(pkgdata, separators=(",", ":")) + "\n", encoding="utf-8") - - -def load_scripts(repo: Path) -> dict[str, str]: - mapping = { - "pre-install": repo / "init/systemd/before-install.sh", - "post-install": repo / "init/systemd/after-install.sh", - "pre-deinstall": repo / "init/systemd/before-remove.sh", - } - out = {} - for name, path in mapping.items(): - if not path.is_file(): - sys.exit(f"missing pkg script {path}") - out[name] = path.read_text(encoding="utf-8") - return out - - -def gnu_tar() -> str | None: - for name in ("tar", "gtar", "gnutar"): - path = shutil.which(name) - if not path: - continue - try: - proc = subprocess.run([path, "--version"], capture_output=True, text=True, check=False) - except OSError: - continue - if "GNU tar" in (proc.stdout + proc.stderr): - return path - return None - - -def pack_txz(staging: Path, members: list[str], dest: Path) -> None: - dest.parent.mkdir(parents=True, exist_ok=True) - file_list = [ "+COMPACT_MANIFEST", "+MANIFEST", *members ] - tar = gnu_tar() - if tar: - # Match fpm: no ./ prefix; leading / on payload files; +MANIFEST at top. - list_file = staging / ".file_list" - list_file.write_text("\n".join(file_list) + "\n", encoding="utf-8") - subprocess.run( - [ - tar, - "--owner=0", - "--group=0", - "--numeric-owner", - "-Jcf", - str(dest), - "-C", - str(staging), - "--files-from", - str(list_file), - "--transform", - r"s|^\([^+]\)|/\1|", - ], - check=True, - ) - list_file.unlink(missing_ok=True) - return - - with tarfile.open(dest, "w:xz", format=tarfile.GNU_FORMAT, dereference=True) as tf: - for name in file_list: - path = staging / name - info = tf.gettarinfo(str(path), arcname=name) - info.uname = "root" - info.gname = "wheel" - info.uid = 0 - info.gid = 0 - if not name.startswith("+"): - info.name = "/" + name - with path.open("rb") as fh: - tf.addfile(info, fh) - - -def build_one( - *, - repo: Path, - dist: Path, - binary: Path, - version: str, - goarch: str, - scripts: dict[str, str], -) -> Path: - if goarch not in ARCH_MAP: - sys.exit(f"unsupported freebsd goarch {goarch}") - pkgarch, abi_arch = ARCH_MAP[goarch] - dest = dist / f"unpackerr-{version}.{pkgarch}.txz" - with tempfile.TemporaryDirectory(prefix="unpackerr-txz-") as tmp: - staging = Path(tmp) - members = stage_files(repo, binary, staging) - write_manifests(staging, version=version, abi_arch=abi_arch, members=members, scripts=scripts) - pack_txz(staging, members, dest) - dest.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) - print(f"wrote {dest.name} from {binary} ({dest.stat().st_size} bytes)", file=sys.stderr) - return dest - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("dist", type=Path, help="dist/freebsd (split output)") - parser.add_argument("--repo", type=Path, default=repo_root()) - parser.add_argument("--version", default="") - args = parser.parse_args() - dist = args.dist.resolve() - repo = args.repo.resolve() - artifacts_path = dist / "artifacts.json" - if not artifacts_path.is_file(): - sys.exit(f"missing {artifacts_path}") - - artifacts = json.loads(artifacts_path.read_text()) - if not isinstance(artifacts, list): - sys.exit(f"{artifacts_path} is not a JSON array") - version = load_version(dist, args.version) - scripts = load_scripts(repo) - built: list[tuple[dict, Path]] = [] - seen_arch: set[str] = set() - for art in freebsd_binaries(artifacts): - goarch = str(art.get("goarch") or "") - if goarch in seen_arch: - continue - seen_arch.add(goarch) - binary = resolve_binary(dist, str(art.get("path") or "")) - dest = build_one( - repo=repo, - dist=dist, - binary=binary, - version=version, - goarch=goarch, - scripts=scripts, - ) - built.append((art, dest)) - - expected = {"amd64", "386", "arm", "arm64"} - if not expected.issubset(seen_arch): - sys.exit(f"freebsd txz missing arches {sorted(expected - seen_arch)}; built {sorted(seen_arch)}") - - for art, dest in built: - extra = dict(art.get("extra") or {}) - extra["ID"] = "freebsd-pkg" - extra["Format"] = "txz" - artifacts.append( - { - "name": dest.name, - "path": dest.name, - "goos": "freebsd", - "goarch": art.get("goarch"), - "goarm": art.get("goarm"), - "goamd64": art.get("goamd64"), - "type": "Archive", - "extra": extra, - } - ) - artifacts_path.write_text(json.dumps(artifacts, indent=2) + "\n", encoding="utf-8") - print(f"appended {len(built)} txz archives to {artifacts_path}", file=sys.stderr) - return 0 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except subprocess.CalledProcessError as exc: - sys.exit(f"tar failed: {exc}") diff --git a/.github/scripts/freebsd_txz.sh b/.github/scripts/freebsd_txz.sh new file mode 100755 index 00000000..a11ae267 --- /dev/null +++ b/.github/scripts/freebsd_txz.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# Build FreeBSD pkgng .txz with fpm after `goreleaser --split`. +# +# nFPM has no freebsd target. The old Makefile used `fpm -s dir -t freebsd`. +# v0.15.2 GitHub assets are that output. This wrapper stages the same layout, +# calls fpm, and appends Archive entries to dist/freebsd/artifacts.json so +# merge uploads unpackerr-.{amd64,i386,armhf,arm64}.txz. +set -euo pipefail + +DIST=${1:?usage: freebsd_txz.sh dist/freebsd} +REPO=$(cd "$(dirname "$0")/../.." && pwd) +DIST=$(cd "${DIST}" && pwd) + +need() { command -v "$1" >/dev/null || { echo "missing $1" >&2; exit 1; }; } +need fpm +need jq +need python3 + +# fpm's freebsd.rb invokes `tar --transform`. BSD tar (macOS) cannot. +if ! tar --version 2>/dev/null | grep -q 'GNU tar'; then + if command -v gtar >/dev/null; then + _gnutar_bin=$(mktemp -d) + ln -s "$(command -v gtar)" "${_gnutar_bin}/tar" + export PATH="${_gnutar_bin}:${PATH}" + else + echo "fpm -t freebsd needs GNU tar" >&2 + exit 1 + fi +fi + +artifacts="${DIST}/artifacts.json" +metadata="${DIST}/metadata.json" +[[ -f ${artifacts} ]] || { echo "missing ${artifacts}" >&2; exit 1; } +[[ -f ${metadata} ]] || { echo "missing ${metadata}" >&2; exit 1; } + +VERSION=$(jq -r '.version // empty' "${metadata}") +if [[ -z ${VERSION} || ${VERSION} == unknown || ${VERSION} == unstable ]]; then + echo "refusing version '${VERSION}' from ${metadata}" >&2 + exit 1 +fi + +# Filename arch (install.sh / check.go) / fpm -a / pkg uname -p. +# fpm maps unknown -a to getconf LONG_BIT: v0.15.2 i386+armhf are FreeBSD:13:64 +# because the Makefile passed -a 386 / -a arm. Pass names fpm knows, then +# rewrite the ABI CPU when fpm still emits the wrong one (arm64→aarch64). +arch_tuple() { + case "$1" in + amd64) echo "amd64 amd64 amd64" ;; + 386) echo "i386 i386 i386" ;; + arm) echo "armhf amd64 armv7" ;; + arm64) echo "arm64 aarch64 aarch64" ;; + *) echo "unsupported freebsd goarch $1" >&2; return 1 ;; + esac +} + +rewrite_abi() { + local pkg=$1 abi=$2 tmp list + tmp=$(mktemp -d) + list=$(mktemp) + tar --transform 's|^/||' -xJf "${pkg}" -C "${tmp}" + python3 - "${tmp}" "${abi}" <<'PY' +import json, sys +from pathlib import Path +root, arch = Path(sys.argv[1]), sys.argv[2] +for name in ("+COMPACT_MANIFEST", "+MANIFEST"): + path = root / name + data = json.loads(path.read_text()) + if data.get("arch") == arch: + continue + data["arch"] = arch + path.write_text(json.dumps(data, separators=(",", ":")) + "\n") +PY + { + printf '%s\n' +COMPACT_MANIFEST +MANIFEST + (cd "${tmp}" && find usr -type f | sort) + } > "${list}" + tar --owner=0 --group=0 --numeric-owner -Jcf "${pkg}" -C "${tmp}" \ + --files-from "${list}" --transform 's|^\([^+]\)|/\1|' + rm -rf "${tmp}" "${list}" +} + +stage() { + local binary=$1 dest=$2 + mkdir -p \ + "${dest}/usr/local/bin" \ + "${dest}/usr/local/etc/unpackerr" \ + "${dest}/usr/local/etc/rc.d" \ + "${dest}/usr/local/share/man/man1" \ + "${dest}/usr/local/share/doc/unpackerr" + install -m 755 "${binary}" "${dest}/usr/local/bin/unpackerr" + install -m 755 "${REPO}/init/bsd/freebsd.rc.d" "${dest}/usr/local/etc/rc.d/unpackerr" + install -m 644 "${REPO}/examples/unpackerr.conf.example" "${dest}/usr/local/etc/unpackerr/unpackerr.conf" + install -m 644 "${REPO}/examples/unpackerr.conf.example" "${dest}/usr/local/etc/unpackerr/unpackerr.conf.example" + [[ -f ${REPO}/unpackerr.1.gz ]] || { echo "missing ${REPO}/unpackerr.1.gz" >&2; exit 1; } + install -m 644 "${REPO}/unpackerr.1.gz" "${dest}/usr/local/share/man/man1/unpackerr.1.gz" + install -m 644 "${REPO}/LICENSE" "${dest}/usr/local/share/doc/unpackerr/LICENSE" + # Same extras as the v0.15.2 fpm glob, minus examples.go. + local src dst + for src in \ + "${REPO}/examples/MANUAL.md" \ + "${REPO}/examples/MANUAL.html" \ + "${REPO}/examples/docker-compose.yml" \ + "${REPO}/examples/unpackerr.conf.example" \ + "${REPO}/README.html" + do + [[ -f ${src} ]] || continue + dst=$(basename "${src}") + [[ ${src} == */MANUAL.html ]] && dst=unpackerr_manual.html + install -m 644 "${src}" "${dest}/usr/local/share/doc/unpackerr/${dst}" + done +} + +arts=() +names=() +seen=() +while IFS= read -r art; do + goarch=$(jq -r '.goarch // empty' <<<"${art}") + goarm=$(jq -r '.goarm // empty' <<<"${art}") + [[ ${goarch} == arm && ${goarm} == 6 ]] && continue + [[ " ${seen[*]} " == *" ${goarch} "* ]] && continue + seen+=("${goarch}") + + path=$(jq -r '.path // empty' <<<"${art}") + binary=${path} + [[ -f ${binary} ]] || binary="${DIST}/${path}" + [[ -f ${binary} ]] || { echo "freebsd binary missing: ${path}" >&2; exit 1; } + + read -r pkgarch fpm_a abi_cpu <<<"$(arch_tuple "${goarch}")" + dest="${DIST}/unpackerr-${VERSION}.${pkgarch}.txz" + tmp=$(mktemp -d) + stage "${binary}" "${tmp}" + rm -f "${dest}" + fpm -s dir -t freebsd \ + --name unpackerr \ + -v "${VERSION}" \ + -a "${fpm_a}" \ + --license MIT \ + --url https://unpackerr.zip \ + --maintainer "David Newhall II " \ + --description "Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them." \ + --freebsd-origin https://github.com/Unpackerr/unpackerr \ + --freebsd-osversion '*' \ + --before-install "${REPO}/init/systemd/before-install.sh" \ + --after-install "${REPO}/init/systemd/after-install.sh" \ + --before-remove "${REPO}/init/systemd/before-remove.sh" \ + -C "${tmp}" \ + -p "${dest}" \ + . + rm -rf "${tmp}" + rewrite_abi "${dest}" "FreeBSD:*:${abi_cpu}" + echo "wrote ${dest##*/} from ${binary} ($(wc -c <"${dest}" | tr -d ' ') bytes)" >&2 + arts+=("${art}") + names+=("${dest##*/}") +done < <(jq -c '.[] | select(.type=="Binary" and .goos=="freebsd")' "${artifacts}") + +for need_arch in amd64 386 arm arm64; do + [[ " ${seen[*]} " == *" ${need_arch} "* ]] || { + echo "freebsd txz missing goarch ${need_arch}; built ${seen[*]}" >&2 + exit 1 + } +done + +artifacts_tmp=$(mktemp) +cp "${artifacts}" "${artifacts_tmp}" +for i in "${!arts[@]}"; do + jq --arg name "${names[$i]}" --argjson art "${arts[$i]}" \ + '. + [{ + name: $name, + path: $name, + goos: "freebsd", + goarch: $art.goarch, + goarm: $art.goarm, + goamd64: $art.goamd64, + type: "Archive", + extra: (($art.extra // {}) + {ID: "freebsd-pkg", Format: "txz"}) + }]' "${artifacts_tmp}" > "${artifacts_tmp}.new" + mv "${artifacts_tmp}.new" "${artifacts_tmp}" +done +mv "${artifacts_tmp}" "${artifacts}" +echo "appended ${#names[@]} txz archives to ${artifacts}" >&2 diff --git a/.github/workflows/README.md b/.github/workflows/README.md index ecd3b05e..47a99cd9 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -28,13 +28,13 @@ GoReleaser Pro `--split` / `--continue --merge` builds each GOOS in its own job, - Darwin needs **CGO** (`energye/systray` Cocoa) and **native** `codesign` / `notarytool`. Quill on Linux can sign a naked binary; a `.app` inside a DMG is rejected by Gatekeeper unless the bundle is signed on macOS. - Windows Authenticode talks to house **signerd** (`golift.io/codesign`) and needs `id-token: write` for GitHub OIDC. That is ubuntu, not macOS. -- Linux nFPM (deb/rpm) needs `rpm` + GPG. FreeBSD pkgng `.txz` is built after `--split` by `.github/scripts/freebsd_txz.py` (nFPM has no freebsd target). +- Linux nFPM (deb/rpm) needs `rpm` + GPG. FreeBSD pkgng `.txz` is built after `--split` by `fpm -t freebsd` (nFPM has no freebsd target). Wrapper: `.github/scripts/freebsd_txz.sh`. So: 1. **channel** — compute `CHANNEL`, extra args, and `REVISION` (`git rev-list --count --all`). This is the only place the count is taken; later jobs pass `needs.channel.outputs.revision`. 2. **require secrets** — fail closed if any signing/upload/push secret needed for that channel is empty. Missing secrets used to skip Docker Hub, Windows Authenticode, or unstable.golift.io and still go green. -3. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. FreeBSD then runs `freebsd_txz.py`. +3. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. FreeBSD then runs `freebsd_txz.sh` (`fpm -t freebsd`). 4. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. 5. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f0b7d36..6b17961d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,6 +112,9 @@ jobs: - name: Install package tooling if: matrix.goos == 'linux' run: sudo apt-get update && sudo apt-get install -y rpm + - name: Install fpm + if: matrix.goos == 'freebsd' + run: sudo gem install --no-document fpm - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 @@ -152,7 +155,7 @@ jobs: CODESIGN_WEBSITE: https://unpackerr.zip - name: Build FreeBSD pkgng txz if: matrix.goos == 'freebsd' - run: python3 .github/scripts/freebsd_txz.py dist/freebsd + run: bash .github/scripts/freebsd_txz.sh dist/freebsd - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-${{ matrix.goos }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 59befab9..975788e4 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -216,8 +216,8 @@ archives: - src: examples/MANUAL.html dst: unpackerr-manual.html - README.html - # Real pkgng .txz is built after --split by .github/scripts/freebsd_txz.py. - # nFPM has no freebsd target; a tar.xz is not `pkg install`-able. + # Real pkgng .txz is built after --split by fpm (-t freebsd), same as the + # old Makefile. nFPM has no freebsd target; a tar.xz is not `pkg install`-able. nfpms: - id: unpackerr-packages From 5607a74659e2d1d6e1cd802e2c99e2f18e5cbca1 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 21:56:10 -0700 Subject: [PATCH 33/41] Drop the embedded Python from the FreeBSD fpm wrapper. jq can rewrite .arch, and amd64/i386 already come out of fpm with the right ABI so those packages are not unpacked. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.sh | 213 ++++++++++++--------------------- 1 file changed, 75 insertions(+), 138 deletions(-) diff --git a/.github/scripts/freebsd_txz.sh b/.github/scripts/freebsd_txz.sh index a11ae267..cddfb9fc 100755 --- a/.github/scripts/freebsd_txz.sh +++ b/.github/scripts/freebsd_txz.sh @@ -1,180 +1,117 @@ #!/usr/bin/env bash -# Build FreeBSD pkgng .txz with fpm after `goreleaser --split`. -# -# nFPM has no freebsd target. The old Makefile used `fpm -s dir -t freebsd`. -# v0.15.2 GitHub assets are that output. This wrapper stages the same layout, -# calls fpm, and appends Archive entries to dist/freebsd/artifacts.json so -# merge uploads unpackerr-.{amd64,i386,armhf,arm64}.txz. +# After goreleaser --split, turn freebsd binaries into pkgng .txz (nFPM cannot). +# Same tool as v0.15.2: fpm -s dir -t freebsd. This only stages files, calls fpm, +# and lists the archives in artifacts.json for merge. set -euo pipefail DIST=${1:?usage: freebsd_txz.sh dist/freebsd} REPO=$(cd "$(dirname "$0")/../.." && pwd) DIST=$(cd "${DIST}" && pwd) +command -v fpm >/dev/null && command -v jq >/dev/null || { echo "need fpm and jq" >&2; exit 1; } -need() { command -v "$1" >/dev/null || { echo "missing $1" >&2; exit 1; }; } -need fpm -need jq -need python3 - -# fpm's freebsd.rb invokes `tar --transform`. BSD tar (macOS) cannot. +# fpm freebsd.rb runs `tar --transform` (GNU). Put gtar first on macOS. if ! tar --version 2>/dev/null | grep -q 'GNU tar'; then - if command -v gtar >/dev/null; then - _gnutar_bin=$(mktemp -d) - ln -s "$(command -v gtar)" "${_gnutar_bin}/tar" - export PATH="${_gnutar_bin}:${PATH}" - else - echo "fpm -t freebsd needs GNU tar" >&2 - exit 1 - fi + command -v gtar >/dev/null || { echo "fpm -t freebsd needs GNU tar" >&2; exit 1; } + _gnutar_bin=$(mktemp -d) + ln -s "$(command -v gtar)" "${_gnutar_bin}/tar" + export PATH="${_gnutar_bin}:${PATH}" fi -artifacts="${DIST}/artifacts.json" -metadata="${DIST}/metadata.json" -[[ -f ${artifacts} ]] || { echo "missing ${artifacts}" >&2; exit 1; } -[[ -f ${metadata} ]] || { echo "missing ${metadata}" >&2; exit 1; } - -VERSION=$(jq -r '.version // empty' "${metadata}") +artifacts=${DIST}/artifacts.json +VERSION=$(jq -r '.version // empty' "${DIST}/metadata.json") if [[ -z ${VERSION} || ${VERSION} == unknown || ${VERSION} == unstable ]]; then - echo "refusing version '${VERSION}' from ${metadata}" >&2 + echo "refusing version '${VERSION}'" >&2 exit 1 fi -# Filename arch (install.sh / check.go) / fpm -a / pkg uname -p. -# fpm maps unknown -a to getconf LONG_BIT: v0.15.2 i386+armhf are FreeBSD:13:64 -# because the Makefile passed -a 386 / -a arm. Pass names fpm knows, then -# rewrite the ABI CPU when fpm still emits the wrong one (arm64→aarch64). -arch_tuple() { - case "$1" in - amd64) echo "amd64 amd64 amd64" ;; - 386) echo "i386 i386 i386" ;; - arm) echo "armhf amd64 armv7" ;; - arm64) echo "arm64 aarch64 aarch64" ;; +# goarch -> asset suffix / fpm -a / pkg ABI CPU (uname -p). +# fpm only knows amd64, i386, aarch64(→arm64). Unknown -a becomes getconf LONG_BIT +# (v0.15.2 i386+armhf are FreeBSD:13:64). We pass a known -a, then fix ARM CPU. +pkg_for() { + case $1 in + amd64) echo amd64 amd64 amd64 ;; + 386) echo i386 i386 i386 ;; + arm) echo armhf amd64 armv7 ;; + arm64) echo arm64 aarch64 aarch64 ;; *) echo "unsupported freebsd goarch $1" >&2; return 1 ;; esac } -rewrite_abi() { +# fpm already wrote the right ABI: do nothing. Otherwise unpack, jq .arch, retar. +set_abi() { local pkg=$1 abi=$2 tmp list + [[ $(tar -xJOf "${pkg}" +COMPACT_MANIFEST | jq -r .arch) == "${abi}" ]] && return tmp=$(mktemp -d) - list=$(mktemp) tar --transform 's|^/||' -xJf "${pkg}" -C "${tmp}" - python3 - "${tmp}" "${abi}" <<'PY' -import json, sys -from pathlib import Path -root, arch = Path(sys.argv[1]), sys.argv[2] -for name in ("+COMPACT_MANIFEST", "+MANIFEST"): - path = root / name - data = json.loads(path.read_text()) - if data.get("arch") == arch: - continue - data["arch"] = arch - path.write_text(json.dumps(data, separators=(",", ":")) + "\n") -PY - { - printf '%s\n' +COMPACT_MANIFEST +MANIFEST - (cd "${tmp}" && find usr -type f | sort) - } > "${list}" + jq --arg a "${abi}" '.arch=$a' "${tmp}/+COMPACT_MANIFEST" > "${tmp}/c.json" + jq --arg a "${abi}" '.arch=$a' "${tmp}/+MANIFEST" > "${tmp}/m.json" + mv "${tmp}/c.json" "${tmp}/+COMPACT_MANIFEST" + mv "${tmp}/m.json" "${tmp}/+MANIFEST" + list=$(mktemp) + { printf '%s\n' +COMPACT_MANIFEST +MANIFEST; (cd "${tmp}" && find usr -type f | sort); } > "${list}" tar --owner=0 --group=0 --numeric-owner -Jcf "${pkg}" -C "${tmp}" \ --files-from "${list}" --transform 's|^\([^+]\)|/\1|' rm -rf "${tmp}" "${list}" } stage() { - local binary=$1 dest=$2 - mkdir -p \ - "${dest}/usr/local/bin" \ - "${dest}/usr/local/etc/unpackerr" \ - "${dest}/usr/local/etc/rc.d" \ - "${dest}/usr/local/share/man/man1" \ - "${dest}/usr/local/share/doc/unpackerr" - install -m 755 "${binary}" "${dest}/usr/local/bin/unpackerr" - install -m 755 "${REPO}/init/bsd/freebsd.rc.d" "${dest}/usr/local/etc/rc.d/unpackerr" - install -m 644 "${REPO}/examples/unpackerr.conf.example" "${dest}/usr/local/etc/unpackerr/unpackerr.conf" - install -m 644 "${REPO}/examples/unpackerr.conf.example" "${dest}/usr/local/etc/unpackerr/unpackerr.conf.example" - [[ -f ${REPO}/unpackerr.1.gz ]] || { echo "missing ${REPO}/unpackerr.1.gz" >&2; exit 1; } - install -m 644 "${REPO}/unpackerr.1.gz" "${dest}/usr/local/share/man/man1/unpackerr.1.gz" - install -m 644 "${REPO}/LICENSE" "${dest}/usr/local/share/doc/unpackerr/LICENSE" - # Same extras as the v0.15.2 fpm glob, minus examples.go. - local src dst - for src in \ - "${REPO}/examples/MANUAL.md" \ - "${REPO}/examples/MANUAL.html" \ - "${REPO}/examples/docker-compose.yml" \ - "${REPO}/examples/unpackerr.conf.example" \ - "${REPO}/README.html" - do - [[ -f ${src} ]] || continue - dst=$(basename "${src}") - [[ ${src} == */MANUAL.html ]] && dst=unpackerr_manual.html - install -m 644 "${src}" "${dest}/usr/local/share/doc/unpackerr/${dst}" - done + local bin=$1 root=$2 doc=${2}/usr/local/share/doc/unpackerr + mkdir -p "${root}/usr/local/bin" "${root}/usr/local/etc/unpackerr" \ + "${root}/usr/local/etc/rc.d" "${root}/usr/local/share/man/man1" "${doc}" + install -m 755 "${bin}" "${root}/usr/local/bin/unpackerr" + install -m 755 "${REPO}/init/bsd/freebsd.rc.d" "${root}/usr/local/etc/rc.d/unpackerr" + install -m 644 "${REPO}/examples/unpackerr.conf.example" "${root}/usr/local/etc/unpackerr/unpackerr.conf" + install -m 644 "${REPO}/examples/unpackerr.conf.example" "${root}/usr/local/etc/unpackerr/unpackerr.conf.example" + [[ -f ${REPO}/unpackerr.1.gz ]] || { echo "missing unpackerr.1.gz" >&2; exit 1; } + install -m 644 "${REPO}/unpackerr.1.gz" "${root}/usr/local/share/man/man1/unpackerr.1.gz" + install -m 644 "${REPO}/LICENSE" "${doc}/LICENSE" + install -m 644 "${REPO}/examples/MANUAL.md" "${doc}/MANUAL.md" + install -m 644 "${REPO}/examples/MANUAL.html" "${doc}/unpackerr_manual.html" + install -m 644 "${REPO}/examples/docker-compose.yml" "${doc}/docker-compose.yml" + install -m 644 "${REPO}/examples/unpackerr.conf.example" "${doc}/unpackerr.conf.example" + [[ -f ${REPO}/README.html ]] && install -m 644 "${REPO}/README.html" "${doc}/README.html" } -arts=() -names=() -seen=() -while IFS= read -r art; do - goarch=$(jq -r '.goarch // empty' <<<"${art}") - goarm=$(jq -r '.goarm // empty' <<<"${art}") +extra=$(mktemp) +echo '[]' > "${extra}" +seen= +while IFS='|' read -r goarch goarm path; do [[ ${goarch} == arm && ${goarm} == 6 ]] && continue - [[ " ${seen[*]} " == *" ${goarch} "* ]] && continue - seen+=("${goarch}") + [[ ${seen} == *"|${goarch}|"* ]] && continue + seen+="|${goarch}|" - path=$(jq -r '.path // empty' <<<"${art}") - binary=${path} - [[ -f ${binary} ]] || binary="${DIST}/${path}" - [[ -f ${binary} ]] || { echo "freebsd binary missing: ${path}" >&2; exit 1; } + bin=${path} + [[ -f ${bin} ]] || bin=${DIST}/${path} + [[ -f ${bin} ]] || { echo "freebsd binary missing: ${path}" >&2; exit 1; } - read -r pkgarch fpm_a abi_cpu <<<"$(arch_tuple "${goarch}")" - dest="${DIST}/unpackerr-${VERSION}.${pkgarch}.txz" + read -r suffix fpm_a cpu <<<"$(pkg_for "${goarch}")" + dest=${DIST}/unpackerr-${VERSION}.${suffix}.txz tmp=$(mktemp -d) - stage "${binary}" "${tmp}" + stage "${bin}" "${tmp}" rm -f "${dest}" - fpm -s dir -t freebsd \ - --name unpackerr \ - -v "${VERSION}" \ - -a "${fpm_a}" \ - --license MIT \ - --url https://unpackerr.zip \ + fpm -s dir -t freebsd --name unpackerr -v "${VERSION}" -a "${fpm_a}" \ + --license MIT --url https://unpackerr.zip \ --maintainer "David Newhall II " \ --description "Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them." \ - --freebsd-origin https://github.com/Unpackerr/unpackerr \ - --freebsd-osversion '*' \ + --freebsd-origin https://github.com/Unpackerr/unpackerr --freebsd-osversion '*' \ --before-install "${REPO}/init/systemd/before-install.sh" \ --after-install "${REPO}/init/systemd/after-install.sh" \ --before-remove "${REPO}/init/systemd/before-remove.sh" \ - -C "${tmp}" \ - -p "${dest}" \ - . + -C "${tmp}" -p "${dest}" . rm -rf "${tmp}" - rewrite_abi "${dest}" "FreeBSD:*:${abi_cpu}" - echo "wrote ${dest##*/} from ${binary} ($(wc -c <"${dest}" | tr -d ' ') bytes)" >&2 - arts+=("${art}") - names+=("${dest##*/}") -done < <(jq -c '.[] | select(.type=="Binary" and .goos=="freebsd")' "${artifacts}") - -for need_arch in amd64 386 arm arm64; do - [[ " ${seen[*]} " == *" ${need_arch} "* ]] || { - echo "freebsd txz missing goarch ${need_arch}; built ${seen[*]}" >&2 - exit 1 - } -done + set_abi "${dest}" "FreeBSD:*:${cpu}" + echo "wrote ${dest##*/}" >&2 + jq --arg name "${dest##*/}" --arg goarch "${goarch}" --arg goarm "${goarm}" \ + '. + [{name:$name, path:$name, goos:"freebsd", goarch:$goarch, + goarm: (if $goarm == "" then null else $goarm end), + type:"Archive", extra:{ID:"freebsd-pkg", Format:"txz"}}]' \ + "${extra}" > "${extra}.n" && mv "${extra}.n" "${extra}" +done < <(jq -r '.[] | select(.type=="Binary" and .goos=="freebsd") | [.goarch, (.goarm // ""), .path] | join("|")' "${artifacts}") -artifacts_tmp=$(mktemp) -cp "${artifacts}" "${artifacts_tmp}" -for i in "${!arts[@]}"; do - jq --arg name "${names[$i]}" --argjson art "${arts[$i]}" \ - '. + [{ - name: $name, - path: $name, - goos: "freebsd", - goarch: $art.goarch, - goarm: $art.goarm, - goamd64: $art.goamd64, - type: "Archive", - extra: (($art.extra // {}) + {ID: "freebsd-pkg", Format: "txz"}) - }]' "${artifacts_tmp}" > "${artifacts_tmp}.new" - mv "${artifacts_tmp}.new" "${artifacts_tmp}" +for a in amd64 386 arm arm64; do + [[ ${seen} == *"|${a}|"* ]] || { echo "freebsd txz missing goarch ${a}" >&2; exit 1; } done -mv "${artifacts_tmp}" "${artifacts}" -echo "appended ${#names[@]} txz archives to ${artifacts}" >&2 +jq --slurpfile extra "${extra}" '. + $extra[0]' "${artifacts}" > "${artifacts}.n" +mv "${artifacts}.n" "${artifacts}" +rm -f "${extra}" +echo "appended txz archives to ${artifacts}" >&2 From 37707e481a8ddf375b234c8f6814fd38284f9ea0 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 22:00:59 -0700 Subject: [PATCH 34/41] Fail unstable upload if any arch is missing, and default the workflow token to read. A partial upload would leave the previous binary at that stable URL. Write tokens stay on the merge job that actually publishes. Co-authored-by: Cursor --- .github/scripts/unstable_upload.sh | 28 ++++++++++++++++++++++++---- .github/workflows/release.yml | 4 ++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/scripts/unstable_upload.sh b/.github/scripts/unstable_upload.sh index fd0634cb..bf06bc20 100755 --- a/.github/scripts/unstable_upload.sh +++ b/.github/scripts/unstable_upload.sh @@ -173,13 +173,33 @@ else exit 1 fi +# Stable URLs keep the previous file if we skip an arch. Require the full set. +required=( + unpackerr.amd64.exe.zip + unpackerr.amd64.linux.gz + unpackerr.386.linux.gz + unpackerr.arm.linux.gz + unpackerr.arm64.linux.gz + unpackerr.amd64.freebsd.gz + unpackerr.i386.freebsd.gz + unpackerr.armhf.freebsd.gz + unpackerr.arm64.freebsd.gz +) +if [ "${CHANNEL:-}" != nightly ]; then + required+=(Unpackerr.dmg) +fi +missing=() +for name in "${required[@]}"; do + [ -f "${stage}/${name}" ] || missing+=("${name}") +done +if [ ${#missing[@]} -ne 0 ]; then + echo "unstable upload missing: ${missing[*]}" >&2 + ls -la "${stage}" >&2 || true + exit 1 +fi shopt -s nullglob staged=("${stage}"/*) shopt -u nullglob -if [ ${#staged[@]} -eq 0 ]; then - echo "no unstable artifacts staged from ${artifacts}" >&2 - exit 1 -fi upload() { local file=$1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b17961d..29b5f829 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,9 +9,9 @@ on: - unstable tags: - v* +# Jobs that publish (GitHub Release, GHCR) set write on the job. The rest only checkout. permissions: - contents: write - packages: write + contents: read # Darwin App/DMG notarization needs xcrun/codesign (macOS). Linux/Windows/FreeBSD # split on ubuntu; merge publishes Docker, GitHub, Homebrew, AUR, packagecloud. From c672e4de6a96b85296b98242fbedab532c268810 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 22:14:15 -0700 Subject: [PATCH 35/41] Pin rsrc, keep Windows OIDC off the other split jobs, and mark the FreeBSD conf as config. Do not interpolate the GPG key into the shell. fpm still ignores --config-files on freebsd, so the wrapper writes the pkg config array after packing. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.sh | 19 ++++++--- .github/workflows/README.md | 2 +- .github/workflows/release.yml | 77 ++++++++++++++++++++++++++-------- .goreleaser.yaml | 2 +- 4 files changed, 74 insertions(+), 26 deletions(-) diff --git a/.github/scripts/freebsd_txz.sh b/.github/scripts/freebsd_txz.sh index cddfb9fc..4dec0157 100755 --- a/.github/scripts/freebsd_txz.sh +++ b/.github/scripts/freebsd_txz.sh @@ -37,14 +37,18 @@ pkg_for() { esac } -# fpm already wrote the right ABI: do nothing. Otherwise unpack, jq .arch, retar. -set_abi() { - local pkg=$1 abi=$2 tmp list - [[ $(tar -xJOf "${pkg}" +COMPACT_MANIFEST | jq -r .arch) == "${abi}" ]] && return +CONF=/usr/local/etc/unpackerr/unpackerr.conf +# fpm --config-files is ignored by freebsd.rb (v0.15.2 had no config array). +# Unpack only when ABI or config still needs a patch. +fix_pkg() { + local pkg=$1 abi=$2 tmp list got cfg + got=$(tar -xJOf "${pkg}" +COMPACT_MANIFEST | jq -r .arch) + cfg=$(tar -xJOf "${pkg}" +MANIFEST | jq -r '.config[0] // empty') + [[ ${got} == "${abi}" && ${cfg} == "${CONF}" ]] && return tmp=$(mktemp -d) tar --transform 's|^/||' -xJf "${pkg}" -C "${tmp}" jq --arg a "${abi}" '.arch=$a' "${tmp}/+COMPACT_MANIFEST" > "${tmp}/c.json" - jq --arg a "${abi}" '.arch=$a' "${tmp}/+MANIFEST" > "${tmp}/m.json" + jq --arg a "${abi}" --arg c "${CONF}" '.arch=$a | .config=[$c]' "${tmp}/+MANIFEST" > "${tmp}/m.json" mv "${tmp}/c.json" "${tmp}/+COMPACT_MANIFEST" mv "${tmp}/m.json" "${tmp}/+MANIFEST" list=$(mktemp) @@ -75,6 +79,7 @@ stage() { extra=$(mktemp) echo '[]' > "${extra}" seen= + while IFS='|' read -r goarch goarm path; do [[ ${goarch} == arm && ${goarm} == 6 ]] && continue [[ ${seen} == *"|${goarch}|"* ]] && continue @@ -97,9 +102,10 @@ while IFS='|' read -r goarch goarm path; do --before-install "${REPO}/init/systemd/before-install.sh" \ --after-install "${REPO}/init/systemd/after-install.sh" \ --before-remove "${REPO}/init/systemd/before-remove.sh" \ + --config-files "${CONF}" \ -C "${tmp}" -p "${dest}" . rm -rf "${tmp}" - set_abi "${dest}" "FreeBSD:*:${cpu}" + fix_pkg "${dest}" "FreeBSD:*:${cpu}" echo "wrote ${dest##*/}" >&2 jq --arg name "${dest##*/}" --arg goarch "${goarch}" --arg goarm "${goarm}" \ '. + [{name:$name, path:$name, goos:"freebsd", goarch:$goarch, @@ -111,6 +117,7 @@ done < <(jq -r '.[] | select(.type=="Binary" and .goos=="freebsd") | [.goarch, ( for a in amd64 386 arm arm64; do [[ ${seen} == *"|${a}|"* ]] || { echo "freebsd txz missing goarch ${a}" >&2; exit 1; } done + jq --slurpfile extra "${extra}" '. + $extra[0]' "${artifacts}" > "${artifacts}.n" mv "${artifacts}.n" "${artifacts}" rm -f "${extra}" diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 47a99cd9..4ab1f465 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -34,7 +34,7 @@ So: 1. **channel** — compute `CHANNEL`, extra args, and `REVISION` (`git rev-list --count --all`). This is the only place the count is taken; later jobs pass `needs.channel.outputs.revision`. 2. **require secrets** — fail closed if any signing/upload/push secret needed for that channel is empty. Missing secrets used to skip Docker Hub, Windows Authenticode, or unstable.golift.io and still go green. -3. **Build: linux / windows / freebsd** (`split` on ubuntu) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. FreeBSD then runs `freebsd_txz.sh` (`fpm -t freebsd`). +3. **Build: linux / freebsd** (`split` on ubuntu) and **Build: windows** (own job: OIDC + signerd certs stay off the other legs) — `release --clean --split`. Filter with **`GGOOS`**, not `GOOS`. `GOOS` leaks into `go run` before-hooks (man pages, rsrc) and they then target the wrong OS. FreeBSD then runs `freebsd_txz.sh` (`fpm -t freebsd`). 4. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. 5. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29b5f829..c0a63184 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,15 +90,10 @@ jobs: strategy: fail-fast: false matrix: - goos: [linux, windows, freebsd] + goos: [linux, freebsd] runs-on: ubuntu-latest permissions: contents: read - # golift/codesign fetches a GitHub OIDC token (audience = CODESIGN_URL). - # Without this, signerd returns 401 unauthorized. - id-token: write - env: - CODESIGN_URL: ${{ secrets.CODESIGN_URL }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -121,9 +116,60 @@ jobs: with: gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - name: Create GPG_SIGNING_KEY file - run: echo '${{ secrets.GPG_SIGNING_KEY }}' > /tmp/key.gpg + env: + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + run: printf '%s\n' "${GPG_SIGNING_KEY}" > /tmp/key.gpg + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser-pro + version: "~> v2" + args: release --clean --split --timeout 60m ${{ needs.channel.outputs.extra }} + env: + # GGOOS filters targets without leaking GOOS into before-hook `go run`. + GGOOS: ${{ matrix.goos }} + CHANNEL: ${{ needs.channel.outputs.channel }} + REVISION: ${{ needs.channel.outputs.revision }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} + USER: github-actions + GPG_SIGNING_KEY: /tmp/key.gpg + GPG_SIGNING_KEY_ID: ${{ steps.import_gpg.outputs.keyid }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + - name: Build FreeBSD pkgng txz + if: matrix.goos == 'freebsd' + run: bash .github/scripts/freebsd_txz.sh dist/freebsd + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist-${{ matrix.goos }} + path: dist/${{ matrix.goos }} + include-hidden-files: true + compression-level: 0 + if-no-files-found: error + retention-days: 1 + + # OIDC + mTLS certs stay off linux/freebsd. signerd is Windows-only. + split-windows: + name: "Build: windows" + needs: [channel, require-secrets] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'schedule' && 'refs/heads/main' || github.ref }} + - run: git fetch --force --tags + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + - name: Create GPG_SIGNING_KEY file + env: + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + run: printf '%s\n' "${GPG_SIGNING_KEY}" > /tmp/key.gpg - name: Install golift codesign - if: matrix.goos == 'windows' env: GOBIN: ${{ runner.temp }}/codesign-bin run: | @@ -138,28 +184,22 @@ jobs: version: "~> v2" args: release --clean --split --timeout 60m ${{ needs.channel.outputs.extra }} env: - # GGOOS filters targets without leaking GOOS into before-hook `go run`. - GGOOS: ${{ matrix.goos }} + GGOOS: windows CHANNEL: ${{ needs.channel.outputs.channel }} REVISION: ${{ needs.channel.outputs.revision }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} USER: github-actions GPG_SIGNING_KEY: /tmp/key.gpg - GPG_SIGNING_KEY_ID: ${{ steps.import_gpg.outputs.keyid }} - GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} CODESIGN_URL: ${{ secrets.CODESIGN_URL }} CODESIGN_CLIENT_CERT: ${{ secrets.CODESIGN_CLIENT_CERT }} CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} CODESIGN_NAME: Unpackerr CODESIGN_WEBSITE: https://unpackerr.zip - - name: Build FreeBSD pkgng txz - if: matrix.goos == 'freebsd' - run: bash .github/scripts/freebsd_txz.sh dist/freebsd - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: dist-${{ matrix.goos }} - path: dist/${{ matrix.goos }} + name: dist-windows + path: dist/windows include-hidden-files: true compression-level: 0 if-no-files-found: error @@ -230,13 +270,14 @@ jobs: release: name: "release ${{ needs.channel.outputs.revision }}" - needs: [channel, require-secrets, split, split-darwin] + needs: [channel, require-secrets, split, split-windows, split-darwin] if: >- always() && !cancelled() && needs.channel.result == 'success' && needs.require-secrets.result == 'success' && needs.split.result == 'success' && + needs.split-windows.result == 'success' && (needs.split-darwin.result == 'success' || needs.split-darwin.result == 'skipped') runs-on: ubuntu-latest timeout-minutes: 90 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 975788e4..5d4b55c1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -24,7 +24,7 @@ before: - go run github.com/davidnewhall/md2roff@v0.0.1 --manual unpackerr --version "{{.Version}}" --date "{{.Date}}" README.md - gzip -9nf examples/MANUAL - mv -f examples/MANUAL.gz unpackerr.1.gz - - go run github.com/akavel/rsrc@latest -arch amd64 -ico init/windows/application.ico -manifest init/windows/manifest.xml -o rsrc_windows_amd64.syso + - go run github.com/akavel/rsrc@v0.10.2 -arch amd64 -ico init/windows/application.ico -manifest init/windows/manifest.xml -o rsrc_windows_amd64.syso builds: - id: unpackerr From e194791435a3847fefc734954ddd3d23331c24ba Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 22:29:54 -0700 Subject: [PATCH 36/41] Keep RPM armv7hl, and teach install.sh the 32-bit ARM name family. armhf is Debian; nFPM is right to use armv7hl inside the RPM. Also sign Arch packages, actually verify the app signature, and only publish unstable/release on push. Co-authored-by: Cursor --- .github/scripts/macos_staple.sh | 5 +++-- .github/workflows/README.md | 2 ++ .github/workflows/release.yml | 4 ++-- .goreleaser.yaml | 5 +++++ init/install.sh | 3 ++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/scripts/macos_staple.sh b/.github/scripts/macos_staple.sh index ce86ddc2..94038145 100644 --- a/.github/scripts/macos_staple.sh +++ b/.github/scripts/macos_staple.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Confirm Developer ID on Unpackerr.app, then staple the notarized DMG. # codesign -dv writes to stderr; grep -q in a pipefail pipeline is a false fail -# (tee gets SIGPIPE after the first match). +# (tee gets SIGPIPE after the first match). Dump to a file, then grep. set -euo pipefail root="${1:-dist/darwin}" @@ -30,7 +30,8 @@ if [ ! -d "${app}" ]; then exit 1 fi -codesign -dv --verbose=2 "${app}" >"${dump}" 2>&1 || true +codesign --verify --deep --strict "${app}" +codesign -dv --verbose=2 "${app}" >"${dump}" 2>&1 if ! grep -F "Developer ID Application" "${dump}" >/dev/null; then echo "Unpackerr.app is not Developer ID signed; macos_native skipped or failed" >&2 cat "${dump}" >&2 diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 4ab1f465..07e0b96d 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -12,6 +12,8 @@ Two workflows. `test-and-lint` (`codetests.yml`) runs tests and golangci-lint on | Push branch `unstable` | `unstable` | `--nightly` | Docker `:unstable`, packagecloud `golift/unstable`, [unstable.golift.io](https://unstable.golift.io/?dir=unpackerr) | | Cron `27 12 * * *` UTC, or `workflow_dispatch` on `main` | `nightly` | `--nightly` | Docker `:nightly` only | +`workflow_dispatch` on any other ref is refused. Tagged and unstable publishes are **push** only. + `unstable` is a **manual publish branch**. Recut it by pushing the commit you want: ```bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0a63184..025f8815 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,9 +42,9 @@ jobs: elif [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch && "${GITHUB_REF}" == refs/heads/main ]]; then channel=nightly extra=--nightly - elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + elif [[ "${GITHUB_EVENT_NAME}" == push && "${GITHUB_REF}" == refs/tags/v* ]]; then channel=release - elif [[ "${GITHUB_REF}" == refs/heads/unstable ]]; then + elif [[ "${GITHUB_EVENT_NAME}" == push && "${GITHUB_REF}" == refs/heads/unstable ]]; then channel=unstable extra=--nightly else diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5d4b55c1..28d7ed0d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -529,6 +529,11 @@ checksum: signs: - artifacts: checksum + # Detached .pkg.tar.zst.sig like the old Makefile. Deb/rpm already have + # embedded signatures; pacman wants a sidecar. + - id: archlinux + artifacts: package + if: '{{ hasSuffix .Name "pkg.tar.zst" }}' release: disable: "{{ .IsNightly }}" diff --git a/init/install.sh b/init/install.sh index 657ba507..688ae6be 100644 --- a/init/install.sh +++ b/init/install.sh @@ -48,7 +48,8 @@ elif [[ $ARCH = *386* ]] || [[ $ARCH = *686* ]]; then elif [[ $ARCH = *arm64* ]] || [[ $ARCH = *armv8* ]] || [[ $ARCH = *aarch64* ]]; then ARCH="arm64" elif [[ $ARCH = *armv6* ]] || [[ $ARCH = *armv7* ]]; then - ARCH="armhf" + # Deb/old rpm/freebsd: armhf. RPM: armv7hl. Arch: armv7h. Old zst: arm7hf. + ARCH="armv7hl|armv7h|armhf|arm7hf|armv7" else echo "${P} [ERROR] Unknown Architecture: ${ARCH}" echo "${P} $(uname -a) | $(head -n 1 /etc/issue): ${ID}" From 4e7f763a896189864e346a6d9cb5d5ad7ef9acd8 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 22:30:05 -0700 Subject: [PATCH 37/41] Match Arch packages by path as well as name when signing. Co-authored-by: Cursor --- .goreleaser.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 28d7ed0d..3bf4525f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -533,7 +533,7 @@ signs: # embedded signatures; pacman wants a sidecar. - id: archlinux artifacts: package - if: '{{ hasSuffix .Name "pkg.tar.zst" }}' + if: '{{ or (hasSuffix .Name "pkg.tar.zst") (hasSuffix .Path "pkg.tar.zst") }}' release: disable: "{{ .IsNightly }}" From d7eabb866da0384acfccf6272af8804060e55260 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 22:59:27 -0700 Subject: [PATCH 38/41] Ship the notarized Homebrew cask and record FreeBSD txz artifacts for merge. The cask was installing the unsigned darwin tarball; Gatekeeper rejects that. Merge was also dropping the pkgng archives because artifacts.json lacked internal_type and a repo-relative path. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.sh | 11 ++++++++--- .github/workflows/README.md | 2 +- .goreleaser.yaml | 30 ++++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.github/scripts/freebsd_txz.sh b/.github/scripts/freebsd_txz.sh index 4dec0157..61f98a74 100755 --- a/.github/scripts/freebsd_txz.sh +++ b/.github/scripts/freebsd_txz.sh @@ -107,10 +107,15 @@ while IFS='|' read -r goarch goarm path; do rm -rf "${tmp}" fix_pkg "${dest}" "FreeBSD:*:${cpu}" echo "wrote ${dest##*/}" >&2 - jq --arg name "${dest##*/}" --arg goarch "${goarch}" --arg goarm "${goarm}" \ - '. + [{name:$name, path:$name, goos:"freebsd", goarch:$goarch, + # Merge unmarshals Type from internal_type (int), not type (string). + # UploadableArchive = 1. Path must be repo-relative so checksum/GitHub can open it. + relpath=${dest#"${REPO}/"} + jq --arg name "${dest##*/}" --arg path "${relpath}" --arg goarch "${goarch}" \ + --arg goarm "${goarm}" --argjson itype 1 \ + '. + [{name:$name, path:$path, goos:"freebsd", goarch:$goarch, goarm: (if $goarm == "" then null else $goarm end), - type:"Archive", extra:{ID:"freebsd-pkg", Format:"txz"}}]' \ + type:"Archive", internal_type:$itype, + extra:{ID:"freebsd-pkg", Format:"txz", Ext:".txz"}}]' \ "${extra}" > "${extra}.n" && mv "${extra}.n" "${extra}" done < <(jq -r '.[] | select(.type=="Binary" and .goos=="freebsd") | [.goarch, (.goarm // ""), .path] | join("|")' "${artifacts}") diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 07e0b96d..93db8e29 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -52,7 +52,7 @@ So: - **Docker** — always `ghcr.io/unpackerr/unpackerr` and Hub `docker.io/golift/unpackerr` (`DOCKERHUB_PUBLISH=1`). Empty `DOCKERHUB_PASSWORD` fails the merge job. Platforms: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. - **GitHub Release** — tagged `v*` only (`release.disable: "{{ .IsNightly }}"`). Windows assets are `unpackerr.amd64.exe.zip`. FreeBSD assets are pkgng `unpackerr-.{amd64,i386,armhf,arm64}.txz`. -- **Homebrew** — `homebrew_casks` → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly`. +- **Homebrew** — notarized `Unpackerr.app` from the `unpackerr-dmg` DMG → `golift/homebrew-mugs` `Casks/`. Skip on `--nightly` (and that channel has no Darwin job). - **AUR** — `aur_sources` over SSH. Skip on `--nightly`. - **packagecloud** — `golift/pkgs` vs `golift/unstable`. Skip when `CHANNEL=nightly`. - **unstable.golift.io** — only `CHANNEL=unstable`. Auto-update URLs are **stable names**; version lives in a sibling `.txt` (plain `0.15.3-1056`, not JSON). Payload is a gzipped/zipped **binary**, not the versioned `tar.gz`. Script: `.github/scripts/unstable_upload.sh` (reads `dist/$GOOS/artifacts.json` after split/merge). Upload overwrites by name. Empty `UNSTABLE_UPLOAD_KEY` fails in GitHub Actions. diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3bf4525f..329dd775 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -168,6 +168,10 @@ app_bundles: extra_files: - src: init/macos/Unpackerr.app/Contents/Resources/terminal-notifier.app dst: Contents/Resources/terminal-notifier.app + - src: examples/unpackerr.conf.example + dst: Contents/Resources/unpackerr.conf.example + - src: unpackerr.1.gz + dst: Contents/Resources/unpackerr.1.gz templated_extra_files: - src: init/macos/Info.plist.tmpl dst: Contents/Info.plist @@ -187,7 +191,7 @@ archives: - id: default ids: [unpackerr, unpackerr-darwin] formats: [tar.gz] - # Flat so the Homebrew cask can find binary/conf/manpage without a rename. + # Flat tarball for GitHub. Homebrew installs the notarized DMG, not this archive. wrap_in_directory: false files: - LICENSE @@ -445,11 +449,12 @@ aur_sources: homebrew_casks: - name: unpackerr - ids: [default] + # Notarized DMG (macos_native signs unpackerr-app / unpackerr-dmg). The + # unsigned darwin tar.gz (ids: default) is GitHub-only; Gatekeeper rejects it. + ids: [unpackerr-dmg] + app: Unpackerr.app binaries: - - unpackerr - manpages: - - unpackerr.1.gz + - '#{appdir}/Unpackerr.app/Contents/MacOS/unpackerr' skip_upload: "{{ .IsNightly }}" repository: owner: golift @@ -472,16 +477,21 @@ homebrew_casks: hooks: post: install: | + staged_app = "#{staged_path}/Unpackerr.app" + conf_src = "#{staged_app}/Contents/Resources/unpackerr.conf.example" conf_dir = HOMEBREW_PREFIX/"etc/unpackerr" conf_dir.mkpath - FileUtils.cp "#{staged_path}/unpackerr.conf.example", conf_dir/"unpackerr.conf.example" + FileUtils.cp conf_src, conf_dir/"unpackerr.conf.example" had_conf = (conf_dir/"unpackerr.conf").exist? - FileUtils.cp "#{staged_path}/unpackerr.conf.example", conf_dir/"unpackerr.conf" unless had_conf + FileUtils.cp conf_src, conf_dir/"unpackerr.conf" unless had_conf + man1 = HOMEBREW_PREFIX/"share/man/man1" + man1.mkpath + FileUtils.cp "#{staged_app}/Contents/Resources/unpackerr.1.gz", man1/"unpackerr.1.gz" (HOMEBREW_PREFIX/"var/log").mkpath label = "io.golift.unpackerr" agent = Pathname.new(Dir.home)/"Library/LaunchAgents/#{label}.plist" agent.dirname.mkpath - binary = HOMEBREW_PREFIX/"bin/unpackerr" + binary = "#{appdir}/Unpackerr.app/Contents/MacOS/unpackerr" conf = conf_dir/"unpackerr.conf" log = HOMEBREW_PREFIX/"var/log/unpackerr.log" agent.write <<~XML @@ -514,6 +524,8 @@ homebrew_casks: agent = Pathname.new(Dir.home)/"Library/LaunchAgents/#{label}.plist" system "/bin/launchctl", "bootout", "gui/#{Process.uid}/#{label}" agent.delete if agent.exist? + man = HOMEBREW_PREFIX/"share/man/man1/unpackerr.1.gz" + man.delete if man.exist? caveats: | First install: edit #{HOMEBREW_PREFIX}/etc/unpackerr/unpackerr.conf then: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.golift.unpackerr.plist @@ -521,6 +533,8 @@ homebrew_casks: Restart: launchctl kickstart -k gui/$(id -u)/io.golift.unpackerr Stop: launchctl bootout gui/$(id -u)/io.golift.unpackerr (brew services does not manage casks.) + Menu bar: open Unpackerr.app (do not also run the LaunchAgent). + The LaunchAgent runs the signed CLI inside the app (no menu bar). The manual explains the config file options: man unpackerr checksum: From e3564d8942dce951c56298c8744f3c70f0bac8ac Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 23:14:17 -0700 Subject: [PATCH 39/41] Restore tagged package revisions, drop unused merge OIDC, and create the AUR log directory. Deb/rpm/txz on tags are again 0.15.3-REVISION (and 0.15.3_REVISION for pkgng). The merge job only needed GITHUB_TOKEN. AUR now ships tmpfiles.d so User=unpackerr can write /var/log/unpackerr. Co-authored-by: Cursor --- .github/scripts/freebsd_txz.sh | 6 ++++++ .github/workflows/README.md | 2 +- .github/workflows/release.yml | 17 +++++++++++++++-- .goreleaser.yaml | 10 +++++++++- init/systemd/unpackerr.tmpfiles | 2 ++ 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 init/systemd/unpackerr.tmpfiles diff --git a/.github/scripts/freebsd_txz.sh b/.github/scripts/freebsd_txz.sh index 61f98a74..7a321d3d 100755 --- a/.github/scripts/freebsd_txz.sh +++ b/.github/scripts/freebsd_txz.sh @@ -23,6 +23,12 @@ if [[ -z ${VERSION} || ${VERSION} == unknown || ${VERSION} == unstable ]]; then echo "refusing version '${VERSION}'" >&2 exit 1 fi +# Tagged pkgng version is 0.15.3_REVISION (same as old fpm). --nightly already +# puts REVISION in metadata.json (.Version). +if [[ ${VERSION} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + [[ -n ${REVISION:-} ]] || { echo "REVISION required for tagged freebsd pkg" >&2; exit 1; } + VERSION="${VERSION}_${REVISION}" +fi # goarch -> asset suffix / fpm -a / pkg ABI CPU (uname -p). # fpm only knows amd64, i386, aarch64(→arm64). Unknown -a becomes getconf LONG_BIT diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 93db8e29..616d7f10 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -40,7 +40,7 @@ So: 4. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. 5. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. -`REVISION` must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). On `--nightly`, `nightly.version_template` already bakes it into `{{ .Version }}` (example `0.15.3-1056`), so man-page hooks use `{{ .Version }}` only — do not append `REVISION` again. The env var is still required: that template *creates* `.Version`, and tagged builds keep `.Version` as the semver while ldflags `Revision` / Darwin `CFBundleVersion` still need the count. nFPM `release` is not templated; uniqueness is the version string. Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata` (that produced `0.15.3~1056+git` on Debian). +`REVISION` must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). On `--nightly`, `nightly.version_template` already bakes it into `{{ .Version }}` (example `0.15.3-1056`), so man-page hooks use `{{ .Version }}` only — do not append `REVISION` again. The env var is still required: that template *creates* `.Version`, and tagged builds keep `.Version` as the semver while ldflags `Revision` / Darwin `CFBundleVersion` still need the count. nFPM `release` is `${PKG_RELEASE}` (GoReleaser does not template that field): tags get `0.15.3-REVISION`, `--nightly` keeps release `1` because uniqueness is already in `.Version`. Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata` (that produced `0.15.3~1056+git` on Debian). Tagged FreeBSD packages are `unpackerr-0.15.3_REVISION.amd64.txz`. ## Darwin signing diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 025f8815..cf2dcf82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: channel: ${{ steps.channel.outputs.channel }} extra: ${{ steps.channel.outputs.extra }} revision: ${{ steps.revision.outputs.revision }} + pkg_release: ${{ steps.channel.outputs.pkg_release }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -33,6 +34,8 @@ jobs: run: echo "revision=$(git rev-list --count --all || echo 0)" >> "${GITHUB_OUTPUT}" - name: Choose release channel id: channel + env: + REVISION: ${{ steps.revision.outputs.revision }} run: | set -euo pipefail extra= @@ -51,9 +54,17 @@ jobs: echo "refusing to release from ${GITHUB_REF} (${GITHUB_EVENT_NAME})" >&2 exit 1 fi + # Tagged debs/rpms are 0.15.3-REVISION (nFPM release). Nightly already + # bakes REVISION into .Version, so keep release at 1. + if [[ ${channel} == release ]]; then + pkg_release=${REVISION} + else + pkg_release=1 + fi echo "channel=${channel}" >> "${GITHUB_OUTPUT}" echo "extra=${extra}" >> "${GITHUB_OUTPUT}" - echo "CHANNEL=${channel} extra=${extra}" + echo "pkg_release=${pkg_release}" >> "${GITHUB_OUTPUT}" + echo "CHANNEL=${channel} extra=${extra} pkg_release=${pkg_release}" require-secrets: name: require secrets @@ -129,6 +140,7 @@ jobs: GGOOS: ${{ matrix.goos }} CHANNEL: ${{ needs.channel.outputs.channel }} REVISION: ${{ needs.channel.outputs.revision }} + PKG_RELEASE: ${{ needs.channel.outputs.pkg_release }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} USER: github-actions @@ -137,6 +149,8 @@ jobs: GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} - name: Build FreeBSD pkgng txz if: matrix.goos == 'freebsd' + env: + REVISION: ${{ needs.channel.outputs.revision }} run: bash .github/scripts/freebsd_txz.sh dist/freebsd - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -284,7 +298,6 @@ jobs: permissions: contents: write packages: write - id-token: write env: CHANNEL: ${{ needs.channel.outputs.channel }} steps: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 329dd775..53911453 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,7 +13,7 @@ nightly: # though {{ .Version }} already includes it on --nightly: this template is # what *creates* that version. Tagged builds keep .Version as the semver # tag; ldflags Revision and Darwin CFBundleVersion still need the count. - # nfpms.release is not templated. Do not put CHANNEL in the package version. + # nFPM release is ${PKG_RELEASE}: REVISION on tags, 1 on --nightly. version_template: "{{ incpatch .Version }}-{{ .Env.REVISION }}" before: @@ -231,6 +231,9 @@ nfpms: maintainer: David Newhall II description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. license: MIT + # GoReleaser does not template this field; nFPM expands ${PKG_RELEASE}. + # Tags: 0.15.3-REVISION. --nightly: .Version already has REVISION, release 1. + release: "${PKG_RELEASE}" formats: - deb - rpm @@ -295,6 +298,10 @@ nfpms: type: config file_info: mode: 0644 + - src: init/systemd/unpackerr.tmpfiles + dst: /usr/lib/tmpfiles.d/unpackerr.conf + file_info: + mode: 0644 dockers_v2: - id: unpackerr-release @@ -446,6 +453,7 @@ aur_sources: install -D -m 644 init/systemd/unpackerr.service "${pkgdir}/usr/lib/systemd/system/unpackerr.service" echo 'u unpackerr - "unpackerr daemon"' > unpackerr.sysusers install -D -m 644 unpackerr.sysusers "${pkgdir}/usr/lib/sysusers.d/unpackerr.conf" + install -D -m 644 init/systemd/unpackerr.tmpfiles "${pkgdir}/usr/lib/tmpfiles.d/unpackerr.conf" homebrew_casks: - name: unpackerr diff --git a/init/systemd/unpackerr.tmpfiles b/init/systemd/unpackerr.tmpfiles new file mode 100644 index 00000000..b57e7c1a --- /dev/null +++ b/init/systemd/unpackerr.tmpfiles @@ -0,0 +1,2 @@ +# systemd-tmpfiles.d — log dir for User=unpackerr (see unpackerr.service). +d /var/log/unpackerr 0755 unpackerr unpackerr - From 390c0b622f5dbde58100335c1c7c8dcbf0c05dc8 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Thu, 27 Aug 2026 23:55:45 -0700 Subject: [PATCH 40/41] Use conventional Linux package names and stop shipping ${PKG_RELEASE} in the version. GoReleaser copies nfpms.release verbatim, so Packagecloud rejected debs whose control Version was 0.15.3~1081-${PKG_RELEASE}. Name files like v0.15.2 (i386.deb, not linux_386.deb) and only insert a real revision on tagged builds. Co-authored-by: Cursor --- .github/scripts/nfpm_release.sh | 55 +++++++++++++++++++++++++++++++++ .github/workflows/README.md | 6 ++-- .github/workflows/release.yml | 14 ++++++--- .goreleaser.yaml | 16 +++++++--- init/install.sh | 6 ++-- 5 files changed, 84 insertions(+), 13 deletions(-) create mode 100755 .github/scripts/nfpm_release.sh diff --git a/.github/scripts/nfpm_release.sh b/.github/scripts/nfpm_release.sh new file mode 100755 index 00000000..0d299bf6 --- /dev/null +++ b/.github/scripts/nfpm_release.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# GoReleaser copies nfpms.release into nFPM with no templates and no ${ENV} +# expansion (nFPM only expands env when loading its own YAML CLI config). +# The 1081 unstable debs shipped Version: 0.15.3~1081-${PKG_RELEASE} and +# Packagecloud refused to parse them. +# +# Tags: insert `release: REVISION` so Debian/RPM are 0.15.3-REVISION like v0.15.2. +# --nightly / local snapshots: no-op. .Version is already unique; setting +# release would double it (0.15.3~REVISION-REVISION). +set -euo pipefail + +file=${1:-.goreleaser.yaml} +marker='nfpm-release: Linux split inserts `release: REVISION` on tags only.' +rel=${PKG_RELEASE-} + +if [[ ! -f ${file} ]]; then + echo "missing ${file}" >&2 + exit 1 +fi +if ! grep -qF "${marker}" "${file}"; then + echo "${file} has no nFPM release marker" >&2 + exit 1 +fi + +if [[ -z ${rel} ]]; then + echo "nFPM release omitted (--nightly version already unique)" + exit 0 +fi + +if [[ ! ${rel} =~ ^[0-9]+$ ]]; then + echo "refusing nFPM release ${rel}" >&2 + exit 1 +fi + +python3 - "${file}" "${marker}" "${rel}" <<'PY' +import pathlib, sys +path = pathlib.Path(sys.argv[1]) +marker, rel = sys.argv[2], sys.argv[3] +text = path.read_text() +if f"\n release: {rel}\n" in text and marker in text: + print(f"nFPM release={rel} (already set)") + raise SystemExit(0) +needle = None +for line in text.splitlines(True): + if marker in line: + needle = line + break +if needle is None: + raise SystemExit("nFPM release marker line missing") +insert = needle + f" release: {rel}\n" +if text.count(needle) != 1: + raise SystemExit("nFPM release marker is not unique") +path.write_text(text.replace(needle, insert, 1)) +print(f"nFPM release={rel}") +PY diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 616d7f10..f3a9d165 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -40,7 +40,9 @@ So: 4. **Build: darwin** (`split-darwin` on macos-latest, skipped on nightly) — import Developer ID + App Store Connect key, same `--split` with `GGOOS=darwin`, staple the DMG. 5. **release N** — download `dist-*` artifacts, import GPG (checksum signatures are created at merge, not split), `continue --merge`. Display name is `release` plus that `REVISION`. This is the only job that pushes Docker / GitHub / brew / AUR / packagecloud / unstable.golift.io. -`REVISION` must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). On `--nightly`, `nightly.version_template` already bakes it into `{{ .Version }}` (example `0.15.3-1056`), so man-page hooks use `{{ .Version }}` only — do not append `REVISION` again. The env var is still required: that template *creates* `.Version`, and tagged builds keep `.Version` as the semver while ldflags `Revision` / Darwin `CFBundleVersion` still need the count. nFPM `release` is `${PKG_RELEASE}` (GoReleaser does not template that field): tags get `0.15.3-REVISION`, `--nightly` keeps release `1` because uniqueness is already in `.Version`. Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata` (that produced `0.15.3~1056+git` on Debian). Tagged FreeBSD packages are `unpackerr-0.15.3_REVISION.amd64.txz`. +`REVISION` must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). On `--nightly`, `nightly.version_template` already bakes it into `{{ .Version }}` (example `0.15.3-1056`), so man-page hooks use `{{ .Version }}` only — do not append `REVISION` again. The env var is still required: that template *creates* `.Version`, and tagged builds keep `.Version` as the semver while ldflags `Revision` / Darwin `CFBundleVersion` still need the count. + +nFPM `release` is **not** templated and **not** `${ENV}`-expanded (GoReleaser constructs `nfpm.Info` in-process; nFPM only expands env in its CLI YAML). The Linux split runs `.github/scripts/nfpm_release.sh`: tags insert `release: REVISION` so debs/rpms are `0.15.3-REVISION` like v0.15.2; `--nightly` leaves the key unset so uniqueness stays in `.Version`. A literal `${PKG_RELEASE}` became Debian `Version: 0.15.3~1081-${PKG_RELEASE}` and Packagecloud refused the package. nFPM `file_name_template` is `{{ replace "~" "-" .ConventionalFileName }}` (`unpackerr_0.15.2-960_i386.deb`, not `…_linux_386.deb`). Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata` (that produced `0.15.3~1056+git` on Debian). Tagged FreeBSD packages are `unpackerr-0.15.3_REVISION.amd64.txz`. ## Darwin signing @@ -64,7 +66,7 @@ So: | `unpackerr.{amd64,386,arm,arm64}.linux.gz` | gzipped binary | | `unpackerr.{amd64,i386,armhf,arm64}.freebsd.gz` | gzipped binary | -Linux nFPM arches are amd64, arm64, i386, armv7 (one `armhf`). Darwin min macOS 13. Windows is `-H=windowsgui`. Empty `CODESIGN_URL` fails in GitHub Actions (local snapshots still skip). +Linux nFPM names are conventional (`unpackerr_0.15.3-1081_amd64.deb`, `unpackerr-0.15.3-1081.x86_64.rpm`). Arches: amd64, arm64, i386, armv7 (one `armhf` / RPM `armv7hl`). Darwin min macOS 13. Windows is `-H=windowsgui`. Empty `CODESIGN_URL` fails in GitHub Actions (local snapshots still skip). ## Secrets diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf2dcf82..a5864a2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,12 +54,14 @@ jobs: echo "refusing to release from ${GITHUB_REF} (${GITHUB_EVENT_NAME})" >&2 exit 1 fi - # Tagged debs/rpms are 0.15.3-REVISION (nFPM release). Nightly already - # bakes REVISION into .Version, so keep release at 1. + # Tagged debs/rpms are 0.15.3-REVISION (nFPM release, baked into yaml + # because GoReleaser copies that field verbatim). --nightly already + # puts REVISION in .Version; leave pkg_release empty so it is not + # doubled as 0.15.3~REVISION-REVISION (or left as ${PKG_RELEASE}). if [[ ${channel} == release ]]; then pkg_release=${REVISION} else - pkg_release=1 + pkg_release= fi echo "channel=${channel}" >> "${GITHUB_OUTPUT}" echo "extra=${extra}" >> "${GITHUB_OUTPUT}" @@ -130,6 +132,11 @@ jobs: env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} run: printf '%s\n' "${GPG_SIGNING_KEY}" > /tmp/key.gpg + - name: Bake nFPM release + if: matrix.goos == 'linux' + env: + PKG_RELEASE: ${{ needs.channel.outputs.pkg_release }} + run: bash .github/scripts/nfpm_release.sh .goreleaser.yaml - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro @@ -140,7 +147,6 @@ jobs: GGOOS: ${{ matrix.goos }} CHANNEL: ${{ needs.channel.outputs.channel }} REVISION: ${{ needs.channel.outputs.revision }} - PKG_RELEASE: ${{ needs.channel.outputs.pkg_release }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} USER: github-actions diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 53911453..32c7e4a0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,7 +13,7 @@ nightly: # though {{ .Version }} already includes it on --nightly: this template is # what *creates* that version. Tagged builds keep .Version as the semver # tag; ldflags Revision and Darwin CFBundleVersion still need the count. - # nFPM release is ${PKG_RELEASE}: REVISION on tags, 1 on --nightly. + # nFPM release is baked in the Linux split (GoReleaser does not template it). version_template: "{{ incpatch .Version }}-{{ .Env.REVISION }}" before: @@ -226,14 +226,18 @@ archives: nfpms: - id: unpackerr-packages ids: [unpackerr] + # Default is unpackerr_VERSION_linux_386.deb. Conventional names match + # v0.15.2 (unpackerr_0.15.2-960_i386.deb). nFPM uses ~ for semver + # prerelease; GitHub replaces tilde with a dot, so swap it for a hyphen. + file_name_template: '{{ replace "~" "-" .ConventionalFileName }}' vendor: Go Lift homepage: https://unpackerr.zip maintainer: David Newhall II description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. license: MIT - # GoReleaser does not template this field; nFPM expands ${PKG_RELEASE}. - # Tags: 0.15.3-REVISION. --nightly: .Version already has REVISION, release 1. - release: "${PKG_RELEASE}" + # GoReleaser copies nfpms.release into nFPM with no templates and no ${ENV} + # expand. --nightly leaves it unset: .Version is already 0.15.3-REVISION. + # nfpm-release: Linux split inserts `release: REVISION` on tags only. formats: - deb - rpm @@ -555,7 +559,9 @@ signs: # embedded signatures; pacman wants a sidecar. - id: archlinux artifacts: package - if: '{{ or (hasSuffix .Name "pkg.tar.zst") (hasSuffix .Path "pkg.tar.zst") }}' + # Merge artifact filters do not load sprig (hasSuffix is undefined there). + # eq is a Go template builtin; Extra.Format is archlinux/deb/rpm. + if: '{{ eq .Format "archlinux" }}' release: disable: "{{ .IsNightly }}" diff --git a/init/install.sh b/init/install.sh index 688ae6be..359a1e32 100644 --- a/init/install.sh +++ b/init/install.sh @@ -44,9 +44,11 @@ fi if [[ $ARCH = x86_64 ]] || [[ $ARCH = amd64 ]]; then ARCH="x86_64|amd64" elif [[ $ARCH = *386* ]] || [[ $ARCH = *686* ]]; then - ARCH="i386" + # Deb: i386. Arch pkg.tar.zst: i686. + ARCH="i386|i686" elif [[ $ARCH = *arm64* ]] || [[ $ARCH = *armv8* ]] || [[ $ARCH = *aarch64* ]]; then - ARCH="arm64" + # Deb: arm64. RPM/Arch: aarch64. + ARCH="arm64|aarch64" elif [[ $ARCH = *armv6* ]] || [[ $ARCH = *armv7* ]]; then # Deb/old rpm/freebsd: armhf. RPM: armv7hl. Arch: armv7h. Old zst: arm7hf. ARCH="armv7hl|armv7h|armhf|arm7hf|armv7" From 08ae075d128b8404b328d949f903ea3562dc4fa6 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Fri, 28 Aug 2026 00:00:07 -0700 Subject: [PATCH 41/41] Drop the Python nFPM helper; insert tagged release with awk in the workflow. Co-authored-by: Cursor --- .github/scripts/nfpm_release.sh | 55 --------------------------------- .github/workflows/README.md | 2 +- .github/workflows/release.yml | 28 +++++++---------- .goreleaser.yaml | 7 ++--- 4 files changed, 16 insertions(+), 76 deletions(-) delete mode 100755 .github/scripts/nfpm_release.sh diff --git a/.github/scripts/nfpm_release.sh b/.github/scripts/nfpm_release.sh deleted file mode 100755 index 0d299bf6..00000000 --- a/.github/scripts/nfpm_release.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# GoReleaser copies nfpms.release into nFPM with no templates and no ${ENV} -# expansion (nFPM only expands env when loading its own YAML CLI config). -# The 1081 unstable debs shipped Version: 0.15.3~1081-${PKG_RELEASE} and -# Packagecloud refused to parse them. -# -# Tags: insert `release: REVISION` so Debian/RPM are 0.15.3-REVISION like v0.15.2. -# --nightly / local snapshots: no-op. .Version is already unique; setting -# release would double it (0.15.3~REVISION-REVISION). -set -euo pipefail - -file=${1:-.goreleaser.yaml} -marker='nfpm-release: Linux split inserts `release: REVISION` on tags only.' -rel=${PKG_RELEASE-} - -if [[ ! -f ${file} ]]; then - echo "missing ${file}" >&2 - exit 1 -fi -if ! grep -qF "${marker}" "${file}"; then - echo "${file} has no nFPM release marker" >&2 - exit 1 -fi - -if [[ -z ${rel} ]]; then - echo "nFPM release omitted (--nightly version already unique)" - exit 0 -fi - -if [[ ! ${rel} =~ ^[0-9]+$ ]]; then - echo "refusing nFPM release ${rel}" >&2 - exit 1 -fi - -python3 - "${file}" "${marker}" "${rel}" <<'PY' -import pathlib, sys -path = pathlib.Path(sys.argv[1]) -marker, rel = sys.argv[2], sys.argv[3] -text = path.read_text() -if f"\n release: {rel}\n" in text and marker in text: - print(f"nFPM release={rel} (already set)") - raise SystemExit(0) -needle = None -for line in text.splitlines(True): - if marker in line: - needle = line - break -if needle is None: - raise SystemExit("nFPM release marker line missing") -insert = needle + f" release: {rel}\n" -if text.count(needle) != 1: - raise SystemExit("nFPM release marker is not unique") -path.write_text(text.replace(needle, insert, 1)) -print(f"nFPM release={rel}") -PY diff --git a/.github/workflows/README.md b/.github/workflows/README.md index f3a9d165..780f4f2e 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -42,7 +42,7 @@ So: `REVISION` must be in the goreleaser-action `env:` map (Actions does not automatically forward `GITHUB_ENV` into a later step’s `env:` block). On `--nightly`, `nightly.version_template` already bakes it into `{{ .Version }}` (example `0.15.3-1056`), so man-page hooks use `{{ .Version }}` only — do not append `REVISION` again. The env var is still required: that template *creates* `.Version`, and tagged builds keep `.Version` as the semver while ldflags `Revision` / Darwin `CFBundleVersion` still need the count. -nFPM `release` is **not** templated and **not** `${ENV}`-expanded (GoReleaser constructs `nfpm.Info` in-process; nFPM only expands env in its CLI YAML). The Linux split runs `.github/scripts/nfpm_release.sh`: tags insert `release: REVISION` so debs/rpms are `0.15.3-REVISION` like v0.15.2; `--nightly` leaves the key unset so uniqueness stays in `.Version`. A literal `${PKG_RELEASE}` became Debian `Version: 0.15.3~1081-${PKG_RELEASE}` and Packagecloud refused the package. nFPM `file_name_template` is `{{ replace "~" "-" .ConventionalFileName }}` (`unpackerr_0.15.2-960_i386.deb`, not `…_linux_386.deb`). Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata` (that produced `0.15.3~1056+git` on Debian). Tagged FreeBSD packages are `unpackerr-0.15.3_REVISION.amd64.txz`. +nFPM `release` is not templated (GoReleaser copies it verbatim). `${PKG_RELEASE}` stayed literal and Packagecloud rejected `Version: 0.15.3~1081-${PKG_RELEASE}`. The tagged linux split inserts `release: REVISION` after the `nfpm-release:` marker; `--nightly` leaves it unset. Package files use `{{ replace "~" "-" .ConventionalFileName }}` (`unpackerr_0.15.2-960_i386.deb`, not `_linux_386`). Do not put `CHANNEL` in the package version. Do not set nFPM `version_metadata`. Tagged FreeBSD packages are `unpackerr-0.15.3_REVISION.amd64.txz`. ## Darwin signing diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5864a2e..f1241a55 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,6 @@ jobs: channel: ${{ steps.channel.outputs.channel }} extra: ${{ steps.channel.outputs.extra }} revision: ${{ steps.revision.outputs.revision }} - pkg_release: ${{ steps.channel.outputs.pkg_release }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -54,19 +53,9 @@ jobs: echo "refusing to release from ${GITHUB_REF} (${GITHUB_EVENT_NAME})" >&2 exit 1 fi - # Tagged debs/rpms are 0.15.3-REVISION (nFPM release, baked into yaml - # because GoReleaser copies that field verbatim). --nightly already - # puts REVISION in .Version; leave pkg_release empty so it is not - # doubled as 0.15.3~REVISION-REVISION (or left as ${PKG_RELEASE}). - if [[ ${channel} == release ]]; then - pkg_release=${REVISION} - else - pkg_release= - fi echo "channel=${channel}" >> "${GITHUB_OUTPUT}" echo "extra=${extra}" >> "${GITHUB_OUTPUT}" - echo "pkg_release=${pkg_release}" >> "${GITHUB_OUTPUT}" - echo "CHANNEL=${channel} extra=${extra} pkg_release=${pkg_release}" + echo "CHANNEL=${channel} extra=${extra}" require-secrets: name: require secrets @@ -132,11 +121,18 @@ jobs: env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} run: printf '%s\n' "${GPG_SIGNING_KEY}" > /tmp/key.gpg - - name: Bake nFPM release - if: matrix.goos == 'linux' + - name: Set tagged nFPM release + if: matrix.goos == 'linux' && needs.channel.outputs.channel == 'release' env: - PKG_RELEASE: ${{ needs.channel.outputs.pkg_release }} - run: bash .github/scripts/nfpm_release.sh .goreleaser.yaml + REVISION: ${{ needs.channel.outputs.revision }} + run: | + set -euo pipefail + [[ ${REVISION} =~ ^[0-9]+$ ]] + awk -v r="${REVISION}" ' + { print } + /nfpm-release:/ { print " release: " r } + ' .goreleaser.yaml > .goreleaser.yaml.nfpm + mv .goreleaser.yaml.nfpm .goreleaser.yaml - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser-pro diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 32c7e4a0..7e2316ca 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,7 +13,6 @@ nightly: # though {{ .Version }} already includes it on --nightly: this template is # what *creates* that version. Tagged builds keep .Version as the semver # tag; ldflags Revision and Darwin CFBundleVersion still need the count. - # nFPM release is baked in the Linux split (GoReleaser does not template it). version_template: "{{ incpatch .Version }}-{{ .Env.REVISION }}" before: @@ -235,9 +234,9 @@ nfpms: maintainer: David Newhall II description: Extracts downloads so Radarr, Sonarr, Lidarr or Readarr may import them. license: MIT - # GoReleaser copies nfpms.release into nFPM with no templates and no ${ENV} - # expand. --nightly leaves it unset: .Version is already 0.15.3-REVISION. - # nfpm-release: Linux split inserts `release: REVISION` on tags only. + # GoReleaser does not template nfpms.release. Tagged linux split inserts + # `release: REVISION` after this marker. --nightly leaves it unset. + # nfpm-release: formats: - deb - rpm