Skip to content

chore(deps): update actions/upload-artifact action to v6 #66

chore(deps): update actions/upload-artifact action to v6

chore(deps): update actions/upload-artifact action to v6 #66

Workflow file for this run

name: CI Build
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: write
packages: write
# Only allow one workflow run per branch at a time
# New commits cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build ${{ matrix.os }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# Linux x64
- os: linux
arch: x64
runner: ubuntu-latest
artifact_name: IntuneManager.bin
output_path: IntuneManager
asset_name: IntuneManager-linux-x64.bin
content_type: application/octet-stream
# Linux ARM64
- os: linux
arch: arm64
runner: ubuntu-24.04-arm
artifact_name: IntuneManager.bin
output_path: IntuneManager
asset_name: IntuneManager-linux-arm64.bin
content_type: application/octet-stream
# Windows x64
# - os: windows
# arch: x64
# runner: windows-latest
# artifact_name: IntuneManager.exe
# output_path: IntuneManager.exe
# asset_name: IntuneManager-windows-x64.exe
# content_type: application/vnd.microsoft.portable-executable
# # Windows ARM64
# - os: windows
# arch: arm64
# runner: windows-11-arm
# artifact_name: IntuneManager.exe
# output_path: IntuneManager.exe
# asset_name: IntuneManager-windows-arm64.exe
# content_type: application/vnd.microsoft.portable-executable
# macOS Intel
- os: macos
arch: x64
runner: macos-13
artifact_name: IntuneManager
output_path: intune_manager.app
asset_name: IntuneManager-macos-x64.app.zip
content_type: application/zip
# macOS ARM64
- os: macos
arch: arm64
runner: macos-latest
artifact_name: IntuneManager
output_path: intune_manager.app
asset_name: IntuneManager-macos-arm64.app.zip
content_type: application/zip
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python 3.13
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
# Setup compiler caching (ccache for Linux/macOS, clcache for Windows)
- name: Setup ccache (Linux/macOS)
if: runner.os != 'Windows'
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ matrix.os }}-${{ matrix.arch }}
restore-keys: |
${{ matrix.os }}-${{ matrix.arch }}-
${{ matrix.os }}-
max-size: 2G
verbose: 2
create-symlink: true # Create compiler wrapper symlinks so gcc/clang use ccache
- name: Configure ccache (Linux/macOS)
if: runner.os != 'Windows'
run: |
echo "NUITKA_CCACHE_BINARY=$(which ccache)" >> $GITHUB_ENV
echo "::group::ccache configuration"
ccache --version
ccache --show-config
ccache --zero-stats
ccache --max-size=2G
echo "::endgroup::"
echo "::group::Compiler symlinks verification"
which gcc || true
which g++ || true
which clang || true
which clang++ || true
ls -la "$(which gcc)" || true
ls -la "$(which g++)" || true
echo "::endgroup::"
# Windows compiler caching with clcache (built into Nuitka)
# - name: Configure clcache (Windows)
# if: runner.os == 'Windows'
# shell: bash
# run: |
# echo "NUITKA_CLCACHE_BINARY=clcache" >> $GITHUB_ENV
# echo "CLCACHE_DIR=${{ runner.temp }}/clcache" >> $GITHUB_ENV
# echo "CLCACHE_COMPRESS=1" >> $GITHUB_ENV
# echo "CLCACHE_COMPRESSLEVEL=6" >> $GITHUB_ENV
# # Increase cache size for better hit rate
# echo "CLCACHE_HARDLINK=1" >> $GITHUB_ENV # Use hardlinks to save space
# mkdir -p "${{ runner.temp }}/clcache"
# Cache clcache for Windows (stable key per platform, clcache handles source changes internally)
# - name: Cache clcache (Windows)
# if: runner.os == 'Windows'
# uses: actions/cache@v4
# with:
# path: ${{ runner.temp }}/clcache
# key: clcache-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('pyproject.toml') }}
# restore-keys: |
# clcache-${{ matrix.os }}-${{ matrix.arch }}-
# Configure Nuitka cache directories
- name: Configure Nuitka environment
shell: bash
run: |
echo "NUITKA_CACHE_DIR=${{ runner.temp }}/nuitka-cache" >> $GITHUB_ENV
echo "NUITKA_CACHE_DIR_DOWNLOADS=${{ runner.temp }}/nuitka-cache/downloads" >> $GITHUB_ENV
echo "NUITKA_CACHE_DIR_CCACHE=${{ runner.temp }}/nuitka-cache/ccache" >> $GITHUB_ENV
echo "NUITKA_CACHE_DIR_CLCACHE=${{ runner.temp }}/nuitka-cache/clcache" >> $GITHUB_ENV
echo "NUITKA_CACHE_DIR_BYTECODE=${{ runner.temp }}/nuitka-cache/bytecode" >> $GITHUB_ENV
echo "NUITKA_CACHE_DIR_DLL_DEPENDENCIES=${{ runner.temp }}/nuitka-cache/dll-dependencies" >> $GITHUB_ENV
mkdir -p "${{ runner.temp }}/nuitka-cache"
# Cache Nuitka build directory (stable key per platform, invalidates only when dependencies/config change)
- name: Cache Nuitka build
uses: actions/cache@v4
with:
path: |
intune_manager.build
intune_manager.dist
intune_manager.onefile-build
${{ runner.temp }}/nuitka-cache
key: nuitka-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('pyproject.toml', 'src/intune_manager/__main__.py') }}
restore-keys: |
nuitka-${{ matrix.os }}-${{ matrix.arch }}-
- name: Install dependencies
run: uv sync --all-groups
- name: Get version from pyproject.toml
id: version
shell: bash
run: |
# For main branch CI builds, use a fixed development version
VERSION="0.0.0.1"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION (CI build)"
- name: Build with Nuitka
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
# Show compiler cache status before build
if [ "${{ runner.os }}" != "Windows" ]; then
echo "::group::Pre-build ccache statistics"
ccache --show-stats
echo "::endgroup::"
fi
# Run Nuitka build with caching enabled
uv run nuitka \
--product-version="$VERSION" \
--assume-yes-for-downloads \
src/intune_manager
- name: Verify build output
shell: bash
run: |
echo "Checking for build output at: ${{ matrix.output_path }}"
if [ -e "${{ matrix.output_path }}" ]; then
echo "✓ Build artifact found"
ls -lh "${{ matrix.output_path }}"
else
echo "✗ Build artifact not found!"
echo "Contents of / directory:"
ls -larth
echo "Contents of src directory:"
ls -larth src/
exit 1
fi
# ============================================
# Code Sign macOS App Bundle
# ============================================
- name: Code sign macOS app bundle
if: matrix.os == 'macos'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.MACOS_CI_KEYCHAIN_PWD }}
run: |
echo "::group::Decode and verify certificate"
# Decode certificate from base64
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
# Verify the .p12 file is valid
if ! openssl pkcs12 -info -in certificate.p12 -passin pass:"$MACOS_CERTIFICATE_PWD" -noout 2>/dev/null; then
echo "❌ Error: Invalid .p12 certificate file or incorrect password"
exit 1
fi
echo "✓ Certificate file decoded and validated successfully"
echo "::endgroup::"
echo "::group::Create and configure keychain"
# Create temporary keychain
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
echo "✓ Keychain created and unlocked"
echo "::endgroup::"
echo "::group::Import certificate"
# Import certificate to keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
echo "✓ Certificate imported to keychain"
echo "::endgroup::"
echo "::group::Verify signing identity"
# List available identities to verify import succeeded
echo "Available code signing identities:"
security find-identity -v -p codesigning build.keychain
# Verify the specific identity we need exists
if ! security find-identity -v -p codesigning build.keychain | grep -q "$MACOS_CERTIFICATE_NAME"; then
echo "❌ Error: Required signing identity not found: $MACOS_CERTIFICATE_NAME"
echo "Available identities:"
security find-identity -v -p codesigning build.keychain
exit 1
fi
echo "✓ Signing identity verified: $MACOS_CERTIFICATE_NAME"
echo "::endgroup::"
echo "::group::Sign app bundle"
# Sign the app bundle (deep sign all nested code)
/usr/bin/codesign --force --deep --sign "$MACOS_CERTIFICATE_NAME" \
--options runtime \
--timestamp \
"${{ matrix.output_path }}" -v
echo "✓ App bundle signed successfully"
echo "::endgroup::"
echo "::group::Verify signature"
# Verify the signature
/usr/bin/codesign --verify --deep --strict --verbose=2 "${{ matrix.output_path }}"
echo "✓ Signature verification passed"
echo "::endgroup::"
# Clean up certificate file
rm certificate.p12
# ============================================
# Notarize macOS App Bundle
# ============================================
- name: Notarize macOS app bundle
if: matrix.os == 'macos'
env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
MACOS_NOTARIZATION_PWD: ${{ secrets.MACOS_NOTARIZATION_PWD }}
run: |
# Store notarization credentials
xcrun notarytool store-credentials "notarytool-profile" \
--apple-id "$MACOS_NOTARIZATION_APPLE_ID" \
--team-id "$MACOS_NOTARIZATION_TEAM_ID" \
--password "$MACOS_NOTARIZATION_PWD"
# Create a zip for notarization (required format)
ditto -c -k --keepParent "${{ matrix.output_path }}" notarization.zip
# Submit for notarization and wait for result
echo "Submitting app for notarization..."
xcrun notarytool submit notarization.zip \
--keychain-profile "notarytool-profile" \
--wait
# Check if notarization succeeded
NOTARIZATION_STATUS=$?
if [ $NOTARIZATION_STATUS -ne 0 ]; then
echo "❌ Notarization failed! Fetching logs..."
# Get the latest submission ID for logs
SUBMISSION_ID=$(xcrun notarytool history --keychain-profile "notarytool-profile" | head -2 | tail -1 | awk '{print $5}')
xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "notarytool-profile"
exit 1
fi
# Staple the notarization ticket to the app
echo "Stapling notarization ticket..."
xcrun stapler staple "${{ matrix.output_path }}"
# Verify stapling
xcrun stapler validate "${{ matrix.output_path }}"
# Clean up
rm notarization.zip
echo "✅ Code signing and notarization complete!"
# Upload compilation report for debugging (short retention since it's for troubleshooting)
- name: Upload compilation report
if: always()
uses: actions/upload-artifact@v6
with:
name: compilation-report-${{ matrix.os }}-${{ matrix.arch }}
path: compilation-report.xml
retention-days: 5
if-no-files-found: warn
# Package macOS .app bundles using ditto (preserves app bundle structure)
- name: Package macOS app bundle
if: matrix.os == 'macos'
shell: bash
run: |
# Use ditto with --keepParent to ensure .app bundle structure is preserved on extraction
ditto -c -k --sequesterRsrc --keepParent "${{ matrix.output_path }}" "${{ matrix.asset_name }}"
echo "Created: ${{ matrix.asset_name }}"
ls -lh "${{ matrix.asset_name }}"
# For non-macOS, just copy the binary
- name: Prepare package asset
if: matrix.os != 'macos'
shell: bash
run: |
cp "${{ matrix.output_path }}" "${{ matrix.asset_name }}"
echo "Prepared: ${{ matrix.asset_name }}"
ls -lh "${{ matrix.asset_name }}"
# Upload as workflow artifact (7 day retention since we're publishing to packages)
- name: Upload workflow artifact
uses: actions/upload-artifact@v6
with:
name: IntuneManager-${{ steps.version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}
path: ${{ matrix.asset_name }}
retention-days: 7
compression-level: 9
# Publish to GitHub Packages as rolling pre-release
- name: Determine release tag
id: release_tag
shell: bash
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# For PRs, use pr-{number}-{os}-{arch} format
TAG="ci-pr-${{ github.event.pull_request.number }}-${{ matrix.os }}-${{ matrix.arch }}"
else
# For main branch, use main-{os}-{arch} format
TAG="ci-main-${{ matrix.os }}-${{ matrix.arch }}"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Release tag: $TAG"
- name: Publish to GitHub Packages
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.release_tag.outputs.tag }}
name: "CI Build: ${{ matrix.os }}-${{ matrix.arch }} (${{ github.ref_name }})"
files: ${{ matrix.asset_name }}
prerelease: true
draft: false
make_latest: false
body: |
**Automated CI Build**
- **Platform**: ${{ matrix.os }}-${{ matrix.arch }}
- **Branch**: ${{ github.ref_name }}
- **Commit**: ${{ github.sha }}
- **Build Date**: ${{ github.event.head_commit.timestamp }}
This is an automated build from the CI pipeline.
For stable releases, see the [Releases page](https://github.com/${{ github.repository }}/releases).
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ============================================
# Cleanup Keychain (always runs, even on failure)
# ============================================
- name: Clean up keychain
if: always() && matrix.os == 'macos'
run: |
security delete-keychain build.keychain || true
- name: Save compiler cache statistics
if: always()
shell: bash
run: |
if [ "${{ runner.os }}" != "Windows" ]; then
echo "::group::ccache statistics"
ccache --show-stats || true
ccache --show-config || true
echo "Cache directory: $(ccache --get-config cache_dir)" || true
echo "Cache size: $(du -sh $(ccache --get-config cache_dir) 2>/dev/null || echo 'N/A')"
echo "::endgroup::"
# Add to job summary
echo "## 🔧 Compiler Cache Statistics (${{ matrix.os }}-${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### ccache (Linux/macOS)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Extract key metrics from ccache stats
STATS=$(ccache --show-stats 2>/dev/null || echo "")
CACHE_DIR=$(ccache --get-config cache_dir 2>/dev/null || echo "N/A")
CACHE_SIZE=$(du -sh "$CACHE_DIR" 2>/dev/null | cut -f1 || echo "N/A")
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
# Parse ccache stats for key metrics
if [ -n "$STATS" ]; then
CACHE_HIT=$(echo "$STATS" | grep -i "cache hit" | head -1 | awk '{print $NF}' || echo "N/A")
CACHE_MISS=$(echo "$STATS" | grep -i "cache miss" | head -1 | awk '{print $NF}' || echo "N/A")
HIT_RATE=$(echo "$STATS" | grep -i "hit rate" | head -1 | awk '{print $NF}' || echo "N/A")
echo "| Cache hits | ${CACHE_HIT} |" >> $GITHUB_STEP_SUMMARY
echo "| Cache misses | ${CACHE_MISS} |" >> $GITHUB_STEP_SUMMARY
echo "| Hit rate | ${HIT_RATE} |" >> $GITHUB_STEP_SUMMARY
fi
echo "| Cache directory | \`${CACHE_DIR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Cache size | ${CACHE_SIZE} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
else
echo "::group::clcache statistics"
echo "CLCACHE_DIR: $CLCACHE_DIR"
if [ -d "$CLCACHE_DIR" ]; then
echo "Cache size: $(du -sh $CLCACHE_DIR 2>/dev/null || echo 'N/A')"
echo "Number of cached files: $(find $CLCACHE_DIR -type f | wc -l)"
else
echo "No clcache directory found"
fi
echo "::endgroup::"
# Add to job summary
echo "## 🔧 Compiler Cache Statistics (${{ matrix.os }}-${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### clcache (Windows)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
if [ -d "$CLCACHE_DIR" ]; then
CACHE_SIZE=$(du -sh "$CLCACHE_DIR" 2>/dev/null | cut -f1 || echo "N/A")
NUM_FILES=$(find "$CLCACHE_DIR" -type f | wc -l)
echo "| Cache directory | \`${CLCACHE_DIR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Cache size | ${CACHE_SIZE} |" >> $GITHUB_STEP_SUMMARY
echo "| Cached files | ${NUM_FILES} |" >> $GITHUB_STEP_SUMMARY
else
echo "| Status | ❌ No cache directory found |" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
fi