Skip to content
Open
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
134 changes: 78 additions & 56 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ on:
schedule:
- cron: "0 23 * * *" # Daily at 11 PM UTC
workflow_dispatch: # Allow manual triggers
inputs:
version-bump:
description: 'Version bump type'
required: false
default: 'auto'
type: choice
options:
- auto
- patch
- minor
- major

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# Default permissions
permissions: read-all
permissions:
contents: read

env:
DOTNET_VERSION: "10.0" # Only needed for actions/setup-dotnet
Expand All @@ -35,7 +47,6 @@ jobs:
version: ${{ steps.pipeline.outputs.version }}
release_hash: ${{ steps.pipeline.outputs.release_hash }}
should_release: ${{ steps.pipeline.outputs.should_release }}
skipped_release: ${{ steps.pipeline.outputs.skipped_release }}

steps:
- name: Set up JDK 17
Expand Down Expand Up @@ -95,72 +106,73 @@ jobs:
New-Item -Path .\.sonar\scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner

- name: Configure SonarQube exclusions
shell: bash
run: |
EXCLUSIONS="_temp/**,_actions/**"
if [ "${{ github.event.repository.name }}" != "KtsuBuild" ]; then
EXCLUSIONS="$EXCLUSIONS,**/KtsuBuild/**"
fi
echo "SONAR_EXCLUSIONS=$EXCLUSIONS" >> $GITHUB_ENV

- name: Begin SonarQube
if: ${{ env.SONAR_TOKEN != '' }}
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: powershell
run: |
.\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths="coverage/coverage.xml" /d:sonar.coverage.exclusions="**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll" /d:sonar.cs.vstest.reportsPaths="coverage/TestResults/**/*.trx"
.\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths="coverage/coverage.xml" /d:sonar.coverage.exclusions="**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll" /d:sonar.cs.vstest.reportsPaths="coverage/TestResults/**/*.trx" /d:sonar.exclusions="${{ env.SONAR_EXCLUSIONS }}"

- name: Run PSBuild Pipeline
- name: Clone KtsuBuild (Latest Tag)
run: |
LATEST_TAG=$(git ls-remote --tags https://github.com/ktsu-dev/KtsuBuild.git | grep -o 'refs/tags/v[0-9]*\.[0-9]*\.[0-9]*$' | sed 's/refs\/tags\///' | sort -V | tail -1 || true)
if [ -z "$LATEST_TAG" ]; then
echo "No version tags found, falling back to HEAD"
git clone --depth 1 https://github.com/ktsu-dev/KtsuBuild.git "${{ runner.temp }}/KtsuBuild"
else
echo "Cloning KtsuBuild at tag: $LATEST_TAG"
git clone --depth 1 --branch "$LATEST_TAG" https://github.com/ktsu-dev/KtsuBuild.git "${{ runner.temp }}/KtsuBuild"
fi
shell: bash

- name: Run KtsuBuild CI Pipeline
id: pipeline
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
NUGET_API_KEY: ${{ secrets.NUGET_KEY }}
KTSU_PACKAGE_KEY: ${{ secrets.KTSU_PACKAGE_KEY }}
EXPECTED_OWNER: ktsu-dev
run: |
# Import the PSBuild module
Import-Module ${{ github.workspace }}/scripts/PSBuild.psm1

# Get build configuration
$buildConfig = Get-BuildConfiguration `
-ServerUrl "${{ github.server_url }}" `
-GitRef "${{ github.ref }}" `
-GitSha "${{ github.sha }}" `
-GitHubOwner "${{ github.repository_owner }}" `
-GitHubRepo "${{ github.repository }}" `
-GithubToken "${{ github.token }}" `
-NuGetApiKey "${{ secrets.NUGET_KEY }}" `
-KtsuPackageKey "${{ secrets.KTSU_PACKAGE_KEY }}" `
-WorkspacePath "${{ github.workspace }}" `
-ExpectedOwner "ktsu-dev" `
-ChangelogFile "CHANGELOG.md" `
-AssetPatterns @("staging/*.nupkg", "staging/*.zip")

if (-not $buildConfig.Success) {
throw $buildConfig.Error
# Run the CI pipeline
$versionBump = "${{ github.event.inputs.version-bump }}"

# Build arguments array - only add --version-bump if explicitly set (for backward compatibility during bootstrap)
$args = @("ci", "--workspace", "${{ github.workspace }}", "--verbose")
if (![string]::IsNullOrEmpty($versionBump) -and $versionBump -ne "auto") {
$args += @("--version-bump", $versionBump)
}

# Run the complete CI/CD pipeline
$result = Invoke-CIPipeline `
-BuildConfiguration $buildConfig.Data
& dotnet run --project "${{ runner.temp }}/KtsuBuild/KtsuBuild.CLI" -- @args

if (-not $result.Success) {
Write-Information "CI/CD pipeline failed: $($result.Error)" -Tags "Invoke-CIPipeline"
Write-Information "Stack Trace: $($result.StackTrace)" -Tags "Invoke-CIPipeline"
Write-Information "Build Configuration: $($buildConfig.Data | ConvertTo-Json -Depth 10)" -Tags "Invoke-CIPipeline"
throw $result.Error
}
# Set outputs for downstream jobs
$version = (Get-Content "${{ github.workspace }}/VERSION.md" -Raw).Trim()
"version=$version" >> $env:GITHUB_OUTPUT

# Set outputs for GitHub Actions from build configuration and pipeline result
# Use pipeline result values when available (for skipped releases), otherwise use buildConfig
if ($result.Data.SkippedRelease) {
"version=$($result.Data.Version)" >> $env:GITHUB_OUTPUT
"release_hash=$($result.Data.ReleaseHash)" >> $env:GITHUB_OUTPUT
"should_release=$($buildConfig.Data.ShouldRelease)" >> $env:GITHUB_OUTPUT
"skipped_release=true" >> $env:GITHUB_OUTPUT
} else {
"version=$($buildConfig.Data.Version)" >> $env:GITHUB_OUTPUT
"release_hash=$($buildConfig.Data.ReleaseHash)" >> $env:GITHUB_OUTPUT
"should_release=$($buildConfig.Data.ShouldRelease)" >> $env:GITHUB_OUTPUT
# Check for skipped release from buildConfig as fallback
if ($buildConfig.Data.SkippedRelease) {
"skipped_release=true" >> $env:GITHUB_OUTPUT
}
}
$releaseHash = git rev-parse HEAD
"release_hash=$releaseHash" >> $env:GITHUB_OUTPUT

# Compute should_release (same logic as BuildConfigurationProvider)
$isMain = "${{ github.ref }}" -eq "refs/heads/main"
$isTagged = [bool](git tag --points-at "${{ github.sha }}" 2>$null)
$isFork = "${{ github.event.repository.fork }}" -eq "true"
$isExpectedOwner = "${{ github.repository_owner }}" -eq "ktsu-dev"
$isOfficial = (-not $isFork) -and $isExpectedOwner
$shouldRelease = $isMain -and (-not $isTagged) -and $isOfficial
"should_release=$($shouldRelease.ToString().ToLower())" >> $env:GITHUB_OUTPUT

- name: End SonarQube
if: env.SONAR_TOKEN != '' && steps.pipeline.outputs.skipped_release != 'true'
if: env.SONAR_TOKEN != ''
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: powershell
Expand All @@ -169,7 +181,7 @@ jobs:

- name: Upload Coverage Report
uses: actions/upload-artifact@v4
if: always() && steps.pipeline.outputs.skipped_release != 'true'
if: always()
with:
name: coverage-report
path: |
Expand All @@ -179,7 +191,7 @@ jobs:
winget:
name: Update Winget Manifests
needs: build
if: needs.build.outputs.should_release == 'true' && needs.build.outputs.skipped_release != 'true'
if: needs.build.outputs.should_release == 'true'
runs-on: windows-latest
timeout-minutes: 10
permissions:
Expand All @@ -197,14 +209,24 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}.x

- name: Clone KtsuBuild (Latest Tag)
run: |
LATEST_TAG=$(git ls-remote --tags https://github.com/ktsu-dev/KtsuBuild.git | grep -o 'refs/tags/v[0-9]*\.[0-9]*\.[0-9]*$' | sed 's/refs\/tags\///' | sort -V | tail -1 || true)
if [ -z "$LATEST_TAG" ]; then
echo "No version tags found, falling back to HEAD"
git clone --depth 1 https://github.com/ktsu-dev/KtsuBuild.git "${{ runner.temp }}/KtsuBuild"
else
echo "Cloning KtsuBuild at tag: $LATEST_TAG"
git clone --depth 1 --branch "$LATEST_TAG" https://github.com/ktsu-dev/KtsuBuild.git "${{ runner.temp }}/KtsuBuild"
fi
shell: bash

- name: Update Winget Manifests
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
# Use enhanced script with auto-detection capabilities
Write-Host "Updating winget manifests for version ${{ needs.build.outputs.version }}"
.\scripts\update-winget-manifests.ps1 -Version "${{ needs.build.outputs.version }}"
dotnet run --project "${{ runner.temp }}/KtsuBuild/KtsuBuild.CLI" -- winget generate --version "${{ needs.build.outputs.version }}" --workspace "${{ github.workspace }}" --verbose

- name: Upload Updated Manifests
uses: actions/upload-artifact@v4
Expand All @@ -216,7 +238,7 @@ jobs:
security:
name: Security Scanning
needs: build
if: needs.build.outputs.should_release == 'true' && needs.build.outputs.skipped_release != 'true'
if: needs.build.outputs.should_release == 'true'
runs-on: windows-latest
timeout-minutes: 10
permissions:
Expand Down
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="ktsu.DeepClone" Version="2.0.8" />
<PackageVersion Include="ktsu.Extensions" Version="1.5.6" />
<PackageVersion Include="ktsu.FuzzySearch" Version="1.2.5" />
<PackageVersion Include="ktsu.IntervalAction" Version="1.3.7" />
<PackageVersion Include="ktsu.Keybinding.Core" Version="1.0.6" />
<PackageVersion Include="ktsu.UndoRedo.Core" Version="1.0.6" />
<PackageVersion Include="ktsu.RoundTripStringJsonConverter" Version="1.0.7" />
<PackageVersion Include="ktsu.SemanticString" Version="1.4.0" />
<PackageVersion Include="ktsu.Semantics.Strings" Version="1.0.28" />
Expand Down
54 changes: 54 additions & 0 deletions Schema/Models/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,60 @@ public static bool TryGetChild<TName, TChild>(TName name, Collection<TChild> col
return null;
}

/// <summary>
/// Restores a previously removed child back into a collection.
/// Used for undo operations where the original object reference is preserved.
/// </summary>
/// <typeparam name="TChild">The type of the child.</typeparam>
/// <typeparam name="TName">The type of the name.</typeparam>
/// <param name="child">The child to restore.</param>
/// <param name="collection">The collection to restore the child into.</param>
/// <returns>True if the child was restored; false if a child with the same name already exists.</returns>
public bool RestoreChild<TChild, TName>(TChild child, Collection<TChild> collection)
where TChild : SchemaChild<TName>, new()
where TName : SemanticString<TName>, ISchemaChildName, new()
{
Ensure.NotNull(child);
Ensure.NotNull(collection);

if (GetChild(child.Name, collection) is not null)
{
return false;
}

child.AssociateWith(this);
collection.Add(child);
return true;
}

/// <summary>
/// Restores a previously removed class back into the schema.
/// </summary>
/// <param name="schemaClass">The class to restore.</param>
/// <returns>True if restored; false if a class with the same name already exists.</returns>
public bool RestoreClass(SchemaClass schemaClass) => RestoreChild<SchemaClass, ClassName>(schemaClass, ClassesInternal);

/// <summary>
/// Restores a previously removed enum back into the schema.
/// </summary>
/// <param name="schemaEnum">The enum to restore.</param>
/// <returns>True if restored; false if an enum with the same name already exists.</returns>
public bool RestoreEnum(SchemaEnum schemaEnum) => RestoreChild<SchemaEnum, EnumName>(schemaEnum, EnumsInternal);

/// <summary>
/// Restores a previously removed data source back into the schema.
/// </summary>
/// <param name="dataSource">The data source to restore.</param>
/// <returns>True if restored; false if a data source with the same name already exists.</returns>
public bool RestoreDataSource(DataSource dataSource) => RestoreChild<DataSource, DataSourceName>(dataSource, DataSourcesInternal);

/// <summary>
/// Restores a previously removed code generator back into the schema.
/// </summary>
/// <param name="codeGenerator">The code generator to restore.</param>
/// <returns>True if restored; false if a code generator with the same name already exists.</returns>
public bool RestoreCodeGenerator(SchemaCodeGenerator codeGenerator) => RestoreChild<SchemaCodeGenerator, CodeGeneratorName>(codeGenerator, CodeGeneratorsInternal);

internal bool TryRemoveEnum(SchemaEnum schemaEnum) => TryRemoveChild(schemaEnum, EnumsInternal);

internal bool TryRemoveClass(SchemaClass schemaClass) => TryRemoveChild(schemaClass, ClassesInternal);
Expand Down
20 changes: 20 additions & 0 deletions Schema/Models/SchemaClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ public class SchemaClass : SchemaChild<ClassName>
/// <returns>True if the member was removed; otherwise, false.</returns>
internal bool TryRemoveMember(SchemaMember member) => MembersInternal.Remove(member);

/// <summary>
/// Restores a previously removed member back into the class.
/// Used for undo operations where the original object reference is preserved.
/// </summary>
/// <param name="member">The member to restore.</param>
/// <returns>True if the member was restored; false if a member with the same name already exists.</returns>
public bool RestoreMember(SchemaMember member)
{
Ensure.NotNull(member);

if (MembersInternal.Any(m => m.Name == member.Name))
{
return false;
}

member.AssociateWith(this);
MembersInternal.Add(member);
return true;
}

/// <summary>
/// Tries to get a member by name.
/// </summary>
Expand Down
Loading
Loading