Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,149 @@
name: Sign artifacts
description: >
Signs files via azure/artifact-signing-action with up to three attempts.
The Microsoft timestamp server (timestamp.acs.microsoft.com) fails
intermittently; re-signing already-signed files is safe because signtool
replaces existing signatures, so failed batches can simply be retried.
inputs:
azure-tenant-id:
description: Azure tenant id used for authentication.
required: true
azure-client-id:
description: Azure client id used for authentication.
required: true
azure-client-secret:
description: Azure client secret used for authentication.
required: true
files:
description: Newline-separated list of files to sign.
required: false
default: ''
files-folder:
description: Folder containing the files to sign.
required: false
default: ''
files-folder-filter:
description: Comma-separated file extensions to sign within files-folder.
required: false
default: ''
files-folder-recurse:
description: Whether to search files-folder recursively.
required: false
default: 'false'
endpoint:
description: Artifact Signing endpoint.
required: false
default: https://weu.codesigning.azure.net/
signing-account-name:
description: Artifact Signing account name.
required: false
default: CentronCodesigning
certificate-profile-name:
description: Certificate profile name.
required: false
default: centroncert
runs:
using: composite
steps:
- name: Sign (attempt 1)
id: attempt1
continue-on-error: true
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ inputs.azure-tenant-id }}
azure-client-id: ${{ inputs.azure-client-id }}
azure-client-secret: ${{ inputs.azure-client-secret }}
endpoint: ${{ inputs.endpoint }}
signing-account-name: ${{ inputs.signing-account-name }}
certificate-profile-name: ${{ inputs.certificate-profile-name }}
files: ${{ inputs.files }}
files-folder: ${{ inputs.files-folder }}
files-folder-filter: ${{ inputs.files-folder-filter }}
files-folder-recurse: ${{ inputs.files-folder-recurse }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: false
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Wait before retry (attempt 2)
if: steps.attempt1.outcome == 'failure'
shell: pwsh
run: |
Write-Host 'Signing failed, retrying in 30 seconds...'
Start-Sleep -Seconds 30
- name: Sign (attempt 2)
id: attempt2
if: steps.attempt1.outcome == 'failure'
continue-on-error: true
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ inputs.azure-tenant-id }}
azure-client-id: ${{ inputs.azure-client-id }}
azure-client-secret: ${{ inputs.azure-client-secret }}
endpoint: ${{ inputs.endpoint }}
signing-account-name: ${{ inputs.signing-account-name }}
certificate-profile-name: ${{ inputs.certificate-profile-name }}
files: ${{ inputs.files }}
files-folder: ${{ inputs.files-folder }}
files-folder-filter: ${{ inputs.files-folder-filter }}
files-folder-recurse: ${{ inputs.files-folder-recurse }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: false
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Wait before retry (attempt 3)
if: steps.attempt1.outcome == 'failure' && steps.attempt2.outcome == 'failure'
shell: pwsh
run: |
Write-Host 'Signing failed again, retrying in 90 seconds...'
Start-Sleep -Seconds 90
- name: Sign (attempt 3)
if: steps.attempt1.outcome == 'failure' && steps.attempt2.outcome == 'failure'
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ inputs.azure-tenant-id }}
azure-client-id: ${{ inputs.azure-client-id }}
azure-client-secret: ${{ inputs.azure-client-secret }}
endpoint: ${{ inputs.endpoint }}
signing-account-name: ${{ inputs.signing-account-name }}
certificate-profile-name: ${{ inputs.certificate-profile-name }}
files: ${{ inputs.files }}
files-folder: ${{ inputs.files-folder }}
files-folder-filter: ${{ inputs.files-folder-filter }}
files-folder-recurse: ${{ inputs.files-folder-recurse }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: false
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
+431
View File
@@ -0,0 +1,431 @@
name: Build, sign, and publish
on:
workflow_dispatch:
pull_request:
branches:
- main
- 'release/**'
types:
- opened
- synchronize
- reopened
- ready_for_review
push:
branches:
- main
- 'release/**'
permissions:
contents: read
# Supersede in-flight runs of this workflow for the same pull request. Pushes to main and
# release branches are excluded so every commit there still gets a full, recorded result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Build and sign
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) }}
runs-on:
group: self-hosted
labels: [self-hosted, Windows, X64]
timeout-minutes: 180
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
main_version: ${{ steps.version.outputs.main_version }}
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
CENTRON_BUILD_RUNNING_IN_AZURE_PIPELINE: true
CENTRON_BUILD_IS_DEV_BUILD: false
steps:
- name: Show runner information
shell: pwsh
run: |
Write-Host "Runner: $env:RUNNER_NAME"
Write-Host "Computer: $env:COMPUTERNAME"
Write-Host "PowerShell: $($PSVersionTable.PSVersion)"
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Normalize Git repository format
shell: pwsh
run: |
$workspace = [IO.Path]::GetFullPath($env:GITHUB_WORKSPACE)
$safeDirectories = @(git config --global --get-all safe.directory)
$safeDirectoriesExitCode = $LASTEXITCODE
if ($safeDirectoriesExitCode -notin 0, 1) {
throw 'Could not determine the configured Git safe directories.'
}
if ($safeDirectories -notcontains $workspace) {
git config --global --add safe.directory $workspace
if ($LASTEXITCODE -ne 0) {
throw "Could not register the GitHub workspace as a safe directory: $workspace"
}
}
$repositoryFormatVersion = git config --local --get core.repositoryFormatVersion
if ($LASTEXITCODE -ne 0) {
throw 'Could not determine the Git repository format version.'
}
if ($repositoryFormatVersion -eq '1') {
$extensions = @(git config --local --name-only --get-regexp '^extensions\.')
$extensionsExitCode = $LASTEXITCODE
if ($extensionsExitCode -notin 0, 1) {
throw 'Could not determine the active Git repository extensions.'
}
if ($extensions.Count -gt 0) {
throw "Cannot normalize the Git repository while extensions are active: $($extensions -join ', ')"
}
git config --local core.repositoryFormatVersion 0
if ($LASTEXITCODE -ne 0) {
throw 'Could not normalize the Git repository format for Nerdbank.GitVersioning.'
}
}
- name: Verify build environment
shell: pwsh
run: |
$sdkVersion = dotnet --version
if ($LASTEXITCODE -ne 0) {
throw "The .NET SDK could not be resolved using global.json."
}
if ($sdkVersion -notmatch '^10\.0\.') {
throw "Expected a .NET 10 SDK, but resolved '$sdkVersion'."
}
if ((git rev-parse --is-shallow-repository) -ne 'false') {
throw 'Repository was checked out without its full history.'
}
Write-Host "Resolved .NET SDK: $sdkVersion"
dotnet --info
- name: Set version and create NuGet packages
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- create-nuget-packages
- name: Determine build version
id: version
shell: pwsh
run: |
$versionText = [string](
& ".\dotnettools\nbgv.exe" get-version -v Version
)
if ($LASTEXITCODE -ne 0) {
throw "Nerdbank.GitVersioning failed with exit code $LASTEXITCODE."
}
$version = [Version]$versionText
$buildVersion = $version.ToString()
$mainVersion = $version.ToString(3)
Write-Host "Build version: $buildVersion"
Write-Host "Main version: $mainVersion"
"version=$buildVersion" >> $env:GITHUB_OUTPUT
"main_version=$mainVersion" >> $env:GITHUB_OUTPUT
$metadataDirectory = Join-Path $env:RUNNER_TEMP 'centron-build-metadata'
New-Item -Path $metadataDirectory -ItemType Directory -Force | Out-Null
Set-Content `
-LiteralPath (Join-Path $metadataDirectory 'version.txt') `
-Value $buildVersion `
-Encoding utf8NoBOM
- name: Prepare Artifact Signing dependencies
shell: powershell
run: |
if ((Get-PackageProvider -Name NuGet -ErrorAction Ignore) -eq $null) {
Install-PackageProvider -Name NuGet -Force -Scope CurrentUser
}
if ((Get-InstalledModule -Name PowerShellGet -MinimumVersion 2.2.1 -ErrorAction Ignore) -eq $null) {
Install-Module -Name PowerShellGet -MinimumVersion 2.2.1 -Scope CurrentUser -Force -AllowClobber
}
- name: Build Web Service
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- build-web-service-only
- name: Sign Web Service
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: |
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Host.WindowsService.exe
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Host.WindowsService.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\ConnectionManager\c-entron Connection Manager.exe
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\ConnectionManager\c-entron Connection Manager.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Interfaces.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.WebServices.Core.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Core.dll
- name: Create Web Service installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-dependencies-web-service build-web-service-installer-only
- name: Sign Web Service installer
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files-folder: ${{ github.workspace }}\deployment\centron\WebServiceSetupProject\bin\Release
files-folder-filter: exe,dll,msi
files-folder-recurse: true
- name: Package Web Service installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-zip-directory-web-service
- name: Build c-entron.NET
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- build-centron-net-only
- name: Sign c-entron.NET
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: |
${{ github.workspace }}\src\centron\Centron.WPF.UI\bin\Release\net10.0-windows\win-x64\publish\c-entron 2.0.exe
${{ github.workspace }}\src\centron\Centron.WPF.UI\bin\Release\net10.0-windows\win-x64\publish\c-entron 2.0.dll
- name: Create c-entron.NET installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-dependencies-centron-net build-centron-net-installer-only
- name: Sign c-entron.NET installer
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files-folder: ${{ github.workspace }}\deployment\centron\CentronSetupProject\bin\Release
files-folder-filter: exe,dll,msi
files-folder-recurse: true
- name: Package c-entron.NET installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-zip-directory-centron-net
- name: Build Nexus
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- build-nexus
- name: Sign Nexus
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: |
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.Host.exe
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.Host.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.OutlookAddIn.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.Core.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.WebServices.Core.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.Interfaces.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.Office.Client.dll
- name: Prepare Nexus installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- zip-nexus-directory
dotnet tool update --global wix --version 5.0.2 --allow-downgrade
if ($LASTEXITCODE -ne 0) {
dotnet tool install --global wix --version 5.0.2
}
wix extension add -g WixToolset.UI.wixext/5.0.2
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- build-nexus-installer
- name: Sign Nexus installer
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: ${{ github.workspace }}\deployment\WixSharpInstaller\bin\Release\net10.0-windows\c-entron Nexus.msi
- name: Verify signed installers
shell: pwsh
run: |
$files = @(
'.\deployment\centron\WebServiceSetupProject\bin\Release\c-entron Web-Service Installer.msi'
'.\deployment\centron\CentronSetupProject\bin\Release\c-entron.NET Installer.msi'
'.\deployment\WixSharpInstaller\bin\Release\net10.0-windows\c-entron Nexus.msi'
)
foreach ($file in $files) {
if (-not (Test-Path -LiteralPath $file -PathType Leaf)) {
throw "Signed installer not found: $file"
}
$signature = Get-AuthenticodeSignature -LiteralPath $file
if ($signature.Status -ne 'Valid') {
throw "Invalid signature for '$file': $($signature.StatusMessage)"
}
Write-Host "Valid signature: $file"
}
- name: Package Nexus installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- zip-nexus-singleFile
- name: Upload build artifacts
id: build-artifact
uses: actions/upload-artifact@v7
with:
name: centron-build
path: artifacts/
if-no-files-found: error
retention-days: 14
- name: Upload build version metadata
uses: actions/upload-artifact@v7
with:
name: centron-build-version-${{ steps.version.outputs.version }}
path: ${{ runner.temp }}/centron-build-metadata/version.txt
if-no-files-found: error
retention-days: 14
- name: Add artifact download link
shell: pwsh
run: |
"### Signed build artifacts" >> $env:GITHUB_STEP_SUMMARY
"Build version: ${{ steps.version.outputs.version }}" >> $env:GITHUB_STEP_SUMMARY
"[Download centron-build](${{ steps.build-artifact.outputs.artifact-url }})" >> $env:GITHUB_STEP_SUMMARY
upload-centron-net:
name: Upload c-entron.NET
needs: build
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') }}
permissions:
actions: read
contents: read
id-token: write
uses: NEXOWARE-Systems/ci-cd-reusable-workflows/.github/workflows/upload-software-build.yml@main
with:
artifact_name: centron-build
source_path: c-entron.NET Installer.zip
destination_folder: c-entron.NET
destination_file: c-entron.NET Installer.zip
version: ${{ needs.build.outputs.version }}
main_version: ${{ needs.build.outputs.main_version }}
environment_name: SoftwareBuilds
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
upload-web-service:
name: Upload c-entron Web Service
needs: build
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') }}
permissions:
actions: read
contents: read
id-token: write
uses: NEXOWARE-Systems/ci-cd-reusable-workflows/.github/workflows/upload-software-build.yml@main
with:
artifact_name: centron-build
source_path: c-entron Web-Service Installer.zip
destination_folder: c-entron Web-Service
destination_file: c-entron Web-Service Installer.zip
version: ${{ needs.build.outputs.version }}
main_version: ${{ needs.build.outputs.main_version }}
environment_name: SoftwareBuilds
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
upload-nexus:
name: Upload c-entron Nexus
needs: build
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') }}
permissions:
actions: read
contents: read
id-token: write
uses: NEXOWARE-Systems/ci-cd-reusable-workflows/.github/workflows/upload-software-build.yml@main
with:
artifact_name: centron-build
source_path: c-entron Nexus Installer.zip
destination_folder: c-entron Nexus
destination_file: c-entron Nexus Installer.zip
version: ${{ needs.build.outputs.version }}
main_version: ${{ needs.build.outputs.main_version }}
environment_name: SoftwareBuilds
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
@@ -0,0 +1,117 @@
name: Clean up closed PR artifacts
on:
pull_request_target:
types:
- closed
permissions:
actions: write
pull-requests: read
jobs:
delete-artifacts:
name: Delete PR artifacts
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Delete artifacts from PR workflow runs
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;
const pullRequest = context.payload.pull_request;
const workflowRuns = await github.paginate(
github.rest.actions.listWorkflowRunsForRepo,
{
owner,
repo,
event: 'pull_request',
branch: pullRequest.head.ref,
per_page: 100
}
);
const pullRequestRuns = [];
const associatedPullRequestsBySha = new Map();
for (const run of workflowRuns) {
if (!associatedPullRequestsBySha.has(run.head_sha)) {
const associatedPullRequests = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit,
{
owner,
repo,
commit_sha: run.head_sha,
per_page: 100
}
);
associatedPullRequestsBySha.set(run.head_sha, associatedPullRequests);
}
const belongsToPullRequest = associatedPullRequestsBySha
.get(run.head_sha)
.some(pr => pr.number === pullRequest.number);
if (belongsToPullRequest) {
pullRequestRuns.push(run);
}
}
let deletedArtifacts = 0;
let deletedBytes = 0;
for (const run of pullRequestRuns) {
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts,
{
owner,
repo,
run_id: run.id,
per_page: 100
}
);
for (const artifact of artifacts) {
if (artifact.expired) {
core.info(`Skipping expired artifact ${artifact.name} (${artifact.id}).`);
continue;
}
try {
await github.rest.actions.deleteArtifact({
owner,
repo,
artifact_id: artifact.id
});
deletedArtifacts += 1;
deletedBytes += artifact.size_in_bytes;
core.info(`Deleted artifact ${artifact.name} (${artifact.id}).`);
} catch (error) {
if (error.status === 404) {
core.info(`Artifact ${artifact.name} (${artifact.id}) was already deleted.`);
continue;
}
throw error;
}
}
}
const deletedMiB = (deletedBytes / 1024 / 1024).toFixed(1);
core.info(
`Deleted ${deletedArtifacts} artifact(s) (${deletedMiB} MiB) ` +
`from ${pullRequestRuns.length} workflow run(s) for PR #${pullRequest.number}.`
);
await core.summary
.addHeading('PR artifact cleanup')
.addRaw(`Pull request: #${pullRequest.number}`, true)
.addRaw(`Workflow runs inspected: ${pullRequestRuns.length}`, true)
.addRaw(`Artifacts deleted: ${deletedArtifacts}`, true)
.addRaw(`Storage released: ${deletedMiB} MiB`, true)
.write();
@@ -0,0 +1,269 @@
name: Regression tests
on:
workflow_dispatch:
pull_request:
branches:
- main
- 'release/**'
push:
branches:
- main
- 'release/**'
permissions:
contents: read
# Supersede in-flight runs of this workflow for the same pull request. Pushes to main and
# release branches are excluded so every commit there still gets a full, recorded result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
regression-tests:
name: End-to-end regression tests
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DevExpress_License: ${{ secrets.DEVEXPRESS_LICENSE }}
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
DB_USERNAME: sa
DB_PASSWORD: SA!password
DB_PORT: 1433
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
# Verifier.OverrideExpectedFiles = true rewrites the expected files instead of comparing
# against them, so a test that keeps the flag silently verifies nothing. It is only meant
# to be set temporarily while regenerating snapshots and must never be committed.
- name: Check for committed OverrideExpectedFiles
shell: pwsh
run: |
$violations = foreach ($file in Get-ChildItem -Path ./tests -Recurse -Filter *.cs) {
$segments = $file.FullName -split '[\\/]'
if ($segments -contains 'bin' -or $segments -contains 'obj') { continue }
$text = Get-Content -LiteralPath $file.FullName -Raw
if ([string]::IsNullOrEmpty($text)) { continue }
# Blank out block comments, keeping newlines so reported line numbers stay correct.
$text = [regex]::Replace($text, '(?s)/\*.*?\*/', { param($m) $m.Value -replace '[^\r\n]', '' })
$lineNumber = 0
foreach ($line in $text -split '\r?\n') {
$lineNumber++
# The assignment must start the line, optionally behind a dotted receiver such as
# this.Verifier. - anything else in front (other code, // or /// ) means it is not a
# statement. Deliberately not stripping // comments: that would also cut a real
# assignment that follows a string containing '//', turning a false positive into a
# false negative.
if ($line -match '^\s*(?:[A-Za-z_][A-Za-z0-9_.]*\.)?OverrideExpectedFiles\s*=\s*true') {
[pscustomobject]@{ Path = $file.FullName; Line = $lineNumber }
}
}
}
if ($violations) {
foreach ($violation in $violations) {
Write-Host "::error file=$($violation.Path),line=$($violation.Line)::Remove 'OverrideExpectedFiles = true' before committing - the test compares nothing while it is set."
}
throw "Found $(@($violations).Count) committed 'OverrideExpectedFiles = true' assignment(s). Regenerate the expected files locally, then remove the flag."
}
Write-Host 'No committed OverrideExpectedFiles assignments found.'
- name: Set up .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Verify test environment
shell: pwsh
run: |
$sdkVersion = dotnet --version
if ($LASTEXITCODE -ne 0 -or $sdkVersion -notmatch '^10\.0\.') {
throw "Expected a .NET 10 SDK, but resolved '$sdkVersion'."
}
docker version
if ($LASTEXITCODE -ne 0) {
throw 'Docker is unavailable on the self-hosted runner.'
}
- name: Sign in to Azure Container Registry
shell: pwsh
run: |
if ([string]::IsNullOrWhiteSpace($env:ACR_USERNAME) -or
[string]::IsNullOrWhiteSpace($env:ACR_PASSWORD)) {
throw 'ACR_USERNAME and ACR_PASSWORD secrets are required on GitHub-hosted runners.'
}
$env:ACR_PASSWORD | docker login centron.azurecr.io --username $env:ACR_USERNAME --password-stdin
if ($LASTEXITCODE -ne 0) {
throw 'Azure Container Registry login failed.'
}
# Superseded runs are now cancelled mid-test. "Stop regression database" below uses
# if: always() and therefore still runs on cancellation, but a hard runner failure can
# leave a container behind. Drop anything older than the job timeout so leftovers cannot
# pile up on the self-hosted runner. Younger containers may belong to a concurrent run of
# another pull request and are left alone.
- name: Remove stale regression containers
continue-on-error: true
shell: pwsh
run: |
$cutoff = (Get-Date).ToUniversalTime().AddHours(-4)
# The docker name filter is a regex over a substring, so an unanchored pattern would
# also match something like backup-centron-regression-db. '^/?' anchors it and works
# whether the daemon matches the bare name or the internal '/name'.
foreach ($id in @(docker ps --all --quiet --filter 'name=^/?centron-regression-')) {
if ([string]::IsNullOrWhiteSpace($id)) { continue }
$parts = (docker inspect --format '{{.Name}}|{{.Created}}' $id) -split '\|', 2
if ($parts.Count -ne 2) { continue }
$name = $parts[0].TrimStart('/')
$created = $parts[1]
# Second guard: this force-removes containers, so never act on a name that does not
# actually carry the prefix, whatever the daemon's filter semantics happen to be.
if (-not $name.StartsWith('centron-regression-')) { continue }
$parsed = [datetime]::MinValue
$isParsed = [datetime]::TryParse(
$created,
[cultureinfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AdjustToUniversal,
[ref] $parsed)
if ($isParsed -and $parsed -lt $cutoff) {
Write-Host "Removing stale regression container $name created at $created."
docker rm --force $id | Out-Null
}
}
$global:LASTEXITCODE = 0
- name: Start regression database
shell: pwsh
run: |
$containerName = "centron-regression-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT".ToLowerInvariant()
"DB_CONTAINER_NAME=$containerName" >> $env:GITHUB_ENV
docker pull centron.azurecr.io/centron_db/regression_tests:latest
if ($LASTEXITCODE -ne 0) {
throw 'Could not pull the regression database image.'
}
docker run --detach `
--name $containerName `
--env "MSSQL_SA_PASSWORD=$env:DB_PASSWORD" `
--env ACCEPT_EULA=Y `
--env MSSQL_PID=Standard `
--publish "${env:DB_PORT}:1433" `
centron.azurecr.io/centron_db/regression_tests:latest | Out-Null
if ($LASTEXITCODE -ne 0) {
throw 'Could not start the regression database container.'
}
- name: Wait for regression database
shell: pwsh
run: |
for ($attempt = 1; $attempt -le 120; $attempt++) {
$client = [Net.Sockets.TcpClient]::new()
try {
$connected = $client.ConnectAsync('localhost', [int]$env:DB_PORT).Wait(1000)
if ($connected -and $client.Connected) {
Write-Host 'Regression database is reachable.'
exit 0
}
}
catch {
# Database is still starting.
}
finally {
$client.Dispose()
}
Start-Sleep -Seconds 2
}
throw 'Regression database did not become reachable within four minutes.'
- name: Build regression tests
shell: pwsh
run: |
dotnet build `
"./tests/Centron.Tests.EndToEnd/Centron.Tests.EndToEnd.csproj" `
--configuration Release `
--framework net10.0 `
-nodeReuse:false
- name: Run regression tests
shell: pwsh
env:
CENTRON_TESTS_DATABASE_SERVER: localhost,1433
CENTRON_TESTS_DATABASE_USERNAME: sa
CENTRON_TESTS_DATABASE_PASSWORD: SA!password
DATABASE_BACKUP_PATH: /var/opt/mssql/backup/DatabaseBackup.bak
run: |
dotnet test `
"./tests/Centron.Tests.EndToEnd/Centron.Tests.EndToEnd.csproj" `
--configuration Release `
--framework net10.0 `
--no-build `
--logger "trx;LogFileName=TestResults.trx" `
--results-directory "./artifacts/EndToEndTests" `
-nodeReuse:false
- name: Capture database logs
if: always()
continue-on-error: true
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path './artifacts/EndToEndTests' | Out-Null
docker logs $env:DB_CONTAINER_NAME *>&1 |
Set-Content -LiteralPath './artifacts/EndToEndTests/database.log'
- name: Stop regression database
if: always()
continue-on-error: true
shell: pwsh
run: |
if (-not [string]::IsNullOrWhiteSpace($env:DB_CONTAINER_NAME)) {
docker rm --force $env:DB_CONTAINER_NAME 2>$null
}
$global:LASTEXITCODE = 0
- name: Upload regression results
if: always()
uses: actions/upload-artifact@v7
with:
name: regression-test-results
path: artifacts/EndToEndTests/
if-no-files-found: warn
retention-days: 14
+105
View File
@@ -0,0 +1,105 @@
name: Unit tests
on:
workflow_dispatch:
pull_request:
branches:
- main
- 'release/**'
push:
branches:
- main
- 'release/**'
permissions:
contents: read
# Supersede in-flight runs of this workflow for the same pull request. Pushes to main and
# release branches are excluded so every commit there still gets a full, recorded result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
name: ${{ matrix.display_name }}
runs-on: windows-2025
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- name: backend-bl
display_name: Backend BL tests
project: .\tests\backend\Centron.Tests.BL\Centron.Tests.BL.csproj
results_directory: artifacts/tests/backend/bl
- name: backend-dao
display_name: Backend DAO tests
project: .\tests\backend\Centron.Tests.DAO\Centron.Tests.DAO.csproj
results_directory: artifacts/tests/backend/dao
- name: shared-core
display_name: Shared Core tests
project: .\tests\shared\Centron.Tests.Core\Centron.Tests.Core.csproj
results_directory: artifacts/tests/shared/core
- name: nexus
display_name: Nexus tests
project: .\tests\CentronNexusTests\CentronNexusTests.csproj
results_directory: artifacts/tests/nexus
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
CENTRON_BUILD_IS_DEV_BUILD: true
DevExpress_License: ${{ secrets.DEVEXPRESS_LICENSE }}
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Verify .NET SDK
shell: pwsh
run: |
$sdkVersion = dotnet --version
if ($LASTEXITCODE -ne 0 -or $sdkVersion -notmatch '^10\.0\.') {
throw "Expected a .NET 10 SDK, but resolved '$sdkVersion'."
}
Write-Host "Resolved .NET SDK: $sdkVersion"
- name: Set build version
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- setup-versioning
- name: Run tests
shell: pwsh
run: |
dotnet test "${{ matrix.project }}" `
--configuration Release `
--framework net10.0 `
--logger "trx;LogFileName=TestResults.trx" `
--results-directory "${{ matrix.results_directory }}" `
-nodeReuse:false
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.name }}
path: ${{ matrix.results_directory }}/TestResults.trx
if-no-files-found: warn
retention-days: 14