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
49 changes: 49 additions & 0 deletions IntegrationTests/IntegrationTests/AuthorPackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,55 @@ public async Task MissingCredential_FailsPackWithSC102()
await Assert.That(result.Combined).Contains("GitHub Sponsors: API token required");
}

[Test]
public async Task PullRequestBuild_SkipsBundling()
{
// On PR CI the platform credential is normally unavailable, so rather than failing SC102 the
// bundler is skipped and the package packs cleanly without the verifier. Simulate a PR via
// the AppVeyor signal — MSBuild reads the env-var-named property directly.
// forceBundleInPullRequest:false so the suite's hermeticity guard doesn't re-enable bundling.
var (result, feed) = await ThePackageBuilder.TryPackToFeed(
"ThePackage",
extraProperties: new Dictionary<string, string>
{
["APPVEYOR_PULL_REQUEST_NUMBER"] = "7"
},
forceBundleInPullRequest: false);

await Assert.That(result.ExitCode).IsEqualTo(0).Because(result.Combined);
await Assert.That(result.Combined).Contains("skipping sponsor-list bundling");

var nupkg = Directory.GetFiles(feed, "ThePackage.*.nupkg").Single();
using var zip = ZipFile.OpenRead(nupkg);
var entries = zip.Entries.Select(_ => _.FullName).ToList();
// No verifier, no bundled sponsor data, no tasks/ DLLs — all live inside the skipped target.
await Assert.That(entries.Any(_ => _ == "build/ThePackage.targets")).IsFalse();
await Assert.That(entries.Any(_ => _.StartsWith("build/SponsorCheck."))).IsFalse();
await Assert.That(entries.Any(_ => _.StartsWith("tasks/"))).IsFalse();
}

[Test]
public async Task PullRequestBuild_OverrideForcesBundling()
{
// <SponsorCheckBundleInPullRequest>true</> opts back in: the bundler runs even on a PR, so
// the verifier and bundled sponsor data are present exactly as on a normal build.
var (result, feed) = await ThePackageBuilder.TryPackToFeed(
"ThePackage",
extraProperties: new Dictionary<string, string>
{
["APPVEYOR_PULL_REQUEST_NUMBER"] = "7"
},
forceBundleInPullRequest: true);

await Assert.That(result.ExitCode).IsEqualTo(0).Because(result.Combined);

var nupkg = Directory.GetFiles(feed, "ThePackage.*.nupkg").Single();
using var zip = ZipFile.OpenRead(nupkg);
var entries = zip.Entries.Select(_ => _.FullName).ToList();
await Assert.That(entries).Contains("build/ThePackage.targets");
await Assert.That(entries).Contains("build/SponsorCheck.SponsorHashes.txt");
}

[Test]
public async Task SeverityOverrides_InvalidValue_FailsPackWithSC104()
{
Expand Down
32 changes: 29 additions & 3 deletions IntegrationTests/IntegrationTests/ThePackageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ public static async Task<string> EnsureBuilt(string fixtureName)
["SponsorCheckVersion"] = sponsorCheckVersion,
// Backdate pack so Consumer.RecentSponsor can have a SponsorshipStart that is
// both AFTER the pack date and BEFORE today (i.e. not in the future).
["SponsorCheck_PackDateOverride"] = "2024-01-01"
["SponsorCheck_PackDateOverride"] = "2024-01-01",
// Force bundling so the suite is hermetic: if it runs on a PR CI build, the
// ambient PR env var would otherwise trip the bundler's pull-request skip and
// these fixtures would pack without the verifier, breaking pack assertions.
["SponsorCheckBundleInPullRequest"] = "true"
},
workDir,
packagesDir).ConfigureAwait(false);
Expand Down Expand Up @@ -130,7 +134,9 @@ public static async Task<string> EnsureBuiltCombined(params string[] fixtureName
["SponsorListOverride"] = TestEnvironment.OverrideListPath,
["PackageOutputPath"] = feed,
["SponsorCheckVersion"] = sponsorCheckVersion,
["SponsorCheck_PackDateOverride"] = "2024-01-01"
["SponsorCheck_PackDateOverride"] = "2024-01-01",
// Force bundling for hermeticity — see the note in EnsureBuilt.
["SponsorCheckBundleInPullRequest"] = "true"
},
workDir,
packagesDir).ConfigureAwait(false);
Expand Down Expand Up @@ -162,6 +168,20 @@ public static async Task<CliResult> TryPack(
string fixtureName,
bool useOverrideList = true,
IReadOnlyDictionary<string, string>? extraProperties = null)
{
var (result, _) = await TryPackToFeed(fixtureName, useOverrideList, extraProperties).ConfigureAwait(false);
return result;
}

/// Like <see cref="TryPack"/> but also returns the fresh feed dir so the caller can open the
/// produced nupkg. <paramref name="forceBundleInPullRequest"/> defaults to true for hermeticity
/// (so an ambient PR env var on CI doesn't disable the bundler); the PR-skip tests pass false to
/// exercise the actual pull-request skip.
public static async Task<(CliResult Result, string Feed)> TryPackToFeed(
string fixtureName,
bool useOverrideList = true,
IReadOnlyDictionary<string, string>? extraProperties = null,
bool forceBundleInPullRequest = true)
{
var feed = Path.Combine(Path.GetTempPath(), "sponsorcheck-it-feed", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(feed);
Expand All @@ -183,6 +203,11 @@ public static async Task<CliResult> TryPack(
["SponsorCheckVersion"] = sponsorCheckVersion,
["SponsorCheck_PackDateOverride"] = "2024-01-01"
};
if (forceBundleInPullRequest)
{
properties["SponsorCheckBundleInPullRequest"] = "true";
}

if (useOverrideList)
{
properties["SponsorListOverride"] = TestEnvironment.OverrideListPath;
Expand All @@ -196,13 +221,14 @@ public static async Task<CliResult> TryPack(
}
}

return await DotnetCliRunner.Run(
var result = await DotnetCliRunner.Run(
"pack",
Path.Combine(workDir, $"{fixtureName}.csproj"),
"Release",
properties,
workDir,
packagesDir).ConfigureAwait(false);
return (result, feed);
}

static string ExtractVersion(string nupkgPath)
Expand Down
1 change: 1 addition & 0 deletions docs/BundlerDiagnosticCodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Every emitted message is prefixed with the code's short **Name** (e.g. `Platform
- **Meaning:** A platform that requires a credential is missing one (GitHub Sponsors, Polar). Setup advice flips between user-secrets-first (local) and env-var-only (CI) based on `BuildServerDetector`.
- **Syntax:** `{platformLabel}: API token required. {advice}` where `advice` is `Run \`dotnet user-secrets set SponsorCheck:{Platform}Token <pat>\` (recommended for local dev), or set the <{Platform}Token> MSBuild property, or set the '{Platform}Token' env var.` locally, or `Set the '{Platform}Token' env var (CI providers should expose their encrypted secret under this name; MSBuild auto-imports it as the <{Platform}Token> property).` on CI.
- **Example:** `GitHub Sponsors: API token required. Run \`dotnet user-secrets set SponsorCheck:GitHubToken <pat>\` (recommended for local dev), or set the <GitHubToken> MSBuild property, or set the 'GitHubToken' env var.`
- **Pull-request builds:** on a detected pull-request CI build the bundler is *skipped* rather than failing with SC102 — the credential is normally unavailable on PRs and the PR package is throwaway (packs without the verifier). Force bundling on PRs with `<SponsorCheckBundleInPullRequest>true</SponsorCheckBundleInPullRequest>`. See readme → "Pull request builds".


### SC103
Expand Down
15 changes: 15 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,21 @@ dotnet user-secrets set "SponsorCheck:PolarToken" "polar_yyy"
Recommended for CI builds, where there's no per-developer profile to hold a user-secrets file. Encrypt the token in the CI provider's secret store (AppVeyor "secure variable", GitHub Actions secret, Azure DevOps secret variable, etc.) and surface it as an env var named `GitHubToken`, `OpenCollectiveToken`, or `PolarToken`. MSBuild auto-imports env vars as properties, so no extra wiring is needed — the bundler picks them up via the same `<GitHubToken>` / `<OpenCollectiveToken>` / `<PolarToken>` resolution path. The env var name must match the MSBuild property name modulo case (`GitHubToken`, `githubtoken`, and `GITHUBTOKEN` all resolve via case-insensitive property lookup), but punctuation matters — conventional CI names like `GITHUB_TOKEN` won't auto-flow.


#### Pull request builds

Most CI providers withhold encrypted secrets from pull-request builds (especially PRs from forks), so the credential above isn't available — a pack there would otherwise fail with [SC102](docs/BundlerDiagnosticCodes.md#sc102). A PR build also never publishes the package it produces, so the sponsorship verifier that bundling would embed is throwaway anyway.

So on a detected pull-request build the bundler is **skipped**: the package still packs — without the verifier — and no credential is required. A high-importance build message records that it happened. Detection covers the common providers via their PR-only signals — AppVeyor (`APPVEYOR_PULL_REQUEST_NUMBER`), GitHub Actions (`GITHUB_EVENT_NAME=pull_request`), Azure DevOps (`SYSTEM_PULLREQUEST_PULLREQUESTID`), GitLab, Bitbucket, Jenkins, Travis, CircleCI, Buildkite — and is deliberately conservative, so a real release build is never mistaken for a PR.

To validate the verifier on PR builds that *do* have the credential, opt back in:

```xml
<PropertyGroup>
<SponsorCheckBundleInPullRequest>true</SponsorCheckBundleInPullRequest>
</PropertyGroup>
```


### Multiple packable projects in one repo

For repos that produce multiple NuGet packages, configure once and let MSBuild's normal cascading mechanisms apply:
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;NU1608;NU1109;NU1901;PolyfillTargetsForNuget</NoWarn>
<Version>0.13.0</Version>
<Version>0.14.0</Version>
<LangVersion>preview</LangVersion>
<!-- Do NOT pin AssemblyVersion. SponsorCheck.dll is an MSBuild task assembly shipped inside many
packages (Morph, Parchment, OpenXmlHtml, Excelsior, ...). A fixed AssemblyVersion makes MSBuild's
Expand Down
38 changes: 37 additions & 1 deletion src/SponsorCheck/build/SponsorCheck.targets
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,45 @@
<UsingTask TaskName="BundleSponsorListTask"
AssemblyFile="$(_SponsorCheck_TasksAssembly)" />

<!-- Pull-request detection. On PR CI the platform credential (e.g. GitHubToken) is normally not
exposed to the build, so bundling the sponsor list would fail with SC102 — and a PR build
never publishes the package it produces, so the verifier it would carry is throwaway anyway.
When a PR is detected the bundler is skipped: the package still packs, just without the
sponsorship verifier. Authors who DO have a credential on PR builds (and want the verifier
validated there) opt back in with <SponsorCheckBundleInPullRequest>true</...>.

Detection is deliberately conservative — every signal below is a CI-provider PR-only variable,
so a real release build is never mistaken for a PR (a *missed* detection just falls back to the
normal credential-required path, which is safe). MSBuild auto-imports environment variables as
properties of the same name, so these read the CI environment directly. -->
<PropertyGroup>
<_SponsorCheck_IsPullRequest>false</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(APPVEYOR_PULL_REQUEST_NUMBER)' != ''">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(SYSTEM_PULLREQUEST_PULLREQUESTID)' != ''">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(GITHUB_EVENT_NAME)' == 'pull_request' or '$(GITHUB_EVENT_NAME)' == 'pull_request_target'">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(CHANGE_ID)' != ''">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(CI_MERGE_REQUEST_IID)' != ''">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(BITBUCKET_PR_ID)' != ''">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(CIRCLE_PULL_REQUEST)' != ''">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(TRAVIS_PULL_REQUEST)' != '' and '$(TRAVIS_PULL_REQUEST)' != 'false'">true</_SponsorCheck_IsPullRequest>
<_SponsorCheck_IsPullRequest Condition="'$(BUILDKITE_PULL_REQUEST)' != '' and '$(BUILDKITE_PULL_REQUEST)' != 'false'">true</_SponsorCheck_IsPullRequest>

<_SponsorCheck_SkipForPullRequest>false</_SponsorCheck_SkipForPullRequest>
<_SponsorCheck_SkipForPullRequest Condition="'$(_SponsorCheck_IsPullRequest)' == 'true' and '$(SponsorCheckBundleInPullRequest)' != 'true'">true</_SponsorCheck_SkipForPullRequest>
</PropertyGroup>

<!-- Informs the author (and audit log) why the verifier is absent when a PR skip happens. High
importance so it surfaces at the default 'minimal' verbosity of dotnet build/pack. -->
<Target Name="_SponsorCheck_NotePullRequestSkip"
BeforeTargets="_GetPackageFiles;GenerateNuspec"
Condition="'$(Configuration)' == 'Release' and '$(IsPackable)' == 'true' and '$(_SponsorCheck_SkipForPullRequest)' == 'true'">
<Message Importance="high"
Text="SponsorCheck: pull-request build detected; skipping sponsor-list bundling (the platform credential is normally unavailable on PR CI). The package packs without the sponsorship verifier. Set &lt;SponsorCheckBundleInPullRequest&gt;true&lt;/SponsorCheckBundleInPullRequest&gt; to force bundling." />
</Target>

<Target Name="_SponsorCheck_BundleSponsorList"
BeforeTargets="_GetPackageFiles;GenerateNuspec"
Condition="'$(Configuration)' == 'Release' and '$(IsPackable)' == 'true'">
Condition="'$(Configuration)' == 'Release' and '$(IsPackable)' == 'true' and '$(_SponsorCheck_SkipForPullRequest)' != 'true'">
<PropertyGroup>
<_SponsorCheck_HashListPath>$(IntermediateOutputPath)SponsorCheck\SponsorHashes.txt</_SponsorCheck_HashListPath>
<_SponsorCheck_GeneratedTargetsPath>$(IntermediateOutputPath)SponsorCheck\$(PackageId).targets</_SponsorCheck_GeneratedTargetsPath>
Expand Down
Loading