1818using Nuke . Common . Tools . ReportGenerator ;
1919using static Nuke . Common . Tools . Docker . DockerTasks ;
2020using static Nuke . Common . Tools . DotNet . DotNetTasks ;
21+ using static Nuke . Common . Tools . Git . GitTasks ;
2122using static Nuke . Common . Tools . ReportGenerator . ReportGeneratorTasks ;
2223using static Serilog . Log ;
2324
2425[ UnsetVisualStudioEnvironmentVariables ]
2526[ DotNetVerbosityMapping ]
2627class Build : NukeBuild
2728{
29+ public const string MainBranch = "main" ;
30+ public const string ProjectName = ProductName + ".Shared" ;
31+ public const string ProductName = "DendroDocs" ;
32+ public const string RepositoryOwner = "dendrodocs" ;
33+ public const string RepositoryUrl = "https://github.com/" + RepositoryOwner + "/dotnet-shared-lib" ;
34+ public const string SbomNamespaceBase = "https://sbom.dendrodocs.dev" ;
35+ public const string SbomManifestRelPath = "_manifest/spdx_2.2/manifest.spdx.json" ;
36+ public const string GithubFeedSource = "GitHub - " + RepositoryOwner ;
37+ public const string NugetSourceUrl = "https://api.nuget.org/v3/index.json" ;
38+ public const string SbomNamespace = "https://sbom.dendrodocs.dev" ;
39+
40+ enum BuildFlows
41+ {
42+ Local ,
43+ PrRemote , // PR from fork (remote)
44+ PrLocal , // PR from same repo/branch
45+ Push , // Push to main
46+ Release // Tag/release
47+ }
48+
2849 // Entrypoint for Nuke CLI
2950 public static int Main ( ) => Execute < Build > ( x => x . Push ) ;
3051
3152 GitHubActions GitHubActions => GitHubActions . Instance ;
3253
3354 // Pipeline state
34- string BranchSpec => GitHubActions ? . Ref ;
55+ string GitHubRef => GitHubActions ? . Ref ;
56+ string GitHubEventName => GitHubActions ? . EventName ;
57+ string GitHubRepository => GitHubActions ? . Repository ;
58+ string GitHubHeadRepository => Environment . GetEnvironmentVariable ( "GITHUB_HEAD_REPOSITORY" ) ;
3559 string BuildNumber => GitHubActions ? . RunNumber . ToString ( ) ;
3660
61+ BuildFlows BuildFlow => GetBuildFlow ( ) ;
62+
3763 [ Parameter ( "Configuration to build - Default is 'Debug' (local) or 'Release' (server)" ) ]
3864 readonly Configuration Configuration = IsLocalBuild ? Configuration . Debug : Configuration . Release ;
3965
@@ -42,7 +68,7 @@ class Build : NukeBuild
4268 readonly string NuGetApiKey ;
4369
4470 [ Parameter ]
45- readonly string GitHubUser = GitHubActions . Instance ? . RepositoryOwner ?? "DendroDocs" ;
71+ readonly string GitHubUser = GitHubActions . Instance ? . RepositoryOwner ?? RepositoryOwner ;
4672
4773 [ Parameter ]
4874 [ Secret ]
@@ -72,26 +98,28 @@ class Build : NukeBuild
7298 string SemVer ;
7399
74100 bool IsPullRequest => GitHubActions ? . IsPullRequest ?? false ;
75- bool IsTag => BranchSpec is not null && BranchSpec . Contains ( "refs/tags" , StringComparison . OrdinalIgnoreCase ) ;
76- bool IsVersionTag => GitVersion ? . SemVer is not null && Regex . IsMatch ( GitVersion . SemVer , @"^\d+\.\d+\.\d+(-.*)?$" ) ;
101+ bool IsTag => GitHubRef is not null && GitHubRef . Contains ( "refs/tags" , StringComparison . OrdinalIgnoreCase ) ;
77102
78103 // Clean ensures output dirs are reset, for reproducible builds
79104 Target Clean => _ => _
80105 . Executes ( ( ) =>
81106 {
82107 ArtifactsDirectory . CreateOrCleanDirectory ( ) ;
83108 TestResultsDirectory . CreateOrCleanDirectory ( ) ;
109+ SbomDirectory . CreateOrCleanDirectory ( ) ;
110+ TrivyCacheDirectory . CreateDirectory ( ) ;
84111 } ) ;
85112
86113 // CI safety: only build from a clean git state (prevents accidental, local-only changes from leaking into artifacts)
87114 Target VerifyCleanGit => _ => _
88115 . OnlyWhenStatic ( ( ) => ! IsLocalBuild )
89116 . Executes ( ( ) =>
90117 {
91- var result = ProcessTasks . StartProcess ( "git" , "status --porcelain" ) ;
92- result . AssertZeroExitCode ( ) ;
93- var output = result . Output . Select ( x => x . Text ) . ToList ( ) ;
94- if ( output . Any ( ) )
118+ var output = Git ( "status --porcelain" )
119+ . Select ( x => x . Text )
120+ . ToList ( ) ;
121+
122+ if ( output . Count > 0 )
95123 throw new Exception ( "Repository is not clean. Commit or stash changes before running CI." ) ;
96124 } ) ;
97125
@@ -103,7 +131,7 @@ class Build : NukeBuild
103131
104132 if ( IsPullRequest )
105133 {
106- Information ( "Branch spec {BranchSpec} is a pull request. Adding build number {BuildNumber}" , BranchSpec , BuildNumber ) ;
134+ Information ( "Branch spec {BranchSpec} is a pull request. Adding build number {BuildNumber}" , GitHubRef , BuildNumber ) ;
107135
108136 SemVer = string . Join ( '.' , GitVersion . SemVer . Split ( '.' ) . Take ( 3 ) . Union ( [ BuildNumber ] ) ) ;
109137 }
@@ -155,11 +183,26 @@ class Build : NukeBuild
155183
156184 // Generate an SBOM for all artifacts using Microsoft's sbom-tool
157185 Target SbomDeliverable => _ => _
186+ // Only run SBOM creation for main branch and releases, not PRs
187+ . OnlyWhenDynamic ( ( ) => BuildFlow == BuildFlows . Push || BuildFlow == BuildFlows . Release )
158188 . DependsOn ( Pack )
159189 . Executes ( ( ) =>
160190 {
161- SbomDirectory . CreateOrCleanDirectory ( ) ;
162- Sbom ( $ "generate -b \" { ArtifactsDirectory } \" -bc . -m \" { SbomDirectory } \" -pn DendroDocs.Shared -pv { SemVer } -nsb https://sbom.dendrodocs.dev -ps DendroDocs -li true -pm true") ;
191+ var sbomArgs = new [ ]
192+ {
193+ "generate" ,
194+ "-b" , $ "\" { ArtifactsDirectory } \" ", // Base path for the build output
195+ "-bc" , "." , // Build config root
196+ "-m" , $ "\" { SbomDirectory } \" ", // Output SBOM manifest dir
197+ "-pn" , ProjectName , // Package name",
198+ "-pv" , SemVer ,
199+ "-nsb" , SbomNamespace ,
200+ "-ps" , RepositoryOwner ,
201+ "-li" , "true" , // Enable license info
202+ "-pm" , "true" // Enable package manifest
203+ } ;
204+
205+ Sbom ( arguments : string . Join ( " " , sbomArgs ) ) ;
163206 } ) ;
164207
165208 // Run Trivy via Docker, scanning the *source tree* for vulnerabilities, secrets, and misconfigurations
@@ -199,6 +242,7 @@ class Build : NukeBuild
199242 "--disable-telemetry" ,
200243 "--no-progress" ,
201244 "--skip-dirs" , ".nuke/temp" ,
245+ "--exit-code" , "1" ,
202246 "/src" ) ) ;
203247 } ) ;
204248
@@ -304,7 +348,7 @@ class Build : NukeBuild
304348 // Push to NuGet.org with precondition checks and integrity verification
305349 Target PushNuget => _ => _
306350 . DependsOn ( Proof )
307- . OnlyWhenDynamic ( ( ) => ! IsLocalBuild && IsTag && IsVersionTag )
351+ . OnlyWhenDynamic ( ( ) => BuildFlow == BuildFlows . Release )
308352 . ProceedAfterFailure ( )
309353 . Executes ( ( ) =>
310354 {
@@ -322,53 +366,78 @@ class Build : NukeBuild
322366 // Push to GitHub Packages, after all proof steps and only from trusted context
323367 Target PushGithub => _ => _
324368 . DependsOn ( Proof )
325- . OnlyWhenDynamic ( ( ) => ! IsLocalBuild && ! IsTag && IsPullRequest )
326- . OnlyWhenDynamic ( ( ) => GitHubUser == "dendrodocs" )
369+ . OnlyWhenDynamic ( ( ) => BuildFlow == BuildFlows . Push )
370+ . OnlyWhenDynamic ( ( ) => GitHubUser == RepositoryOwner )
327371 . ProceedAfterFailure ( )
328372 . Executes ( ( ) =>
329373 {
330- try
331- {
332- DotNetNuGetAddSource ( _ => _
333- . SetName ( $ "GitHub - { GitHubUser } ")
334- . SetUsername ( GitHubUser )
335- . SetPassword ( FeedGitHubToken )
336- . EnableStorePasswordInClearText ( )
337- . SetSource ( $ "https://nuget.pkg.github.com/{ GitHubUser } /index.json")
338- ) ;
339- }
340- catch
341- {
342- Information ( "Source already added" ) ;
343- }
344-
345374 VerifyPackageHashes ( ) ;
346375
347376 DotNetNuGetPush ( _ => _
348377 . SetApiKey ( FeedGitHubToken )
349378 . SetTargetPath ( ArtifactsDirectory / "*.nupkg" )
350379 . EnableSkipDuplicate ( )
351- . SetSource ( $ "GitHub - { GitHubUser } ")
380+ . SetSource ( $ "https://nuget.pkg.github.com/ { RepositoryOwner } /index.json ")
352381 . EnableNoSymbols ( )
353382 ) ;
354383 } ) ;
355384
356- // Always verify artifact hashes before publishing—no accidental or malicious tampering!
385+ BuildFlows GetBuildFlow ( )
386+ {
387+ if ( IsLocalBuild )
388+ {
389+ return BuildFlows . Local ;
390+ }
391+
392+ // PR event
393+ if ( GitHubEventName ? . StartsWith ( "pull_request" ) == true )
394+ {
395+ // Forked repo PRs (external contributors)
396+ if ( ! string . IsNullOrWhiteSpace ( GitHubHeadRepository ) && ! string . Equals ( GitHubHeadRepository , GitHubRepository , StringComparison . OrdinalIgnoreCase ) )
397+ {
398+ return BuildFlows . PrRemote ;
399+ }
400+
401+ // In-repo branch PRs
402+ return BuildFlows . PrLocal ;
403+ }
404+
405+ // Tag/release
406+ if ( IsTag || GitHubEventName == "release" )
407+ {
408+ return BuildFlows . Release ;
409+ }
410+
411+ // Branch push
412+ if ( GitHubRef ? . StartsWith ( "refs/heads/" ) == true )
413+ {
414+ return BuildFlows . Push ;
415+ }
416+
417+ return BuildFlows . Local ;
418+ }
419+
420+ // Always verify artifact hashes before publishing—no accidental or malicious tampering
357421 void VerifyPackageHashes ( )
358422 {
359423 var packages = Directory . GetFiles ( ArtifactsDirectory , "*.nupkg" ) ;
360424 foreach ( var package in packages )
361425 {
362426 var hashFile = package + ".sha256" ;
363427
364- if ( ! File . Exists ( hashFile ) ) throw new Exception ( $ "Missing SHA256 file for { package } ") ;
428+ if ( ! File . Exists ( hashFile ) )
429+ {
430+ throw new Exception ( $ "Missing SHA256 file for { package } ") ;
431+ }
365432
366433 var computedHash = SHA256 . HashData ( File . ReadAllBytes ( package ) ) ;
367434 var expectedHash = File . ReadAllText ( hashFile ) . Trim ( ) ;
368435 var actualHash = BitConverter . ToString ( computedHash ) . Replace ( "-" , string . Empty ) . ToLowerInvariant ( ) ;
369436
370437 if ( ! string . Equals ( expectedHash , actualHash , StringComparison . OrdinalIgnoreCase ) )
438+ {
371439 throw new Exception ( $ "SHA256 mismatch for { package } : expected { expectedHash } , got { actualHash } ") ;
440+ }
372441 }
373442 }
374443}
0 commit comments