Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow configuring persist-credentials for GitHub Actions checkout step #1533

Open
schnerring opened this issue Mar 23, 2025 · 1 comment
Open

Comments

@schnerring
Copy link

schnerring commented Mar 23, 2025

Description

Allow generating the following via PersistCredentials = false option on the GitHubActionsAttribute:

- name: Checkout
   uses: actions/checkout@v2
   with:
      persist-credentials: false

From the actions/checkout readme:

The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set persist-credentials: false to opt-out.


I currently use GitHubActionsAttributes to generate two GH workflows:

  • The deploy pipeline is triggered on v* tags, and will deploy a website, NuGets, some zips etc.
  • The release pipeline basically just runs the tests. If the tests succeed, I run semantic-release which analyzes the git history and decides whether or to add and push a git release tag.
Target RestoreNpm => _ => _
    .Executes(() => NpmTasks.NpmCi());

Target Release => _ => _
    .DependsOn(Test, RestoreNpm)
    .Executes(() =>
    {
        Npx("semantic-release")
    });

So the release workflow pushes release tags, which should trigger the deploy workflow. But using the platform-provided GITHUB_TOKEN to push tags will not trigger any other workflows. This is a basic security measure to protect users from defining recursive workflows.

The well-known workaround is using a Personal Access Token (PAT) to push tags, which allows a workflow to trigger another workflow.

I tried the following workaround:

Npx("semantic-release",
    environmentVariables: EnvironmentInfo.Variables
        .ToDictionary(env => env.Key, env => env.Value)
        .SetKeyValue("GH_TOKEN", SemanticReleaseGitHubPat)
        .AsReadOnly());

I tried everything, fine-grained PATs, classic ones, using NpmTasks.RunNpm(). But the only thing that helps is adding setting persist-credentials: false.

At first I thought that this is what EnableGitHubToken = false should do, but that's not the case.

Usage Example

[GitHubActions(
    "release",
    GitHubActionsImage.UbuntuLatest,
    OnPushBranches = ["main"],
    InvokedTargets = [nameof(Release)],
    FetchDepth = 0,
    Filter = "tree:0",
    PersistCredentials = false
]

Alternative

No response

Could you help with a pull-request?

Yes

@schnerring
Copy link
Author

schnerring commented Mar 23, 2025

Made it work!

[SemanticReleaseWorkflow(
    "release",
    GitHubActionsImage.UbuntuLatest,
    OnPushBranches = ["main"],
    InvokedTargets = [nameof(Release)],
    ImportSecrets = [nameof(SemanticReleaseGitHubPat)],
    CacheKeyFiles =
    [
        // NuGet lock files
        "**/packages.lock.json", "!**/ bin/**", "!**/obj/**",
        // npm lock file
        "package-lock.json"
    ],
    CacheIncludePatterns = [ ".nuke/temp", "~/.nuget/packages", "~/.npm" ])]
partial class Build
{
    [Secret]
    [Parameter("Fine-grained GitHub personal access token (PAT). " +
               "semantic-release requires `contents: write` permissions to push tags. " +
               "Optionally add `issues: write` and `pull-requests: write` permissions to allow " +
               "semantic-release to comment on released issues and PRs.")]
    readonly string SemanticReleaseGitHubPat;

    Target RestoreNpm => _ => _
        .Executes(() => NpmTasks.NpmCi());

    Target Release => _ => _
        .DependsOn(RestoreNpm, Test)
        .Executes(() =>
        {
            Npx("semantic-release",
                environmentVariables: EnvironmentInfo.Variables
                    .ToDictionary(env => env.Key, env => env.Value) // TODO(refactor): obsolete?
                    .SetKeyValue("GH_TOKEN", SemanticReleaseGitHubPat)
                    .AsReadOnly());
        });
}

I implemented the following custom attribute. It's a bit hacky, but the most NUKEsque way I could come up with:

public class SemanticReleaseWorkflowAttribute
    : GitHubActionsAttribute
{
    public SemanticReleaseWorkflowAttribute(
        string name, GitHubActionsImage image,
        params GitHubActionsImage[] images) : base(name, image, images)
    {
        // treeless clone
        FetchDepth = 0;
        Filter = "tree:0";
    }

    protected override GitHubActionsJob GetJobs(
        GitHubActionsImage image,
        IReadOnlyCollection<ExecutableTarget> relevantTargets)
    {
        var jobs = base.GetJobs(image, relevantTargets);

        // TODO: override GetSteps()
        var checkoutStep = jobs.Steps.OfType<GitHubActionsCheckoutStep>().Single();
        var checkoutStepIndex = Array.IndexOf(jobs.Steps, checkoutStep);

        jobs.Steps[checkoutStepIndex] = new CheckoutStepWithCredentialPersistence
        {
            PersistCredentials = false, // 🪨🧑‍💻️

            FetchDepth = checkoutStep.FetchDepth,
            Filter = checkoutStep.Filter,
            Lfs = checkoutStep.Lfs,
            Progress = checkoutStep.Progress,
            Submodules = checkoutStep.Submodules
        };

        return jobs;
    }

    private class CheckoutStepWithCredentialPersistence : GitHubActionsCheckoutStep
    {
        public bool? PersistCredentials { get; init; }

        // TODO: refactor base class implementation detail 👇 🏞
        private bool WithKeyAlreadyWritten =>
            Submodules.HasValue || Lfs.HasValue || FetchDepth.HasValue || Progress.HasValue ||
            !Filter.IsNullOrWhiteSpace();

        public override void Write(CustomFileWriter writer)
        {
            base.Write(writer);

            if (!PersistCredentials.HasValue) return;

            using (writer.Indent())
            {
                if (!WithKeyAlreadyWritten)
                    writer.WriteLine("with:");

                using (writer.Indent())
                {
                    writer.WriteLine($"persist-credentials: {PersistCredentials.ToString().ToLowerInvariant()}");
                }
            }
        }
    }
}

This could be easily added to the GitHubActionsCheckoutStep writer logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant