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
8 changes: 7 additions & 1 deletion .github/actions/purview-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ inputs:
packages without per-package "Manage Actions access" grants.
required: false
default: ""
source-mapping:
description: >-
Optional semicolon-separated package ID patterns pinned to GitHub Packages. Pass the repository
variable `PURVIEW_NUGET_SOURCE_MAPPING`; a composite action has no `vars` context.
required: false
default: ""

runs:
using: composite
Expand All @@ -53,7 +59,7 @@ runs:
with:
namespace: ${{ github.repository_owner }}
token: ${{ inputs.packages-read-token || github.token }}
source-mapping: ${{ vars.PURVIEW_NUGET_SOURCE_MAPPING }}
source-mapping: ${{ inputs.source-mapping }}

- name: Install shared build
shell: bash
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/purview-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ jobs:
validate-pack: ${{ inputs.validate-pack }}
test-filter: ${{ inputs.test-filter }}
upload-artifacts: ${{ inputs.upload-artifacts }}
# A composite action has no `secrets` context, so the credentials are handed over here.
# A composite action has no `secrets` or `vars` context, so credentials and repository
# variables are handed over here.
nuget-api-key: ${{ secrets.NUGET_APIKEY || secrets.NUGET__APIKEY }}
packages-read-token: ${{ secrets.PURVIEW_PACKAGES_READ_TOKEN }}
source-mapping: ${{ vars.PURVIEW_NUGET_SOURCE_MAPPING }}
github-packages: ${{ vars.PURVIEW_PUBLISH_GITHUB_PACKAGES == 'true' }}
github-packages-keep-versions: ${{ vars.PURVIEW_GITHUB_PACKAGES_KEEP_VERSIONS || '1' }}
github-packages-retention-hours: ${{ vars.PURVIEW_GITHUB_PACKAGES_RETENTION_HOURS || '24' }}
34 changes: 29 additions & 5 deletions actions/purview-release/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,30 @@ inputs:
uses the organisation token has to pass it here.
required: false
default: ""
source-mapping:
description: >-
Optional semicolon-separated package ID patterns pinned to GitHub Packages. Pass the
repository variable `PURVIEW_NUGET_SOURCE_MAPPING`; a composite action has no `vars` context.
required: false
default: ""
github-packages:
description: >-
Publish the same packages to GitHub Packages as well. Pass `true` only when the repository
variable `PURVIEW_PUBLISH_GITHUB_PACKAGES` is `true`; a composite action has no `vars` context.
required: false
default: "false"
github-packages-keep-versions:
description: >-
Newest versions per package always kept by the mirror cleanup (repository variable
`PURVIEW_GITHUB_PACKAGES_KEEP_VERSIONS`).
required: false
default: "1"
github-packages-retention-hours:
description: >-
How long a mirrored version survives after publication (repository variable
`PURVIEW_GITHUB_PACKAGES_RETENTION_HOURS`).
required: false
default: "24"

runs:
using: composite
Expand Down Expand Up @@ -159,7 +183,7 @@ runs:
with:
namespace: ${{ github.repository_owner }}
token: ${{ inputs.packages-read-token || github.token }}
source-mapping: ${{ vars.PURVIEW_NUGET_SOURCE_MAPPING }}
source-mapping: ${{ inputs.source-mapping }}

- name: Install shared build
shell: bash
Expand Down Expand Up @@ -263,7 +287,7 @@ runs:
Release__Mode: ${{ steps.eligibility.outputs.release_mode }}
Release__UploadArtifacts: ${{ inputs.upload-artifacts }}
NuGet__TrustedPublishing: ${{ inputs.trusted-publishing }}
NuGet__GitHubPackages__Enabled: ${{ vars.PURVIEW_PUBLISH_GITHUB_PACKAGES == 'true' }}
NuGet__GitHubPackages__Enabled: ${{ inputs.github-packages }}
NuGet__GitHubPackages__Owner: ${{ github.repository_owner }}
Build__RunTests: ${{ inputs.run-tests }}
Build__RunLint: ${{ inputs.run-lint }}
Expand Down Expand Up @@ -311,12 +335,12 @@ runs:
# Housekeeping on the mirror only: NuGet.org keeps every version, so this is best effort and
# never fails a release. Only reached when the pipeline (both registries) succeeded.
- name: Prune older GitHub Packages versions
if: steps.eligibility.outputs.verdict == 'Release' && steps.eligibility.outputs.release_mode == 'NuGet' && vars.PURVIEW_PUBLISH_GITHUB_PACKAGES == 'true'
if: steps.eligibility.outputs.verdict == 'Release' && steps.eligibility.outputs.release_mode == 'NuGet' && inputs.github-packages == 'true'
uses: purview-dev/build/actions/prune-github-packages@main
with:
namespace: ${{ github.repository_owner }}
keep-versions: ${{ vars.PURVIEW_GITHUB_PACKAGES_KEEP_VERSIONS || '1' }}
retention-hours: ${{ vars.PURVIEW_GITHUB_PACKAGES_RETENTION_HOURS || '24' }}
keep-versions: ${{ inputs.github-packages-keep-versions }}
retention-hours: ${{ inputs.github-packages-retention-hours }}
version: ${{ steps.version.outputs.version }}

# A composite action cannot declare a post-step, so cleanup lives here and runs even when a
Expand Down
9 changes: 7 additions & 2 deletions src/tests/Build.UnitTests/NuGetSourceSetupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,19 @@ await Assert
public async Task PublishOptIn_IsEvaluatedOnlyWhereAReleaseCanHappen()
{
// Arrange
// A composite action has no `vars` context, so the action takes the decision as an input and
// the delegator is what reads the repository variable.
var release = Read(ReleaseAction);
var releaseWorkflow = Read(ReleaseWorkflow);
var selfRelease = Read(SelfHostedRelease);
var build = Read(BuildWorkflow);
var selfBuild = Read(SelfHostedBuild);

// Act & Assert
await Assert.That(release).Contains("vars.PURVIEW_PUBLISH_GITHUB_PACKAGES");
await Assert.That(release).Contains("inputs.github-packages");
await Assert.That(release).Contains("NuGet__GitHubPackages__Enabled");
await Assert.That(releaseWorkflow).Contains("vars.PURVIEW_PUBLISH_GITHUB_PACKAGES == 'true'");
await Assert.That(releaseWorkflow).Contains("github-packages:");
await Assert.That(selfRelease).Contains("vars.PURVIEW_PUBLISH_GITHUB_PACKAGES");
await Assert.That(selfRelease).Contains("NuGet__GitHubPackages__Enabled");
await Assert
Expand Down Expand Up @@ -283,7 +288,7 @@ public async Task Prune_OnlyRunsAfterASuccessfulReleaseWithTheOptIn()

// Act & Assert
await Assert.That(release).Contains("prune-github-packages");
await Assert.That(release).Contains("vars.PURVIEW_PUBLISH_GITHUB_PACKAGES == 'true'");
await Assert.That(release).Contains("inputs.github-packages == 'true'");
await Assert.That(selfRelease).Contains("prune-github-packages");
await Assert.That(build).DoesNotContain("prune-github-packages");
await Assert.That(composite).DoesNotContain("prune-github-packages");
Expand Down
40 changes: 40 additions & 0 deletions src/tests/Build.UnitTests/WorkflowParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,46 @@ await Assert
);
}

[Test]
public async Task CompositeActions_UseOnlyContextsGitHubAllowsThem()
{
// Arrange
// A composite action has no `vars` or `secrets` context. Referencing either makes GitHub reject
// the manifest — "Unrecognized named-value: 'vars'" — while loading the action, before any step
// runs. actionlint only parses workflow files, so nothing else catches it, and the failure only
// appears when a consumer's release actually loads the action.
var root = RepositoryRoot();
List<string> actions =
[
.. Directory.EnumerateFiles(Path.Combine(root, "actions"), "action.yml", SearchOption.AllDirectories),
.. Directory.EnumerateFiles(
Path.Combine(root, ".github", "actions"),
"action.yml",
SearchOption.AllDirectories
),
];

// Act
var offenders = actions
.Where(path => ForbiddenActionContext().IsMatch(File.ReadAllText(path)))
.Select(path => Path.GetRelativePath(root, path).Replace(Path.DirectorySeparatorChar, '/'))
.ToList();

// Assert
await Assert.That(actions).IsNotEmpty().Because("the guard below would otherwise be vacuous.");
await Assert
.That(offenders)
.IsEmpty()
.Because(
$"{string.Join(", ", offenders)} reference `vars` or `secrets` from a composite action. "
+ "GitHub has no such context there and fails to load the manifest. Pass the value in as "
+ "an input from the calling workflow."
);
}

[GeneratedRegex(@"\$\{\{[^}]*\b(?:vars|secrets)\.")]
private static partial Regex ForbiddenActionContext();

static string ReadWorkflow(string relativePath)
{
var root = RepositoryRoot();
Expand Down
Loading