Skip to content
Closed
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
1 change: 1 addition & 0 deletions .fallout/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"GenerateTools",
"Install",
"Pack",
"PackageGuard",
"Publish",
"References",
"ReportCoverage",
Expand Down
42 changes: 35 additions & 7 deletions .github/workflows/publish-packages-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,36 @@ jobs:
global-json-file: global.json
- name: 'Restore: dotnet tools'
run: dotnet tool restore
- name: 'Run: Test + Pack'
run: dotnet fallout Test Pack
- name: 'Run: Test + Pack + PackageGuard'
run: dotnet fallout Test Pack PackageGuard
env:
# We check out the tag above, so HEAD is detached and matches none of
# version.json's publicReleaseRefSpec entries (they're all branch refs,
# and nbgv never matches that spec against refs/tags/*). Without this,
# NB.GV treats the build as non-public and stamps a git-height suffix
# onto every package — v10.4.0-rc.3 shipped as 10.4.0-rc.3.geabd043cc2.
PublicRelease: true
# PackageGuard's own target (build/Build.PackageGuard.cs) is gated on either being on
# a long-lived branch or running in this workflow (GitHubActions.Workflow ==
# "publish-packages-release") — the tag checkout above leaves HEAD detached, so the
# branch check alone can't pass here; validate-ref already proved the tag is
# reachable from a production branch. GITHUB_TOKEN lets it authenticate to GitHub
# (license lookups) instead of hitting anonymous rate limits.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Upload: package artifacts'
uses: actions/upload-artifact@v7
with:
name: packages
path: output/packages/*.nupkg
retention-days: 7
if-no-files-found: error
- name: 'Upload: PackageGuard SBOM + risk report'
uses: actions/upload-artifact@v7
with:
name: packageguard
path: output/packageguard/*
retention-days: 7
if-no-files-found: error

# Tier 1 — production / nuget.org. **OPT-IN ONLY.** Tag pushes do NOT trigger
# this job; you must invoke workflow_dispatch with publish-to-nugetorg=true.
Expand Down Expand Up @@ -241,9 +255,11 @@ jobs:
--skip-duplicate
done

# Bundled artifact distribution. Attaches all nupkgs to the GitHub Release
# for the tag. Idempotent: if the release already exists (workflow_dispatch
# retry case), uploads or replaces missing assets via --clobber.
# Bundled artifact distribution. Attaches all nupkgs, plus the PackageGuard SBOM and HTML
# risk report, to the GitHub Release for the tag. Idempotent: if the release already exists
# (workflow_dispatch retry case), uploads or replaces missing assets via --clobber. The .sarif
# itself isn't attached here — it's reported to GitHub's code-scanning feature instead
# (security-scan.yml); the HTML is the human-readable counterpart of the same report.
publish-github-releases:
name: publish → GitHub Releases
runs-on: ubuntu-latest
Expand All @@ -262,6 +278,11 @@ jobs:
with:
name: packages
path: output/packages
- name: 'Download: PackageGuard SBOM + risk report'
uses: actions/download-artifact@v8
with:
name: packageguard
path: output/packageguard
- name: 'Create or update GitHub Release with package artifacts'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -273,14 +294,21 @@ jobs:
TAG="${GITHUB_REF_NAME}"
fi
echo "Tag: $TAG"
assets=(output/packages/*.nupkg)
if [ -f output/packageguard/sbom.json ]; then
assets+=("output/packageguard/sbom.json")
fi
if [ -f output/packageguard/risk-report.html ]; then
assets+=("output/packageguard/risk-report.html")
fi
if gh release view "$TAG" > /dev/null 2>&1; then
echo "Release $TAG already exists — uploading assets with --clobber."
gh release upload "$TAG" output/packages/*.nupkg --clobber
gh release upload "$TAG" "${assets[@]}" --clobber
else
echo "Creating new release $TAG."
gh release create "$TAG" \
--title "$TAG" \
--target "$(git rev-parse HEAD)" \
--generate-notes \
output/packages/*.nupkg
"${assets[@]}"
fi
67 changes: 67 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ------------------------------------------------------------------------------
# <auto-generated>
#
# This code was generated.
#
# - To turn off auto-generation set:
#
# [GitHubActions (AutoGenerate = false)]
#
# - To trigger manual generation invoke:
#
# fallout --generate-configuration GitHubActions_security-scan --host GitHubActions
#
# </auto-generated>
# ------------------------------------------------------------------------------

name: security-scan

on:
push:
branches:
- develop
- main
- 'release/*'
- 'support/*'
paths-ignore:
- 'docs/**'
- '.assets/**'
- '**/*.md'

permissions:
security-events: write
contents: read

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

jobs:
ubuntu-latest:
name: ubuntu-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: 'Cache: .fallout/temp, ~/.nuget/packages'
uses: actions/cache@v6
with:
path: |
.fallout/temp
~/.nuget/packages
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
- name: 'Setup: .NET SDK'
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: 'Restore: dotnet tools'
run: dotnet tool restore
- name: 'Run: PackageGuard'
run: dotnet fallout PackageGuard
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Upload risk-report SARIF to GitHub code scanning'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: output/packageguard/risk-report.sarif
57 changes: 53 additions & 4 deletions build/Build.CI.GitHubActions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Collections.Generic;
using Fallout.Common.CI.GitHubActions;
using Fallout.Common.CI.GitHubActions.Configuration;
using Fallout.Components;

// Two generated build workflows. Both run Test+Pack; both are GENERATED from the
// attributes below — edit here and regenerate (`./build.sh`), never hand-edit the
// `.yml`.
// Three generated workflows. build.yml and build-cross-platform.yml both run Test+Pack; all
// three are GENERATED from the attributes below — edit here and regenerate (`./build.sh`),
// never hand-edit the `.yml`.
//
// build.yml — the Linux PR gate, and the ONLY required status
// check (job `ubuntu-latest`; branch protection keys on
Expand Down Expand Up @@ -61,7 +63,34 @@
OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" },
InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) },
PublishArtifacts = false)]
partial class Build
// security-scan.yml — the PackageGuard compliance scan (build/Build.PackageGuard.cs).
// Push-only, and only to the four long-lived branches — this is
// the CI-side half of the branch restriction; the target's own
// OnlyWhenStatic(IsOnLongLivedBranch) guard is the other half, for
// anyone invoking the target outside this workflow. Runs on its own
// schedule rather than piggy-backing on build.yml/build-cross-platform.yml,
// because those check out the PR's source branch (never one of the
// four) — a PackageGuard run gated on being IN one of those workflows
// would just always skip. EnableGitHubToken feeds GITHUB_TOKEN to
// PackageGuard (avoids GitHub API rate-limiting on license lookups)
// and to the upload-sarif step's security-events:write use.
[GitHubActions(
"security-scan",
GitHubActionsImage.UbuntuLatest,
FetchDepth = 0,
ConcurrencyGroup = "${{ github.workflow }}-${{ github.ref }}",
ConcurrencyCancelInProgress = true,
OnPushBranches = new[] { DevelopBranch, MainBranch, ReleaseBranchPattern, SupportBranchPattern },
OnPushExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" },
InvokedTargets = new[] { nameof(PackageGuard) },
EnableGitHubToken = true,
// Specifying any `permissions:` block switches the job from GitHub's default read-all to
// explicit-only — contents:read has to be listed too, or upload-sarif (and checkout) lose
// it. See GitHub's own upload-sarif docs for this exact pairing.
ReadPermissions = new[] { GitHubActionsPermissions.Contents },
WritePermissions = new[] { GitHubActionsPermissions.SecurityEvents },
PublishArtifacts = false)]
partial class Build : IConfigureGitHubActions
{
// The release workflow is intentionally hand-written at
// .github/workflows/publish-packages-release.yml — that lets us name the GitHub
Expand All @@ -71,4 +100,24 @@ partial class Build
// workflow's `name:` — it gates ICreateGitHubRelease.CreateGitHubRelease
// (Build.cs) to the release workflow only.
const string ReleaseWorkflow = "publish-packages-release";

// Injects the SARIF upload after security-scan's "dotnet fallout PackageGuard" run step —
// GitHubActionsStepPosition.PostRun is exactly "after the run block, before the built-in
// artifact upload". Scoped to this one generated job by WorkflowName; other jobs get no
// insertions.
void IConfigureGitHubActions.ConfigureSteps(GitHubActionsStepPipeline pipeline)
{
if (pipeline.WorkflowName == "security-scan")
{
pipeline.Insert(GitHubActionsStepPosition.PostRun, new GitHubActionsCustomStep
{
Name = "Upload risk-report SARIF to GitHub code scanning",
Uses = "github/codeql-action/upload-sarif@v3",
With = new Dictionary<string, string>
{
["sarif_file"] = "output/packageguard/risk-report.sarif",
},
});
}
}
}
57 changes: 57 additions & 0 deletions build/Build.PackageGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Fallout.Common;
using Fallout.Common.Git;
using Fallout.Common.IO;
using Fallout.Common.Tooling;
using Fallout.Common.Tools.PackageGuard;
using Fallout.Components;

partial class Build
{
// PackageGuard's own CLI only exposes an env-var override for the risk-report path
// (see PackageGuard.json's help text on ReportRisk) — there's no `--report-risk <path>`
// argument in our wrapper, matching the repo-wide convention that bool CLI flags stay
// presence-only. Setting this process env var instead pins the SARIF/HTML pair to a
// deterministic path we can reference from CI (upload-sarif, the release asset step).
const string PackageGuardReportRiskPathOverrideEnvironmentVariable = "PACKAGEGUARD_REPORT_RISK_PATH_OVERRIDE";

AbsolutePath PackageGuardDirectory => OutputDirectory / "packageguard";
AbsolutePath PackageGuardSbomFile => PackageGuardDirectory / "sbom.json";
AbsolutePath PackageGuardSarifFile => PackageGuardDirectory / "risk-report.sarif";

// Every real invocation path is already tied to one of these four branches:
// - the dedicated "security-scan" workflow only triggers on a push to develop/main/
// release/*/support/* (Build.CI.GitHubActions.cs), so GitRepository.Branch resolves
// to one of them there.
// - the tag-triggered release workflow checks out a detached HEAD (no branch to
// resolve), but its own validate-ref job already proves the tag is reachable from
// main/release/*/support/* before this ever runs — hence the GitHubActions.Workflow
// fallback below.
// A local run on a feature branch, or the PR gate (build.yml checks out the contributor's
// branch via github.head_ref), matches neither and skips — which is the point: this scan
// is deliberately not part of every PR's Test+Pack run.
bool IsOnLongLivedBranch =>
GitRepository.IsOnMainBranch() ||
GitRepository.IsOnDevelopBranch() ||
GitRepository.IsOnReleaseBranch() ||
GitRepository.IsOnSupportBranch() ||
GitHubActions?.Workflow == ReleaseWorkflow;

Target PackageGuard => _ => _
.DependsOn<IRestore>()
.OnlyWhenStatic(() => IsOnLongLivedBranch)
.Produces(PackageGuardSbomFile)
.Produces(PackageGuardSarifFile)
.Produces(PackageGuardDirectory / "*.html")
.Executes(() =>
{
PackageGuardDirectory.CreateOrCleanDirectory();

PackageGuardTasks.PackageGuard(_ => _
.SetProjectPath(Solution.Path)
.EnableReportRisk()
.SetSbom(SbomFormat.cyclonedx)
.SetSbomOutput(PackageGuardSbomFile)
.SetGitHubApiKey(From<ICreateGitHubRelease>().GitHubToken)
.SetProcessEnvironmentVariable(PackageGuardReportRiskPathOverrideEnvironmentVariable, PackageGuardSarifFile));
});
}
Loading
Loading