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
61 changes: 57 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Build (Windows)

on:
push:
branches: [ "master", "main" ]
branches: [ "master", "main", "sow/**" ]
pull_request:
branches: [ "master", "main" ]
branches: [ "master", "main", "sow/**" ]
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -35,6 +35,36 @@ jobs:

- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Compute SAMVersion
id: ver
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
# Prefer head_ref when its a sow branch (release-promotion PRs from sow/* to main),
# else use base_ref on PR events / ref_name on push events.
$headRef = '${{ github.head_ref }}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape GitHub refs before injecting into PowerShell

These assignments interpolate untrusted ref names directly inside single-quoted PowerShell literals. Branch names can legally contain apostrophes (for example feature/don't-break), which would terminate the string early and break the workflow script at parse time on PRs/pushes using such refs.

Useful? React with 👍 / 👎.

$baseRef = '${{ github.base_ref }}'
$refName = '${{ github.ref_name }}'
if ($headRef -match '^sow/\d{4}-Q\d$') {
$ref = $headRef
} elseif ('${{ github.event_name }}' -eq 'pull_request') {
$ref = $baseRef
} else {
$ref = $refName
}
# .NET AssemblyVersion components are UInt16 (max 65535). Cap to 60000 for headroom.
$run = ${{ github.run_number }} % 60000
if ($ref -match 'sow/(\d{4})-Q(\d)') {
$v = "$($Matches[1]).$($Matches[2]).$run.0"
$src = "branch '$ref'"
} else {
$now = (Get-Date).ToUniversalTime()
$quarter = [int][Math]::Ceiling($now.Month / 3.0)
$v = "$($now.Year).$quarter.$run.0"
$src = "date $($now.ToString('yyyy-MM-dd')) (ref '$ref' not sow/yyyy-Qx)"
}
Write-Host "SAMVersion = $v (from $src)"
"samversion=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8

- name: Clone dependency repos (siblings)
shell: pwsh
Expand All @@ -55,10 +85,32 @@ jobs:
$deps = $buildOrder[0..($buildOrder.Count-2)]
foreach ($r in $deps) {
if (Test-Path $r) { continue }
Write-Host "Cloning https://github.com/$org/$r.git"
git clone --depth 1 "https://github.com/$org/$r.git" $r
$headRef = '${{ github.head_ref }}'
$baseRef = '${{ github.base_ref }}'
$refName = '${{ github.ref_name }}'
$candidates = @()
if ($headRef) { $candidates += $headRef }
# Current sow branch: base_ref on PR events, ref_name on push events.
$sowRef = if ($baseRef -match '^sow/') { $baseRef } elseif ($refName -match '^sow/') { $refName } else { '' }
if ($sowRef -and $sowRef -ne $headRef) { $candidates += $sowRef }
$candidates += 'sow/2026-Q2'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clone dependencies from matching branch family

The new fallback candidate forces dependency repos to sow/2026-Q2 whenever head_ref/base_ref/ref_name are not sow branches (for example, a normal push to main). In that case CI can build this repo against quarter-branch dependencies instead of main/default, which can produce false failures or false greens due to cross-repo version skew.

Useful? React with 👍 / 👎.

$cloned = $false
foreach ($cand in $candidates) {
$has = (git ls-remote --heads "https://github.com/$org/$r.git" $cand 2>$null | Out-String).Trim()
if ($has) {
Write-Host "Cloning $org/$r @ $cand"
git clone --depth 1 --branch $cand "https://github.com/$org/$r.git" $r
$cloned = $true
break
}
}
if (-not $cloned) {
Write-Host "Cloning $org/$r @ default branch"
git clone --depth 1 "https://github.com/$org/$r.git" $r
}
}


# Ensure ReferencePath exists even before SAM_Windows builds
New-Item -ItemType Directory -Force -Path "SAM_Windows\build" | Out-Null

Expand All @@ -77,6 +129,7 @@ jobs:
'/v:m'
'/p:Configuration=Release'
'/p:UseSharedCompilation=false'
'/p:SAMVersion=${{ steps.ver.outputs.samversion }}'
'/p:RunPostBuildEvent=OnOutputUpdated'
"/p:ReferencePath=$windowsRef"
)
Expand Down
81 changes: 63 additions & 18 deletions .github/workflows/spdx-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,89 @@ name: SPDX + Copyright header check

on:
pull_request:
workflow_dispatch:

jobs:
spdx:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read

steps:
- uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Check header in changed .cs files
shell: bash
run: |
set -e
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
set -euo pipefail

if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
HEAD="${{ github.sha }}"
BASE="$(git rev-parse "${HEAD}^" 2>/dev/null || true)"
fi

FILES=$(git diff --name-only "$BASE" "$HEAD" -- '*.cs' || true)
echo "Base SHA: ${BASE:-<none>}"
echo "Head SHA: $HEAD"
echo

if [ -z "$FILES" ]; then
if [ -z "${BASE:-}" ]; then
mapfile -t files < <(git ls-files '*.cs')
else
mapfile -t files < <(git diff --diff-filter=ACMR --name-only "$BASE" "$HEAD" -- '*.cs' || true)
fi

if [ "${#files[@]}" -eq 0 ]; then
echo "No C# files changed."
exit 0
fi

MISSING=""
for f in $FILES; do
HEADBLOCK=$(head -n 6 "$f")
echo "Changed C# files:"
printf ' - %s\n' "${files[@]}"
echo

missing=()

for f in "${files[@]}"; do
if [ ! -f "$f" ]; then
echo "Skipping missing file: $f"
continue
fi

headblock="$(head -n 20 "$f" | sed '1s/^\xEF\xBB\xBF//' | tr -d '\r')"

echo "Checking: $f"

if ! grep -q "SPDX-License-Identifier: LGPL-3.0-or-later" <<< "$headblock"; then
echo " Missing SPDX line"
missing+=("$f")
continue
fi

if ! grep -qE "Copyright \(c\) 2020[-–]2026 Michal Dengusiak & Jakub Ziolkowski and contributors" <<< "$headblock"; then
echo " Missing copyright line"
missing+=("$f")
continue
fi

echo "$HEADBLOCK" | grep -q "// SPDX-License-Identifier: LGPL-3.0-or-later" || MISSING="$MISSING $f"
echo "$HEADBLOCK" | grep -q "// Copyright (c) 2020–2026 Michal Dengusiak & Jakub Ziolkowski and contributors" || MISSING="$MISSING $f"
echo " OK"
done

if [ -n "$MISSING" ]; then
echo "❌ Missing required header in:"
for f in $MISSING; do echo " - $f"; done
echo ""
echo "Each changed .cs file must start with:"
echo
if [ "${#missing[@]}" -gt 0 ]; then
echo "Missing required header in:"
printf ' - %s\n' "${missing[@]}"
echo
echo "Each checked .cs file must contain within the first 20 lines:"
echo "// SPDX-License-Identifier: LGPL-3.0-or-later"
echo "// Copyright (c) 20202026 Michal Dengusiak & Jakub Ziolkowski and contributors"
echo "// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors"
exit 1
fi

echo "SPDX + copyright headers OK."
echo "SPDX + copyright headers OK."
57 changes: 57 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<Project>

<PropertyGroup>
<!-- Local dev builds default to 1.0.0.0. CI passes /p:SAMVersion=YYYY.Q.<run_number>.0. -->
<SAMVersion Condition="'$(SAMVersion)' == ''">1.0.0.0</SAMVersion>
<AssemblyVersion>$(SAMVersion)</AssemblyVersion>
<FileVersion>$(SAMVersion)</FileVersion>
<!--
InformationalVersion: the content-identity stamp (SemVer +build metadata).
Composed from SAMVersion + SAMSourceRevision when CI sets the latter. If CI already
set InformationalVersion directly (e.g. SAM_Deploy installer.yml does this), respect
that and don't overwrite. Local dev builds leave both empty -> no SHA attribute emitted.
-->
<InformationalVersion Condition="'$(InformationalVersion)' == '' AND '$(SAMSourceRevision)' != ''">$(SAMVersion)+$(SAMSourceRevision)</InformationalVersion>
<!--
Suppress the SDK's SourceLink auto-append behaviour. By default, when SourceLink is
configured on a project (via NuGet or repo settings), the SDK appends the project's
OWN commit SHA to InformationalVersion (e.g. '2026.2.164.0+998af06.dd02a4c2...').
We set InformationalVersion explicitly to SAM_Deploy's SHA (which encodes all 22
submodule pointers), so the SDK's extra append is noise. False = keep our value clean.
-->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Deterministic>true</Deterministic>
</PropertyGroup>

<!--
Projects where SDK assembly-attribute generation is disabled (legacy AssemblyInfo.cs
with GenerateAssemblyInfo=false) OR projects where the SDK doesn't auto-generate
attributes at all (classic-style csprojs that don't set the property — '' evaluates
as not 'true') ignore the AssemblyVersion/FileVersion MSBuild properties above.
Inject the attributes via a generated source file so those assemblies also get
stamped by CI.
-->
<Target Name="_GenerateSAMVersionFile"
BeforeTargets="CoreCompile"
Condition="'$(GenerateAssemblyInfo)' != 'true'">
<ItemGroup>
<_SAMVersionLine Include="// &lt;auto-generated /&gt;" />
<_SAMVersionLine Include="[assembly: System.Reflection.AssemblyVersionAttribute(&quot;$(SAMVersion)&quot;)]" />
<_SAMVersionLine Include="[assembly: System.Reflection.AssemblyFileVersionAttribute(&quot;$(SAMVersion)&quot;)]" />
<!-- Only emit InformationalVersion attribute when CI provided a value (SDK projects
would otherwise get it via PropertyGroup -> SDK auto-gen). -->
<_SAMVersionLine Include="[assembly: System.Reflection.AssemblyInformationalVersionAttribute(&quot;$(InformationalVersion)&quot;)]" Condition="'$(InformationalVersion)' != ''" />
</ItemGroup>
<MakeDir Directories="$(IntermediateOutputPath)" />
<WriteLinesToFile
File="$(IntermediateOutputPath)SAMVersion.g.cs"
Lines="@(_SAMVersionLine)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)SAMVersion.g.cs" KeepDuplicates="false" />
<FileWrites Include="$(IntermediateOutputPath)SAMVersion.g.cs" />
</ItemGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Grasshopper.Kernel;
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020–2026 Michal Dengusiak & Jakub Ziolkowski and contributors

using Grasshopper.Kernel;
using SAM.Core.Grasshopper.Python.Properties;
using System;
using System.Drawing;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/*
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
/*
* This file is part of the Sustaiable Analytical Model (SAM)
* Copyright (c) 2020, the respective contributors. All rights reserved.
*
Expand Down Expand Up @@ -53,6 +55,3 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.*")]
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<UseWindowsForms>true</UseWindowsForms>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
<StartProgram>C:\Program Files\Rhino 8\System\Rhino.exe</StartProgram>
<Deterministic>false</Deterministic>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<CopyLocalLockFileAssemblies>True</CopyLocalLockFileAssemblies>
Expand All @@ -31,7 +30,7 @@
<PackageReference Include="Grasshopper" Version="8.21.25188.17001" ExcludeAssets="runtime" />
<PackageReference Include="IronPython" Version="3.4.2" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
Expand Down
19 changes: 11 additions & 8 deletions SAM_Python/SAM.Core.Python/Classes/Variable.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using Newtonsoft.Json.Linq;
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020–2026 Michal Dengusiak & Jakub Ziolkowski and contributors
using System;
using System.Drawing;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace SAM.Core.Python
{
Expand Down Expand Up @@ -67,7 +70,7 @@ public object Value
}
}

public bool FromJObject(JObject jObject)
public bool FromJsonObject(JsonObject jObject)
{
if (jObject == null)
{
Expand All @@ -76,30 +79,30 @@ public bool FromJObject(JObject jObject)

if (jObject.ContainsKey("VariableType"))
{
variableType = new VariableType(jObject.Value<JObject>("VariableType"));
variableType = new VariableType(jObject["VariableType"] as JsonObject);
}

if (jObject.ContainsKey("Value"))
{
value = jObject.Value<object>("Value");
value = jObject["Value"]?.Deserialize<object>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deserialize Value using variable type instead of object

FromJsonObject now reads Value via Deserialize<object>(), but in System.Text.Json deserializing to object produces JsonElement rather than inferred CLR primitives (for example double, bool, DateTime). That changes the runtime type of Variable.Value after a JSON round-trip, so downstream consumers like scriptScope.SetVariable(input.Name, input.Value) can receive JsonElement and break Python execution/casting logic that previously got native values. Use the declared variableType (or a converter) when deserializing Value to preserve the original type.

Useful? React with 👍 / 👎.

}

return true;
}

public JObject ToJObject()
public JsonObject ToJsonObject()
{
JObject jObject = new JObject();
JsonObject jObject = new JsonObject();
jObject.Add("_type", Query.FullTypeName(this));

if (variableType != null)
{
jObject.Add("VariableType", variableType.ToJObject());
jObject.Add("VariableType", variableType.ToJsonObject());
}

if (value != null)
{
jObject.Add("Value", value as dynamic);
jObject.Add("Value", JsonValue.Create(value));
}

return jObject;
Expand Down
Loading