name: Release on: push: tags: - 'v*' workflow_dispatch: permissions: contents: write jobs: checks: name: Release quality gates runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@v7 - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ pkg-config \ libfuse3-dev \ libgtk-4-dev \ libadwaita-1-dev \ libwebkitgtk-6.0-dev \ libsecret-1-dev \ libdbus-1-dev - name: Set up Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: '1.96' components: rustfmt, clippy cache: false # A tag can read the caches of main but its own caches are useless to the next # tag, so this only restores what CI saved on main. - name: Cache Cargo target uses: swatinem/rust-cache@v2 with: shared-key: debug save-if: false - name: Verify tag and manifest versions if: startsWith(github.ref, 'refs/tags/') shell: bash run: | tag_version="${GITHUB_REF_NAME#v}" manifest_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)" pkgbuild_version="$(sed -n 's/^pkgver=//p' packaging/PKGBUILD | head -n1)" spec_version="$(sed -n 's/^%{!?version: %global version \([^}]*\)}/\1/p' packaging/proton-drive-linux.spec)" test "$tag_version" = "$manifest_version" test "$tag_version" = "$pkgbuild_version" test "$tag_version" = "$spec_version" - name: Release quality gates run: | cargo fmt --all -- --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace --locked scripts/fuse-acceptance.sh --offline-only build-release: name: Build packages runs-on: ubuntu-24.04 environment: 'rpm signing' steps: - name: Checkout repository uses: actions/checkout@v7 - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ pkg-config \ libfuse3-dev \ libgtk-4-dev \ libadwaita-1-dev \ libwebkitgtk-6.0-dev \ libsecret-1-dev \ libdbus-1-dev \ desktop-file-utils \ gettext \ rpm - name: Set up Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: '1.96' cache: false # Restores the release-profile dependencies that CI builds on main. - name: Cache Cargo target uses: swatinem/rust-cache@v2 with: shared-key: release save-if: false - name: Build release binaries run: | cargo build --release --locked - name: Determine version run: | if [[ $GITHUB_REF == refs/tags/* ]]; then # Extract version from tag (e.g. v0.1.0 -> 0.1.0) VERSION=${GITHUB_REF#refs/tags/} VERSION=${VERSION#v} else # Fallback for manual workflow runs: extract version from Cargo.toml VERSION=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) VERSION="${VERSION}-dev-${GITHUB_SHA::7}" fi echo "VERSION=$VERSION" >> $GITHUB_ENV echo "Building version: $VERSION" - name: Package raw binaries (.tar.gz) run: | mkdir -p dist/binaries cp target/release/pdfs dist/binaries/ cp target/release/pdfs-tray dist/binaries/ cp target/release/pdfs-app dist/binaries/ cp target/release/pdfs-prompt dist/binaries/ po/build.sh dist/binaries/locale # Create tarball tar -czf proton-drive-linux-${{ env.VERSION }}-x86_64.tar.gz -C dist/binaries . # Every crate the build needs, so COPR can build the spec without network. - name: Package vendored crates (.tar.xz) run: | cargo vendor --locked dist/vendor > /dev/null tar -cJf proton-drive-linux-vendor-${{ env.VERSION }}.tar.xz -C dist vendor - name: Package Debian package (.deb) run: | mkdir -p deb/DEBIAN mkdir -p deb/usr/bin mkdir -p deb/usr/share/applications mkdir -p deb/usr/share/icons/hicolor/scalable/apps mkdir -p deb/usr/share/licenses/proton-drive-linux mkdir -p deb/usr/lib/systemd/user mkdir -p deb/etc/xdg/autostart # Copy binaries cp target/release/pdfs deb/usr/bin/ cp target/release/pdfs-tray deb/usr/bin/ cp target/release/pdfs-app deb/usr/bin/ cp target/release/pdfs-prompt deb/usr/bin/ # Copy desktop integration files cp packaging/io.narl.proton-drive-linux.desktop deb/usr/share/applications/ cp packaging/io.narl.proton-drive-linux-tray.desktop deb/etc/xdg/autostart/ cp packaging/io.narl.proton-drive-linux.svg deb/usr/share/icons/hicolor/scalable/apps/ cp packaging/proton-drive.service deb/usr/lib/systemd/user/ cp LICENSE deb/usr/share/licenses/proton-drive-linux/ po/build.sh deb/usr/share/locale # Stops the running daemon before removal, so the mount stops mirroring deletes install -m755 packaging/deb/prerm deb/DEBIAN/prerm # Calculate Installed-Size in KB for the control file INSTALLED_SIZE=$(du -sk deb | cut -f1) # Write DEBIAN/control file cat < deb/DEBIAN/control Package: proton-drive-linux Version: ${{ env.VERSION }} Section: utils Priority: optional Architecture: amd64 Maintainer: Nils Pukropp Depends: fuse3, libgtk-4-1 (>= 4.12.0), libadwaita-1-0 (>= 1.5.0), libsecret-1-0, libwebkitgtk-6.0-4, libimage-exiftool-perl Installed-Size: ${INSTALLED_SIZE} Homepage: https://proton-drive.narl.io Description: Unofficial Proton Drive client for Linux Files-on-demand Proton Drive for the Linux desktop, unaffiliated with Proton AG: - FUSE virtual mount with block-level caching - Scriptable command-line interface (pdfs) - GTK4 desktop application, system tray, and search launcher EOF # Build Debian package dpkg-deb --build deb proton-drive-linux_${{ env.VERSION }}_amd64.deb - name: Package Fedora package (.rpm) run: | # Sanitize version (replace hyphens with dots for RPM compatibility) RPM_VERSION=$(echo "${{ env.VERSION }}" | tr '-' '.') # Create RPM build directories mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS,BUILDROOT,db} # Build RPM package rpmbuild -bb \ --nodeps \ --define "_topdir $PWD/rpmbuild" \ --define "_sourcedir $PWD" \ --define "git_dir $PWD" \ --define "prebuilt 1" \ --define "version ${RPM_VERSION}" \ --define "_dbpath $PWD/rpmbuild/db" \ packaging/proton-drive-linux.spec # Copy built RPM to root directory cp rpmbuild/RPMS/x86_64/*.rpm . - name: Sign Fedora package (.rpm) env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | if [ -n "$GPG_PRIVATE_KEY" ] && [ -n "$GPG_KEY_NAME" ]; then echo "$GPG_PRIVATE_KEY" | gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --import cat < ~/.rpmmacros %_signature gpg %_gpg_name ${GPG_KEY_NAME} %__gpg_sign_cmd %{__gpg} gpg --batch --verbose --no-armor --pinentry-mode loopback --passphrase %{_gpg_passphrase} --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} --digest-algo sha256 %{__plaintext_filename} EOF rpm --addsign --define "_gpg_passphrase $GPG_PASSPHRASE" *.rpm else echo "Skipping RPM signing: GPG_PRIVATE_KEY or GPG_KEY_NAME secret is not set." fi - name: Upload Workflow Artifacts uses: actions/upload-artifact@v7 with: name: proton-drive-linux-builds path: | *.deb *.tar.gz *.tar.xz *.rpm if-no-files-found: error # The flake is the Nix release: a tag is what `github:narrrl/proton-drive-linux/vX.Y.Z` # pins, so it has to build before the release is announced. nix: name: Build the Nix package runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 - uses: cachix/install-nix-action@v31 # Pulls from the project's binary cache, and pushes what this run built # when the token is there; a fork's pull request has none and only pulls. - uses: cachix/cachix-action@v17 with: name: proton-drive-linux authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Build the flake run: nix build --print-build-logs .#proton-drive-linux - name: Check the version run: | built="$(./result/bin/pdfs --version)" echo "$built" case "$GITHUB_REF" in refs/tags/v*) grep -qF "${GITHUB_REF_NAME#v}" <<< "$built" ;; esac publish: name: Create GitHub Release needs: [checks, build-release, nix] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v7 - name: Download packages uses: actions/download-artifact@v8 with: name: proton-drive-linux-builds # The release notes are this version's section of the changelog, from its # heading up to the next one. - name: Extract release notes run: | version="${GITHUB_REF_NAME#v}" awk -v heading="## [$version]" \ 'index($0, heading) == 1 { found = 1; print; next } found && /^## \[/ { exit } found' docs/CHANGELOG.md > release-notes.md if [ ! -s release-notes.md ]; then echo "::error::docs/CHANGELOG.md has no section for $version" exit 1 fi - name: Create GitHub Release uses: softprops/action-gh-release@v3 with: body_path: release-notes.md files: | *.deb *.tar.gz *.tar.xz *.rpm draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} aur: name: Publish AUR packages needs: publish if: startsWith(github.ref, 'refs/tags/') uses: ./.github/workflows/aur.yml with: version: ${{ github.ref_name }} secrets: AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} copr: name: Build COPR packages needs: publish if: startsWith(github.ref, 'refs/tags/') uses: ./.github/workflows/copr.yml with: version: ${{ github.ref_name }} secrets: COPR_CONFIG: ${{ secrets.COPR_CONFIG }} wiki: name: Publish the wiki needs: publish if: startsWith(github.ref, 'refs/tags/') uses: ./.github/workflows/wiki.yml with: ref: ${{ github.ref_name }}