diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index f3e82ef9ae..70328c8710 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -30,9 +30,6 @@ on: pull_request_number: description: 'Pull Request Number' required: true - pull_request_merge_sha: - description: 'Pull Request Merge SHA' - required: true env: TGS_DOTNET_VERSION: 8 @@ -50,8 +47,8 @@ concurrency: cancel-in-progress: true jobs: - release-notes-build: - name: Build Release Notes for Other Jobs + ci-start-gate: + name: CI Start Gate runs-on: ubuntu-latest steps: - name: Setup dotnet @@ -78,7 +75,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -106,8 +103,12 @@ jobs: name: release_notes_bins path: ./release_notes_bins/ + - name: Set CI Check Run (Started) + run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll --ci-check ${{ github.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} Started ${{ github.run_id }} + code-scanning: name: Code Scanning + needs: ci-start-gate runs-on: ubuntu-latest permissions: security-events: write @@ -139,7 +140,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -177,6 +178,7 @@ jobs: dmapi-build: name: Build DMAPI + needs: ci-start-gate strategy: fail-fast: false matrix: @@ -244,7 +246,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -282,6 +284,7 @@ jobs: opendream-build: name: Build DMAPI (OpenDream) + needs: ci-start-gate strategy: fail-fast: false matrix: @@ -320,7 +323,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -366,6 +369,7 @@ jobs: efcore-version-match: name: Check Nuget Versions Match Tools + needs: ci-start-gate runs-on: ubuntu-latest steps: - name: Checkout (Branch) @@ -386,7 +390,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -446,7 +450,7 @@ jobs: pages-build: name: Build gh-pages - needs: release-notes-build + needs: ci-start-gate runs-on: ubuntu-latest steps: - name: Setup dotnet @@ -511,6 +515,7 @@ jobs: docker-build: name: Build Docker Image + needs: ci-start-gate runs-on: ubuntu-latest env: TGS_TELEMETRY_KEY_FILE: tgs_telemetry_key.txt @@ -533,7 +538,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -562,6 +567,7 @@ jobs: linux-unit-tests: name: Linux Tests + needs: ci-start-gate strategy: fail-fast: false matrix: @@ -607,7 +613,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -657,6 +663,7 @@ jobs: windows-unit-tests: name: Windows Tests + needs: ci-start-gate strategy: fail-fast: false matrix: @@ -696,7 +703,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -854,7 +861,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -1102,7 +1109,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -1214,7 +1221,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -1262,7 +1269,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -1520,6 +1527,7 @@ jobs: build-deb: name: Build .deb Package # Can't do i386 due to https://github.com/dotnet/core/issues/4595 + needs: ci-start-gate runs-on: ubuntu-latest env: TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt @@ -1574,7 +1582,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -1658,6 +1666,7 @@ jobs: build-msi: name: Build Windows Installer .exe + needs: ci-start-gate runs-on: windows-latest env: TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt @@ -1696,7 +1705,7 @@ jobs: - name: Abort if PR Merge SHA has Changed uses: actions/github-script@v7 - if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != inputs.pull_request_merge_sha + if: github.event_name != 'push' && github.event_name != 'schedule' && steps.get-pr-sha.outputs.head_sha != github.sha with: script: | const delay = ms => new Promise(res => setTimeout(res, ms)); @@ -1830,7 +1839,7 @@ jobs: check-winget-pr-template: name: Check winget-pkgs Pull Request Template is up to date - needs: release-notes-build + needs: ci-start-gate runs-on: ubuntu-latest steps: - name: Setup dotnet @@ -1855,12 +1864,38 @@ jobs: run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll --winget-template-check ${{ steps.get-sha.outputs.pr_template_sha }} ci-completion-gate: - name: CI Completion Gate # Used as a branch protection ruleset target - needs: [ pages-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template, code-scanning, efcore-version-match ] + name: CI Completion Gate + needs: [ pages-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template, code-scanning, efcore-version-match, ci-start-gate ] runs-on: ubuntu-latest + if: always() && needs.ci-start-gate.result == 'success' steps: - - name: Mandatory Empty Step - run: exit 0 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + + - name: Retrieve ReleaseNotes Binaries + uses: actions/download-artifact@v4 + with: + name: release_notes_bins + path: release_notes_bins + + - name: Update CI Check Run (Cancelled) + if: needs.pages-build.result == 'cancelled' || needs.docker-build.result == 'cancelled' || needs.build-deb.result == 'cancelled' || needs.build-msi.result == 'cancelled' || needs.validate-openapi-spec.result == 'cancelled' || needs.upload-code-coverage.result == 'cancelled' || needs.check-winget-pr-template.result == 'cancelled' || needs.code-scanning.result == 'cancelled' || needs.efcore-version-match.result == 'cancelled' || needs.pages-build.result == 'cancelled' + run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll --ci-check ${{ github.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} Cancelled ${{ github.run_id }} + + - name: Update CI Check Run (Failure) + if: (!(needs.pages-build.result == 'cancelled' || needs.docker-build.result == 'cancelled' || needs.build-deb.result == 'cancelled' || needs.build-msi.result == 'cancelled' || needs.validate-openapi-spec.result == 'cancelled' || needs.upload-code-coverage.result == 'cancelled' || needs.check-winget-pr-template.result == 'cancelled' || needs.code-scanning.result == 'cancelled' || needs.efcore-version-match.result == 'cancelled' || needs.pages-build.result == 'cancelled') && (needs.pages-build.result == 'failure' || needs.docker-build.result == 'failure' || needs.build-deb.result == 'failure' || needs.build-msi.result == 'failure' || needs.validate-openapi-spec.result == 'failure' || needs.upload-code-coverage.result == 'failure' || needs.check-winget-pr-template.result == 'failure' || needs.code-scanning.result == 'failure' || needs.efcore-version-match.result == 'failure' || needs.pages-build.result == 'failure')) + run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll --ci-check ${{ github.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} Failure ${{ github.run_id }} + + - name: Update CI Check Run (Success) + if: needs.pages-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.build-msi.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && needs.check-winget-pr-template.result == 'success' && needs.code-scanning.result == 'success' && needs.efcore-version-match.result == 'success' && needs.pages-build.result == 'success' + run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll --ci-check ${{ github.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} Success ${{ github.run_id }} + + - name: Fail Job if Prerequisites Failed + if: (!(needs.pages-build.result == 'cancelled' || needs.docker-build.result == 'cancelled' || needs.build-deb.result == 'cancelled' || needs.build-msi.result == 'cancelled' || needs.validate-openapi-spec.result == 'cancelled' || needs.upload-code-coverage.result == 'cancelled' || needs.check-winget-pr-template.result == 'cancelled' || needs.code-scanning.result == 'cancelled' || needs.efcore-version-match.result == 'cancelled' || needs.pages-build.result == 'cancelled') && (needs.pages-build.result == 'failure' || needs.docker-build.result == 'failure' || needs.build-deb.result == 'failure' || needs.build-msi.result == 'failure' || needs.validate-openapi-spec.result == 'failure' || needs.upload-code-coverage.result == 'failure' || needs.check-winget-pr-template.result == 'failure' || needs.code-scanning.result == 'failure' || needs.efcore-version-match.result == 'failure' || needs.pages-build.result == 'failure')) + run: exit 1 deployment-gate: name: Deployment Start Gate diff --git a/.github/workflows/ci-security.yml b/.github/workflows/ci-security.yml index 2e6926d523..3ada9fae3f 100644 --- a/.github/workflows/ci-security.yml +++ b/.github/workflows/ci-security.yml @@ -74,7 +74,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: refs/pull/${{ github.event.pull_request.number }}/head + ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Restore run: dotnet restore @@ -82,22 +82,14 @@ jobs: - name: Build ReleaseNotes run: dotnet publish -c Release -p:TGS_HOST_NO_WEBPANEL=true -o release_notes_bins tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj - - name: Checkout - uses: actions/checkout@v4 - with: - ref: refs/pull/${{ github.event.pull_request.number }}/merge - path: merge_workspace - - name: Read Current SHA # Can't rely on github.sha as it's for the base branch id: get-pr-sha - run: | - cd merge_workspace - echo "merge_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + run: echo "head_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - - name: Generate Temporary Branch to Reference Head + - name: Generate Temporary Branch to Reference Merge run: | - git checkout -b ${{ github.event.pull_request.number }}-head - git push -f -u origin ${{ github.event.pull_request.number }}-head + git checkout -b ${{ github.event.pull_request.number }}-merge + git push -f -u origin ${{ github.event.pull_request.number }}-merge - name: Send Workflow Dispatch uses: lasith-kg/dispatch-workflow@5623bf13f09bbbbdb549ec692b070307f39b66ac #v2.0.0 + setup_node@v4 @@ -106,15 +98,17 @@ jobs: dispatch-method: workflow_dispatch owner: ${{ github.repository_owner }} repo: ${{ github.event.pull_request.base.repo.name }} - ref: ${{ github.event.pull_request.number }}-head + ref: ${{ github.event.pull_request.number }}-merge workflow: ci-pipeline.yml token: ${{ github.token }} workflow-inputs: | { "pull_request_number": "${{ github.event.pull_request.number }}" - "pull_request_merge_sha": "${{ steps.get-pr-sha.outputs.merge_sha }}" } + - name: Set CI Check Run (Pending) + run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll --ci-check ${{ steps.get-pr-sha.outputs.head_sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} Pending ${{ steps.dispatch.outputs.run-id }} + - name: Delete Temporary Branch if: always() - run: git push -d origin ${{ github.event.pull_request.number }}-head + run: git push -d origin ${{ github.event.pull_request.number }}-merge diff --git a/tools/Tgstation.Server.ReleaseNotes/Program.cs b/tools/Tgstation.Server.ReleaseNotes/Program.cs index 46c887fd5f..b1b83c4a49 100644 --- a/tools/Tgstation.Server.ReleaseNotes/Program.cs +++ b/tools/Tgstation.Server.ReleaseNotes/Program.cs @@ -59,6 +59,7 @@ namespace Tgstation.Server.ReleaseNotes var shaCheck = versionString.Equals("--winget-template-check", StringComparison.OrdinalIgnoreCase); var fullNotes = versionString.Equals("--generate-full-notes", StringComparison.OrdinalIgnoreCase); var nuget = versionString.Equals("--nuget", StringComparison.OrdinalIgnoreCase); + var ciCheck = versionString.Equals("--ci-check", StringComparison.OrdinalIgnoreCase); var genToken = versionString.Equals("--token-output-file", StringComparison.OrdinalIgnoreCase); if ((!Version.TryParse(versionString, out var version) || version.Revision != -1) @@ -67,6 +68,7 @@ namespace Tgstation.Server.ReleaseNotes && !shaCheck && !fullNotes && !nuget + && !ciCheck && !genToken) { Console.WriteLine("Invalid version: " + versionString); @@ -124,6 +126,17 @@ namespace Tgstation.Server.ReleaseNotes return await EnsureRelease(client); } + if (ciCheck) + { + if (args.Length < 5) + { + Console.WriteLine("Missing check parameters!"); + return 4543; + } + + return await CICheck(client, args[1], args[2], Enum.Parse(args[3]), Int64.Parse(args[4])); + } + if (genToken) { if (args.Length < 3) @@ -1662,6 +1675,72 @@ package (version) distribution(s); urgency=urgency gitHubClient.Credentials = new Credentials(installToken.Token); } + enum CheckMode + { + Pending, + Started, + Cancelled, + Success, + Failure, + } + + static async ValueTask CICheck(GitHubClient gitHubClient, string ciTargetSha, string pemBase64, CheckMode mode, long runID) + { + await GenerateAppCredentials(gitHubClient, pemBase64, false); + + switch (mode) + { + case CheckMode.Pending: + await gitHubClient.Check.Run.Create(RepoOwner, RepoName, new NewCheckRun("CI Pipeline", ciTargetSha) + { + DetailsUrl = $"https://github.com/{RepoOwner}/{RepoName}/actions/runs/{runID}", + }); + break; + case CheckMode.Started: + var prChecks = await gitHubClient.Check.Run.GetAllForReference(RepoOwner, RepoName, ciTargetSha); + var theCheckWeWant = prChecks.CheckRuns.FirstOrDefault(x => x.App.Id == AppId); + if (theCheckWeWant != null) + { + await gitHubClient.Check.Run.Update(RepoOwner, RepoName, theCheckWeWant.Id, new CheckRunUpdate + { + Status = CheckStatus.InProgress, + StartedAt = DateTimeOffset.UtcNow, + }); + } + else + await gitHubClient.Check.Run.Create(RepoOwner, RepoName, new NewCheckRun("CI Pipeline", ciTargetSha) + { + DetailsUrl = $"https://github.com/{RepoOwner}/{RepoName}/actions/runs/{runID}", + Status = CheckStatus.InProgress, + StartedAt = DateTimeOffset.UtcNow, + }); + + break; + case CheckMode.Cancelled: + case CheckMode.Failure: + case CheckMode.Success: + var conclusion = mode switch + { + CheckMode.Cancelled => CheckConclusion.Cancelled, + CheckMode.Failure => CheckConclusion.Failure, + CheckMode.Success => CheckConclusion.Success, + _ => throw new InvalidOperationException("Impossible"), + }; + + var prChecks2 = await gitHubClient.Check.Run.GetAllForReference(RepoOwner, RepoName, ciTargetSha); + var theCheckWeWant2 = prChecks2.CheckRuns.First(x => x.App.Id == AppId); + await gitHubClient.Check.Run.Update(RepoOwner, RepoName, theCheckWeWant2.Id, new CheckRunUpdate + { + Status = CheckStatus.Completed, + CompletedAt = DateTimeOffset.UtcNow, + Conclusion = conclusion, + }); + break; + } + + return 0; + } + static void DebugAssert(bool condition, string message = null) { // This exists because one of the fucking asserts evaluates an enumerable or something and it was getting optimized out in release