Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

root = true

# All files: spaces, 4-space indent, no trailing whitespace, LF-terminated final line, UTF-8.
# All files — space indent (4), LF line endings, trim trailing whitespace, insert final newline, UTF-8.
[*]
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
Expand All @@ -20,6 +21,10 @@ indent_size = 2
[*.{yml,yaml,json}]
indent_size = 2

# Markdown: preserve trailing two-space hard line breaks (CommonMark <br> syntax).
[*.md]
trim_trailing_whitespace = false

[*.cs]
# IDE1006 — Method names must be PascalCase.
dotnet_naming_rule.methods_pascal_case.symbols = method_symbols
Expand Down
5 changes: 2 additions & 3 deletions .github/actions/prepare-runner/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ runs:
with:
dotnet-version: ${{ inputs.dotnet_version }}

# github.action_sha is the DevOps_Toolkit commit SHA — available in composite contexts
# without a git clone or API call. Keying the cache on this SHA ensures a new binary
# is published whenever the action's source changes.
# github.action_sha is the DevOps_Toolkit SHA in composite contexts (no clone needed).
# Cache keyed on this SHA; any source change triggers a cache miss and fresh publish.
- name: Get DevOps_Toolkit HEAD SHA
id: sha
shell: pwsh
Expand Down
17 changes: 6 additions & 11 deletions .github/actions/resolve-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ runs:
path: |
~/.nuget/packages
~\AppData\Local\Microsoft\MSBuild\Cache
# dotnet_version intentionally excluded: NuGet package binaries are SDK-version-agnostic
# for the net8.0 targets used by BHoM repos.
# dotnet_version excluded: NuGet binaries are SDK-version-agnostic for net8.0 targets.
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj','**/global.json','**/nuget.config') }}
restore-keys: ${{ runner.os }}-nuget-

Expand All @@ -81,14 +80,11 @@ runs:
if (Test-Path deps/_order.txt) { Remove-Item deps/_order.txt -Force }
if (Test-Path deps/_selection.txt) { Remove-Item deps/_selection.txt -Force }

# C:\bhom-deps\ — isolated clone root, sits at the drive root so no caller
# repo Directory.Build.props exists above it. Each dependency repo builds
# with only its own MSBuild configuration.
# C:\bhom-deps\ — at the drive root so no caller Directory.Build.props propagates into dep builds.
New-Item -ItemType Directory -Force -Path "C:\bhom-deps" | Out-Null

# Pin the dotnet SDK for all builds under C:\bhom-deps\. setup-dotnet installs
# a SDK but dotnet selects the highest installed version at invocation time
# unless a global.json is present. rollForward latestMinor accepts any patch.
# Pin SDK version via global.json: without it dotnet selects the highest installed.
# rollForward: latestMinor accepts any patch of the specified minor version.
$sdkVersion = "${{ inputs.dotnet_version }}"
if ($sdkVersion -match '^\d+\.\d+$') { $sdkVersion = "$sdkVersion.0" }
[ordered]@{ sdk = [ordered]@{ version = $sdkVersion; rollForward = "latestMinor" } } `
Expand Down Expand Up @@ -130,9 +126,8 @@ runs:

"keypart=$keypart" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

# cache-hit is true only on an exact key match. A restore-key (prefix) match
# restores a stale cache but leaves cache-hit false, triggering a rebuild.
# Skipped entirely when keypart is empty (no dependencies to build or cache).
# Exact key hit = skip build; prefix hit restores stale cache but leaves cache-hit
# false, still triggering a rebuild. Skipped when keypart is empty (no deps).
- name: Restore assembly cache
id: cache
if: steps.shas.outputs.keypart != ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ function Invoke-BHoMBuild {
throw "packages.config detected in $targetDir — legacy NuGet/MSBuild is not supported. Migrate all projects to SDK-style."
}

# Push into the repo directory before invoking dotnet.
# Keeps relative path resolution consistent.
# Push into the repo directory for consistent relative path resolution.
Push-Location $targetDir
try {
dotnet restore $Target
Expand All @@ -47,8 +46,7 @@ if (-not (Test-Path $orderOut)) {
$order = @()
}
else {
# _order.txt contains owner/repo lines; derive repo name and path from each.
# Filter blank lines that result when the file was written empty (no dependencies).
# Filter blank lines: _order.txt may be written empty when there are no dependencies.
$order = @(Get-Content $orderOut | Where-Object { $_ -match '\S' })
}

Expand All @@ -62,7 +60,11 @@ foreach ($ownerRepo in $order) {
$repoName = $ownerRepo.Split("/")[-1]
$repoPath = Join-Path $cloneRoot $repoName
if (-not (Test-Path $repoPath)) {
Write-Host "::warning::Clone not found at $repoPath — skipping $ownerRepo"
# Hard-fail on missing clone: resolution recorded this repo but the clone failed
# (auth/network error). Continuing would yield a confusing "assembly not found" later.
Write-Host "::error title=Build::Clone not found at $repoPath — $ownerRepo was in the build order but was never cloned. Check for earlier auth or network errors in the dependency resolution step."
$overallFailures += "$repoName"
Write-Host "::endgroup::"
continue
}

Expand Down Expand Up @@ -105,9 +107,8 @@ foreach ($ownerRepo in $order) {
Write-Host "::endgroup::"
}

# Assemblies are staged to C:\ProgramData\BHoM\Assemblies by each repo's
# own PostBuildEvent (xcopy). No collection step needed — that directory
# is the canonical output and is cached directly by the action.
# Assemblies are staged to ProgramData\BHoM\Assemblies by each repo's PostBuildEvent.
# That directory is the canonical output cached by the calling action.
$bhomAssemblies = Join-Path $env:ProgramData "BHoM\Assemblies"
$totalAssemblies = @(Get-ChildItem $bhomAssemblies -Filter *.dll -ErrorAction SilentlyContinue).Count
Write-Host "Total assemblies in ${bhomAssemblies}: $totalAssemblies"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ New-Item -ItemType Directory -Force -Path $cloneRoot | Out-Null

if (Test-Path $selectFile) { Remove-Item $selectFile -Force }

# Reads non-blank, non-comment lines from a file and validates format.
# Valid forms: owner/repo or owner/repo@branch|tag|sha (no whitespace).
# Parses owner/repo or owner/repo@ref lines; rejects blank, comment, and malformed entries.
function Lines([string]$path) {
if (Test-Path $path) {
return Get-Content $path |
Expand Down Expand Up @@ -53,9 +52,8 @@ function Parse-RepoSpec([string]$spec) {
return @{ Key=$spec; Ref=$ref }
}

# Branch preference: explicit @ref → PR_BRANCH → BASE_BRANCH → remote default
# $Prefer is null when PR_BRANCH is unset (push/dispatch events) so the ls-remote
# round-trip is skipped entirely rather than probing a sentinel that never exists.
# Resolution order: explicit @ref → PR head branch → base branch → develop → remote default.
# $Prefer is null on push/dispatch events, skipping the ls-remote probe.
$Prefer = if ([string]::IsNullOrWhiteSpace($env:PR_BRANCH)) { $null } else { $env:PR_BRANCH.Trim() }
$Fallback = if ([string]::IsNullOrWhiteSpace($env:BASE_BRANCH)) { 'develop' } else { $env:BASE_BRANCH.Trim() }

Expand All @@ -64,12 +62,13 @@ $visited = New-Object System.Collections.Generic.HashSet[string] # guards again
$nameMap = @{} # owner/repo -> folder
$pathMap = @{} # owner/repo -> path

# Route all GitHub HTTPS clones through the token via insteadOf so the token never appears
# in git command arguments, process listings, or git's own error messages.
# DEP_TOKEN is masked in Actions logs; writing it into a URL rewrite rule instead of a clone
# URL also means cloned repos' .git/config files never contain the credential.
# Use insteadOf to inject the token at the git config level — keeps it out of command
# arguments, process listings, and cloned repos' .git/config. Removed at end of script
# to avoid persisting the credential on self-hosted runners between jobs.
$tokenInsteadOfKey = $null
if (-not [string]::IsNullOrWhiteSpace($env:DEP_TOKEN)) {
git config --global url."https://x-access-token:$($env:DEP_TOKEN)@github.com/".insteadOf "https://github.com/"
$tokenInsteadOfKey = "url.https://x-access-token:$($env:DEP_TOKEN)@github.com/.insteadOf"
git config --global $tokenInsteadOfKey "https://github.com/"
}

function Get-FolderName([string]$ownerRepo) {
Expand Down Expand Up @@ -121,8 +120,7 @@ function Clone-And-Checkout([string]$ownerRepo, [string]$ref) {
if ($LASTEXITCODE -ne 0) { throw "git checkout FETCH_HEAD failed for '$ownerRepo' (ref=$Fallback)" }
$selectedRef = $Fallback
} else {
# Neither PR branch nor base branch exist on this dep repo — fall back to
# its remote default branch (main / next / etc.)
# Neither PR nor base branch exists on this dep; fall back to remote default.
git fetch origin HEAD --depth 1 | Out-Null
git checkout -q FETCH_HEAD
if ($LASTEXITCODE -ne 0) { throw "git checkout FETCH_HEAD failed for '$ownerRepo' (remote default)" }
Expand Down Expand Up @@ -161,8 +159,7 @@ function Build-Chain([string]$ownerRepo, [bool]$includeSelf=$false, [string]$ref
Clone-And-Checkout $ownerRepo $ref | Out-Null
}

# Guard against circular dependencies: if this repo's transitive graph has already
# been expanded in an ancestor call, skip re-expansion to prevent infinite recursion.
# Already visited: skip re-expansion to prevent infinite recursion on circular deps.
if ($visited.Contains($ownerRepo)) {
if ($includeSelf -and $pathMap.ContainsKey($ownerRepo)) {
$chain.Add(@{ Key=$ownerRepo; Name=$nameMap[$ownerRepo]; Path=$pathMap[$ownerRepo] }) | Out-Null
Expand Down Expand Up @@ -232,8 +229,7 @@ else {
}
}

# Additional seeds (caller mode only): appended after caller graph so caller assemblies
# build first. Ignored when mode=seeds since that mode already accepts multiple repos.
# Additional seeds (caller mode only): appended after the caller graph so caller assemblies build first.
if ($Mode -ne "seeds" -and -not [string]::IsNullOrWhiteSpace($AdditionalSeeds)) {
Write-Host "----- Additional seeds (appended to caller graph) -----"

Expand Down Expand Up @@ -264,8 +260,7 @@ foreach ($k in $phaseList) {
}
}

# Write _order.txt — Build-Dependencies derives clone path from repo name.
# Always create the file (even when empty) so Build-Dependencies.ps1's Get-Content never throws.
# Always write _order.txt even when empty — Build-Dependencies.ps1 relies on it existing.
if ($merged.Count -gt 0) {
$merged | Set-Content -Path $orderOut -Encoding utf8
} else {
Expand Down Expand Up @@ -299,4 +294,11 @@ if (Test-Path $selectFile) {
if ($env:GITHUB_STEP_SUMMARY) {
$mdLines | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
}
}

# Remove the credential rewrite — on self-hosted runners ~/.gitconfig persists between
# jobs and would leak the short-lived token to subsequent jobs.
if ($null -ne $tokenInsteadOfKey) {
git config --global --unset $tokenInsteadOfKey 2>$null
Write-Host "::debug::Removed git credential rewrite from global config."
}
1 change: 1 addition & 0 deletions .github/config/governance-orgs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["BuroHappoldEngineeringSandbox", "BHoM-migration-test-1"]
5 changes: 2 additions & 3 deletions .github/scripts/check-format-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

set -euo pipefail

# Apply canonical EditorConfig.
# TOOLKIT_DIR is the path where DevOps_Toolkit was checked out (default: _toolkit).
# TOOLKIT_DIR can be overridden if DevOps_Toolkit was checked out at a non-default path.
TOOLKIT_DIR="${TOOLKIT_DIR:-_toolkit}"
CANONICAL_EDITORCONFIG="$TOOLKIT_DIR/.editorconfig"

Expand All @@ -33,7 +32,7 @@ while IFS= read -r file; do
dir=$(dirname "$file")
csproj=""
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
# shellcheck disable=SC2012

found=$(find "$dir" -maxdepth 1 -name "*.csproj" 2>/dev/null | head -1)
if [ -n "$found" ]; then
csproj="$found"
Expand Down
40 changes: 27 additions & 13 deletions .github/scripts/distribute-editorconfig.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Distributes config/.editorconfig to all target BHoM repos.
# Distributes .editorconfig to all target BHoM repos.
#
# Usage: distribute-editorconfig.sh <repo-list-file>
#
Expand All @@ -12,11 +12,15 @@
# - Skips if the repo's .editorconfig already matches the canonical file.
# - Creates branch governance/update-editorconfig-YYYY-MM-DD (datestamped to
# prevent stale approvals from carrying over if the branch is re-pushed).
# - Closes any open PRs targeting older governance/update-editorconfig-* branches
# - Closes any open PRs opened from older governance/update-editorconfig-* branches
# before opening a fresh PR for the new branch.
# - Failures are collected and reported at the end without stopping the loop.
set -euo pipefail

# Remove cloned repos on any exit (normal or error). Safe on ephemeral runners;
# prevents disk accumulation on self-hosted runners.
trap 'rm -rf targets/' EXIT

REPO_FILE="${1:?Usage: distribute-editorconfig.sh <repo-list-file>}"
CANONICAL_ABS="$(pwd)/.editorconfig"
BRANCH="governance/update-editorconfig-$(date +%Y-%m-%d)"
Expand Down Expand Up @@ -54,11 +58,21 @@ while IFS= read -r repo; do
TARGET_DIR="targets/$repo"
rm -rf "$TARGET_DIR"

if ! gh repo clone "$ORG/$repo" "$TARGET_DIR" -- --depth=1 --branch develop --quiet; then
echo "::error::Failed to clone $ORG/$repo"
FAILURES+=("$repo")
echo "::endgroup::"
continue
BASE_BRANCH="develop"
if ! gh repo clone "$ORG/$repo" "$TARGET_DIR" -- --depth=1 --branch develop --quiet 2>/dev/null; then
BASE_BRANCH=$(gh repo view "$ORG/$repo" --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || true)
if [ -z "$BASE_BRANCH" ]; then
echo "::error::Failed to determine default branch for $ORG/$repo"
FAILURES+=("$repo")
echo "::endgroup::"
continue
fi
if ! gh repo clone "$ORG/$repo" "$TARGET_DIR" -- --depth=1 --branch "$BASE_BRANCH" --quiet; then
echo "::error::Failed to clone $ORG/$repo (branch: $BASE_BRANCH)"
FAILURES+=("$repo")
echo "::endgroup::"
continue
fi
fi

if diff -q "$CANONICAL_ABS" "$TARGET_DIR/.editorconfig" >/dev/null 2>&1; then
Expand Down Expand Up @@ -88,16 +102,15 @@ while IFS= read -r repo; do
# --force is safe: this branch is exclusively owned by this workflow.
git push origin "$BRANCH" --force

# Close any stale PRs from older datestamped branches.
# Close PRs opened from earlier governance branches so reviewers see only one open PR per repo.
gh pr list \
--repo "$ORG/$repo" \
--state open \
--json number,headRefName \
--jq ".[] | select(.headRefName | startswith(\"${BRANCH_PREFIX}\")) | select(.headRefName != \"${BRANCH}\") | .number" \
| xargs -r -I{} gh pr close {} --repo "$ORG/$repo" --comment "Superseded by a newer sync run."

# Open a PR if one doesn't already exist — a new commit on the existing
# branch is sufficient to update an open PR; no need to recreate it.
# A new commit on an already-open PR branch updates it automatically; only create if none exists.
if gh pr list \
--repo "$ORG/$repo" \
--state open \
Expand All @@ -109,9 +122,9 @@ while IFS= read -r repo; do
PR_URL=$(gh pr create \
--repo "$ORG/$repo" \
--head "$BRANCH" \
--base develop \
--base "$BASE_BRANCH" \
--title "chore: update .editorconfig" \
--body "Automated update of \`.editorconfig\` from [DevOps_Toolkit](https://github.com/${ORG}/DevOps_Toolkit/blob/main/config/.editorconfig).
--body "Automated update of \`.editorconfig\` from [DevOps_Toolkit](https://github.com/${ORG}/DevOps_Toolkit/blob/main/.editorconfig).

Formatting rules are managed centrally in DevOps_Toolkit. This PR was opened automatically.

Expand All @@ -135,7 +148,7 @@ done < "$REPO_FILE"
# ── Step summary ─────────────────────────────────────────────────────────────
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### .editorconfig distribution"
echo "### .editorconfig distribution — ${ORG}"
echo ""
echo "| | Count |"
echo "|---|---|"
Expand Down Expand Up @@ -168,3 +181,4 @@ if [ "$DRY_RUN" = "true" ]; then
else
echo "::notice::Distribution complete. Updated: $UPDATED Skipped: $SKIPPED"
fi
# targets/ cleanup is handled by the EXIT trap.
Loading
Loading