diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 0000000..0fbf5de --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,83 @@ +# Reusable workflow that publishes a GitHub release or pre-release of a version, tagging the commit +# the run is for. A pre-release replaces the one of the same name, so a pull request can publish its +# X.Y.Z-PRE- on every push; a release never replaces one. See README.md for how to call it. +name: GitHub release + +on: + workflow_call: + inputs: + version: + description: Tag and name of the release, e.g. the version output of semver-version.yml + type: string + required: true + prerelease: + description: Publish a pre-release + type: boolean + default: false + title: + description: Title of the release (default the version) + type: string + default: "" + target: + description: Commit to tag (default the commit the run is for) + type: string + default: "" + files: + description: Newline-separated globs of the files to attach, relative to the repository root + type: string + default: "" + artifact-pattern: + description: Artifacts of the same run to download into release-assets/ before attaching the files + type: string + default: "" + +jobs: + release: + name: GitHub release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out the repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ inputs.target || github.sha }} + persist-credentials: false + + # the script from the same commit as this workflow, not from the caller's repository + - name: Locate this workflow + env: + WORKFLOW_REPOSITORY: ${{ job.workflow_repository }} + WORKFLOW_SHA: ${{ job.workflow_sha }} + run: | + if [ -z "$WORKFLOW_REPOSITORY" ] || [ -z "$WORKFLOW_SHA" ]; then + echo "::error::job.workflow_repository and job.workflow_sha are empty: this workflow has to run as a reusable workflow on github.com" + exit 1 + fi + echo "Using $WORKFLOW_REPOSITORY@$WORKFLOW_SHA" + + - name: Check out the release script + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: .github-workflows + persist-credentials: false + + - name: Download the artifacts to attach + if: inputs.artifact-pattern != '' + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + path: release-assets + pattern: ${{ inputs.artifact-pattern }} + merge-multiple: true + + - name: Publish the release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.version }} + TARGET: ${{ inputs.target || github.sha }} + PRERELEASE: ${{ inputs.prerelease }} + TITLE: ${{ inputs.title }} + FILES: ${{ inputs.files }} + run: .github-workflows/scripts/github-release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4f0f65..a347656 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,13 +22,16 @@ concurrency: group: release cancel-in-progress: false -permissions: - contents: write - jobs: - release: + version: if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true runs-on: ubuntu-latest + permissions: + contents: read + outputs: + version: ${{ steps.version.outputs.version }} + major: ${{ steps.version.outputs.major }} + target: ${{ steps.version.outputs.target }} steps: - name: Check out the merged commit uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -66,24 +69,40 @@ jobs: echo "::error::v$version already exists" exit 1 fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "major=v${version%%.*}" >> "$GITHUB_OUTPUT" + { + echo "version=v$version" + echo "major=v${version%%.*}" + echo "target=$(git rev-parse HEAD)" + } >> "$GITHUB_OUTPUT" echo "Releasing v$version" - - name: Tag the release + # the release workflow this repository offers, from this very commit + release: + needs: version + uses: ./.github/workflows/github-release.yml + permissions: + contents: write + with: + version: ${{ needs.version.outputs.version }} + target: ${{ needs.version.outputs.target }} + + major-tag: + needs: [version, release] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out the merged commit + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ needs.version.outputs.target }} + + - name: Move the major tag to the release env: - VERSION: v${{ steps.version.outputs.version }} - MAJOR: ${{ steps.version.outputs.major }} + VERSION: ${{ needs.version.outputs.version }} + MAJOR: ${{ needs.version.outputs.major }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -a "$VERSION" -m "$VERSION" git tag -f -a "$MAJOR" -m "$MAJOR, currently $VERSION" - git push origin "refs/tags/$VERSION" git push --force origin "refs/tags/$MAJOR" - - - name: Publish the GitHub release - env: - GH_TOKEN: ${{ github.token }} - VERSION: v${{ steps.version.outputs.version }} - run: gh release create "$VERSION" --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --title "$VERSION" diff --git a/.github/workflows/semver-version.yml b/.github/workflows/semver-version.yml index d3db628..522e6af 100644 --- a/.github/workflows/semver-version.yml +++ b/.github/workflows/semver-version.yml @@ -11,6 +11,12 @@ on: description: Text the release tags start with (default v); empty for tags like 1.2.3 type: string default: v + default-bump: + description: >- + major, minor or patch for a push that no labelled pull request carries; empty (the default) + fails such a push instead + type: string + default: "" outputs: version: description: X.Y.Z on a push, X.Y.Z-PRE- on a pull request @@ -62,30 +68,14 @@ jobs: # chart-pr.yml). On a push, the labels of the pull request the pushed commit was merged from. - name: Read the semver label id: label + working-directory: repo env: GH_TOKEN: ${{ github.token }} EVENT: ${{ github.event_name }} PR: ${{ github.event.pull_request.number }} SHA: ${{ github.sha }} - run: | - if [ "$EVENT" = pull_request ]; then - labels=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq '[.labels[].name]') - else - pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/pulls" \ - --jq '[.[] | select(.merged_at != null)][0] // empty | {number, labels: [.labels[].name]}') - if [ -z "$pr" ]; then - echo "::error::No merged pull request contains $SHA, so there is no semver label to release it with" - exit 1 - fi - echo "Releasing pull request #$(jq -r .number <<< "$pr")" - labels=$(jq -c .labels <<< "$pr") - fi - bumps=$(jq -r '[.[] | select(. == "major" or . == "minor" or . == "patch")] | join(" ")' <<< "$labels") - if [ "$(wc -w <<< "$bumps")" -ne 1 ]; then - echo "::error::The pull request needs exactly one of the labels major, minor or patch (found: ${bumps:-none})" - exit 1 - fi - echo "bump=$bumps" >> "$GITHUB_OUTPUT" + DEFAULT_BUMP: ${{ inputs.default-bump }} + run: ../semver-workflows/scripts/semver-label.sh - name: Ask for the label if: failure() && steps.label.outcome == 'failure' && github.event_name == 'pull_request' diff --git a/README.md b/README.md index 19ea5b5..7eeb0dd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Reusable GitHub Actions workflows shared by FIWARE repositories, so that a fix i | [`chart-release.yml`](#chart-release-chart-releaseyml) | Publishes the charts, failing when a changed chart has no new version | Push to the main branch | | [`chart-dependencies.yml`](#chart-dependencies-chart-dependenciesyml) | Opens pull requests for newer chart dependencies | Schedule | | [`semver-version.yml`](#semver-version-semver-versionyml) | Works out the version to release from the semver label of the pull request | Pull requests and push to the main branch | +| [`github-release.yml`](#github-release-github-releaseyml) | Publishes a GitHub release or pre-release of a version | Pull requests and push to the main branch | | [`stale.yml`](#stale-issues-staleyml) | Marks and closes issues and pull requests without activity | Schedule | Together, the chart workflows cover the life of a chart: a pull request is linted and gets its new version, and merging it publishes that version. @@ -145,7 +146,7 @@ The chart version is bumped in the pull request itself because pull requests ope For repositories that release an application, not charts, with the version driven by the semver label of the pull request. It works out the version and leaves the build and the release to the caller: - on a pull request, it requires exactly one of the labels `major`, `minor` or `patch` (read when the job runs, as in `chart-pr.yml`), commenting on the pull request when it is missing, and returns `X.Y.Z-PRE-`; -- on a push to the main branch, it reads the label of the pull request the pushed commit was merged from, and returns `X.Y.Z`. A commit that no merged pull request contains, or one merged without a label, fails. +- on a push to the main branch, it reads the label of the pull request the pushed commit was merged from, and returns `X.Y.Z`. A commit that no merged pull request contains, or one merged without a label, fails, unless `default-bump` says how to release it. `X.Y.Z` is the highest release tag bumped by the label. Release tags are `X.Y.Z`; other tags, such as pre-releases (`1.2.3-PRE-4`), are ignored. @@ -173,12 +174,53 @@ jobs: | Input | Default | Description | |---|---|---| | `tag-prefix` | `v` | Text the release tags start with; empty for tags like `1.2.3` | +| `default-bump` | (none) | `major`, `minor` or `patch` for a push that no labelled pull request carries; without it, such a push fails | | Output | Description | |---|---| | `version` | `X.Y.Z` on a push, `X.Y.Z-PRE-` on a pull request | | `bump` | `major`, `minor` or `patch`, from the label | +## GitHub release (`github-release.yml`) + +Publishes a GitHub release of a version, tagging the commit the run is for, with notes generated from the merged pull requests. It pairs with `semver-version.yml`: + +- a **pre-release** replaces the release and the tag of the same name, so a pull request publishes its `X.Y.Z-PRE-` again on every push; +- a **release** never replaces one: a version that already exists fails the job. + +```yaml +jobs: + generate-version: + uses: FIWARE/github-workflows/.github/workflows/semver-version.yml@v1 + permissions: + contents: read + pull-requests: write + with: + tag-prefix: "" + + git-release: + needs: [generate-version, build] + uses: FIWARE/github-workflows/.github/workflows/github-release.yml@v1 + permissions: + contents: write + with: + version: ${{ needs.generate-version.outputs.version }} + prerelease: ${{ github.event_name == 'pull_request' }} + files: | + LICENSE + release-assets/my-binary-* + artifact-pattern: my-binary-* # artifacts of this run, downloaded into release-assets/ +``` + +| Input | Default | Description | +|---|---|---| +| `version` | (required) | Tag and name of the release | +| `prerelease` | `false` | Publish a pre-release | +| `title` | the version | Title of the release | +| `target` | the commit of the run | Commit to tag | +| `files` | (none) | Newline-separated globs of the files to attach, relative to the repository root; a glob that matches nothing fails | +| `artifact-pattern` | (none) | Artifacts of the same run to download into `release-assets/` first | + ## Stale issues (`stale.yml`) Marks issues and pull requests without activity as stale, and closes them when they stay so, with [actions/stale](https://github.com/actions/stale). @@ -211,8 +253,8 @@ Callers use the `v1` tag, which moves with every compatible release (`v1.0.1`, ` Every pull request to this repository needs one of the labels `major`, `minor` or `patch`. When it is merged, `release.yml`: 1. takes the highest `vX.Y.Z` tag and bumps it by the label; -2. tags the merge commit with the new `vX.Y.Z` and moves the major tag (`vX`) to it. A `major` release creates the next major tag and leaves the previous one where it was, so its callers keep working; -3. publishes a GitHub release with notes generated from the merged pull requests. +2. publishes the GitHub release `vX.Y.Z` of the merge commit with this repository's own `github-release.yml`, so every release also exercises it; +3. moves the major tag (`vX`) to it. A `major` release creates the next major tag and leaves the previous one where it was, so its callers keep working. A pull request merged without a label is not released, and the workflow fails saying so. The first release, or any release no pull request carries, is made by running the Release workflow by hand on `main` with the version (`X.Y.Z`). diff --git a/scripts/github-release.sh b/scripts/github-release.sh new file mode 100755 index 0000000..5d685ff --- /dev/null +++ b/scripts/github-release.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Publishes a GitHub release, tagging TARGET with VERSION. +# +# A pre-release replaces the release and the tag of the same name, if there are any: a pull request +# publishes the same X.Y.Z-PRE- on every push. A release refuses to replace one: a version, +# once released, does not change. The release notes are generated from the merged pull requests. +# +# Environment: +# VERSION tag and name of the release (required) +# TARGET commit to tag (required) +# PRERELEASE true for a pre-release (default: false) +# TITLE title of the release (default: VERSION) +# FILES newline- or space-separated globs of the files to attach, relative to the current +# directory; a glob that matches nothing fails +# REPOSITORY owner/repo (default: $GITHUB_REPOSITORY) +# GH gh binary (default: gh) + +set -euo pipefail + +: "${VERSION:?VERSION must name the release}" +: "${TARGET:?TARGET must name the commit to tag}" +PRERELEASE="${PRERELEASE:-false}" +TITLE="${TITLE:-$VERSION}" +FILES="${FILES:-}" +REPOSITORY="${REPOSITORY:-${GITHUB_REPOSITORY:?REPOSITORY or GITHUB_REPOSITORY must name the repository}}" +GH="${GH:-gh}" + +case "$PRERELEASE" in + true|false) ;; + *) echo "::error::PRERELEASE must be true or false, got '$PRERELEASE'" >&2; exit 1 ;; +esac + +# the globs are split without being expanded, so that a glob matching nothing is noticed +set -f +read -r -d '' -a patterns <<< "$FILES" || true +set +f +assets=() +shopt -s nullglob +for pattern in "${patterns[@]}"; do + # shellcheck disable=SC2206 # the glob is expanded on purpose + matches=($pattern) + if [ "${#matches[@]}" -eq 0 ]; then + echo "::error::No file matches '$pattern'" >&2 + exit 1 + fi + assets+=("${matches[@]}") +done +shopt -u nullglob + +release_exists=false +if "$GH" release view "$VERSION" --repo "$REPOSITORY" --json tagName >/dev/null 2>&1; then + release_exists=true +fi +tag_exists=false +if "$GH" api "repos/$REPOSITORY/git/ref/tags/$VERSION" >/dev/null 2>&1; then + tag_exists=true +fi + +if [ "$release_exists" = true ] || [ "$tag_exists" = true ]; then + if [ "$PRERELEASE" != true ]; then + echo "::error::$VERSION already exists: a release is not replaced" >&2 + exit 1 + fi + echo "Replacing the pre-release $VERSION" + if [ "$release_exists" = true ]; then + "$GH" release delete "$VERSION" --repo "$REPOSITORY" --yes + fi + if [ "$tag_exists" = true ]; then + "$GH" api -X DELETE "repos/$REPOSITORY/git/refs/tags/$VERSION" >/dev/null + fi +fi + +args=(release create "$VERSION" --repo "$REPOSITORY" --target "$TARGET" --title "$TITLE" --generate-notes) +if [ "$PRERELEASE" = true ]; then + args+=(--prerelease) +fi +"$GH" "${args[@]}" "${assets[@]}" +echo "Published $VERSION on $TARGET${assets:+ with ${#assets[@]} file(s)}" diff --git a/scripts/semver-label.sh b/scripts/semver-label.sh new file mode 100755 index 0000000..fda6a6f --- /dev/null +++ b/scripts/semver-label.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Prints the semver bump (major, minor or patch) of a run from the label of its pull request. +# +# On a pull request, the labels it has now, not the ones of the event that started the run: a pull +# request opened by a bot gets its label right after it is opened. On a push, the labels of the +# merged pull request the pushed commit comes from. The pull request needs exactly one of the labels +# major, minor and patch. On a push, DEFAULT_BUMP, when set, is used instead of failing when no +# merged pull request contains the commit or it has no such label. +# +# Environment: +# EVENT pull_request or push (required) +# PR pull request number, on a pull request +# SHA pushed commit, on a push +# DEFAULT_BUMP major, minor or patch for a push without a labelled pull request (default: none) +# REPOSITORY owner/repo (default: $GITHUB_REPOSITORY) +# GH gh binary (default: gh) +# GITHUB_OUTPUT when set, receives bump= + +set -euo pipefail + +: "${EVENT:?EVENT must be pull_request or push}" +DEFAULT_BUMP="${DEFAULT_BUMP:-}" +REPOSITORY="${REPOSITORY:-${GITHUB_REPOSITORY:?REPOSITORY or GITHUB_REPOSITORY must name the repository}}" +GH="${GH:-gh}" + +case "$DEFAULT_BUMP" in + ""|major|minor|patch) ;; + *) echo "::error::DEFAULT_BUMP must be major, minor or patch, got '$DEFAULT_BUMP'" >&2; exit 1 ;; +esac + +labels="[]" +if [ "$EVENT" = pull_request ] || [ "$EVENT" = pull_request_target ]; then + : "${PR:?PR must be the pull request number}" + labels=$("$GH" api "repos/$REPOSITORY/pulls/$PR" --jq '[.labels[].name]') +else + : "${SHA:?SHA must be the pushed commit}" + pr=$("$GH" api "repos/$REPOSITORY/commits/$SHA/pulls" \ + --jq '[.[] | select(.merged_at != null)][0] // empty | {number, labels: [.labels[].name]}') + if [ -z "$pr" ]; then + if [ -n "$DEFAULT_BUMP" ]; then + echo "No merged pull request contains $SHA, releasing it as $DEFAULT_BUMP" >&2 + echo "bump=$DEFAULT_BUMP" >> "${GITHUB_OUTPUT:-/dev/null}" + echo "$DEFAULT_BUMP" + exit 0 + fi + echo "::error::No merged pull request contains $SHA, so there is no semver label to release it with" >&2 + exit 1 + fi + echo "Releasing pull request #$(jq -r .number <<< "$pr")" >&2 + labels=$(jq -c .labels <<< "$pr") +fi + +bumps=$(jq -r '[.[] | select(. == "major" or . == "minor" or . == "patch")] | join(" ")' <<< "$labels") +if [ "$(wc -w <<< "$bumps")" -ne 1 ]; then + if [ "$EVENT" = push ] && [ -z "$bumps" ] && [ -n "$DEFAULT_BUMP" ]; then + echo "The pull request has no semver label, releasing it as $DEFAULT_BUMP" >&2 + bumps="$DEFAULT_BUMP" + else + echo "::error::The pull request needs exactly one of the labels major, minor or patch (found: ${bumps:-none})" >&2 + exit 1 + fi +fi + +echo "bump=$bumps" >> "${GITHUB_OUTPUT:-/dev/null}" +echo "$bumps" diff --git a/tests/github-release.bats b/tests/github-release.bats new file mode 100644 index 0000000..800be9f --- /dev/null +++ b/tests/github-release.bats @@ -0,0 +1,81 @@ +#!/usr/bin/env bats +# Tests for scripts/github-release.sh, with a fake gh. $EXISTING_RELEASE and $EXISTING_TAG name the +# release and the tag the fake repository already has. + +load helpers + +SCRIPT="$BATS_TEST_DIRNAME/../scripts/github-release.sh" + +setup() { + mkdir -p "$BATS_TEST_TMPDIR/repo" && cd "$BATS_TEST_TMPDIR/repo" + fake_command gh + cat > "$BATS_TEST_TMPDIR/gh.sh" <<'EOF' +case "$1 $2" in + "release view") [ "$3" = "${EXISTING_RELEASE:-}" ] || exit 1 ;; + "api repos/owner/repo/git/ref/tags/"*) [ "${2##*/}" = "${EXISTING_TAG:-}" ] || exit 1 ;; +esac +EOF + export VERSION=1.2.3 TARGET=abc123 REPOSITORY=owner/repo +} + +@test "a release is created on the target, with generated notes" { + run "$SCRIPT" + [ "$status" -eq 0 ] + grep -qx "release create 1.2.3 --repo owner/repo --target abc123 --title 1.2.3 --generate-notes" "$BATS_TEST_TMPDIR/gh.log" + ! grep -q -- "--prerelease" "$BATS_TEST_TMPDIR/gh.log" + ! grep -q "release delete" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "a pre-release is marked as such and takes the title" { + VERSION=1.2.3-PRE-7 PRERELEASE=true TITLE="Pre-release 7" run "$SCRIPT" + [ "$status" -eq 0 ] + grep -q "release create 1.2.3-PRE-7 .* --title Pre-release 7 --generate-notes --prerelease" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "an existing pre-release and its tag are replaced" { + EXISTING_RELEASE=1.2.3-PRE-7 EXISTING_TAG=1.2.3-PRE-7 VERSION=1.2.3-PRE-7 PRERELEASE=true run "$SCRIPT" + [ "$status" -eq 0 ] + [[ "$output" == *"Replacing the pre-release 1.2.3-PRE-7"* ]] + grep -qx "release delete 1.2.3-PRE-7 --repo owner/repo --yes" "$BATS_TEST_TMPDIR/gh.log" + grep -qx "api -X DELETE repos/owner/repo/git/refs/tags/1.2.3-PRE-7" "$BATS_TEST_TMPDIR/gh.log" + grep -q "release create 1.2.3-PRE-7" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "a pre-release tag left without its release is replaced too" { + EXISTING_TAG=1.2.3-PRE-7 VERSION=1.2.3-PRE-7 PRERELEASE=true run "$SCRIPT" + [ "$status" -eq 0 ] + ! grep -q "release delete" "$BATS_TEST_TMPDIR/gh.log" + grep -q "api -X DELETE repos/owner/repo/git/refs/tags/1.2.3-PRE-7" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "an existing release is never replaced" { + EXISTING_RELEASE=1.2.3 EXISTING_TAG=1.2.3 run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"1.2.3 already exists"* ]] + ! grep -q "release delete\|release create\|DELETE" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "an existing tag without a release is not replaced by a release" { + EXISTING_TAG=1.2.3 run "$SCRIPT" + [ "$status" -eq 1 ] + ! grep -q "release create" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "the files to attach are expanded from their globs" { + touch LICENSE && mkdir release-assets && touch release-assets/bin-linux-amd64 release-assets/bin-linux-arm64 + FILES=$'LICENSE\nrelease-assets/bin-*' run "$SCRIPT" + [ "$status" -eq 0 ] + grep -q "release create 1.2.3 .* --generate-notes LICENSE release-assets/bin-linux-amd64 release-assets/bin-linux-arm64$" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "a glob that matches nothing fails before publishing" { + FILES="missing-*" run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"No file matches 'missing-*'"* ]] + ! grep -q "release create" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "an invalid prerelease flag fails" { + PRERELEASE=yes run "$SCRIPT" + [ "$status" -eq 1 ] +} diff --git a/tests/semver-label.bats b/tests/semver-label.bats new file mode 100644 index 0000000..9fb8072 --- /dev/null +++ b/tests/semver-label.bats @@ -0,0 +1,82 @@ +#!/usr/bin/env bats +# Tests for scripts/semver-label.sh, with a fake gh. $PR_LABELS are the labels of the pull request, +# and $MERGED_PR the merged pull request a pushed commit comes from (empty for none). + +load helpers + +SCRIPT="$BATS_TEST_DIRNAME/../scripts/semver-label.sh" + +setup() { + mkdir -p "$BATS_TEST_TMPDIR/repo" && cd "$BATS_TEST_TMPDIR/repo" + fake_command gh + cat > "$BATS_TEST_TMPDIR/gh.sh" <<'EOF' +case "$2" in + repos/owner/repo/pulls/*) echo "${PR_LABELS:-[]}" ;; + repos/owner/repo/commits/*/pulls) echo "${MERGED_PR:-}" ;; +esac +EOF + export REPOSITORY=owner/repo GITHUB_OUTPUT="$BATS_TEST_TMPDIR/output" + : > "$GITHUB_OUTPUT" +} + +@test "a pull request takes the label it has now" { + EVENT=pull_request PR=7 PR_LABELS='["documentation","minor"]' run "$SCRIPT" + [ "$status" -eq 0 ] + [ "${lines[-1]}" = minor ] + grep -qx "bump=minor" "$GITHUB_OUTPUT" + grep -q "api repos/owner/repo/pulls/7" "$BATS_TEST_TMPDIR/gh.log" +} + +@test "a pull request without a semver label fails" { + EVENT=pull_request PR=7 PR_LABELS='["documentation"]' run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"needs exactly one of the labels major, minor or patch (found: none)"* ]] +} + +@test "a pull request with two semver labels fails" { + EVENT=pull_request PR=7 PR_LABELS='["minor","patch"]' run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"(found: minor patch)"* ]] +} + +@test "a default bump does not apply to pull requests" { + EVENT=pull_request PR=7 PR_LABELS='[]' DEFAULT_BUMP=patch run "$SCRIPT" + [ "$status" -eq 1 ] +} + +@test "a push takes the label of the merged pull request" { + EVENT=push SHA=abc MERGED_PR='{"number":9,"labels":["major"]}' run "$SCRIPT" + [ "$status" -eq 0 ] + [ "${lines[-1]}" = major ] + [[ "$output" == *"Releasing pull request #9"* ]] +} + +@test "a push that no merged pull request carries fails" { + EVENT=push SHA=abc run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"No merged pull request contains abc"* ]] +} + +@test "a push that no merged pull request carries takes the default bump" { + EVENT=push SHA=abc DEFAULT_BUMP=patch run "$SCRIPT" + [ "$status" -eq 0 ] + [ "${lines[-1]}" = patch ] + grep -qx "bump=patch" "$GITHUB_OUTPUT" +} + +@test "a push from a pull request without a semver label takes the default bump" { + EVENT=push SHA=abc DEFAULT_BUMP=patch MERGED_PR='{"number":9,"labels":[]}' run "$SCRIPT" + [ "$status" -eq 0 ] + [ "${lines[-1]}" = patch ] +} + +@test "the label of the pull request wins over the default bump" { + EVENT=push SHA=abc DEFAULT_BUMP=patch MERGED_PR='{"number":9,"labels":["minor"]}' run "$SCRIPT" + [ "$status" -eq 0 ] + [ "${lines[-1]}" = minor ] +} + +@test "an invalid default bump fails" { + EVENT=push SHA=abc DEFAULT_BUMP=huge run "$SCRIPT" + [ "$status" -eq 1 ] +}