name: CI / Build / Publish on: push: branches: ["**"] tags: ["v*.*.*"] pull_request: branches: [master, "release/3.x"] env: PACKAGE_NAME: truenas-proxmox TRANSITIONAL_PACKAGE_NAME: freenas-proxmox FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' # Cancel in-flight runs for the same branch on new push concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # ── Job 1: Lint ────────────────────────────────────────────────────────────── jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.3.1 - name: Install lint tools run: | sudo apt-get update -qq sudo apt-get install -y \ shellcheck libperl-critic-perl \ libwww-perl libio-socket-ssl-perl libjson-perl # Stub PVE modules not available outside Proxmox hosts mkdir -p /tmp/pve-stub/PVE/Storage printf 'package PVE::SafeSyslog;\nuse Exporter "import";\nour @EXPORT = qw(syslog);\nsub syslog {}\n1;\n' \ > /tmp/pve-stub/PVE/SafeSyslog.pm printf 'package PVE::Tools;\nuse Exporter "import";\nour @EXPORT_OK = qw(run_command);\nsub run_command {}\n1;\n' \ > /tmp/pve-stub/PVE/Tools.pm printf 'package PVE::Storage::Plugin;\nsub new {}\nsub register {}\nsub lookup_types { [] }\nsub properties { {} }\nsub options { {} }\n1;\n' \ > /tmp/pve-stub/PVE/Storage/Plugin.pm - name: Perl syntax check run: | echo "==> Checking Perl syntax..." perl -c -I/tmp/pve-stub perl5/PVE/Storage/Custom/TrueNAS.pm \ && echo "TrueNAS.pm: OK" perl -c -I/tmp/pve-stub -Iperl5 \ perl5/PVE/Storage/Custom/TrueNASMultipath.pm \ && echo "TrueNASMultipath.pm: OK" - name: Perl static analysis (perlcritic) run: | echo "==> Running perlcritic..." perlcritic --profile .perlcriticrc \ perl5/PVE/Storage/Custom/TrueNAS.pm \ perl5/PVE/Storage/Custom/TrueNASMultipath.pm - name: Shell script lint (shellcheck) run: | echo "==> Running shellcheck..." shellcheck --severity=warning \ packaging/DEBIAN/postinst \ packaging/DEBIAN/postrm \ packaging/DEBIAN-transitional/postinst \ packaging/DEBIAN-multipath/postinst \ packaging/DEBIAN-multipath/postrm echo "Shell scripts OK" # ── Job 2: Build .deb ──────────────────────────────────────────────────────── build: name: Build Package runs-on: ubuntu-latest needs: [lint] outputs: version: ${{ steps.vars.outputs.version }} deb_file: ${{ steps.vars.outputs.deb_file }} transitional_deb_file: ${{ steps.vars.outputs.transitional_deb_file }} multipath_deb_file: ${{ steps.vars.outputs.multipath_deb_file }} channel: ${{ steps.vars.outputs.channel }} cloudsmith_repo: ${{ steps.vars.outputs.cloudsmith_repo }} is_release: ${{ steps.vars.outputs.is_release }} steps: - uses: actions/checkout@v4.3.1 with: fetch-depth: 0 # ── Version resolution ───────────────────────────────────────────────── # Version strategy: # Tagged release (v3.x.x) → 3.x.x-1 (stable channel) # release/3.x branch → ~beta+ (testing channel) # master branch → ~beta+ (testing channel) # release/* branches → ~alpha+ (development) # everything else / PRs → ~dev+ (no publish) # # Tilde (~) sorts BELOW the base version in dpkg, so pre-release builds # never upgrade over a stable package. Stable uses "-1" Debian revision. - name: Resolve version and channel id: vars run: | SHORT_SHA="${GITHUB_SHA:0:7}" REF="${{ github.ref }}" IS_RELEASE="false" # $VERSION in TrueNAS.pm is the single source of truth. BASE_VERSION="$(perl -ne 'if (/our\s+\$VERSION\s*=\s*['"'"'"]([^'"'"'"]+)/) { print $1; exit }' \ perl5/PVE/Storage/Custom/TrueNAS.pm 2>/dev/null || echo '0.0.0')" if [[ "$REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then VERSION="${REF#refs/tags/v}-1" CHANNEL="stable" CLOUDSMITH_REPO="truenas-proxmox" IS_RELEASE="true" if [[ "${REF#refs/tags/v}" != "$BASE_VERSION" ]]; then echo "::warning::Tag version (${REF#refs/tags/v}) does not match \$VERSION in TrueNAS.pm ($BASE_VERSION)" fi elif [[ "$REF" == refs/heads/master || "$REF" == refs/heads/release/3.x ]]; then VERSION="${BASE_VERSION}~beta+${SHORT_SHA}" CHANNEL="testing" CLOUDSMITH_REPO="truenas-proxmox-testing" elif [[ "$REF" == refs/heads/release/* ]]; then VERSION="${BASE_VERSION}~alpha+${SHORT_SHA}" CHANNEL="development" CLOUDSMITH_REPO="truenas-proxmox-snapshots" else VERSION="${BASE_VERSION}~dev+${SHORT_SHA}" CHANNEL="none" CLOUDSMITH_REPO="" fi DEB_FILE="${PACKAGE_NAME}_${VERSION}_all.deb" TRANSITIONAL_DEB_FILE="${TRANSITIONAL_PACKAGE_NAME}_${VERSION}_all.deb" MULTIPATH_DEB_FILE="truenas-proxmox-multipath_${VERSION}_all.deb" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT" echo "transitional_deb_file=${TRANSITIONAL_DEB_FILE}" >> "$GITHUB_OUTPUT" echo "multipath_deb_file=${MULTIPATH_DEB_FILE}" >> "$GITHUB_OUTPUT" echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT" echo "cloudsmith_repo=${CLOUDSMITH_REPO}" >> "$GITHUB_OUTPUT" echo "is_release=${IS_RELEASE}" >> "$GITHUB_OUTPUT" { echo "### Build Summary" echo "| | |" echo "|---|---|" echo "| Version | \`${VERSION}\` |" echo "| Channel | \`${CHANNEL}\` |" echo "| Package | \`${DEB_FILE}\` |" } >> "$GITHUB_STEP_SUMMARY" # ── Assemble staging directory ───────────────────────────────────────── - name: Assemble package staging directory run: | VERSION="${{ steps.vars.outputs.version }}" STAGING="dist" mkdir -p "${STAGING}/DEBIAN" mkdir -p "${STAGING}/usr/share/truenas-proxmox" # Generate control file from template sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN/control.j2 \ > "${STAGING}/DEBIAN/control" # Maintainer scripts cp packaging/DEBIAN/postinst "${STAGING}/DEBIAN/postinst" cp packaging/DEBIAN/postrm "${STAGING}/DEBIAN/postrm" chmod 0755 "${STAGING}/DEBIAN/postinst" "${STAGING}/DEBIAN/postrm" # Plugin files (postinst copies these to their final destinations) cp perl5/PVE/Storage/Custom/TrueNAS.pm "${STAGING}/usr/share/truenas-proxmox/TrueNAS.pm" cp ui/truenas-storage.js "${STAGING}/usr/share/truenas-proxmox/truenas-storage.js" cp ui/truenas-storage-help.html "${STAGING}/usr/share/truenas-proxmox/truenas-storage-help.html" # Debian changelog (required by Debian policy §12.7) mkdir -p "${STAGING}/usr/share/doc/truenas-proxmox" gzip -9c packaging/changelog.Debian \ > "${STAGING}/usr/share/doc/truenas-proxmox/changelog.Debian.gz" echo "==> Package contents:" find "${STAGING}" | sort - name: Assemble transitional package staging directory run: | VERSION="${{ steps.vars.outputs.version }}" STAGING="dist-transitional" mkdir -p "${STAGING}/DEBIAN" sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN-transitional/control.j2 \ > "${STAGING}/DEBIAN/control" cp packaging/DEBIAN-transitional/postinst "${STAGING}/DEBIAN/postinst" chmod 0755 "${STAGING}/DEBIAN/postinst" # Debian changelog mkdir -p "${STAGING}/usr/share/doc/freenas-proxmox" gzip -9c packaging/DEBIAN-transitional/changelog.Debian \ > "${STAGING}/usr/share/doc/freenas-proxmox/changelog.Debian.gz" echo "==> Transitional package contents:" find "${STAGING}" | sort - name: Assemble multipath package staging directory run: | VERSION="${{ steps.vars.outputs.version }}" STAGING="dist-multipath" mkdir -p "${STAGING}/DEBIAN" mkdir -p "${STAGING}/usr/share/truenas-proxmox-multipath" sed "s/\${VERSION}/${VERSION}/" packaging/DEBIAN-multipath/control.j2 \ > "${STAGING}/DEBIAN/control" cp packaging/DEBIAN-multipath/postinst "${STAGING}/DEBIAN/postinst" cp packaging/DEBIAN-multipath/postrm "${STAGING}/DEBIAN/postrm" chmod 0755 "${STAGING}/DEBIAN/postinst" "${STAGING}/DEBIAN/postrm" cp perl5/PVE/Storage/Custom/TrueNASMultipath.pm \ "${STAGING}/usr/share/truenas-proxmox-multipath/TrueNASMultipath.pm" cp ui/truenas-multipath.js \ "${STAGING}/usr/share/truenas-proxmox-multipath/truenas-multipath.js" cp packaging/DEBIAN-multipath/multipath.conf.example \ "${STAGING}/usr/share/truenas-proxmox-multipath/multipath.conf.example" mkdir -p "${STAGING}/usr/share/doc/truenas-proxmox-multipath" gzip -9c packaging/DEBIAN-multipath/changelog.Debian \ > "${STAGING}/usr/share/doc/truenas-proxmox-multipath/changelog.Debian.gz" echo "==> Multipath package contents:" find "${STAGING}" | sort - name: Build .deb packages run: | sudo dpkg-deb -Zgzip --build dist "${{ steps.vars.outputs.deb_file }}" sudo dpkg-deb -Zgzip --build dist-transitional "${{ steps.vars.outputs.transitional_deb_file }}" sudo dpkg-deb -Zgzip --build dist-multipath "${{ steps.vars.outputs.multipath_deb_file }}" - name: Verify .deb packages run: | echo "==> Package info (truenas-proxmox):" dpkg-deb --info "${{ steps.vars.outputs.deb_file }}" echo "" echo "==> Package info (freenas-proxmox transitional):" dpkg-deb --info "${{ steps.vars.outputs.transitional_deb_file }}" echo "" echo "==> Package info (truenas-proxmox-multipath):" dpkg-deb --info "${{ steps.vars.outputs.multipath_deb_file }}" - name: Upload package artifacts uses: actions/upload-artifact@v4 with: name: packages-${{ steps.vars.outputs.version }} path: | ${{ steps.vars.outputs.deb_file }} ${{ steps.vars.outputs.transitional_deb_file }} ${{ steps.vars.outputs.multipath_deb_file }} retention-days: 30 # ── Job 3: Security scan ───────────────────────────────────────────────────── security: name: Security Scan runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v4.3.1 - name: Run Trivy (repo scan — secrets + misconfig) uses: aquasecurity/trivy-action@master with: scan-type: fs scan-ref: . scanners: secret,misconfig severity: HIGH,CRITICAL exit-code: 1 format: table - name: Download built packages uses: actions/download-artifact@v4 with: name: packages-${{ needs.build.outputs.version }} - name: Extract and scan .deb contents run: | mkdir -p deb-contents dpkg-deb --extract "${{ needs.build.outputs.deb_file }}" deb-contents/ - name: Run Trivy (package contents — vuln + secret) uses: aquasecurity/trivy-action@master with: scan-type: fs scan-ref: deb-contents scanners: vuln,secret severity: HIGH,CRITICAL exit-code: 1 format: table # ── Job 4: Publish ─────────────────────────────────────────────────────────── publish: name: Publish runs-on: ubuntu-latest needs: [build, security] # Only publish on direct pushes (not PRs) to tracked branches or tags if: github.event_name == 'push' && needs.build.outputs.channel != 'none' permissions: contents: write steps: - uses: actions/checkout@v4.3.1 - name: Download built packages uses: actions/download-artifact@v4 with: name: packages-${{ needs.build.outputs.version }} - name: Publish truenas-proxmox to Cloudsmith # Cloudsmith now serves v2.x stable only. v3+ stable → GitHub Pages. # beta/alpha → GitHub Pages testing dist (see step below). if: >- startsWith(needs.build.outputs.version, '2.') && needs.build.outputs.channel == 'stable' uses: cloudsmith-io/action@v0.6.14 with: api-key: ${{ secrets.CLOUDSMITH_API_KEY }} command: push format: deb owner: ksatechnologies repo: ${{ needs.build.outputs.cloudsmith_repo }} distro: debian release: any-version file: ${{ needs.build.outputs.deb_file }} - name: Publish transitional freenas-proxmox to Cloudsmith if: >- startsWith(needs.build.outputs.version, '2.') && needs.build.outputs.channel == 'stable' uses: cloudsmith-io/action@v0.6.14 with: api-key: ${{ secrets.CLOUDSMITH_API_KEY }} command: push format: deb owner: ksatechnologies repo: ${{ needs.build.outputs.cloudsmith_repo }} distro: debian release: any-version file: ${{ needs.build.outputs.transitional_deb_file }} - name: Publish to GitHub Pages APT repo if: needs.build.outputs.is_release == 'true' env: APT_SIGNING_KEY: ${{ secrets.APT_SIGNING_KEY }} APT_SIGNING_KEY_PASSPHRASE: ${{ secrets.APT_SIGNING_KEY_PASSPHRASE }} GH_TOKEN: ${{ github.token }} run: | # Abort clearly if signing key not configured if [[ -z "$APT_SIGNING_KEY" ]]; then echo "::error::APT_SIGNING_KEY secret is not set — run scripts/setup-apt-signing-key.sh and add the secret" exit 1 fi # Determine major version from package version (e.g. 3.0.0-1 → 3) VERSION="${{ needs.build.outputs.version }}" MAJOR="${VERSION%%.*}" # Import GPG signing key echo "$APT_SIGNING_KEY" | base64 -d | gpg --batch --import GPG_KEY_ID="$(gpg --list-secret-keys --with-colons 2>/dev/null \ | awk -F: '/^sec/{print $5; exit}')" # Checkout gh-pages branch into a temp directory PAGES_DIR="$(mktemp -d)" git clone --branch gh-pages \ "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \ "$PAGES_DIR" # Add .deb files to the versioned pool directories mkdir -p "${PAGES_DIR}/pool/v${MAJOR}" mkdir -p "${PAGES_DIR}/pool/multipath/v${MAJOR}" cp "${{ needs.build.outputs.deb_file }}" "${PAGES_DIR}/pool/v${MAJOR}/" cp "${{ needs.build.outputs.transitional_deb_file }}" "${PAGES_DIR}/pool/v${MAJOR}/" cp "${{ needs.build.outputs.multipath_deb_file }}" "${PAGES_DIR}/pool/multipath/v${MAJOR}/" # Install apt tooling sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils # Determine which dists to update if [[ "$MAJOR" == "2" ]]; then DISTS="v2 main" else DISTS="v${MAJOR}" fi _gen_component() { local PAGES_DIR="$1" DIST="$2" COMP="$3" POOL="$4" mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all" mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64" mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64" dpkg-scanpackages --multiversion "$POOL" \ | sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \ > "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages" gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages" touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages" gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages" touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages" gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages" } _dist_codename() { case "$1" in v2|main) echo "limelight" ;; v3) echo "error" ;; v4) echo "rivendell" ;; *) echo "$1" ;; esac } for DIST in $DISTS; do _gen_component "$PAGES_DIR" "$DIST" "main" \ "${PAGES_DIR}/pool/v${MAJOR}" _gen_component "$PAGES_DIR" "$DIST" "multipath" \ "${PAGES_DIR}/pool/multipath/v${MAJOR}" CODENAME=$(_dist_codename "$DIST") # Generate Release file listing both components apt-ftparchive \ -o "APT::FTPArchive::Release::Origin=truenas-proxmox" \ -o "APT::FTPArchive::Release::Label=truenas-proxmox" \ -o "APT::FTPArchive::Release::Suite=${DIST}" \ -o "APT::FTPArchive::Release::Codename=${CODENAME}" \ -o "APT::FTPArchive::Release::Components=main multipath" \ -o "APT::FTPArchive::Release::Architectures=all amd64 arm64" \ release "${PAGES_DIR}/dists/${DIST}" \ > "${PAGES_DIR}/dists/${DIST}/Release" gpg --batch --yes \ --passphrase "${APT_SIGNING_KEY_PASSPHRASE}" \ --default-key "${GPG_KEY_ID}" \ --clearsign \ -o "${PAGES_DIR}/dists/${DIST}/InRelease" \ "${PAGES_DIR}/dists/${DIST}/Release" done # Commit and push cd "$PAGES_DIR" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add . git diff --staged --quiet || \ git commit -m "apt: publish ${VERSION} to dist(s): ${DISTS}" git push - name: Publish to GitHub Pages testing dist if: >- needs.build.outputs.channel == 'testing' || needs.build.outputs.channel == 'development' env: APT_SIGNING_KEY: ${{ secrets.APT_SIGNING_KEY }} APT_SIGNING_KEY_PASSPHRASE: ${{ secrets.APT_SIGNING_KEY_PASSPHRASE }} GH_TOKEN: ${{ github.token }} run: | if [[ -z "$APT_SIGNING_KEY" ]]; then echo "::error::APT_SIGNING_KEY secret is not set" exit 1 fi VERSION="${{ needs.build.outputs.version }}" echo "$APT_SIGNING_KEY" | base64 -d | gpg --batch --import GPG_KEY_ID="$(gpg --list-secret-keys --with-colons 2>/dev/null \ | awk -F: '/^sec/{print $5; exit}')" PAGES_DIR="$(mktemp -d)" git clone --branch gh-pages \ "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \ "$PAGES_DIR" # Replace pool/testing/ contents with the latest build only rm -rf "${PAGES_DIR}/pool/testing" rm -rf "${PAGES_DIR}/pool/multipath/testing" mkdir -p "${PAGES_DIR}/pool/testing" mkdir -p "${PAGES_DIR}/pool/multipath/testing" cp "${{ needs.build.outputs.deb_file }}" "${PAGES_DIR}/pool/testing/" cp "${{ needs.build.outputs.transitional_deb_file }}" "${PAGES_DIR}/pool/testing/" cp "${{ needs.build.outputs.multipath_deb_file }}" "${PAGES_DIR}/pool/multipath/testing/" sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils DIST="testing" _gen_component() { local PAGES_DIR="$1" DIST="$2" COMP="$3" POOL="$4" mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all" mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64" mkdir -p "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64" dpkg-scanpackages --multiversion "$POOL" \ | sed "s|^Filename: ${PAGES_DIR}/|Filename: |" \ > "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages" gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-all/Packages" touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages" gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-amd64/Packages" touch "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages" gzip -kf "${PAGES_DIR}/dists/${DIST}/${COMP}/binary-arm64/Packages" } _gen_component "$PAGES_DIR" "$DIST" "main" "${PAGES_DIR}/pool/testing" _gen_component "$PAGES_DIR" "$DIST" "multipath" "${PAGES_DIR}/pool/multipath/testing" apt-ftparchive \ -o "APT::FTPArchive::Release::Origin=truenas-proxmox" \ -o "APT::FTPArchive::Release::Label=truenas-proxmox" \ -o "APT::FTPArchive::Release::Suite=${DIST}" \ -o "APT::FTPArchive::Release::Codename=testing" \ -o "APT::FTPArchive::Release::Components=main multipath" \ -o "APT::FTPArchive::Release::Architectures=all amd64 arm64" \ release "${PAGES_DIR}/dists/${DIST}" \ > "${PAGES_DIR}/dists/${DIST}/Release" gpg --batch --yes \ --passphrase "${APT_SIGNING_KEY_PASSPHRASE}" \ --default-key "${GPG_KEY_ID}" \ --clearsign \ -o "${PAGES_DIR}/dists/${DIST}/InRelease" \ "${PAGES_DIR}/dists/${DIST}/Release" cd "$PAGES_DIR" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add . git diff --staged --quiet || \ git commit -m "apt: publish ${VERSION} to testing dist" git push - name: Create draft GitHub Release if: needs.build.outputs.is_release == 'true' uses: softprops/action-gh-release@v3 with: name: "v${{ needs.build.outputs.version }}" draft: true files: | ${{ needs.build.outputs.deb_file }} ${{ needs.build.outputs.transitional_deb_file }} ${{ needs.build.outputs.multipath_deb_file }} generate_release_notes: false body: | ## truenas-proxmox v${{ needs.build.outputs.version }} > **Edit before publishing** — fill in tested versions before publishing. ### Compatibility | Proxmox VE | Status | |:-----------|:-------| | **PVE 9+** | ✅ Supported | | **PVE 8.x** | ✅ Supported (EOL 2026-08-31 — plan upgrade to PVE 9) | | **PVE 7 or older** | ❌ Not supported — use v2.x | ### Tested Proxmox VE Versions - Proxmox VE 8.4.x (tested: ) - Proxmox VE 9.x (tested: ) ### Tested TrueNAS Versions - TrueNAS CORE 13.x (tested: ) - TrueNAS SCALE 24.10.x (tested: ) ### Installation See [README → Installation](https://github.com/TheGrandWazoo/truenas-proxmox#installation). ### Changes See [CHANGELOG.md](https://github.com/TheGrandWazoo/truenas-proxmox/blob/release/3.x/CHANGELOG.md).