diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml new file mode 100644 index 0000000000..b41265e075 --- /dev/null +++ b/.github/workflows/bump-version.yml @@ -0,0 +1,211 @@ +name: bump-version + +on: + release: + types: [ published ] + workflow_dispatch: + inputs: + version: + description: 'The optional version string for the next release.' + required: false + type: string + default: '' + +permissions: {} + +jobs: + bump-version: + runs-on: [ ubuntu-latest ] + + concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + + permissions: + contents: write + pull-requests: write + + steps: + + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Bump version + id: bump-version + shell: pwsh + env: + NEXT_VERSION: ${{ inputs.version }} + run: | + $properties = Join-Path "." "Directory.Build.props" + + $xml = [xml](Get-Content $properties) + $versionPrefix = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix') + + if (-Not [string]::IsNullOrEmpty(${env:NEXT_VERSION})) { + $version = [System.Version]::new(${env:NEXT_VERSION}) + } else { + $version = [System.Version]::new($versionPrefix.InnerText) + $version = [System.Version]::new($version.Major, $version.Minor, $version.Build + 1) + } + + $updatedVersion = $version.ToString() + $versionPrefix.InnerText = $updatedVersion + + $settings = New-Object System.Xml.XmlWriterSettings + $settings.Encoding = New-Object System.Text.UTF8Encoding($false) + $settings.Indent = $true + $settings.OmitXmlDeclaration = $true + + $writer = [System.Xml.XmlWriter]::Create($properties, $settings) + + $xml.Save($writer) + + $writer.Flush() + $writer.Close() + $writer = $null + + "" >> $properties + + "version=${updatedVersion}" >> $env:GITHUB_OUTPUT + + - name: Push changes to GitHub + id: push-changes + shell: pwsh + env: + GIT_COMMIT_USER_EMAIL: 'github-actions[bot]@users.noreply.github.com' + GIT_COMMIT_USER_NAME: 'github-actions[bot]' + NEXT_VERSION: ${{ steps.bump-version.outputs.version }} + run: | + $gitStatus = (git status --porcelain) + + if ([string]::IsNullOrEmpty($gitStatus)) { + throw "No changes to commit." + } + + git config color.diff always + git --no-pager diff + + $branchName = "bump-version-${env:NEXT_VERSION}" + + git config user.email ${env:GIT_COMMIT_USER_EMAIL} | Out-Null + git config user.name ${env:GIT_COMMIT_USER_NAME} | Out-Null + git fetch origin --no-tags | Out-Null + git rev-parse --verify --quiet "remotes/origin/${branchName}" | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "Branch ${branchName} already exists." + exit 0 + } + + git checkout -b $branchName + git add . + git commit -m "Bump version`n`nBump version to ${env:NEXT_VERSION} for the next release." + git push -u origin $branchName + + "branch-name=${branchName}" >> $env:GITHUB_OUTPUT + "updated-version=true" >> $env:GITHUB_OUTPUT + "version=${env:NEXT_VERSION}" >> $env:GITHUB_OUTPUT + + - name: Create pull request + if: steps.push-changes.outputs.updated-version == 'true' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + HEAD_BRANCH: ${{ steps.push-changes.outputs.branch-name }} + NEXT_VERSION: ${{ steps.push-changes.outputs.version }} + with: + script: | + const nextVersion = process.env.NEXT_VERSION; + const { repo, owner } = context.repo; + const workflowUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + + const { data: pr } = await github.rest.pulls.create({ + title: 'Bump version', + owner, + repo, + head: process.env.HEAD_BRANCH, + base: process.env.BASE_BRANCH, + draft: true, + body: [ + `Bump version to \`${nextVersion}\` for the next release.`, + '', + `This pull request was generated by [GitHub Actions](${workflowUrl}).` + ].join('\n') + }); + + core.notice(`Created pull request ${owner}/${repo}#${pr.number}: ${pr.html_url}`); + + try { + const { data: milestones } = await github.rest.issues.listMilestones({ + owner, + repo, + state: 'open', + }); + + const title = `v${nextVersion}`; + let milestone = milestones.find((p) => p.title === title); + + if (!milestone) { + const created = await github.rest.issues.createMilestone({ + owner, + repo, + title, + }); + milestone = created.data; + } + + await github.rest.issues.update({ + owner, + repo, + issue_number: pr.number, + milestone: milestone.number + }); + } catch (error) { + // Ignore + } + + close-milestone: + runs-on: [ ubuntu-latest ] + if: github.event_name == 'release' + + concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + + permissions: + issues: write + + steps: + + - name: Close milestone + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + RELEASE_DATE: ${{ github.event.release.published_at }} + RELEASE_VERSION: ${{ github.event.release.tag_name }} + with: + script: | + const { repo, owner } = context.repo; + + const { data: milestones } = await github.rest.issues.listMilestones({ + owner, + repo, + state: 'open', + }); + + const milestone = milestones.find((p) => p.title === process.env.RELEASE_VERSION); + + if (!milestone) { + return; + } + + try { + await github.rest.issues.updateMilestone({ + owner, + repo, + milestone_number: milestone.number, + state: 'closed', + due_on: process.env.RELEASE_DATE, + }); + } catch (error) { + // Ignore + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..5409d51818 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: release + +on: + workflow_dispatch: + inputs: + publish: + description: 'If true, does not create the release as a draft.' + required: false + type: boolean + default: false + +permissions: {} + +jobs: + release: + runs-on: [ ubuntu-latest ] + + concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + + permissions: + contents: write + issues: write + + steps: + + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Get version + id: get-version + shell: pwsh + run: | + $properties = Join-Path "." "Directory.Build.props" + $xml = [xml](Get-Content $properties) + $version = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix').InnerText + "version=${version}" >> $env:GITHUB_OUTPUT + + - name: Create release + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DRAFT: ${{ inputs.publish != true }} + VERSION: ${{ steps.get-version.outputs.version }} + with: + script: | + const { repo, owner } = context.repo; + const draft = process.env.DRAFT === 'true'; + const version = process.env.VERSION; + const tag_name = `v${version}`; + const name = tag_name; + + const { data: notes } = await github.rest.repos.generateReleaseNotes({ + owner, + repo, + tag_name, + target_commitish: process.env.DEFAULT_BRANCH, + }); + + const body = notes.body + .split('\n') + .filter((line) => !line.includes(' @dependabot ')) + .filter((line) => !line.includes(' @github-actions ')) + .join('\n'); + + const { data: release } = await github.rest.repos.createRelease({ + owner, + repo, + tag_name, + name, + body, + draft, + }); + + core.notice(`Created release ${release.name}: ${release.html_url}`);