From 23ebfddf1eb23fdc37355743548844aad583c44f Mon Sep 17 00:00:00 2001 From: Declan Smith Date: Sat, 27 Jun 2026 20:29:58 +1000 Subject: [PATCH] feat(github-actions): add workflow to automate GitHub release creation Introduced a new GitHub Actions workflow `CreateRelease` to automate the process of creating GitHub releases. Added support for tagging specific commits or branches and generating corresponding releases. Enhanced `IGithubReleaseHelper` with methods for release creation and artifact uploads, along with detailed documentation updates. --- .github/workflows/CreateRelease.yml | 56 ++++++++++++++ Invex.Atom.sln.DotSettings | 1 + _atom/IBuild.cs | 20 +++++ _atom/Targets/IDeployTargets.cs | 10 +++ docs/modules/github-workflows.md | 17 +++++ .../Helpers/IGithubReleaseHelper.cs | 74 +++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 .github/workflows/CreateRelease.yml diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml new file mode 100644 index 00000000..20a8d3f5 --- /dev/null +++ b/.github/workflows/CreateRelease.yml @@ -0,0 +1,56 @@ +name: CreateRelease + +on: + workflow_dispatch: + +permissions: { } + +jobs: + + SetupBuildInfo: + runs-on: ubuntu-latest + outputs: + build-name: ${{ steps.SetupBuildInfo.outputs.build-name }} + build-id: ${{ steps.SetupBuildInfo.outputs.build-id }} + build-version: ${{ steps.SetupBuildInfo.outputs.build-version }} + build-timestamp: ${{ steps.SetupBuildInfo.outputs.build-timestamp }} + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: SetupBuildInfo + id: SetupBuildInfo + run: dotnet run --project _atom/_atom.csproj -- SetupBuildInfo --skip --headless + + CreateGithubRelease: + permissions: + contents: write + needs: [ SetupBuildInfo ] + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: CreateGithubRelease + id: CreateGithubRelease + run: dotnet run --project _atom/_atom.csproj -- CreateGithubRelease --skip --headless + env: + build-version: ${{ needs.SetupBuildInfo.outputs.build-version }} + github-token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/Invex.Atom.sln.DotSettings b/Invex.Atom.sln.DotSettings index 0611a601..b3880ba2 100644 --- a/Invex.Atom.sln.DotSettings +++ b/Invex.Atom.sln.DotSettings @@ -1,3 +1,4 @@  True + True \ No newline at end of file diff --git a/_atom/IBuild.cs b/_atom/IBuild.cs index 3637db94..fecb196b 100644 --- a/_atom/IBuild.cs +++ b/_atom/IBuild.cs @@ -213,6 +213,26 @@ internal interface IBuild : IWorkflowBuildDefinition, ], Types = [WorkflowTypes.Github.Action], }, + new("CreateRelease") + { + Triggers = [WorkflowTriggers.Manual], + Targets = + [ + new(nameof(SetupBuildInfo)), + new(nameof(CreateGithubRelease)) + { + Options = + [ + BuildOptions.Inject.Secret(nameof(GithubToken)), + new GithubTokenPermissionsOption(new Permissions.Exact(new() + { + Contents = PermissionsLevel.Write, + })), + ], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, // Test devops new("Test_Devops_Build") diff --git a/_atom/Targets/IDeployTargets.cs b/_atom/Targets/IDeployTargets.cs index 6814905b..421cc62c 100644 --- a/_atom/Targets/IDeployTargets.cs +++ b/_atom/Targets/IDeployTargets.cs @@ -61,4 +61,14 @@ await PushPackageToNuget( foreach (var artifact in IBuildTargets.ProjectsToPack.Concat(ITestTargets.ProjectsToTest)) await UploadArtifactToRelease(artifact, $"v{BuildVersion}"); }); + + Target CreateGithubRelease => + d => d + .DescribedAs("Creates a release on GitHub.") + .RequiresParam(nameof(GithubToken)) + .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildVersion)) + .Executes(async () => await CreateRelease( + $"v{BuildVersion.Major}.{BuildVersion.Minor}.{BuildVersion.Patch}", + "main", + $"v{BuildVersion.Major}.{BuildVersion.Minor}.{BuildVersion.Patch}")); } diff --git a/docs/modules/github-workflows.md b/docs/modules/github-workflows.md index febe54c7..7545ca26 100644 --- a/docs/modules/github-workflows.md +++ b/docs/modules/github-workflows.md @@ -50,11 +50,28 @@ if (Github.IsGithubActions) } ``` +### Release Helper + +Implement `IGithubReleaseHelper` to create GitHub Releases and upload artifacts from your targets. `CreateRelease` +tags a commit (or the latest commit on a branch via `targetCommitish`) and creates the release for that tag: + +```csharp +// Tag the latest commit on the 'main' branch and create a release for it. +await CreateRelease($"v{BuildVersion}", "main", name: $"v{BuildVersion}"); + +// Tag a specific commit SHA instead. +await CreateRelease("v1.2.3", "0a1b2c3d", body: "Release notes...", prerelease: true); +``` + +Use `UploadArtifactToRelease` / `UploadAssetToRelease` to attach assets to an existing release. When not running in +GitHub Actions these operations are simulated (logged only) by default. + ### Variable Provider `GithubVariableProvider` writes variables to `$GITHUB_OUTPUT` and reads them from job outputs, enabling cross-job data sharing. + ### Report Writer `GithubSummaryOutcomeReportWriter` writes build report data to the GitHub Actions job summary (`$GITHUB_STEP_SUMMARY`). diff --git a/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs index f99a0074..31eef4c9 100644 --- a/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs @@ -10,6 +10,80 @@ [PublicAPI] public interface IGithubReleaseHelper : IGithubHelper { + /// + /// Creates a GitHub Release for a new tag, tagging a specific commit or the latest commit on a branch. + /// + /// + /// The name of the tag to create for the release (e.g. "v1.0.0"). The tag is created if it does not + /// already exist. + /// + /// + /// Specifies the commitish value that determines where the Git tag is created from. This can be a branch name + /// (in which case the tag is created from the latest commit on that branch) or a commit SHA (in which case the + /// tag is created from that specific commit). If null or empty, the repository's default branch is used. + /// + /// + /// The display name of the release. If null, the is used as the + /// release name. + /// + /// The description/body text of the release. May be null for an empty body. + /// If true, the release is created as an unpublished draft. Defaults to false. + /// If true, the release is flagged as a pre-release. Defaults to false. + /// + /// If true (default), the create operation will be simulated (logged but not executed) + /// when the build is not running in a GitHub Actions environment. + /// + /// + /// A that resolves to the created , or null when the operation + /// was simulated because the build is not running in GitHub Actions. + /// + /// Thrown if the GitHub repository ID cannot be parsed. + /// + /// GitHub automatically creates the Git tag from + /// as part of creating the release. + /// + [PublicAPI] + async Task CreateRelease( + string tagName, + string? targetCommitish = null, + string? name = null, + string? body = null, + bool draft = false, + bool prerelease = false, + bool dryRunWhenNotRunningInGithubActions = true) + { + if (!Github.IsGithubActions && dryRunWhenNotRunningInGithubActions) + { + Logger.LogWarning( + "Not running in GitHub Actions, simulating creation of release {TagName} targeting {TargetCommitish}.", + tagName, + string.IsNullOrEmpty(targetCommitish) + ? "" + : targetCommitish); + + return null; + } + + var client = new GitHubClient(new("Invex.Atom"), new InMemoryCredentialStore(new(GithubToken))); + + var newRelease = new NewRelease(tagName) + { + Name = name ?? tagName, + Body = body, + Draft = draft, + Prerelease = prerelease, + }; + + if (!string.IsNullOrEmpty(targetCommitish)) + newRelease.TargetCommitish = targetCommitish; + + if (!long.TryParse(Github.Variables.RepositoryId, out var repositoryId)) + throw new InvalidOperationException( + $"Unable to parse GitHub repository id from '{Github.VariableNames.RepositoryId}' (value: '{Github.Variables.RepositoryId}')."); + + return await client.Repository.Release.Create(repositoryId, newRelease); + } + /// /// Uploads a build artifact to a specified GitHub Release. ///