Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions .github/workflows/chart-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,30 @@ jobs:
name: Semver label
runs-on: ubuntu-latest
permissions:
contents: read # check out the label script
pull-requests: write
outputs:
bump: ${{ steps.label.outputs.bump }}
steps:
- 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 label script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: chart-workflows
persist-credentials: false

# The labels the pull request has now, not the ones of the event that started the run: a pull
# request opened with a label (gh pr create --label) gets it right after the `opened` event, and
# the `labeled` event of the workflow token starts no run. Reading them now also lets a re-run
Expand All @@ -36,18 +56,13 @@ jobs:
id: label
env:
GH_TOKEN: ${{ github.token }}
EVENT: pull_request
PR: ${{ github.event.pull_request.number }}
run: |
LABELS=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq '[.labels[].name]')
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"
run: chart-workflows/scripts/semver-label.sh

# only when the label is the problem: a failure of the GitHub API is not the author's to fix
- name: Ask for the label
if: failure() && steps.label.outcome == 'failure'
if: failure() && steps.label.outputs.label-missing == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/semver-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ jobs:
DEFAULT_BUMP: ${{ inputs.default-bump }}
run: ../semver-workflows/scripts/semver-label.sh

# only when the label is the problem: a failure of the GitHub API is not the author's to fix
- name: Ask for the label
if: failure() && steps.label.outcome == 'failure' && github.event_name == 'pull_request'
if: failure() && steps.label.outputs.label-missing == 'true' && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:

For repositories that release Helm charts from a `charts/` directory, with the version of each chart driven by the semver label of the pull request that changes it. On every pull request it:

1. requires exactly one of the labels `major`, `minor` or `patch`, and comments on the pull request when it is missing. The labels are read when the job runs, not from the event that started it, so a label added right after the pull request was opened counts, and re-running the job picks up a label added later;
1. requires exactly one of the labels `major`, `minor` or `patch`, and comments on the pull request when it is missing. The labels are read when the job runs, not from the event that started it, so a label added right after the pull request was opened counts, and re-running the job picks up a label added later. The call to the GitHub API is retried, and when it keeps failing the job fails without asking for the label;
2. bumps the `version` of every chart the pull request changes: its version on the base branch, bumped by the label. A chart that is not on the base branch yet keeps its version;
3. commits the new versions ("Update helm chart versions") to the pull request branch. A pull request from a fork, which the workflow token cannot push to, fails instead, listing the versions its author has to set.

Expand Down
46 changes: 38 additions & 8 deletions scripts/semver-label.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,75 @@
# 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.
#
# The GitHub API calls are retried: a transient error answers with an HTML page or a 5xx. When they
# keep failing, the script exits with 2, and with 1 when the label is the problem; only then it also
# writes label-missing=true, so that the caller asks for the label only when it is missing.
#
# 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=<bump>
# RETRIES attempts of each GitHub API call (default: 3)
# RETRY_DELAY seconds between them (default: 5)
# GITHUB_OUTPUT when set, receives bump=<bump>, or label-missing=true

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}"
RETRIES="${RETRIES:-3}"
RETRY_DELAY="${RETRY_DELAY:-5}"

case "$DEFAULT_BUMP" in
""|major|minor|patch) ;;
*) echo "::error::DEFAULT_BUMP must be major, minor or patch, got '$DEFAULT_BUMP'" >&2; exit 1 ;;
esac

# api <gh api arguments>: prints the response, retrying the call
api() {
local attempt response
for ((attempt = 1; attempt <= RETRIES; attempt++)); do
if response=$("$GH" api "$@"); then
printf '%s\n' "$response"
return 0
fi
if [ "$attempt" -lt "$RETRIES" ]; then
echo "The GitHub API call failed (attempt $attempt of $RETRIES), retrying in ${RETRY_DELAY}s" >&2
sleep "$RETRY_DELAY"
fi
done
echo "::error::The GitHub API call failed $RETRIES times, so the semver label could not be read" >&2
return 1
}

# label_missing <message>: fails because of the label itself
label_missing() {
echo "::error::$1" >&2
echo "label-missing=true" >> "${GITHUB_OUTPUT:-/dev/null}"
exit 1
}

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]')
labels=$(api "repos/$REPOSITORY/pulls/$PR" --jq '[.labels[].name]') || exit 2
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]}')
pr=$(api "repos/$REPOSITORY/commits/$SHA/pulls" \
--jq '[.[] | select(.merged_at != null)][0] // empty | {number, labels: [.labels[].name]}') || exit 2
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
label_missing "No merged pull request contains $SHA, so there is no semver label to release it with"
fi
echo "Releasing pull request #$(jq -r .number <<< "$pr")" >&2
labels=$(jq -c .labels <<< "$pr")
Expand All @@ -56,8 +87,7 @@ if [ "$(wc -w <<< "$bumps")" -ne 1 ]; 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
label_missing "The pull request needs exactly one of the labels major, minor or patch (found: ${bumps:-none})"
fi
fi

Expand Down
41 changes: 38 additions & 3 deletions tests/semver-label.bats
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/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).
# and $MERGED_PR the merged pull request a pushed commit comes from (empty for none). The first
# $API_FAILURES calls fail the way gh does when GitHub answers with an error page.

load helpers

Expand All @@ -10,12 +11,18 @@ setup() {
mkdir -p "$BATS_TEST_TMPDIR/repo" && cd "$BATS_TEST_TMPDIR/repo"
fake_command gh
cat > "$BATS_TEST_TMPDIR/gh.sh" <<'EOF'
calls=$(($(cat "$BATS_TEST_TMPDIR/gh.calls" 2>/dev/null || echo 0) + 1))
echo "$calls" > "$BATS_TEST_TMPDIR/gh.calls"
if [ "$calls" -le "${API_FAILURES:-0}" ]; then
echo "invalid character '<' looking for beginning of value" >&2
exit 1
fi
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"
export REPOSITORY=owner/repo GITHUB_OUTPUT="$BATS_TEST_TMPDIR/output" RETRY_DELAY=0
: > "$GITHUB_OUTPUT"
}

Expand All @@ -27,10 +34,11 @@ EOF
grep -q "api repos/owner/repo/pulls/7" "$BATS_TEST_TMPDIR/gh.log"
}

@test "a pull request without a semver label fails" {
@test "a pull request without a semver label fails, saying the label is missing" {
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)"* ]]
grep -qx "label-missing=true" "$GITHUB_OUTPUT"
}

@test "a pull request with two semver labels fails" {
Expand Down Expand Up @@ -80,3 +88,30 @@ EOF
EVENT=push SHA=abc DEFAULT_BUMP=huge run "$SCRIPT"
[ "$status" -eq 1 ]
}

@test "a failed API call is retried" {
EVENT=pull_request PR=7 PR_LABELS='["patch"]' API_FAILURES=2 run "$SCRIPT"
[ "$status" -eq 0 ]
[ "${lines[-1]}" = patch ]
[ "$(grep -c "api repos/owner/repo/pulls/7" "$BATS_TEST_TMPDIR/gh.log")" -eq 3 ]
[[ "$output" == *"retrying"* ]]
}

@test "an API that keeps failing fails without blaming the label" {
EVENT=pull_request PR=7 PR_LABELS='["patch"]' API_FAILURES=3 run "$SCRIPT"
[ "$status" -eq 2 ]
[[ "$output" == *"The GitHub API call failed 3 times"* ]]
! grep -q "label-missing" "$GITHUB_OUTPUT"
}

@test "the merged pull request of a push is read with retries too" {
EVENT=push SHA=abc MERGED_PR='{"number":9,"labels":["minor"]}' API_FAILURES=1 run "$SCRIPT"
[ "$status" -eq 0 ]
[ "${lines[-1]}" = minor ]
}

@test "a push that no merged pull request carries says the label is missing" {
EVENT=push SHA=abc run "$SCRIPT"
[ "$status" -eq 1 ]
grep -qx "label-missing=true" "$GITHUB_OUTPUT"
}
Loading