Skip to content

Update NuGet workflow for package validation and generation #275

Update NuGet workflow for package validation and generation

Update NuGet workflow for package validation and generation #275

name: Publish NuGet Package to GitHub (nupkg + nuspec validation)
on:
push:
branches: [main]
paths:
- "**/*"
- "sysadmin-prosuite.nuspec"
- ".github/workflows/publish-nuget-package-to-github.yml"
release:
types: [published, prerelease]
workflow_dispatch:
concurrency:
group: publish-nuget-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
jobs:
publish-nuget:
name: 📦 Publish NuGet Package (GitHub Packages)
runs-on: ubuntu-latest
timeout-minutes: 25
env:
PKG_ID: "sysadmin-prosuite"
NUGET_VERSION: "6.11.0"
OUT_DIR: "nupkg-out"
STAGE_DIR: "tmp-sysadmin-prosuite"
NUGET_SOURCE: "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
SARIF_IGNORE: "true"
steps:
- name: 🧾 Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: 🧰 Install dependencies
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y mono-complete xmlstarlet curl ca-certificates
url="https://dist.nuget.org/win-x86-commandline/v${NUGET_VERSION}/nuget.exe"
echo "Downloading nuget.exe: $url"
curl -fsSL "$url" -o nuget.exe
chmod +x nuget.exe
sudo mv nuget.exe /usr/local/bin/nuget
mono --version
nuget help | head -n 5 || true
- name: ✅ Validate required repo files
shell: bash
run: |
set -euo pipefail
nuspec="${PKG_ID}.nuspec"
[[ -f "$nuspec" ]] || { echo "::error file=$nuspec::$nuspec not found"; exit 1; }
[[ -f "README.md" ]] || { echo "::error file=README.md::README.md not found"; exit 1; }
[[ -f "LICENSE.txt" ]] || { echo "::error file=LICENSE.txt::LICENSE.txt not found"; exit 1; }
- name: 📁 Prepare and stage files
shell: bash
run: |
set -euo pipefail
mkdir -p "${OUT_DIR}" "${STAGE_DIR}"
nuspec="${PKG_ID}.nuspec"
for dir in BlueTeam-Tools Core-ScriptLibrary ITSM-Templates-SVR ITSM-Templates-WKS SysAdmin-Tools; do
if [[ -d "$dir" ]]; then
mkdir -p "${STAGE_DIR}/${dir}"
cp -r "${dir}/." "${STAGE_DIR}/${dir}/"
else
echo "::notice::Skipping missing directory: $dir"
fi
done
cp "$nuspec" "${STAGE_DIR}/${PKG_ID}.nuspec"
cp README.md "${STAGE_DIR}/README.md"
cp LICENSE.txt "${STAGE_DIR}/LICENSE.txt"
if [[ -f "icon.png" ]]; then
cp icon.png "${STAGE_DIR}/icon.png"
else
echo "::notice::icon.png not found at repo root (OK only if nuspec icon is not required)."
fi
- name: 🔎 Validate .nuspec metadata (readme / repository / license / icon)
shell: bash
run: |
set -euo pipefail
nuspec="${STAGE_DIR}/${PKG_ID}.nuspec"
NS="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"
read_xpath() {
xmlstarlet sel -N n="$NS" -t -v "$1" "$nuspec" 2>/dev/null || true
}
readme="$(read_xpath 'normalize-space(//n:package/n:metadata/n:readme)')"
license_type="$(xmlstarlet sel -N n="$NS" -t -v 'normalize-space(//n:package/n:metadata/n:license/@type)' "$nuspec" 2>/dev/null || true)"
license_file="$(read_xpath 'normalize-space(//n:package/n:metadata/n:license)')"
icon="$(read_xpath 'normalize-space(//n:package/n:metadata/n:icon)')"
repo_url="$(xmlstarlet sel -N n="$NS" -t -v 'normalize-space(//n:package/n:metadata/n:repository/@url)' "$nuspec" 2>/dev/null || true)"
repo_type="$(xmlstarlet sel -N n="$NS" -t -v 'normalize-space(//n:package/n:metadata/n:repository/@type)' "$nuspec" 2>/dev/null || true)"
if [[ -z "${readme}" ]]; then
echo "::error file=${PKG_ID}.nuspec::Missing <readme> in nuspec metadata."
exit 1
fi
if [[ ! -f "${STAGE_DIR}/${readme}" ]]; then
echo "::error file=${PKG_ID}.nuspec::<readme>${readme}</readme> file not found in staging: ${STAGE_DIR}/${readme}"
exit 1
fi
if [[ "${license_type}" != "file" ]]; then
echo "::error file=${PKG_ID}.nuspec::<license> must use type=\"file\" (found: '${license_type:-empty}')."
exit 1
fi
if [[ -z "${license_file}" ]]; then
echo "::error file=${PKG_ID}.nuspec::Missing <license> value (expected a file name like LICENSE.txt)."
exit 1
fi
if [[ ! -f "${STAGE_DIR}/${license_file}" ]]; then
echo "::error file=${PKG_ID}.nuspec::<license>${license_file}</license> file not found in staging: ${STAGE_DIR}/${license_file}"
exit 1
fi
if [[ -z "${icon}" ]]; then
echo "::error file=${PKG_ID}.nuspec::Missing <icon> in nuspec metadata."
exit 1
fi
if [[ ! -f "${STAGE_DIR}/${icon}" ]]; then
echo "::error file=${PKG_ID}.nuspec::<icon>${icon}</icon> file not found in staging: ${STAGE_DIR}/${icon}"
exit 1
fi
if [[ -z "${repo_url}" ]]; then
echo "::error file=${PKG_ID}.nuspec::Missing <repository url=\"...\" /> in nuspec metadata."
exit 1
fi
if [[ -z "${repo_type}" ]]; then
echo "::error file=${PKG_ID}.nuspec::Missing <repository type=\"git\" ... /> in nuspec metadata."
exit 1
fi
if [[ "${repo_type}" != "git" ]]; then
echo "::error file=${PKG_ID}.nuspec::Expected repository type=\"git\" (found: '${repo_type}')."
exit 1
fi
echo "Validated nuspec metadata:"
echo " readme: $readme"
echo " license: type=$license_type file=$license_file"
echo " icon: $icon"
echo " repo: type=$repo_type url=$repo_url"
- name: 🛠️ Pack NuGet package (.nupkg, optional .snupkg when PDBs exist)
shell: bash
run: |
set -euo pipefail
cd "${STAGE_DIR}"
shopt -s nullglob
# If you ever add binaries + PDBs, this automatically enables snupkg.
pdb_count="$(find . -type f -iname "*.pdb" | wc -l | tr -d ' ')"
if [[ "$pdb_count" != "0" ]]; then
echo "PDBs detected ($pdb_count) -> generating .snupkg"
mono /usr/local/bin/nuget pack "${PKG_ID}.nuspec" \
-OutputDirectory "../${OUT_DIR}" \
-NonInteractive \
-Symbols \
-SymbolPackageFormat snupkg
else
echo "::notice::No PDBs detected (content/tools package). Skipping .snupkg generation."
mono /usr/local/bin/nuget pack "${PKG_ID}.nuspec" \
-OutputDirectory "../${OUT_DIR}" \
-NonInteractive
fi
cd ..
echo "Generated outputs:"
ls -la "${OUT_DIR}" || true
nupkgs=("${OUT_DIR}"/*.nupkg)
if [[ ${#nupkgs[@]} -lt 1 ]]; then
echo "::error::No .nupkg generated."
exit 1
fi
- name: 🚀 Push .nupkg to GitHub Packages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
for pkg_path in "${OUT_DIR}"/*.nupkg; do
echo "Pushing: $pkg_path"
mono /usr/local/bin/nuget push "$pkg_path" \
-Source "${NUGET_SOURCE}" \
-ApiKey "${GITHUB_TOKEN}" \
-NonInteractive \
-SkipDuplicate
done
- name: 📦 Upload packages as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: nuget-packages
path: |
${{ env.OUT_DIR }}/*.nupkg
${{ env.OUT_DIR }}/*.snupkg
retention-days: 30
- name: 🧹 Clean up
if: always()
shell: bash
run: |
set -euo pipefail
rm -rf "${STAGE_DIR}" "${OUT_DIR}" || true