diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b71f3960173..c209882bb77 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -26,6 +26,12 @@ "mdsnippets" ] }, + "MartinCostello.WaitForNuGetPackage": { + "version": "1.0.1", + "commands": [ + "dotnet-wait-for-package" + ] + }, "sign": { "version": "0.9.1-beta.24170.3", "commands": [ @@ -33,4 +39,4 @@ ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 556b2abe55d..93c1236e5bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,8 @@ jobs: dotnet-sdk-version: ${{ steps.setup-dotnet.outputs.dotnet-version }} dotnet-sign-version: ${{ steps.get-dotnet-tools-versions.outputs.dotnet-sign-version }} dotnet-validate-version: ${{ steps.get-dotnet-tools-versions.outputs.dotnet-validate-version }} + package-names: ${{ steps.build.outputs.package-names }} + package-version: ${{ steps.build.outputs.package-version }} strategy: fail-fast: false @@ -72,6 +74,7 @@ jobs: restore-keys: ${{ runner.os }}-nuget- - name: Build, Test and Package + id: build shell: pwsh run: ./build.ps1 @@ -343,3 +346,22 @@ jobs: - name: Push signed NuGet packages to NuGet.org run: dotnet nuget push "*.nupkg" --api-key ${{ secrets.NUGET_TOKEN }} --skip-duplicate --source https://api.nuget.org/v3/index.json + + - name: Generate GitHub application token + id: generate-application-token + uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3 # v3.0.0 + with: + application_id: ${{ secrets.POLLY_UPDATER_BOT_APP_ID }} + application_private_key: ${{ secrets.POLLY_UPDATER_BOT_KEY }} + permissions: 'contents:write' + + - name: Publish nuget_packages_published + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + with: + event-type: nuget_packages_published + token: ${{ steps.generate-application-token.outputs.token }} + client-payload: |- + { + "packages": "${{ needs.build.outputs.package-names }}", + "version": "${{ needs.build.outputs.package-version }}" + } diff --git a/.github/workflows/nuget-packages-published.yml b/.github/workflows/nuget-packages-published.yml new file mode 100644 index 00000000000..c3cc26be7d0 --- /dev/null +++ b/.github/workflows/nuget-packages-published.yml @@ -0,0 +1,205 @@ +name: nuget-packages-published +run-name: ${{ github.event.client_payload.version }} + +on: + repository_dispatch: + types: [ nuget_packages_published ] + +permissions: {} + +jobs: + + wait-for-publish: + runs-on: [ ubuntu-latest ] + timeout-minutes: 30 + + concurrency: + group: '${{ github.workflow }}-${{ github.event.client_payload.version }}' + cancel-in-progress: false + + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + DOTNET_MULTILEVEL_LOOKUP: 0 + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: 1 + FORCE_COLOR: 3 + NUGET_XMLDOC_MODE: skip + TERM: xterm + + outputs: + package-names: ${{ github.event.client_payload.packages }} + package-version: ${{ github.event.client_payload.version }} + published: ${{ steps.wait-for-publish.outputs.published }} + + permissions: + contents: read + + steps: + + - name: Checkout code + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 + + - name: Restore .NET tools + shell: pwsh + run: dotnet tool restore + + - name: Wait for NuGet packages to be published + id: wait-for-publish + shell: pwsh + env: + PACKAGE_NAMES: ${{ github.event.client_payload.packages }} + PACKAGE_VERSION: ${{ github.event.client_payload.version }} + PUBLISH_TIMEOUT: '00:25:00' + run: | + $packageNames = ${env:PACKAGE_NAMES} -Split ',' + $packageVersion = ${env:PACKAGE_VERSION}.TrimStart('v') + + $packages = @() + + foreach ($packageName in $packageNames) { + $packages += "${packageName}@${packageVersion}" + } + + dotnet wait-for-package $packages --timeout ${env:PUBLISH_TIMEOUT} + + if ($LASTEXITCODE -ne 0) { + Write-Output "::warning::Failed to wait for NuGet packages to be published and indexed." + exit 0 + } + + "published=true" >> $env:GITHUB_OUTPUT + + notify-release: + runs-on: [ ubuntu-latest ] + needs: [wait-for-publish] + if: needs.wait-for-publish.outputs.published == 'true' + + concurrency: + group: '${{ github.workflow }}-notify' + cancel-in-progress: false + + permissions: + issues: write + pull-requests: write + + steps: + + - name: Comment on issues and pull requests + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + VERSION: ${{ github.event.client_payload.version }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const [ owner, repo ] = process.env.GITHUB_REPOSITORY.split('/'); + const version = process.env.VERSION; + + const { data: milestones } = await github.rest.issues.listMilestones({ + owner, + repo, + state: 'all', + sort: 'completeness', + direction: 'desc', + per_page: 100, + }); + + core.debug(`Found ${milestones.length} milestones.`); + + const milestone = + milestones.find((p) => p.title === version) || + milestones.find((p) => p.title === `v${version}`); + + if (!milestone) { + console.log(`Milestone for version ${version} not found.`); + return; + } + + core.debug(`Found milestone ${milestone.title} (${milestone.number}).`); + + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner, + repo, + milestone: milestone.number, + state: 'all', + }); + + core.debug(`Found ${issues.length} issues and pull requests for milestone.`); + + const ignoreAssociations = [ + 'COLLABORATOR', + 'MEMBER', + 'OWNER', + ]; + + for (const issue of issues) { + const issue_number = issue.number; + if (issue.state === 'closed' && issue.state_reason === 'not_planned') { + core.debug(`Ignoring issue #${issue_number} as it is not planned.`); + continue; + } + + const isPullRequest = !!issue.pull_request; + + if (isPullRequest && !issue.pull_request.merged_at) { + core.debug(`Ignoring pull request #${issue_number} as it was not merged.`); + continue; + } + + const userLogin = issue.user.login; + + if (issue.user.type === 'Bot') { + core.debug(`Ignoring issue #${issue_number} as it was created by ${userLogin}.`); + continue; + } + + if (ignoreAssociations.includes(issue.author_association)) { + core.debug(`Ignoring issue #${issue_number} as it was created by ${userLogin} who has an association of ${issue.author_association}.`); + continue; + } + + const watermark = `\n`; + let comment = null; + + try { + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + }); + + comment = comments.find((p) => p.body.includes(watermark)); + } catch (err) { + core.warning(`Failed to list comments for issue #${issue_number}: ${err}`); + continue; + } + + if (comment) { + core.debug(`Ignoring issue #${issue_number} as it has already been commented on.`); + continue; + } + + let body = isPullRequest ? + `Thanks for your contribution @${userLogin} - the changes from this pull request have been published as part of version ${version} :package:, which is now available from NuGet.org :rocket:` : + `Thanks for creating this issue @${userLogin} - the associated changes have been published as part of version ${version} :package:, which is now available from NuGet.org :rocket:`; + + console.log(`Adding comment to ${isPullRequest ? 'pull request' : 'issue'} #${issue_number}.`); + core.debug(`#${issue_number}: ${body}`); + + body += watermark; + + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } catch (err) { + core.warning(`Failed to add comment to issue #${issue_number}: ${err}`); + } + } diff --git a/eng/Library.targets b/eng/Library.targets index d8b2e1f3736..c341f4972f5 100644 --- a/eng/Library.targets +++ b/eng/Library.targets @@ -54,4 +54,25 @@ + + + <_PackageNamesPath>$(ArtifactsPath)\package-names.txt + + + + + + <_PackageNames Include="$(PackageId)" /> + + + + + + <_UniquePackageNames>@(_UniquePackageNames->'%(Identity)', ',') + + + + + +