From 37fcdf5bf55c7b0529effea891ed610af0578fc7 Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Wed, 29 Jul 2020 12:36:44 +0200 Subject: [PATCH] Automate publishing new releases on crates.io These scripts will help automate the steps required to publish new releases on crates.io: - update version number in `Cargo.toml`, `README.md`, and `src/lib.rs` - update changelog in `README.md` - run `cargo publish` if all tests pass --- .github/workflows/prepare-release.yml | 153 ++++++++++++++++++++++++++ .github/workflows/publish-crate.yml | 63 +++++++++++ 2 files changed, 216 insertions(+) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/publish-crate.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..0c6648f --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,153 @@ +name: Prepare Release PR + +on: + push: + branches: + - 'release-*' + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + name: ${{ steps.vars.outputs.name }} + old-version: ${{ steps.vars.outputs.old-version }} + new-version: ${{ steps.vars.outputs.new-version }} + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set variables + id: vars + run: | + NAME=$(cargo metadata -q --no-deps | jq -r '.packages[0].name') + OLD_VERSION=$(cargo metadata -q --no-deps | jq -r '.packages[0].version') + NEW_VERSION=$(echo ${{ github.ref }} | cut -d '-' -f 2-) + echo "Version from Cargo: $OLD_VERSION" + echo "Version from branch: $NEW_VERSION" + + echo "::set-output name=name::$NAME" + echo "::set-output name=old-version::$OLD_VERSION" + echo "::set-output name=new-version::$NEW_VERSION" + + - name: Verify version format + run: | + echo '${{ steps.vars.outputs.new-version }}' | grep -q '^[0-9]\+\.[0-9]\+\.[0-9]\+$' + + pull-request: + needs: setup + if: ${{ needs.setup.outputs.old-version != needs.setup.outputs.new-version }} + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Configure Git user + run: | + git config user.name "Martin Geisler" + git config user.email "martin@geisler.net" + + - name: Update changelog for version ${{ needs.setup.outputs.new-version }} + id: changelog + uses: actions/github-script@v2 + with: + script: | + var fs = require('fs') + const old_version = '${{ needs.setup.outputs.old-version }}' + const new_version = '${{ needs.setup.outputs.new-version }}' + + let cutoff = '1970-01-01' + const releases = await github.repos.listReleases(context.repo) + for (const release of releases.data) { + if (release.tag_name == old_version) { + cutoff = release.published_at + break + } + } + core.info(`Finding merged PRs after ${cutoff}`) + + let q = [`repo:${context.repo.owner}/${context.repo.repo}`, + 'is:pr', 'is:merged', `merged:>${cutoff}`] + const prs = await github.search.issuesAndPullRequests({ + q: q.join(' '), + sort: 'created', + order: 'asc', + }) + core.info(`Found ${prs.data.items.length} merged PRs`) + + const changelog = prs.data.items.map( + pr => `* [#${pr.number}](${pr.html_url}): ${pr.title}` + ).join('\n\n') + core.exportVariable('CHANGELOG', changelog) + + var readme = fs.readFileSync('README.md', 'utf8') + const today = new Date().toISOString().split('T')[0] + const heading = `## Version ${new_version} (${today})\n` + if (readme.match('## Unreleased')) { + readme = readme.replace('## Unreleased', `${heading}\n${changelog}`) + } else { + readme = readme.replace('# Changelog', `# Changelog\n\n${heading}\n${changelog}`) + } + fs.writeFileSync('README.md', readme) + + - name: Commit changelog + run: | + git commit --all -m "Update changelog for version ${{ needs.setup.outputs.new-version }}" + + - name: Update TOML code blocks + run: | + import fileinput, re, sys + + NAME = '${{ needs.setup.outputs.name }}' + NEW_VERSION = '${{ needs.setup.outputs.new-version }}' + MAJOR_MINOR = '.'.join(NEW_VERSION.split('.')[:2]) + + for line in fileinput.input(inplace=True): + line = re.sub(f'{NAME} = "[^"]+"', + f'{NAME} = "{MAJOR_MINOR}"', line) + line = re.sub(f'{NAME} = {{ version = "[^"]+"', + f'{NAME} = {{ version = "{MAJOR_MINOR}"', line) + sys.stdout.write(line) + shell: python3 {0} README.md + + - name: Update html_root_url + run: | + import fileinput, re, sys + + NAME = '${{ needs.setup.outputs.name }}' + NEW_VERSION = '${{ needs.setup.outputs.new-version }}' + + for line in fileinput.input(inplace=True): + sys.stdout.write( + re.sub(f'html_root_url = "https://docs.rs/{NAME}/[^"]+"', + f'html_root_url = "https://docs.rs/{NAME}/{NEW_VERSION}"', line)) + shell: python3 {0} src/lib.rs + + - name: Update crate version to ${{ needs.setup.outputs.new-version }} + uses: thomaseizinger/set-crate-version@1.0.0 + with: + version: ${{ needs.setup.outputs.new-version }} + + - name: Build and test + run: | + cargo test + + - name: Commit version bump + run: | + git commit --all -m "Bump version to ${{ needs.setup.outputs.new-version }}" + + - name: Push version bump + run: git push origin + + - name: Create pull request + uses: actions/github-script@v2 + with: + script: | + const pr = await github.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + head: 'release-${{ needs.setup.outputs.new-version }}', + base: 'master', + title: 'Release ${{ needs.setup.outputs.new-version }}', + body: process.env.CHANGELOG, + }) + core.info(`Created PR: ${pr.data.html_url}`) diff --git a/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml new file mode 100644 index 0000000..6e9cb2e --- /dev/null +++ b/.github/workflows/publish-crate.yml @@ -0,0 +1,63 @@ +name: Publish Crate + +on: + push: + branches: + - master + paths: + - Cargo.toml + repository_dispatch: + types: publish + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set variables + id: vars + run: | + NAME=$(cargo metadata -q --no-deps | jq -r '.packages[0].name') + VERSION=$(cargo metadata -q --no-deps | jq -r '.packages[0].version') + CHANGELOG=$(awk '/^## Version/ {i++}; i==1 {print}; i>1 {exit}' README.md \ + | python3 -c 'import sys, json; print(json.dumps(sys.stdin.read()))') + echo "::set-output name=name::$NAME" + echo "::set-output name=version::$VERSION" + echo "::set-output name=changelog::$CHANGELOG" + echo "Found $NAME-$VERSION" + + - name: Lookup ${{ steps.vars.outputs.version }} tag + id: need-release + uses: actions/github-script@v2 + with: + script: | + const version = '${{ steps.vars.outputs.version }}' + const tags = await github.repos.listTags(context.repo) + if (tags.data.some(tag => tag.name == version)) { + core.info(`Found ${version} tag -- will skip publish step`) + return false + } + core.info(`Found no ${version} tag -- will proceed with publishing`) + return true + + # The result from above is JSON-encoded, meaning that we + # end up with the string 'true', not the Boolean true. + - if: steps.need-release.outputs.result == 'true' + name: Publish crate to crates.io + run: | + echo "Publishing ${{ steps.vars.outputs.name }}-${{ steps.vars.outputs.version }}" + cargo publish --token ${{ secrets.CARGO_TOKEN }} + + - if: steps.need-release.outputs.result == 'true' + name: Create GitHub release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.vars.outputs.version }} + release_name: ${{ steps.vars.outputs.name }}-${{ steps.vars.outputs.version }} + body: ${{ fromJson(steps.vars.outputs.changelog) }} + draft: false + prerelease: false