Skip to content

Schema Generation

Schema Generation #33

name: Schema Generation
on:
schedule:
- cron: '0 1 * * 6' # Saturday at 1 AM UTC
workflow_dispatch:
permissions:
contents: write
pull-requests: write
env:
TARGET_BRANCH: schema-updates
# This branch is used for automatic schema updates
FALLBACK_BRANCH: develop
jobs:
generate-schemas:
runs-on: windows-latest
steps:
- name: Resolve branch to use
id: resolve_branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_BRANCH: ${{ env.TARGET_BRANCH }}
FALLBACK_BRANCH: ${{ env.FALLBACK_BRANCH }}
run: |
$REPO = "github.com/$env:GITHUB_REPOSITORY"
$targetBranchExists = git ls-remote --heads "https://x-access-token:$env:GITHUB_TOKEN@$REPO" $env:TARGET_BRANCH
if ($targetBranchExists) {
"branch=$env:TARGET_BRANCH" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "Using existing branch: $env:TARGET_BRANCH"
} else {
"branch=$env:FALLBACK_BRANCH" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "Using fallback branch: $env:FALLBACK_BRANCH"
}
shell: powershell
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ steps.resolve_branch.outputs.branch }}
fetch-depth: 0
- name: Show which branch was used
run: "echo \"Using branch: ${{ steps.resolve_branch.outputs.branch }}\""
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Setup NuGet.exe for use with actions
uses: NuGet/[email protected]
- name: Create .repos directory
run: mkdir .repos
shell: cmd
- name: Read repository list
id: read-repos
run: |
$repos = Get-Content "Included-repositories.txt" | Where-Object { $_.Trim() -ne "" }
$repoList = $repos -join ","
echo "repos=$repoList" >> $env:GITHUB_OUTPUT
shell: powershell
- name: Clone all repositories
run: |
$repos = "${{ steps.read-repos.outputs.repos }}" -split ","
foreach ($repo in $repos) {
$repo = $repo.Trim()
if ($repo -ne "") {
Write-Host "Cloning $repo..."
$repoName = $repo.Split('/')[1]
git clone https://github.com/$repo.git .repos/$repoName --branch develop --depth 1
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to clone develop branch for $repo, trying main..."
git clone https://github.com/$repo.git .repos/$repoName --branch main --depth 1
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to clone main branch for $repo, trying master..."
git clone https://github.com/$repo.git .repos/$repoName --branch master --depth 1
}
}
}
}
shell: powershell
- name: Analyze dependencies and determine build order
id: build-order
timeout-minutes: 5
run: |
# Get list of cloned repositories
$validRepos = @()
if (Test-Path ".repos") {
Get-ChildItem ".repos" -Directory | ForEach-Object { $validRepos += $_.Name }
}
if ($validRepos.Count -eq 0) {
Write-Host "No valid repositories to analyze"
echo "order=" >> $env:GITHUB_OUTPUT
exit 0
}
Write-Host "Analyzing dependency order for $($validRepos.Count) repositories..."
# Create dependency map
$dependencyMap = @{}
foreach ($repo in $validRepos) {
$dependencyMap[$repo] = @()
$depsFile = ".repos\$repo\dependencies.txt"
if (Test-Path $depsFile) {
Write-Host "Found dependencies.txt in $repo"
$repoDeps = Get-Content $depsFile | ForEach-Object {
$line = $_.Trim()
if (![string]::IsNullOrWhiteSpace($line) -and !$line.StartsWith("#")) {
# Extract just the repo name from ORG/REPO format
$repoName = $line.Split('/')[-1]
return $repoName
}
} | Where-Object { $_ -ne $null }
# Only include dependencies that are in our valid repos list
$validDeps = $repoDeps | Where-Object { $validRepos -contains $_ }
$dependencyMap[$repo] = $validDeps
if ($validDeps.Count -gt 0) {
Write-Host "$repo depends on: $($validDeps -join ', ')"
} else {
Write-Host "$repo has no dependencies in current build set"
}
} else {
Write-Host "$repo has no dependencies.txt file"
}
}
# Topological sort to determine build order
$script:buildOrder = @()
$script:visited = @{}
$script:visiting = @{}
function Get-BuildOrder {
param([string]$repo)
Write-Host "Processing $repo for build order..."
if ($script:visiting.ContainsKey($repo)) {
Write-Host "⚠ Circular dependency detected involving $repo"
return $false
}
if ($script:visited.ContainsKey($repo)) {
Write-Host "$repo already processed"
return $true
}
$script:visiting[$repo] = $true
Write-Host "Visiting dependencies of $repo..."
# Visit all dependencies first
if ($dependencyMap.ContainsKey($repo) -and $dependencyMap[$repo].Count -gt 0) {
Write-Host "$repo has dependencies: $($dependencyMap[$repo] -join ', ')"
foreach ($dep in $dependencyMap[$repo]) {
if ($validRepos -contains $dep) {
Write-Host "Processing dependency: $dep"
$result = Get-BuildOrder -repo $dep
if (-not $result) {
Write-Host "Failed to process dependency $dep"
return $false
}
} else {
Write-Host "Skipping dependency $dep (not in current build set)"
}
}
} else {
Write-Host "$repo has no dependencies"
}
$script:visiting.Remove($repo)
$script:visited[$repo] = $true
$script:buildOrder += $repo
Write-Host "Added $repo to build order (position $($script:buildOrder.Count))"
return $true
}
# Process all repositories
Write-Host "Starting topological sort for $($validRepos.Count) repositories..."
$sortSuccess = $true
foreach ($repo in $validRepos) {
if (-not $script:visited.ContainsKey($repo)) {
Write-Host "Processing root repository: $repo"
$result = Get-BuildOrder -repo $repo
if (-not $result) {
$sortSuccess = $false
break
}
}
}
if (-not $sortSuccess) {
Write-Host "⚠ Topological sort failed due to circular dependencies. Using original order."
$script:buildOrder = $validRepos
}
$buildOrder = $script:buildOrder
Write-Host "Determined build order:"
for ($i = 0; $i -lt $buildOrder.Count; $i++) {
Write-Host " $($i + 1). $($buildOrder[$i])"
}
$orderString = $buildOrder -join ","
Write-Host "Final build order: $orderString"
echo "order=$orderString" >> $env:GITHUB_OUTPUT
shell: pwsh
- name: Build repositories in dependency order (clean detection)
shell: pwsh
run: |
$buildOrder = "${{ steps.build-order.outputs.order }}" -split ","
foreach ($repo in $buildOrder) {
$repo = $repo.Trim()
if (-not $repo) { continue }
Push-Location ".repos/$repo"
Write-Host "Building $repo..."
# Prefer a solution if present; otherwise build each csproj
$solutions = Get-ChildItem -Filter *.sln
if ($solutions) {
# Build the first solution (or loop them if you have multiple)
$sln = $solutions[0].FullName
../../.github/workflows/build.ps1 -SolutionOrProjectPath $sln
} else {
$projects = Get-ChildItem -Recurse -Filter *.csproj | Where-Object { $_.Directory.Name -notmatch '\.ci' }
foreach ($proj in $projects) {
../../.github/workflows/build.ps1 -SolutionOrProjectPath $proj.FullName
}
}
Pop-Location
}
- name: Build Generation solution
run: |
if (Test-Path ".ci/Generation/SchemaGeneration") {
Write-Host "Building SchemaGeneration solution..."
Push-Location ".ci/Generation/SchemaGeneration"
# Build the solution file
$solutionFile = "SchemaGeneration.sln"
if (Test-Path $solutionFile) {
Write-Host "Building solution: $solutionFile"
dotnet build "$solutionFile" --configuration Release --verbosity minimal
if ($LASTEXITCODE -ne 0) {
Write-Host "Dotnet build failed, trying MSBuild fallback..."
nuget restore "$solutionFile"
msbuild "$solutionFile" /p:Configuration=Release /p:Platform="Any CPU" /verbosity:minimal
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed"
exit 1
}
}
} else {
Write-Host "SchemaGeneration.sln not found"
exit 1
}
Pop-Location
} else {
Write-Host "SchemaGeneration folder not found at .ci/Generation/SchemaGeneration"
exit 1
}
shell: powershell
- name: Run Generation executable
run: |
Write-Host "Looking for SchemaGeneration executable..."
# Look for the executable in the Release build output first, then Debug
$exePath = ".ci/Generation/SchemaGeneration/SchemaGeneration/bin/Release/net8.0/SchemaGeneration.exe"
if (-not (Test-Path $exePath)) {
$exePath = ".ci/Generation/SchemaGeneration/SchemaGeneration/bin/Debug/net8.0/SchemaGeneration.exe"
}
if (Test-Path $exePath) {
$branchName = "${{ github.head_ref || github.ref_name }}"
Write-Host "Running SchemaGeneration with branch: $branchName"
Write-Host "Executable path: $exePath"
& "$exePath" "$branchName"
if ($LASTEXITCODE -ne 0) {
Write-Host "SchemaGeneration failed with exit code: $LASTEXITCODE"
exit 1
}
} else {
# Fallback: try to run with dotnet
$dllPath = ".ci/Generation/SchemaGeneration/SchemaGeneration/bin/Release/net8.0/SchemaGeneration.dll"
if (-not (Test-Path $dllPath)) {
$dllPath = ".ci/Generation/SchemaGeneration/SchemaGeneration/bin/Debug/net8.0/SchemaGeneration.dll"
}
if (Test-Path $dllPath) {
$branchName = "${{ github.head_ref || github.ref_name }}"
Write-Host "Running SchemaGeneration DLL with branch: $branchName"
Write-Host "DLL path: $dllPath"
dotnet "$dllPath" "$branchName"
if ($LASTEXITCODE -ne 0) {
Write-Host "SchemaGeneration failed with exit code: $LASTEXITCODE"
exit 1
}
} else {
Write-Host "SchemaGeneration executable or DLL not found"
exit 1
}
}
shell: powershell
- name: Check for changes
id: check-changes
run: |
git add -A
$changes = git diff --cached --name-only
if ($changes) {
echo "has_changes=true" >> $env:GITHUB_OUTPUT
Write-Host "Changes detected:"
$changes | ForEach-Object { Write-Host " $_" }
} else {
echo "has_changes=false" >> $env:GITHUB_OUTPUT
Write-Host "No changes detected"
}
shell: powershell
- name: Commit and push changes
if: steps.check-changes.outputs.has_changes == 'true'
run: |
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
# Create and checkout target branch (or switch if exists)
$branch_name = "${{ env.TARGET_BRANCH }}"
git checkout $branch_name 2>$null || git checkout -b $branch_name
# Add all changes (including new files and directories)
git add -A
# Check if there are any changes to commit
$changes = git status --porcelain
if ($changes) {
Write-Host "Changes detected, committing..."
git commit -m "Automated schema generation update - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`n`nGenerated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')`n`nThis commit contains automatically generated JSON schema updates based on the latest BHoM repositories."
# Push the branch
git push origin $branch_name
Write-Host "Successfully pushed changes to branch: $branch_name"
# Check if a PR already exists for this branch
$existingPr = gh pr list --head $branch_name --base develop --json number --jq '.[0].number'
if ($existingPr) {
Write-Host "PR already exists (#$existingPr), adding a comment..."
gh pr comment $existingPr --body "Automated schema generation updates pushed to branch '$branch_name' on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')."
Write-Host "✓ Comment added to existing PR"
} else {
Write-Host "Creating pull request..."
$prTitle = "Automated Schema Updates - $(Get-Date -Format 'yyyy-MM-dd')"
$prBody = "## Automated Schema Generation`n`nThis pull request contains automatically generated JSON schema updates.`n`n**Generation Details:**`n- Generated on: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')`n- Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`n`n**Changes:**`nThe schemas have been regenerated based on the latest versions of all BHoM repositories listed in \`\`Included-repositories.txt\`\`.`n`nPlease review the changes before merging."
gh pr create --title "$prTitle" --body "$prBody" --head $branch_name --base develop
Write-Host "✓ Pull request created successfully"
}
} else {
Write-Host "No changes to commit"
}
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}