From 8b71d9d1dd1e0438f83275460d2a2b3873384f8a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:06:31 -0500 Subject: [PATCH] Scaffold Out Release Process --- .github/workflows/pack-and-release.yml | 182 +++++++++++++++++++++ .github/workflows/prerelease.yml | 16 ++ .github/workflows/publish.yml | 94 ----------- .github/workflows/release.yml | 210 +------------------------ .github/workflows/start-release.yml | 67 ++++++++ .github/workflows/version-bump.yml | 101 ++++++++++++ 6 files changed, 373 insertions(+), 297 deletions(-) create mode 100644 .github/workflows/pack-and-release.yml create mode 100644 .github/workflows/prerelease.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/start-release.yml create mode 100644 .github/workflows/version-bump.yml diff --git a/.github/workflows/pack-and-release.yml b/.github/workflows/pack-and-release.yml new file mode 100644 index 0000000..e85e6f0 --- /dev/null +++ b/.github/workflows/pack-and-release.yml @@ -0,0 +1,182 @@ +name: Publish + +on: + workflow_call: + inputs: + prerelease: + type: boolean + + +jobs: + parse: + runs-on: ubuntu-22.04 + outputs: + package: ${{ steps.parse-package.outputs.result }} + steps: + - name: Parse Package + id: parse-package + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + result-encoding: string + script: | + const ref = '${{ github.ref }}'; + + if (!ref.startsWith('refs/heads/')) { + core.setFailed(`Unexpected ref: ${ref}`); + return; + } + + // Example: refs/heads/release/{packageName}/{version} + // 0 /1 /2 /3 /4 + const refParts = ref.split('/'); + + + if (refParts.length < 4) { + core.setFailed(`Not at least 4 parts split by forward slash: ${ref}`); + return; + } + + return refParts[3]; + + pack: + name: Pack + runs-on: ubuntu-22.04 + needs: + - parse + outputs: + current-version: ${{ steps.current-version.outputs.VERSION }} + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up .NET + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 + + - name: Pack + run: dotnet pack -c Release -p:IsPreRelease=$IS_PRERELEASE + working-directory: '${{ needs.parse.outputs.package }}/src' + env: + IS_PRERELEASE: ${{ inputs.prerelease }} + + - name: Get current version + id: current-version + run: echo "VERSION=$(dotnet msbuild -p:IsPreRelease=$IS_PRERELEASE --getProperty:Version)" >> $GITHUB_OUTPUT + working-directory: '${{ needs.parse.outputs.package }}/src' + env: + IS_PRERELEASE: ${{ inputs.prerelease }} + + - name: Upload artifacts + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: packages + path: "**/*.nupkg" + + publish: + name: Publish + runs-on: ubuntu-22.04 + needs: + - pack + steps: + - name: Dispatch publishing + env: + GITHUB_TOKEN: ${{ secrets.PUBLISH_GITHUB_TOKEN }} + # TODO: Make version optional in workflow + run: > + gh workflow run publish-nuget.yml + --repo bitwarden/devops + --field repository=${{ github.event.repository.name }} + --field run-id=${{ github.run_id }} + --field artifact=packages + --field environment=nuget + --field version=1.0.0 + + create-release: + name: Create release + runs-on: ubuntu-22.04 + needs: + - parse + - pack + - publish + steps: + - name: Create GitHub Release + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const package = '${{ inputs.package }}'; + const currentVersion = '${{ needs.pack.outputs.current-version }}'; + + // Configure Git + await exec.exec(`git config user.name "github-actions"`); + await exec.exec(`git config user.email "github-actions@github.com"`); + + // List existing tags so that we could use them to link to the best full changelog + // Debug purposes only right now until there is enough data for me to make this command bullet proof + await exec.exec(`git --no-pager tag --list "${package}_v*" --no-contains $(git rev-parse HEAD)`, [], { + listeners: { + stdout: function stdout(data) { + console.log(`Found tags:\n${data}`); + } + } + }); + + // Create tag + const tag = `${package}_v${currentVersion}`; + + console.log(`Creating tag & release: ${tag}`); + + await exec.exec(`git tag "${tag}"`); + await exec.exec(`git push origin --tags`); + + // Create release + const { data } = await github.rest.repos.createRelease({ + owner: "bitwarden", + repo: "dotnet-extensions", + tag_name: tag, + target_commitish: "${{ github.event.ref }}", + name: tag, + body: "", + prerelease: ${{ inputs.prerelease }}, + generate_release_notes: false, // This creates a link between this and the last tag but that might not be our version + }); + + const templateMarker = data.upload_url.indexOf("{"); + + let url = data.upload_url; + + if (templateMarker > -1) { + url = url.substring(0, templateMarker); + } + + const globber = await glob.create("**/*.nupkg"); + const files = await globber.glob(); + + const fs = require("fs"); + const path = require("path"); + + if (files.length === 0) { + core.setFailed("No files found, cannot create release."); + return; + } + + for (const file of files) { + const endpoint = new URL(url); + endpoint.searchParams.append("name", path.basename(file)); + const endpointString = endpoint.toString(); + console.log(`Uploading file: ${file} to ${endpointString}`); + // do the upload + const uploadResponse = await github.request({ + method: "POST", + url: endpointString, + data: fs.readFileSync(file), + }); + + console.log(`Upload response: ${uploadResponse.status}`); + } + + console.log("Finished creating release."); + + - name: Do version bump + uses: bitwarden/dotnet-extensions/.github/workflows/version-bump.yml@main + with: + package: ${{ needs.parse.outputs.package }} + type: ${{ inputs.prerelease && 'prerelease' || 'ga' }} diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..5c6af99 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,16 @@ +name: Do PreRelease + +on: + push: + paths: + - "release/*" + +jobs: + do-prerelease: + name: Do prerelease + runs-on: ubuntu-22.04 + steps: + - name: Pack & Release + uses: bitwarden/dotnet-extensions/.github/workflows/pack-and-release.yml@main + with: + prerelease: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 07a3d7b..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Publish - -on: - pull_request: - release: - types: - - "published" - -jobs: - version: - name: Calculate version - runs-on: ubuntu-22.04 - permissions: - contents: read - - steps: - - name: Determine stable version - id: stable-version - if: ${{ github.event_name == 'release' }} - run: | - if ! [[ "${{ github.event.release.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z].*)?$ ]]; then - echo "Invalid version tag: ${{ github.event.release.tag_name }}" - exit 1 - fi - - if ! [[ "${{ github.event.release.name }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z].*)?$ ]]; then - echo "Invalid version: ${{ github.event.release.name }}" - exit 1 - fi - - echo "version=${{ github.event.release.name }}" >> $GITHUB_OUTPUT - - - name: Determine prerelease version - id: pre-version - if: ${{ github.event_name != 'release' }} - run: | - hash="${{ github.event.pull_request.head.sha }}" - echo "version=0.0.0-${hash:0:7}" >> $GITHUB_OUTPUT - - outputs: - version: ${{ steps.stable-version.outputs.version || steps.pre-version.outputs.version }} - - pack: - name: Package - needs: version - runs-on: ubuntu-22.04 - permissions: - actions: write - contents: read - - steps: - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up .NET - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - - - name: Pack - run: dotnet pack -p:Version=${{ needs.version.outputs.version }} -p:ContinuousIntegrationBuild=true --configuration Release - - - name: Upload artifacts - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: packages - path: "**/*.nupkg" - - publish: - name: Publish - runs-on: ubuntu-22.04 - if: ${{ github.event_name == 'release' }} - needs: - - version - - pack - strategy: - matrix: - environment: - - ghpr - - nuget - exclude: - # exclude nuget for prereleases - - environment: ${{ github.event.release.prerelease && 'nuget' }} - - steps: - - name: Dispatch publishing - env: - GITHUB_TOKEN: ${{ secrets.PUBLISH_GITHUB_TOKEN }} - run: > - gh workflow run publish-nuget.yml - --repo bitwarden/devops - --field repository=${{ github.event.repository.name }} - --field run-id=${{ github.run_id }} - --field artifact=packages - --field environment=${{ matrix.environment }} - --field version=${{ needs.version.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45f1d0f..3ceb653 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,210 +1,14 @@ -name: Release -run-name: ${{ inputs.type }} release for ${{ inputs.package }} +name: Do Release on: - workflow_dispatch: - inputs: - package: - description: Which package to release - required: true - type: choice - options: - - Bitwarden.Server.Sdk - type: - description: The type of this release - required: true - type: choice - options: - - ga - - prerelease - - hotfix - -permissions: - pull-requests: write - contents: write - -env: - PACKAGE_DIRECTORY: './extensions/${{ inputs.package }}' + workflow_dispatch: {} jobs: - build-artifact: - name: Build artifacts + do-release: + name: Do release runs-on: ubuntu-22.04 steps: - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up .NET - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - - - name: Pack - run: dotnet pack -c Release -p:IsPreRelease=$IS_PRERELEASE - working-directory: '${{ env.PACKAGE_DIRECTORY }}/src' - env: - IS_PRERELEASE: ${{ inputs.type == 'prerelease' }} - - - name: Get current version - id: current-version - run: echo "VERSION=$(dotnet msbuild -p:IsPreRelease=$IS_PRERELEASE --getProperty:Version)" >> $GITHUB_OUTPUT - working-directory: '${{ env.PACKAGE_DIRECTORY }}/src' - env: - IS_PRERELEASE: ${{ inputs.type == 'prerelease' }} - - # Creating a GitHub Release triggers package publishing - - name: Create GitHub Release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const package = '${{ inputs.package }}'; - const currentVersion = '${{ steps.current-version.outputs.VERSION }}'; - - // Configure Git - await exec.exec(`git config user.name "github-actions"`); - await exec.exec(`git config user.email "github-actions@github.com"`); - - // List existing tags so that we could use them to link to the best full changelog - // Debug purposes only right now until there is enough data for me to make this command bullet proof - await exec.exec(`git --no-pager tag --list "${package}_v*" --no-contains $(git rev-parse HEAD)`, [], { - listeners: { - stdout: function stdout(data) { - console.log(`Found tags:\n${data}`); - } - } - }); - - // Create tag - const tag = `${package}_v${currentVersion}`; - - console.log(`Creating tag & release: ${tag}`); - - await exec.exec(`git tag "${tag}"`); - await exec.exec(`git push origin --tags`); - - // Create release - const { data } = await github.rest.repos.createRelease({ - owner: "bitwarden", - repo: "dotnet-extensions", - tag_name: tag, - target_commitish: "${{ github.event.ref }}", - name: tag, - body: "", - prerelease: ${{ inputs.type == 'prerelease' }}, - generate_release_notes: false, // This creates a link between this and the last tag but that might not be our version - }); - - const templateMarker = data.upload_url.indexOf("{"); - - let url = data.upload_url; - - if (templateMarker > -1) { - url = url.substring(0, templateMarker); - } - - const globber = await glob.create("**/*.nupkg"); - const files = await globber.glob(); - - const fs = require("fs"); - const path = require("path"); - - if (files.length === 0) { - core.setFailed("No files found, cannot create release."); - return; - } - - for (const file of files) { - const endpoint = new URL(url); - endpoint.searchParams.append("name", path.basename(file)); - const endpointString = endpoint.toString(); - console.log(`Uploading file: ${file} to ${endpointString}`); - // do the upload - const uploadResponse = await github.request({ - method: "POST", - url: endpointString, - data: fs.readFileSync(file), - }); - - console.log(`Upload response: ${uploadResponse.status}`); - } - - console.log("Finished creating release."); - - - name: Bump version - id: version-bumper - shell: pwsh - env: - PACKAGE_NAME: ${{ inputs.package }} - BUMP_TYPE: ${{ inputs.type }} - run: | - $NEW_VERSION=$(./scripts/UpdatePackageVersion.ps1 -PackageName $env:PACKAGE_NAME -BumpType $env:BUMP_TYPE) - Write-Output "NEW_VERSION=$NEW_VERSION" >> $Env:GITHUB_OUTPUT - - - name: Create PR - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + - name: Pack & Release + uses: bitwarden/dotnet-extensions/.github/workflows/pack-and-release.yml@main with: - script: | - const baseBranch = '${{ github.event.ref }}'; - const packageName = '${{ inputs.package }}'; - const bumpType = '${{ inputs.type }}'; - const newVersion = '${{ steps.version-bumper.outputs.NEW_VERSION }}'; - - if (newVersion === '') { - core.setFailed('New version was not set.'); - return; - } - - // Configure Git - await exec.exec(`git config user.name "github-actions"`); - await exec.exec(`git config user.email "github-actions@github.com"`); - - const versionBumpBranch = `version-bump/${packageName}-to-${newVersion}`; - await exec.exec(`git checkout -b ${versionBumpBranch}`); - - // Skip opening PR if branch already exists on the origin remote since that means it was - // opened earlier and force pushing to the branch updates the existing PR - let shouldOpenPullRequest = true; - try { - await exec.exec(`git ls-remote --exit-code --heads origin ${versionBumpBranch}`); - shouldOpenPullRequest = false; - } catch { } - - // Add and commit changes - const commitMessage = `Bump ${packageName} version to ${newVersion}`; - const gitCommitCommand = `git commit --all --message "${commitMessage}"`; - let gitCommitOutput = `$ ${gitCommitCommand}\n\n`; - let gitCommitFailed = false; - - try { - await exec.exec(gitCommitCommand, [], { - listeners: { - stdout: function stdout(data) { gitCommitOutput += data }, - stderr: function stderr(data) { gitCommitOutput += data } - } - }); - } catch (error) { - gitCommitOutput += error; - gitCommitFailed = true; - } - - if (gitCommitFailed) { - console.log(`Failed:\n\n${gitCommitOutput}`); - throw new Error("git commit command failed."); - } - - await exec.exec(`git push --force --set-upstream origin HEAD:${versionBumpBranch}`); - - const pullRequestBody = ` - Version Bump for ${packageName} to ${newVersion} - - /cc @${{ github.event.sender.login }} - `; - - await github.rest.pulls.create({ - owner: "bitwarden", - repo: "dotnet-extensions", - title: commitMessage, - body: pullRequestBody, - head: versionBumpBranch, - base: baseBranch, - }); - - console.log("Successfully open GitHub PR."); + prerelease: false diff --git a/.github/workflows/start-release.yml b/.github/workflows/start-release.yml new file mode 100644 index 0000000..05578db --- /dev/null +++ b/.github/workflows/start-release.yml @@ -0,0 +1,67 @@ +name: Release +run-name: Starting release for ${{ inputs.package }} + +on: + workflow_dispatch: + inputs: + package: + description: Which package to release + required: true + type: choice + options: + - Bitwarden.Server.Sdk + +permissions: + pull-requests: write + contents: write + +env: + PACKAGE_DIRECTORY: './extensions/${{ inputs.package }}' + +jobs: + start-release: + name: Create Release Candidate + runs-on: ubuntu-22.04 + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up .NET + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 + + # get current version + - name: Get current version + id: current-version + run: echo "VERSION=$(dotnet msbuild --getProperty:Version)" >> $GITHUB_OUTPUT + working-directory: '${{ env.PACKAGE_DIRECTORY }}/src' + + - name: Create release candidate branch + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const packageName = '${{ inputs.package }}'; + const currentVersion = '${{ steps.current-version.outputs.VERSION }}'; + console.log("Creating release candidate branch"); + + // Configure Git + await exec.exec(`git config user.name "github-actions"`); + await exec.exec(`git config user.email "github-actions@github.com"`); + + var versionParts = currentVersion.split('.'); + + if (versionParts.length <= 2) { + core.setFailed(`Invalid current version: ${currentVersion}`); + return; + } + + const releaseCandidateBranch = `release/${packageName}/${versionParts[0]}.${versionParts[1]}`; + + await exec.exec(`git checkout -b ${releaseCandidateBranch}`); + + await exec.exec(`git push --set-upstream origin HEAD:${releaseCandidateBranch}`); + + - name: Do version bump + uses: bitwarden/dotnet-extensions/.github/workflows/version-bump.yml@main + with: + type: ga + package: ${{ inputs.package }} diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..8bdf29f --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,101 @@ +name: Version Bump + +on: + workflow_call: + inputs: + type: + description: The type of this release + required: true + type: string + package: + description: Which package to release + required: true + type: string + +jobs: + version-bump: + name: Version bump + runs-on: ubuntu-22.04 + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up .NET + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 + + - name: Bump version + id: version-bumper + shell: pwsh + env: + PACKAGE_NAME: ${{ inputs.package }} + BUMP_TYPE: ${{ inputs.type }} + run: | + $NEW_VERSION=$(./scripts/UpdatePackageVersion.ps1 -PackageName $env:PACKAGE_NAME -BumpType $env:BUMP_TYPE) + Write-Output "NEW_VERSION=$NEW_VERSION" >> $Env:GITHUB_OUTPUT + + - name: Create PR + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const baseBranch = '${{ github.event.ref }}'; + const packageName = '${{ inputs.package }}'; + const bumpType = '${{ inputs.type }}'; + const newVersion = '${{ steps.version-bumper.outputs.NEW_VERSION }}'; + + if (newVersion === '') { + core.setFailed('New version was not set.'); + return; + } + + const versionBumpBranch = `version-bump/${packageName}-to-${newVersion}`; + await exec.exec(`git checkout -b ${versionBumpBranch}`); + + // Skip opening PR if branch already exists on the origin remote since that means it was + // opened earlier and force pushing to the branch updates the existing PR + let shouldOpenPullRequest = true; + try { + await exec.exec(`git ls-remote --exit-code --heads origin ${versionBumpBranch}`); + shouldOpenPullRequest = false; + } catch { } + + // Add and commit changes + const commitMessage = `Bump ${packageName} version to ${newVersion}`; + const gitCommitCommand = `git commit --all --message "${commitMessage}"`; + let gitCommitOutput = `$ ${gitCommitCommand}\n\n`; + let gitCommitFailed = false; + + try { + await exec.exec(gitCommitCommand, [], { + listeners: { + stdout: function stdout(data) { gitCommitOutput += data }, + stderr: function stderr(data) { gitCommitOutput += data } + } + }); + } catch (error) { + gitCommitOutput += error; + gitCommitFailed = true; + } + + if (gitCommitFailed) { + console.log(`Failed:\n\n${gitCommitOutput}`); + throw new Error("git commit command failed."); + } + + await exec.exec(`git push --force --set-upstream origin HEAD:${versionBumpBranch}`); + + const pullRequestBody = ` + Version Bump for ${packageName} to ${newVersion} + + /cc @${{ github.event.sender.login }} + `; + + await github.rest.pulls.create({ + owner: "bitwarden", + repo: "dotnet-extensions", + title: commitMessage, + body: pullRequestBody, + head: versionBumpBranch, + base: baseBranch, + }); + + console.log("Successfully open GitHub PR.");