name: CI / Build / Publish on: push: branches: ["**"] tags: ["v*.*.*"] pull_request: branches: [master] env: PACKAGE_NAME: freenas-proxmox # 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@v6 - 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 librest-client-perl libjson-perl # PVE::SafeSyslog only exists on Proxmox hosts; stub it for syntax checking mkdir -p /tmp/pve-stub/PVE printf 'package PVE::SafeSyslog;\nuse Exporter "import";\nour @EXPORT = qw(syslog);\nsub syslog {}\n1;\n' \ > /tmp/pve-stub/PVE/SafeSyslog.pm - name: Perl syntax check (all modules) run: | echo "==> Checking Perl syntax..." find perl5 stable-*/perl5 -name "*.pm" -print0 \ | xargs -0 -I{} perl -c -I/tmp/pve-stub {} \ && echo "All .pm files OK" - name: Perl static analysis (perlcritic) run: | echo "==> Running perlcritic..." perlcritic --profile .perlcriticrc \ perl5/PVE/Storage/LunCmd/FreeNAS.pm - name: Shell script lint (shellcheck) run: | echo "==> Running shellcheck..." shellcheck --severity=warning \ packaging/DEBIAN/postinst \ packaging/DEBIAN/postrm echo "Shell scripts OK" # ── Job 2: Validate patches apply cleanly ────────────────────────────────── validate-patches: name: Validate Patches runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: "Dry-run patch: ZFSPlugin (PVE 8)" run: | patch --dry-run --ignore-whitespace \ stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig \ < stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \ && echo "ZFSPlugin PVE-8 patch: OK" - name: "Dry-run patch: ZFSPlugin (PVE 8.4.x)" run: | patch --dry-run --ignore-whitespace \ stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.orig \ < stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch \ && echo "ZFSPlugin PVE-8.4 patch: OK" - name: "Dry-run patch: apidoc.js (PVE 8)" run: | patch --dry-run --ignore-whitespace \ stable-8/pve-docs/api-viewer/apidoc.js.orig \ < stable-8/pve-docs/api-viewer/apidoc.js.patch \ && echo "apidoc PVE-8 patch: OK" # ── Job 3: Build .deb ──────────────────────────────────────────────────────── build: name: Build Package runs-on: ubuntu-latest needs: [lint, validate-patches] outputs: version: ${{ steps.vars.outputs.version }} deb_file: ${{ steps.vars.outputs.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@v6 with: # Fetch full history so tag-based versioning works fetch-depth: 0 # ── Version resolution ───────────────────────────────────────────────── # Version strategy: # Tagged release (v1.2.3) → 1.2.3 (stable channel) # master branch → -beta+ (testing channel) # feature_* branch → -alpha+ (development channel) # any other branch / PR → -dev+ (no publish) # # The base version is derived from the most recent git tag (vX.Y.Z). # No VERSION file needed — the tag IS the version. - name: Resolve version and channel id: vars run: | SHORT_SHA="${GITHUB_SHA:0:7}" REF="${{ github.ref }}" IS_RELEASE="false" # Base version from our $VERSION in FreeNAS.pm — the single source of truth. # Bump that variable when starting a new release series; git tags trigger publishing. BASE_VERSION="$(perl -ne 'if (/our\s+\$VERSION\s*=\s*['"'"'"]([^'"'"'"]+)/) { print $1; exit }' \ perl5/PVE/Storage/LunCmd/FreeNAS.pm 2>/dev/null || echo '0.0.0')" # Only an exact vX.Y.Z tag (no suffix) is a stable release. # Stable packages use a "-1" Debian revision so they sort higher than # any pre-release build (e.g. 2.3.0~beta+sha < 2.3.0-1). # Pre-release builds use "~" (tilde) which sorts BELOW the base version # in dpkg, ensuring stable always wins on apt upgrade. 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 FreeNAS.pm ($BASE_VERSION)" fi elif [[ "$REF" == refs/heads/master ]]; then VERSION="${BASE_VERSION}~beta+${SHORT_SHA}" CHANNEL="testing" CLOUDSMITH_REPO="truenas-proxmox-testing" elif [[ "$REF" == refs/heads/feature_* ]]; 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" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "deb_file=${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/freenas-proxmox/patches/ZFSPlugin" mkdir -p "${STAGING}/usr/share/freenas-proxmox/patches/pvemanagerlib" mkdir -p "${STAGING}/usr/share/freenas-proxmox/patches/apidoc" # 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" cp packaging/DEBIAN/triggers "${STAGING}/DEBIAN/triggers" chmod 0755 "${STAGING}/DEBIAN/postinst" "${STAGING}/DEBIAN/postrm" # Plugin source files cp perl5/PVE/Storage/LunCmd/FreeNAS.pm "${STAGING}/usr/share/freenas-proxmox/FreeNAS.pm" cp perl5/REST/Client.pm "${STAGING}/usr/share/freenas-proxmox/REST-Client.pm" # PVE 8 patches (8.0.x – 8.3.x) cp stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch \ "${STAGING}/usr/share/freenas-proxmox/patches/ZFSPlugin/8.patch" # PVE 8.4.x ZFSPlugin patch — indentation changed in 8.4, incompatible with 8.patch cp stable-8/perl5/PVE/Storage/ZFSPlugin-8.4.14_1.pm.patch \ "${STAGING}/usr/share/freenas-proxmox/patches/ZFSPlugin/8.4.patch" echo "Bundled PVE 8.4.x ZFSPlugin patch" cp stable-8/pve-manager/js/pvemanagerlib.js.patch \ "${STAGING}/usr/share/freenas-proxmox/patches/pvemanagerlib/8.patch" cp stable-8/pve-docs/api-viewer/apidoc.js.patch \ "${STAGING}/usr/share/freenas-proxmox/patches/apidoc/8.patch" # PVE 8.4.x pvemanagerlib patch — JS was reformatted in 8.4, incompatible with 8.patch cp stable-8/pve-manager/js/pvemanagerlib-8.4.14_1.js.patch \ "${STAGING}/usr/share/freenas-proxmox/patches/pvemanagerlib/8.4.patch" echo "Bundled PVE 8.4.x pvemanagerlib patch" # PVE 7 patches (best-effort — use latest versioned patch available) for type in ZFSPlugin pvemanagerlib apidoc; do case "$type" in ZFSPlugin) glob="stable-7/perl5/PVE/Storage/ZFSPlugin-*.pm.patch" ;; pvemanagerlib) glob="stable-7/pve-manager/js/pvemanagerlib-*.js.patch" ;; apidoc) glob="stable-7/pve-docs/api-viewer/apidoc-*.js.patch" ;; esac latest=$(ls $glob 2>/dev/null | sort -V | tail -1 || true) if [ -n "$latest" ]; then cp "$latest" "${STAGING}/usr/share/freenas-proxmox/patches/${type}/7.patch" echo "Bundled PVE-7 ${type} patch: $(basename $latest)" else echo "No PVE-7 ${type} patch found — skipping" fi done echo "==> Package contents:" find "${STAGING}" | sort - name: Build .deb run: | sudo dpkg-deb -Zgzip --build dist "${{ steps.vars.outputs.deb_file }}" - name: Verify .deb run: | echo "==> Package info:" dpkg-deb --info "${{ steps.vars.outputs.deb_file }}" echo "" echo "==> Package contents:" dpkg-deb --contents "${{ steps.vars.outputs.deb_file }}" - name: Upload package artifact uses: actions/upload-artifact@v7 with: name: ${{ steps.vars.outputs.deb_file }} path: ${{ steps.vars.outputs.deb_file }} retention-days: 30 # ── Job 4: Security scan ───────────────────────────────────────────────────── security: name: Security Scan runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v6 # Scan the repository for secrets and known vulnerabilities - 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 # Download and scan the built .deb - name: Download built package uses: actions/download-artifact@v8 with: name: ${{ needs.build.outputs.deb_file }} - 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 5: 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' steps: - uses: actions/checkout@v6 - name: Download built package uses: actions/download-artifact@v8 with: name: ${{ needs.build.outputs.deb_file }} - name: Publish to Cloudsmith 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: 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 }} generate_release_notes: false body: | ## freenas-proxmox v${{ needs.build.outputs.version }} > **Edit before publishing** — fill in tested versions and remove inapplicable notices below. ### ⚠️ Version Compatibility Notices | Your Proxmox VE | What to do | |:----------------|:-----------| | **PVE 9+** | ❌ Do **not** install v2.x — use v3.0 when released | | **PVE 8.x** | ✅ Supported. Note: PVE 8 EOL is **2026-08-31** — plan your upgrade to PVE 9 + v3.0 | | **PVE 7.x** | ⚠️ Best-effort only. This is the **last v2.x release** supporting PVE 7. Do **not** upgrade to v3.0 — stay on v2.x | | **PVE 6 or older** | ❌ Not supported | ### Tested Proxmox VE Versions - Proxmox VE 8.4.x (tested: ) - Proxmox VE 8.3.x (tested: ) - Proxmox VE 7.x (best-effort: ) ### Tested TrueNAS Versions - TrueNAS CORE: - TrueNAS SCALE: ### Installation See [README → Installation](https://github.com/TheGrandWazoo/freenas-proxmox#installation). ### Changes See [CHANGELOG.md](https://github.com/TheGrandWazoo/freenas-proxmox/blob/master/CHANGELOG.md#unreleased).