diff --git a/.github/workflows/check_version.sh b/.github/workflows/check_version.sh new file mode 100755 index 0000000..7bac1dd --- /dev/null +++ b/.github/workflows/check_version.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Checks that every place that names hex_core's version names the version in +# src/hex_core.app.src + +set -euo pipefail + +version=$(erl -noshell -eval '{ok, [{application, hex_core, App}]} = file:consult("src/hex_core.app.src"), io:put_chars(proplists:get_value(vsn, App)), halt().') +failed=false + +# Every line of $1 that matches the extended regex $2 must capture the version +# in the regex's first group, and at least one line must match +check() { + local file=$1 regex=$2 found + found=$(sed -nE "s/.*$regex.*/\\1/p" "$file") + + if [ -z "$found" ]; then + echo "::error file=$file::$file has no line matching $regex" + failed=true + fi + + for found_version in $found; do + if [ "$found_version" != "$version" ]; then + echo "::error file=$file::$file names version $found_version, src/hex_core.app.src has $version" + failed=true + fi + done +} + +check src/hex_core.hrl '^-define\(HEX_CORE_VERSION, "([^"]*)"\)\.$' +check README.md '\{hex_core, "([^"]*)"\}' +check README.md '\{:hex_core, "~> ([^"]*)"\}' + +if [ "$failed" = true ]; then + exit 1 +fi + +echo "Version $version matches in every file" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9a437c..4f75c78 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,3 +56,5 @@ jobs: - run: rebar3 as test proper - run: rebar3 fmt --check if: ${{ matrix.pair.lint }} + - run: .github/workflows/check_version.sh + if: ${{ matrix.pair.lint }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1419a4c..a58a879 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,12 +2,16 @@ name: Release on: push: - tags: - - "v*" + branches: + - main permissions: contents: read +# Two pushes in a row would otherwise both try to tag and publish the version +concurrency: + group: release + jobs: release: name: Release @@ -16,11 +20,12 @@ jobs: # The GitHub environment that hex_core's workload identity on hex.pm names environment: release permissions: - contents: write # create the GitHub release + contents: write # push the version tag and create the GitHub release id-token: write # the OIDC token mix hex.publish exchanges with Workload Identity steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 persist-credentials: false - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 @@ -34,20 +39,20 @@ jobs: - name: Install Hex run: mix archive.install github hexpm/hex ref 1d155e718aa76ab75d03f6f4bdcb0d8042f9a0ad --force - - name: Read the release notes from the changelog - env: - TAG: ${{ github.ref_name }} + - name: Check that every file names the same version + run: .github/workflows/check_version.sh + + - name: Read the version and its release notes from the changelog + id: version run: | version=$(erl -noshell -eval '{ok, [{application, hex_core, App}]} = file:consult("src/hex_core.app.src"), io:put_chars(proplists:get_value(vsn, App)), halt().') + tag="v$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" - if [ "$TAG" != "v$version" ]; then - echo "::error::Tag $TAG doesn't match version $version in src/hex_core.app.src" - exit 1 - fi - - # The lines under "## $TAG (date)" up to the next "## " heading, + # The lines under "## $tag (date)" up to the next "## " heading, # without leading and trailing blank lines - awk -v heading="## $TAG " ' + awk -v heading="## $tag " ' /^## / { if (found) exit found = index($0, heading) == 1 @@ -63,18 +68,41 @@ jobs: ' CHANGELOG.md > "$RUNNER_TEMP/release_notes.md" if [ ! -s "$RUNNER_TEMP/release_notes.md" ]; then - echo "::error::CHANGELOG.md has no section for $TAG" + echo "::error::CHANGELOG.md has no section for $tag" exit 1 fi + - name: Tag the version + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ steps.version.outputs.tag }} + run: | + if [ -n "$(git tag -l "$TAG")" ]; then + echo "$TAG is already tagged" + else + gh api --method POST "repos/$GH_REPO/git/refs" -f "ref=refs/tags/$TAG" -f "sha=$GITHUB_SHA" + fi + - name: Publish to Hex env: MIX_EXS: publish.exs - run: mix hex.publish --yes + VERSION: ${{ steps.version.outputs.version }} + run: | + if mix hex.info hex_core "$VERSION"; then + echo "hex_core $VERSION is already published" + else + mix hex.publish --yes + fi - name: Create the GitHub release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - TAG: ${{ github.ref_name }} - run: gh release create "$TAG" --title "$TAG" --verify-tag --notes-file "$RUNNER_TEMP/release_notes.md" + TAG: ${{ steps.version.outputs.tag }} + run: | + if gh release view "$TAG" > /dev/null; then + echo "The $TAG release already exists" + else + gh release create "$TAG" --title "$TAG" --verify-tag --notes-file "$RUNNER_TEMP/release_notes.md" + fi diff --git a/RELEASE.md b/RELEASE.md index bbd8c8c..6927cf2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,9 +8,9 @@ 4. Commit changes above with title "Release v$VERSION" -5. Push main and the new tag `v$VERSION` +5. Push main -Pushing the tag runs the [Release workflow](.github/workflows/release.yml) in the `release` environment. It publishes the package and docs to Hex with Workload Identity and creates the GitHub release with the version's CHANGELOG section as its notes. To publish by hand instead, run `MIX_EXS=publish.exs mix hex.publish`. +Every push to main runs the [Release workflow](.github/workflows/release.yml) in the `release` environment. When the version in `src/hex_core.app.src` hasn't been released yet, it tags the pushed commit `v$VERSION`, publishes the package and docs to Hex with Workload Identity and creates the GitHub release with the version's CHANGELOG section as its notes. Each step is skipped when it's already done, so re-running a failed run picks up where it stopped. To publish by hand instead, run `MIX_EXS=publish.exs mix hex.publish`. ## Places where version is mentioned @@ -19,3 +19,5 @@ src/hex_core.app.src src/hex_core.hrl README.md ``` + +[`check_version.sh`](.github/workflows/check_version.sh) fails when `src/hex_core.hrl` or `README.md` names a different version than `src/hex_core.app.src`. CI runs it, and the Release workflow runs it before tagging.