diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 258b06c..d39e9aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,96 +11,7 @@ on: - closed branches: - main -env: - IS_RELEASE_CANDIDATE: >- - ${{ - ( - github.event_name == 'pull_request' && - startsWith(github.event.pull_request.title, 'RELEASES:') && - contains(github.event.pull_request.labels.*.name, 'RELEASES') - ) - || - ( - github.event_name == 'push' && - startsWith(github.event.head_commit.message, 'RELEASES:') && - startsWith(github.ref_name, 'RELEASE') - ) - }} jobs: - label: - runs-on: ubuntu-latest - steps: - - name: Apply Label - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: >- - const prefixes = [ - 'INFRA:', - 'PROVISIONS:', - 'RELEASES:', - 'DATA:', - 'BROKERS:', - 'FOUNDATIONS:', - 'PROCESSINGS:', - 'ORCHESTRATIONS:', - 'COORDINATIONS:', - 'MANAGEMENTS:', - 'AGGREGATIONS:', - 'CONTROLLERS:', - 'CLIENTS:', - 'EXPOSERS:', - 'PROVIDERS:', - 'BASE:', - 'COMPONENTS:', - 'VIEWS:', - 'PAGES:', - 'ACCEPTANCE:', - 'INTEGRATIONS:', - 'CODE RUB:', - 'MINOR FIX:', - 'MEDIUM FIX:', - 'MAJOR FIX:', - 'DOCUMENTATION:', - 'CONFIG:', - 'STANDARD:', - 'DESIGN:', - 'BUSINESS:' - ]; - - - const pullRequest = context.payload.pull_request; - - - if (!pullRequest) { - console.log('No pull request context available.'); - return; - } - - - const title = context.payload.pull_request.title; - - const existingLabels = context.payload.pull_request.labels.map(label => label.name); - - - for (const prefix of prefixes) { - if (title.startsWith(prefix)) { - const label = prefix.slice(0, -1); - if (!existingLabels.includes(label)) { - console.log(`Applying label: ${label}`); - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: [label] - }); - } - break; - } - } - permissions: - contents: read - pull-requests: write build: runs-on: ubuntu-latest steps: @@ -109,7 +20,7 @@ jobs: - name: Setup .Net uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.201 + dotnet-version: 9.0.100 - name: Restore run: dotnet restore - name: Build @@ -208,7 +119,7 @@ jobs: - name: Setup .Net uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.201 + dotnet-version: 9.0.100 - name: Restore run: dotnet restore - name: Build diff --git a/.github/workflows/prLinter.yml b/.github/workflows/prLinter.yml new file mode 100644 index 0000000..31d7538 --- /dev/null +++ b/.github/workflows/prLinter.yml @@ -0,0 +1,138 @@ +name: PR Linter +on: + push: + branches: + - main + pull_request: + types: + - opened + - edited + - synchronize + - reopened + - closed + branches: + - main +jobs: + label: + name: Label + runs-on: ubuntu-latest + steps: + - name: Apply Label + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: >- + const prefixes = [ + 'INFRA:', + 'PROVISIONS:', + 'RELEASES:', + 'DATA:', + 'BROKERS:', + 'FOUNDATIONS:', + 'PROCESSINGS:', + 'ORCHESTRATIONS:', + 'COORDINATIONS:', + 'MANAGEMENTS:', + 'AGGREGATIONS:', + 'CONTROLLERS:', + 'CLIENTS:', + 'EXPOSERS:', + 'PROVIDERS:', + 'BASE:', + 'COMPONENTS:', + 'VIEWS:', + 'PAGES:', + 'ACCEPTANCE:', + 'INTEGRATIONS:', + 'CODE RUB:', + 'MINOR FIX:', + 'MEDIUM FIX:', + 'MAJOR FIX:', + 'DOCUMENTATION:', + 'CONFIG:', + 'STANDARD:', + 'DESIGN:', + 'BUSINESS:' + ]; + + + const pullRequest = context.payload.pull_request; + + + if (!pullRequest) { + console.log('No pull request context available.'); + return; + } + + + const title = context.payload.pull_request.title; + + const existingLabels = context.payload.pull_request.labels.map(label => label.name); + + + for (const prefix of prefixes) { + if (title.startsWith(prefix)) { + const label = prefix.slice(0, -1); + if (!existingLabels.includes(label)) { + console.log(`Applying label: ${label}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: [label] + }); + } + break; + } + } + permissions: + contents: read + pull-requests: write + requireIssueOrTask: + name: Require Issue Or Task Association + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v3 + - name: Get PR Information + id: get_pr_info + uses: actions/github-script@v6 + with: + script: >2- + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number + }); + + const prOwner = pr.data.user.login || ""; + const prBody = pr.data.body || ""; + core.setOutput("prOwner", prOwner); + core.setOutput("description", prBody); + console.log(`PR Owner: ${prOwner}`); + console.log(`PR Body: ${prBody}`); + - name: Check For Associated Issues Or Tasks + id: check_for_issues_or_tasks + if: ${{ steps.get_pr_info.outputs.prOwner != 'dependabot[bot]' }} + run: >2- + PR_BODY="${{ steps.get_pr_info.outputs.description }}" + echo "::notice::Raw PR Body: $PR_BODY" + + if [[ -z "$PR_BODY" ]]; then + echo "Error: PR description does not contain any links to issue(s)/task(s) (e.g., 'closes #123' / 'closes AB#123' / 'fixes #123' / 'fixes AB#123')." + exit 1 + fi + + PR_BODY=$(echo "$PR_BODY" | tr -s '\r\n' ' ' | tr '\n' ' ' | xargs) + echo "::notice::Normalized PR Body: $PR_BODY" + + if echo "$PR_BODY" | grep -Piq "((close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*(\[#\d+\]|\#\d+)|(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*(\[AB#\d+\]|AB#\d+))"; then + echo "Valid PR description." + else + echo "Error: PR description does not contain any links to issue(s)/task(s) (e.g., 'closes #123' / 'closes AB#123' / 'fixes #123' / 'fixes AB#123')." + exit 1 + fi + shell: bash + permissions: + contents: read + pull-requests: read diff --git a/ADotNet.Infrastructure.Build/ADotNet.Infrastructure.Build.csproj b/ADotNet.Infrastructure.Build/ADotNet.Infrastructure.Build.csproj index 8a6f39b..af7ae87 100644 --- a/ADotNet.Infrastructure.Build/ADotNet.Infrastructure.Build.csproj +++ b/ADotNet.Infrastructure.Build/ADotNet.Infrastructure.Build.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net9.0 false diff --git a/ADotNet.Infrastructure.Build/Program.cs b/ADotNet.Infrastructure.Build/Program.cs index b800cf7..dc56517 100644 --- a/ADotNet.Infrastructure.Build/Program.cs +++ b/ADotNet.Infrastructure.Build/Program.cs @@ -4,12 +4,7 @@ // See License.txt in the project root for license information. // --------------------------------------------------------------------------- -using System.Collections.Generic; -using System.IO; -using ADotNet.Clients; -using ADotNet.Models.Pipelines.GithubPipelines.DotNets; -using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks; -using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks.SetupDotNetTaskV3s; +using ISL.ReIdentification.Infrastructure.Services; namespace ADotNet.Infrastructure.Build { @@ -17,106 +12,14 @@ internal class Program { static void Main(string[] args) { - string branchName = "main"; - var aDotNetClient = new ADotNetClient(); + var scriptGenerationService = new ScriptGenerationService(); - var githubPipeline = new GithubPipeline - { - Name = "Build", + scriptGenerationService.GenerateBuildScript( + branchName: "main", + projectName: "ADotNet", + dotNetVersion: "9.0.100"); - OnEvents = new Events - { - Push = new PushEvent - { - Branches = new string[] { branchName } - }, - - PullRequest = new PullRequestEvent - { - Types = new string[] { "opened", "synchronize", "reopened", "closed" }, - Branches = new string[] { branchName } - } - }, - - EnvironmentVariables = new Dictionary - { - { "IS_RELEASE_CANDIDATE", EnvironmentVariables.IsGitHubReleaseCandidate() } - }, - - Jobs = new Dictionary - { - { - "label", - new LabelJobV2(runsOn: BuildMachines.UbuntuLatest) - }, - { - "build", - new Job - { - RunsOn = BuildMachines.UbuntuLatest, - - Steps = new List - { - new CheckoutTaskV3 - { - Name = "Check out" - }, - - new SetupDotNetTaskV3 - { - Name = "Setup .Net", - - With = new TargetDotNetVersionV3 - { - DotNetVersion = "7.0.201" - } - }, - - new RestoreTask - { - Name = "Restore" - }, - - new DotNetBuildTask - { - Name = "Build" - }, - - new TestTask - { - Name = "Test" - } - } - } - }, - { - "add_tag", - new TagJob( - runsOn: BuildMachines.UbuntuLatest, - dependsOn: "build", - projectRelativePath: "ADotNet/ADotNet.csproj", - githubToken: "${{ secrets.PAT_FOR_TAGGING }}", - branchName: branchName) - }, - { - "publish", - new PublishJob( - runsOn: BuildMachines.UbuntuLatest, - dependsOn: "add_tag", - nugetApiKey: "${{ secrets.NUGET_ACCESS }}") - } - } - }; - - string buildScriptPath = "../../../../.github/workflows/build.yml"; - string directoryPath = Path.GetDirectoryName(buildScriptPath); - - if (!Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } - - aDotNetClient.SerializeAndWriteToFile(githubPipeline, path: buildScriptPath); + scriptGenerationService.GeneratePrLintScript(branchName: "main"); } } } \ No newline at end of file diff --git a/ADotNet.Infrastructure.Build/Services/ScriptGenerationService.cs b/ADotNet.Infrastructure.Build/Services/ScriptGenerationService.cs new file mode 100644 index 0000000..753e94f --- /dev/null +++ b/ADotNet.Infrastructure.Build/Services/ScriptGenerationService.cs @@ -0,0 +1,164 @@ +// --------------------------------------------------------------------------- +// Copyright (c) Hassan Habib & Shri Humrudha Jagathisun All rights reserved. +// Licensed under the MIT License. +// See License.txt in the project root for license information. +// --------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.IO; +using ADotNet.Clients; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks.SetupDotNetTaskV3s; + +namespace ISL.ReIdentification.Infrastructure.Services +{ + internal class ScriptGenerationService + { + private readonly ADotNetClient adotNetClient; + + public ScriptGenerationService() => + adotNetClient = new ADotNetClient(); + + public void GenerateBuildScript(string branchName, string projectName, string dotNetVersion) + { + var githubPipeline = new GithubPipeline + { + Name = "Build", + + OnEvents = new Events + { + Push = new PushEvent { Branches = [branchName] }, + + PullRequest = new PullRequestEvent + { + Types = ["opened", "synchronize", "reopened", "closed"], + Branches = [branchName] + } + }, + + Jobs = new Dictionary + { + { + "build", + new Job + { + RunsOn = BuildMachines.UbuntuLatest, + + Steps = new List + { + new CheckoutTaskV3 + { + Name = "Check out" + }, + + new SetupDotNetTaskV3 + { + Name = "Setup .Net", + + With = new TargetDotNetVersionV3 + { + DotNetVersion = "9.0.100" + } + }, + + new RestoreTask + { + Name = "Restore" + }, + + new DotNetBuildTask + { + Name = "Build" + }, + + new TestTask + { + Name = "Test" + } + } + } + }, + { + "add_tag", + new TagJob( + runsOn: BuildMachines.UbuntuLatest, + dependsOn: "build", + projectRelativePath: "ADotNet/ADotNet.csproj", + githubToken: "${{ secrets.PAT_FOR_TAGGING }}", + branchName: branchName) + }, + { + "publish", + new PublishJobV2( + runsOn: BuildMachines.UbuntuLatest, + dependsOn: "add_tag", + dotNetVersion: dotNetVersion, + nugetApiKey: "${{ secrets.NUGET_ACCESS }}") + } + } + }; + + string buildScriptPath = "../../../../.github/workflows/build.yml"; + string directoryPath = Path.GetDirectoryName(buildScriptPath); + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + adotNetClient.SerializeAndWriteToFile( + adoPipeline: githubPipeline, + path: buildScriptPath); + } + + public void GeneratePrLintScript(string branchName) + { + var githubPipeline = new GithubPipeline + { + Name = "PR Linter", + + OnEvents = new Events + { + Push = new PushEvent { Branches = [branchName] }, + + PullRequest = new PullRequestEvent + { + Types = ["opened", "edited", "synchronize", "reopened", "closed"], + Branches = [branchName] + } + }, + + Jobs = new Dictionary + { + { + "label", + new LabelJobV2(runsOn: BuildMachines.UbuntuLatest) + { + Name = "Label", + } + }, + { + "requireIssueOrTask", + new RequireIssueOrTaskJob() + { + Name = "Require Issue Or Task Association", + } + }, + } + }; + + string buildScriptPath = "../../../../.github/workflows/prLinter.yml"; + string directoryPath = Path.GetDirectoryName(buildScriptPath); + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + adotNetClient.SerializeAndWriteToFile( + adoPipeline: githubPipeline, + path: buildScriptPath); + } + } +} diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/RequireIssueOrTaskJob.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/RequireIssueOrTaskJob.cs new file mode 100644 index 0000000..cbbbab9 --- /dev/null +++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/RequireIssueOrTaskJob.cs @@ -0,0 +1,131 @@ +// --------------------------------------------------------------------------- +// Copyright (c) Hassan Habib & Shri Humrudha Jagathisun All rights reserved. +// Licensed under the MIT License. +// See License.txt in the project root for license information. +// --------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.ComponentModel; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks; +using YamlDotNet.Serialization; + +namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets +{ + public sealed class RequireIssueOrTaskJob : Job + { + public RequireIssueOrTaskJob() + { + RunsOn = "ubuntu-latest"; + + Permissions = new Dictionary + { + { "contents", "read" }, + { "pull-requests", "read" } + }; + + Steps = new List + { + new CheckoutTaskV3 + { + Name = "Check out" + }, + + new GithubTask() + { + Name = "Get PR Information", + Id = "get_pr_info", + Uses = "actions/github-script@v6", + With = new Dictionary + { + { + "script", + """ + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number + }); + + const prOwner = pr.data.user.login || ""; + const prBody = pr.data.body || ""; + core.setOutput("prOwner", prOwner); + core.setOutput("description", prBody); + console.log(`PR Owner: ${prOwner}`); + console.log(`PR Body: ${prBody}`); + """ + } + } + }, + + new GithubTask() + { + Name = "Check For Associated Issues Or Tasks", + If = "${{ steps.get_pr_info.outputs.prOwner != 'dependabot[bot]' }}", + Id = "check_for_issues_or_tasks", + Shell = "bash", + Run = + """ + PR_BODY="${{ steps.get_pr_info.outputs.description }}" + echo "::notice::Raw PR Body: $PR_BODY" + + if [[ -z "$PR_BODY" ]]; then + echo "Error: PR description does not contain any links to issue(s)/task(s) (e.g., 'closes #123' / 'closes AB#123' / 'fixes #123' / 'fixes AB#123')." + exit 1 + fi + + PR_BODY=$(echo "$PR_BODY" | tr -s '\r\n' ' ' | tr '\n' ' ' | xargs) + echo "::notice::Normalized PR Body: $PR_BODY" + + if echo "$PR_BODY" | grep -Piq "((close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*(\[#\d+\]|\#\d+)|(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*(\[AB#\d+\]|AB#\d+))"; then + echo "Valid PR description." + else + echo "Error: PR description does not contain any links to issue(s)/task(s) (e.g., 'closes #123' / 'closes AB#123' / 'fixes #123' / 'fixes AB#123')." + exit 1 + fi + """, + }, + }; + } + + [YamlMember(Order = 0, DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new string Name { get; set; } + + [YamlMember(Order = 1, Alias = "runs-on")] + public new string RunsOn { get; set; } + + [YamlMember(Order = 2, Alias = "needs", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new string[] Needs { get; set; } + + [YamlMember(Order = 3, Alias = "if", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new string If { get; set; } + + [YamlMember(Order = 4, DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new string Environment { get; set; } + + [YamlMember(Order = 5, DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new DefaultValues Defaults { get; set; } + + [YamlMember(Order = 6)] + public new List Steps { get; set; } + + [DefaultValue(0)] + [YamlMember(Order = 7, Alias = "timeout-minutes", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new int TimeoutInMinutes { get; set; } + + [YamlMember(Order = 8, DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new Strategy Strategy { get; set; } + + [YamlMember(Order = 9, Alias = "env", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new Dictionary EnvironmentVariables { get; set; } + + [YamlMember(Order = 10, DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new Dictionary Outputs { get; set; } + + [DefaultValue(false)] + [YamlMember(Order = 11, Alias = "continue-on-error", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new bool ContinueOnError { get; set; } + + [YamlMember(Order = 12, Alias = "permissions", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public new Dictionary Permissions { get; set; } + } +} diff --git a/AdoNet.Tests.Console/ADotNet.Tests.Console.csproj b/AdoNet.Tests.Console/ADotNet.Tests.Console.csproj index 3b8babe..2880879 100644 --- a/AdoNet.Tests.Console/ADotNet.Tests.Console.csproj +++ b/AdoNet.Tests.Console/ADotNet.Tests.Console.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net9.0 false diff --git a/AdoNet.Tests.Unit/ADotNet.Tests.Unit.csproj b/AdoNet.Tests.Unit/ADotNet.Tests.Unit.csproj index d6a86a4..9d704dd 100644 --- a/AdoNet.Tests.Unit/ADotNet.Tests.Unit.csproj +++ b/AdoNet.Tests.Unit/ADotNet.Tests.Unit.csproj @@ -1,7 +1,7 @@ - net7.0 + net9.0 false