Skip to content

feat: Refactor deployment services to enhance error handling and stat… #1001

feat: Refactor deployment services to enhance error handling and stat…

feat: Refactor deployment services to enhance error handling and stat… #1001

name: Build All Projects
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
schedule:
- cron: "0 6 * * *" # daily at 6 AM UTC
env:
DOTNET_VERSION: "10.0.x"
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
check-ivy-version:
name: Check Ivy Version
runs-on: ubuntu-latest
outputs:
should-build: ${{ steps.check.outputs.should-build }}
latest-version: ${{ steps.check.outputs.latest-version }}
current-version: ${{ steps.check.outputs.current-version }}
has-multiple-versions: ${{ steps.get-current.outputs.has-multiple-versions }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get latest Ivy version from NuGet
id: get-latest
run: |
echo "Fetching latest Ivy version from NuGet..."
IVY_VERSION=$(curl -s "https://api.nuget.org/v3-flatcontainer/ivy/index.json" \
| grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' \
| tail -1 | tr -d '"' || echo "")
if [ -z "$IVY_VERSION" ]; then
echo "Error: Could not fetch Ivy version"
exit 1
fi
echo "Latest Ivy version: $IVY_VERSION"
echo "latest-version=$IVY_VERSION" >> $GITHUB_OUTPUT
- name: Get current Ivy version from csproj
id: get-current
run: |
versions=$(find . -name "*.csproj" -not -path "*/bin/*" -not -path "*/obj/*" \
| xargs grep -h -oE 'PackageReference Include="Ivy" Version="[^"]*"' 2>/dev/null \
| grep -oE 'Version="[^"]*"' \
| sed 's/Version="\(.*\)"/\1/' | sort -u)
unique_versions=$(echo "$versions" | sort -u)
unique_count=$(echo "$unique_versions" | grep -c . || echo "0")
current_version=$(echo "$unique_versions" | head -1 | tr -d '\n' | tr -d '\r')
if [ -z "$current_version" ]; then
echo "current-version=unknown" >> $GITHUB_OUTPUT
echo "has-multiple-versions=false" >> $GITHUB_OUTPUT
echo "Current Ivy version: unknown"
elif [ "$unique_count" -gt 1 ]; then
echo "Current Ivy version: $current_version (multiple versions detected)"
echo "Found versions: $(echo "$unique_versions" | tr '\n' ',' | sed 's/,$//')"
echo "current-version=$current_version" >> $GITHUB_OUTPUT
echo "has-multiple-versions=true" >> $GITHUB_OUTPUT
else
echo "Current Ivy version: $current_version"
echo "current-version=$current_version" >> $GITHUB_OUTPUT
echo "has-multiple-versions=false" >> $GITHUB_OUTPUT
fi
- name: Determine if build is needed
id: check
run: |
latest="${{ steps.get-latest.outputs.latest-version }}"
current="${{ steps.get-current.outputs.current-version }}"
latest_base=$(echo "$latest" | cut -d'-' -f1)
current_base=$(echo "$current" | cut -d'-' -f1)
# Always build for PRs and manual triggers
if [ "${{ github.event_name }}" == "push" ] || [ "${{ github.event_name }}" == "pull_request" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "should-build=true" >> $GITHUB_OUTPUT
echo "latest-version=$latest" >> $GITHUB_OUTPUT
echo "current-version=$current" >> $GITHUB_OUTPUT
elif [ "$latest_base" != "$current_base" ]; then
echo "should-build=true" >> $GITHUB_OUTPUT
echo "latest-version=$latest" >> $GITHUB_OUTPUT
echo "current-version=$current" >> $GITHUB_OUTPUT
else
echo "should-build=false" >> $GITHUB_OUTPUT
echo "latest-version=$latest" >> $GITHUB_OUTPUT
echo "current-version=$current" >> $GITHUB_OUTPUT
fi
discover-projects:
name: Discover Projects
runs-on: ubuntu-latest
needs: [check-ivy-version, update-ivy-version]
if: needs.check-ivy-version.outputs.should-build == 'true'
outputs:
projects: ${{ steps.find-projects.outputs.projects }}
project-count: ${{ steps.find-projects.outputs.project-count }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Download updated csproj files
if: needs.update-ivy-version.outputs.has-changes == 'true'
uses: actions/download-artifact@v4
with:
name: updated-csproj-files
path: .
- name: Find all .NET projects
id: find-projects
run: |
projects=$(find . -name "*.csproj" -not -path "*/bin/*" -not -path "*/obj/*" | sort)
project_array="["
first=true
while IFS= read -r project; do
if [ "$first" = true ]; then
first=false
else
project_array="$project_array,"
fi
dir_name=$(dirname "$project" | xargs basename)
project_array="$project_array{\"path\":\"$project\",\"name\":\"$dir_name\"}"
done <<< "$projects"
project_array="$project_array]"
project_count=$(echo "$projects" | wc -l)
echo "projects=$project_array" >> $GITHUB_OUTPUT
echo "project-count=$project_count" >> $GITHUB_OUTPUT
build-projects:
name: Build
runs-on: ubuntu-latest
needs: [check-ivy-version, discover-projects, update-ivy-version]
if: needs.check-ivy-version.outputs.should-build == 'true' && needs.discover-projects.outputs.project-count > 0
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover-projects.outputs.projects) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Download updated csproj files
if: needs.update-ivy-version.outputs.has-changes == 'true'
uses: actions/download-artifact@v4
with:
name: updated-csproj-files
path: .
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Display .NET info
run: dotnet --info
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore dependencies
run: dotnet restore "${{ matrix.project.path }}"
- name: Build project
id: build
run: |
echo "🔨 Building: ${{ matrix.project.name }}"
dotnet build "${{ matrix.project.path }}" \
--configuration Release \
--no-restore \
--verbosity normal
- name: Save build result
if: always()
run: |
mkdir -p build-results
# Create unique filename from path (replace / and . with -)
SAFE_NAME=$(echo "${{ matrix.project.path }}" | sed 's/[\/\.]/-/g')
if [ "${{ steps.build.outcome }}" == "success" ]; then
echo "success" > "build-results/${SAFE_NAME}.txt"
else
echo "failure" > "build-results/${SAFE_NAME}.txt"
fi
echo "SAFE_NAME=${SAFE_NAME}" >> $GITHUB_ENV
- name: Upload build result
if: always()
uses: actions/upload-artifact@v4
with:
name: build-result-${{ env.SAFE_NAME }}
path: build-results/${{ env.SAFE_NAME }}.txt
retention-days: 1
- name: Upload build logs (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: build-logs-${{ env.SAFE_NAME }}
path: |
**/bin/**/*.log
**/obj/**/*.log
retention-days: 7
detect-resolved-version:
name: Detect Resolved Ivy Version
runs-on: ubuntu-latest
needs: [check-ivy-version, build-projects]
if: always() && needs.check-ivy-version.outputs.should-build == 'true'
outputs:
resolved-version: ${{ steps.get-version.outputs.resolved-version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Get resolved Ivy version
id: get-version
run: |
echo "🔍 Determining resolved Ivy version from packages..."
project_file=$(find . -name "*.csproj" -not -path "*/bin/*" -not -path "*/obj/*" | head -1)
if [ -n "$project_file" ]; then
dotnet restore "$project_file" --verbosity quiet
project_dir=$(dirname "$project_file")
assets_file="$project_dir/obj/project.assets.json"
if [ -f "$assets_file" ]; then
# Extract Ivy version from project.assets.json
# 1. Look at .libraries
# 2. Filter entries where key starts with "Ivy/"
# 3. Split key on "/" and take version part
resolved_version=$(jq -r '.libraries | to_entries[] | select(.key | startswith("Ivy/")) | .key | split("/")[1]' "$assets_file" | head -1)
if [ -n "$resolved_version" ] && [ "$resolved_version" != "null" ]; then
echo "resolved-version=$resolved_version" >> $GITHUB_OUTPUT
else
echo "resolved-version=${{ needs.check-ivy-version.outputs.latest-version }}" >> $GITHUB_OUTPUT
fi
else
echo "resolved-version=${{ needs.check-ivy-version.outputs.latest-version }}" >> $GITHUB_OUTPUT
fi
else
echo "resolved-version=unknown" >> $GITHUB_OUTPUT
fi
update-ivy-version:
name: Update Ivy Version
runs-on: ubuntu-latest
needs: check-ivy-version
if: needs.check-ivy-version.outputs.should-build == 'true'
outputs:
has-changes: ${{ steps.save-changes.outputs.has-changes }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Update Ivy version in csproj files
run: |
IVY_VERSION="${{ needs.check-ivy-version.outputs.latest-version }}"
echo "Comparing against latest stable Ivy version: $IVY_VERSION"
packages=(
"Ivy"
"Ivy.Auth.Supabase"
"Ivy.Auth.Authelia"
"Ivy.Auth.Auth0"
"Ivy.Auth.MicrosoftEntra"
"Ivy.Analyser"
"Ivy.Auth.GitHub"
"Ivy.Auth.Clerk"
"Ivy.Auth.Sliplan"
)
# Shared function: check version guards and update a file
update_package_version() {
local file="$1"
local package="$2"
local pattern="$3" # regex to extract current version
local replace="$4" # sed expression to replace version
proj_version=$(grep -oE "$pattern" "$file" | grep -oE 'Version="[^"]*"' | sed 's/Version="\(.*\)"/\1/' | head -n 1)
if [ -z "$proj_version" ] || [ "$proj_version" == "$IVY_VERSION" ]; then
return
fi
# Always replace wildcard/floating versions (e.g. "1.*", "*", "1.2.*")
if [[ "$proj_version" == *\** ]]; then
echo "Updating: $file ($package) from wildcard $proj_version to $IVY_VERSION"
sed -i "$replace" "$file"
return
fi
proj_base=$(echo "$proj_version" | cut -d'-' -f1)
# Do not downgrade pre-releases of the current stable version
if [[ "$proj_version" == *-* ]] && [ "$proj_base" == "$IVY_VERSION" ]; then
echo "Skipping $file ($package has pre-release $proj_version matching latest stable $IVY_VERSION)"
return
fi
# Do not downgrade to an older stable version if project is on a newer version
higher_version=$(printf "%s\n%s" "$proj_base" "$IVY_VERSION" | sort -V | tail -n1)
if [ "$higher_version" == "$proj_base" ] && [ "$proj_base" != "$IVY_VERSION" ]; then
echo "Skipping $file ($package has newer version $proj_version than latest stable $IVY_VERSION)"
return
fi
echo "Updating: $file ($package) from $proj_version to $IVY_VERSION"
sed -i "$replace" "$file"
}
# --- 1. Update .csproj files (PackageReference) ---
while IFS= read -r -d '' csproj; do
for package in "${packages[@]}"; do
if ! grep -q "PackageReference Include=\"$package\"" "$csproj"; then
continue
fi
update_package_version \
"$csproj" \
"$package" \
"PackageReference Include=\"$package\" Version=\"[^\"]*\"" \
"s|<PackageReference Include=\"$package\" Version=\"[^\"]*\"|<PackageReference Include=\"$package\" Version=\"$IVY_VERSION\"|g"
done
done < <(find . -name "*.csproj" -print0)
# --- 2. Update Directory.Packages.props files (Central Package Management) ---
# CPM uses <PackageVersion Include="Pkg" Version="x.x.x" /> — no version in .csproj
while IFS= read -r -d '' props; do
echo "Checking CPM file: $props"
for package in "${packages[@]}"; do
if ! grep -q "PackageVersion Include=\"$package\"" "$props"; then
continue
fi
update_package_version \
"$props" \
"$package" \
"PackageVersion Include=\"$package\" Version=\"[^\"]*\"" \
"s|<PackageVersion Include=\"$package\" Version=\"[^\"]*\"|<PackageVersion Include=\"$package\" Version=\"$IVY_VERSION\"|g"
done
done < <(find . -name "Directory.Packages.props" -print0)
# --- 3. Update Directory.Build.props / *.props files that pin versions ---
# Some projects define versions via <IvyVersion> or similar MSBuild properties
while IFS= read -r -d '' props; do
# Skip Directory.Packages.props (already handled above)
[[ "$(basename "$props")" == "Directory.Packages.props" ]] && continue
echo "Checking props file: $props"
for package in "${packages[@]}"; do
if ! grep -q "PackageReference Include=\"$package\"" "$props" && \
! grep -q "PackageVersion Include=\"$package\"" "$props"; then
continue
fi
# Handle PackageReference in props
if grep -q "PackageReference Include=\"$package\"" "$props"; then
update_package_version \
"$props" \
"$package" \
"PackageReference Include=\"$package\" Version=\"[^\"]*\"" \
"s|<PackageReference Include=\"$package\" Version=\"[^\"]*\"|<PackageReference Include=\"$package\" Version=\"$IVY_VERSION\"|g"
fi
# Handle PackageVersion in props
if grep -q "PackageVersion Include=\"$package\"" "$props"; then
update_package_version \
"$props" \
"$package" \
"PackageVersion Include=\"$package\" Version=\"[^\"]*\"" \
"s|<PackageVersion Include=\"$package\" Version=\"[^\"]*\"|<PackageVersion Include=\"$package\" Version=\"$IVY_VERSION\"|g"
fi
done
done < <(find . -name "*.props" -not -path "*/bin/*" -not -path "*/obj/*" -print0)
# --- 4. Update file-based programs (.NET 10) ---
# These use "#:package PackageName@version" directives directly in .cs files
while IFS= read -r -d '' csfile; do
for package in "${packages[@]}"; do
if ! grep -q "^#:package ${package}@" "$csfile"; then
continue
fi
proj_version=$(grep -oE "^#:package ${package}@[^ ]+" "$csfile" | sed "s|#:package ${package}@||" | head -n 1)
if [ -z "$proj_version" ] || [ "$proj_version" == "$IVY_VERSION" ]; then
continue
fi
# Always replace wildcard/floating versions (e.g. "1.*", "*", "1.2.*")
if [[ "$proj_version" == *\** ]]; then
echo "Updating: $csfile ($package) from wildcard $proj_version to $IVY_VERSION"
sed -i "s|^#:package ${package}@[^ ]*|#:package ${package}@${IVY_VERSION}|g" "$csfile"
continue
fi
proj_base=$(echo "$proj_version" | cut -d'-' -f1)
# Do not downgrade pre-releases of the current stable version
if [[ "$proj_version" == *-* ]] && [ "$proj_base" == "$IVY_VERSION" ]; then
echo "Skipping $csfile ($package has pre-release $proj_version matching latest stable $IVY_VERSION)"
continue
fi
# Do not downgrade to an older stable version if project is on a newer version
higher_version=$(printf "%s\n%s" "$proj_base" "$IVY_VERSION" | sort -V | tail -n1)
if [ "$higher_version" == "$proj_base" ] && [ "$proj_base" != "$IVY_VERSION" ]; then
echo "Skipping $csfile ($package has newer version $proj_version than latest stable $IVY_VERSION)"
continue
fi
echo "Updating: $csfile ($package) from $proj_version to $IVY_VERSION"
sed -i "s|^#:package ${package}@[^ ]*|#:package ${package}@${IVY_VERSION}|g" "$csfile"
done
done < <(grep -rl "^#:package Ivy" . --include="*.cs" -Z 2>/dev/null)
- name: Save changes for commit
id: save-changes
run: |
if git diff --quiet; then
echo "has-changes=false" >> $GITHUB_OUTPUT
echo "No changes to commit."
else
echo "has-changes=true" >> $GITHUB_OUTPUT
echo "Changes will be committed after successful build."
fi
- name: Upload updated csproj files
if: steps.save-changes.outputs.has-changes == 'true'
uses: actions/upload-artifact@v4
with:
name: updated-csproj-files
path: |
**/*.csproj
**/Directory.Packages.props
**/*.props
**/*.cs
retention-days: 1
check-build-results:
name: Check Build Results
runs-on: ubuntu-latest
needs: [check-ivy-version, discover-projects, build-projects]
if: always() && needs.check-ivy-version.outputs.should-build == 'true'
outputs:
all-succeeded: ${{ steps.calc.outputs.all-succeeded }}
success-count: ${{ steps.calc.outputs.success-count }}
total-count: ${{ steps.calc.outputs.total-count }}
steps:
- name: Download all build results
uses: actions/download-artifact@v4
with:
pattern: build-result-*
path: build-results
merge-multiple: true
- name: Calculate build results
id: calc
run: |
total=${{ needs.discover-projects.outputs.project-count }}
success_count=$(grep -l "success" build-results/*.txt 2>/dev/null | wc -l | tr -d ' ')
failed_count=$(grep -l "failure" build-results/*.txt 2>/dev/null | wc -l | tr -d ' ')
if [ "$success_count" -eq "$total" ]; then
all="true"
else
all="false"
fi
echo "total-count=$total" >> $GITHUB_OUTPUT
echo "success-count=$success_count" >> $GITHUB_OUTPUT
echo "all-succeeded=$all" >> $GITHUB_OUTPUT
echo "## Build Results: $success_count/$total" >> $GITHUB_STEP_SUMMARY
echo "- **Successful:** $success_count" >> $GITHUB_STEP_SUMMARY
echo "- **Failed:** $failed_count" >> $GITHUB_STEP_SUMMARY
if [ "$all" != "true" ]; then
echo "❌ Build failed" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "✅ All builds passed" >> $GITHUB_STEP_SUMMARY
fi
commit-changes:
name: Commit Version Updates
runs-on: ubuntu-latest
needs: [check-ivy-version, update-ivy-version, check-build-results]
if: needs.check-build-results.outputs.all-succeeded == 'true' && needs.update-ivy-version.outputs.has-changes == 'true' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Download updated csproj files
uses: actions/download-artifact@v4
with:
name: updated-csproj-files
path: .
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update Ivy to version ${{ needs.check-ivy-version.outputs.latest-version }}"
git push origin HEAD:${{ github.ref }}
build-summary:
name: Build Summary
runs-on: ubuntu-latest
needs:
[
check-ivy-version,
discover-projects,
build-projects,
check-build-results,
detect-resolved-version,
]
if: always() && needs.check-ivy-version.outputs.should-build == 'true'
steps:
- name: Generate build summary
run: |
echo "# 🚀 Ivy-Examples Build Summary" >> $GITHUB_STEP_SUMMARY
resolved_version="${{ needs.detect-resolved-version.outputs.resolved-version }}"
latest_version="${{ needs.check-ivy-version.outputs.latest-version }}"
success_count="${{ needs.check-build-results.outputs.success-count }}"
total_count="${{ needs.check-build-results.outputs.total-count }}"
echo "## 📦 Ivy Version Information" >> $GITHUB_STEP_SUMMARY
echo "- **Resolved Version:** $resolved_version" >> $GITHUB_STEP_SUMMARY
echo "- **Latest Available:** $latest_version" >> $GITHUB_STEP_SUMMARY
echo "## 📊 Build Results" >> $GITHUB_STEP_SUMMARY
echo "- **Successful:** $success_count/$total_count" >> $GITHUB_STEP_SUMMARY
create-badge:
name: Update Build Badge
runs-on: ubuntu-latest
needs:
[
check-ivy-version,
build-summary,
check-build-results,
detect-resolved-version,
]
if: github.ref == 'refs/heads/main' && always() && needs.check-ivy-version.outputs.should-build == 'true'
steps:
- name: Create build status badge
run: |
if [ "${{ needs.build-summary.result }}" == "success" ]; then
echo "Build passing"
else
echo "Build failing"
fi