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
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ jobs:
- name: Test downstream wait
run: bash downstream/wait.test.bash

version-bump:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Test version-bump
run: bash version-bump/version-bump.test.bash

setup-extension:
runs-on: ubuntu-latest
steps:
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/version-bump-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Reusable workflow: posts a Slack alert when a plugin's composer.json `version` is bumped
# (an early "a release is being prepared" signal). Only a strict increase notifies — no-op
# edits, downgrades and reverts stay silent. Add a caller to the plugin repo, e.g.:
#
# name: Release prep alert
# on:
# pull_request:
# types: [opened, reopened, synchronize]
# branches: [main, master, trunk] # your release branch(es)
# paths: ['composer.json']
# push:
# branches: [main, master, trunk]
# paths: ['composer.json']
# jobs:
# notify:
# uses: shopware/github-actions/.github/workflows/version-bump-notify.yml@main
# secrets:
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # secret stays in the plugin repo
name: Version Bump Notify
on:
workflow_call:
inputs:
path:
description: "Path to the extension root containing composer.json"
required: false
type: string
default: "."
product-name:
description: "Display name used in the Slack message (defaults to the repository name)"
required: false
type: string
default: ""
secrets:
SLACK_WEBHOOK_URL:
description: "Slack incoming webhook URL to post the release-prep alert to"
required: true

permissions:
contents: read

jobs:
run:
name: Notify on version bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Resolve refs
id: refs
run: |
echo "old=${{ github.event.before || github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
echo "new=${{ github.event.after || github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
- name: Detect version bump
id: detect
uses: shopware/github-actions/version-bump@main
with:
old-ref: ${{ steps.refs.outputs.old }}
new-ref: ${{ steps.refs.outputs.new }}
path: ${{ inputs.path }}
- name: Notify Slack
if: steps.detect.outputs.bumped == 'true'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
PRODUCT_NAME: ${{ inputs.product-name || github.event.repository.name }}
PREVIOUS_VERSION: ${{ steps.detect.outputs.previous-version }}
CURRENT_VERSION: ${{ steps.detect.outputs.current-version }}
ACTOR: ${{ github.actor }}
LINK_URL: ${{ github.event.pull_request.html_url || github.event.compare || format('{0}/{1}/commit/{2}', github.server_url, github.repository, github.sha) }}
run: |
if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then
echo "::warning::SLACK_WEBHOOK_URL is empty (e.g. a fork PR, or the secret is not configured) — skipping notification."
exit 0
fi
# Build the payload with jq -n so names/URLs can never break out of the JSON.
payload=$(jq -n \
--arg product "${PRODUCT_NAME}" \
--arg prev "${PREVIOUS_VERSION}" \
--arg cur "${CURRENT_VERSION}" \
--arg actor "${ACTOR}" \
--arg url "${LINK_URL}" \
'
({
type: "section",
text: {
type: "mrkdwn",
text: "*\($product)* is preparing a release\n`composer.json` version bumped *\($prev)* → *\($cur)*"
}
} | if $url != "" then .accessory = {
type: "button",
text: { type: "plain_text", text: "View change" },
url: $url
} else . end) as $section
| {
text: "\($product) is preparing a release: \($prev) → \($cur)",
blocks: [
$section,
{ type: "context", elements: [ { type: "mrkdwn", text: "Triggered by *\($actor)*" } ] }
]
}
')
curl -sS -f -X POST -H 'Content-type: application/json' --data "${payload}" "${SLACK_WEBHOOK_URL}"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ A collection of reusable GitHub Actions and Workflows for Shopware extensions an
|--------|-------------|------|
| [saas-preview-environment](saas-preview-environment/) | Creates, migrates or archives a SaaS Preview Environment | [README](saas-preview-environment/README.md) |

### Notifications

| Action | Description | Link |
|--------|-------------|------|
| [version-bump](version-bump/) | Detects a `composer.json` version increase between two refs | [README](version-bump/README.md) |

## Usage

All actions follow the standard GitHub Actions format:
Expand Down
46 changes: 46 additions & 0 deletions version-bump/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Version Bump

Detects whether the root `version` field in `composer.json` strictly increased between two git references.

## What it does

1. Reads the `version` field from `composer.json` at `old-ref` and at `new-ref`
2. Reports `bumped: true` only when the version strictly increased (version-aware compare, handles 3- and 4-segment versions and treats a pre-release like `1.0.0-rc1` as lower than the final `1.0.0`)
3. Ignores no-op edits, downgrades/reverts, and cases where the version cannot be read (e.g. a newly created branch)

## Inputs

| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `old-ref` | The git ref/sha for the previous state (e.g. the PR base sha) | Yes | - |
| `new-ref` | The git ref/sha for the new state (e.g. the PR head sha) | Yes | - |
| `path` | Path to the extension root containing `composer.json` | No | `.` |

## Outputs

| Output | Description |
|--------|-------------|
| `bumped` | `'true'` if the version strictly increased, otherwise `'false'` |
| `previous-version` | The version at `old-ref` (empty if not found) |
| `current-version` | The version at `new-ref` (empty if not found) |

> **Note:** the caller must check out the repository with `fetch-depth: 0` so both refs are available to `git show`.
Comment thread
Copilot marked this conversation as resolved.
>
> **Note:** this action requires `jq` and GNU `sort` (for `sort -V`) to be available on the runner.

## Usage

```yaml
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: shopware/github-actions/version-bump@main
id: detect
with:
old-ref: ${{ github.event.pull_request.base.sha }}
new-ref: ${{ github.event.pull_request.head.sha }}
- if: steps.detect.outputs.bumped == 'true'
run: echo "Version bumped ${{ steps.detect.outputs.previous-version }} -> ${{ steps.detect.outputs.current-version }}"
```

For the ready-made Slack notification, use the [`version-bump-notify`](../.github/workflows/version-bump-notify.yml) reusable workflow instead of wiring this action up yourself.
41 changes: 41 additions & 0 deletions version-bump/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: version-bump
description: "Detects whether the root version field in composer.json strictly increased between two git refs."
author: "shopware AG"
branding:
color: "yellow"
icon: "bell"

inputs:
old-ref:
description: The git ref/sha for the previous state (e.g. the PR base sha)
required: true
new-ref:
description: The git ref/sha for the new state (e.g. the PR head sha)
required: true
path:
description: Path to the extension root containing composer.json
required: false
default: "."

outputs:
bumped:
description: "'true' if the composer.json version strictly increased, otherwise 'false'"
value: ${{ steps.detect.outputs.bumped }}
previous-version:
description: The version at old-ref (empty if not found)
value: ${{ steps.detect.outputs.previous-version }}
current-version:
description: The version at new-ref (empty if not found)
value: ${{ steps.detect.outputs.current-version }}

runs:
using: "composite"
steps:
- name: Detect version bump
id: detect
shell: bash
env:
OLD_REF: "${{ inputs.old-ref }}"
NEW_REF: "${{ inputs.new-ref }}"
EXT_PATH: "${{ inputs.path }}"
run: ${GITHUB_ACTION_PATH}/version-bump.bash
82 changes: 82 additions & 0 deletions version-bump/version-bump.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash

# Detects whether the root `version` field in composer.json strictly increased
# between two git references.
#
# Global environment variables required:
# OLD_REF - git ref/sha for the previous state (e.g. the PR base sha)
# NEW_REF - git ref/sha for the new state (e.g. the PR head sha)
# EXT_PATH - path to the extension root containing composer.json (default: .)

set -euo pipefail

EXT_PATH="${EXT_PATH:-.}"
if [[ "${EXT_PATH}" == "." || -z "${EXT_PATH}" ]]; then
COMPOSER_PATH="composer.json"
else
COMPOSER_PATH="${EXT_PATH%/}/composer.json"
fi

if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but was not found on PATH" >&2
exit 1
fi

read_version() {
# Missing ref/file -> empty (no notification); a parse error propagates (fail loud).
local json
if ! json=$(git show "${1}:${COMPOSER_PATH}" 2>/dev/null); then
return 0
fi
jq -r '.version // empty' <<<"${json}"
}

OLD_VERSION=$(read_version "${OLD_REF}")
NEW_VERSION=$(read_version "${NEW_REF}")

version_gt() {
local a="${1#v}" b="${2#v}"
local a_core="${a%%-*}" b_core="${b%%-*}"
local a_pre="" b_pre=""
if [[ "${a}" == *-* ]]; then a_pre="${a#*-}"; fi
if [[ "${b}" == *-* ]]; then b_pre="${b#*-}"; fi

if [[ "${a_core}" != "${b_core}" ]]; then
if [[ "$(printf '%s\n%s\n' "${a_core}" "${b_core}" | sort -V | tail -n1)" == "${b_core}" ]]; then
return 0
fi
return 1
fi

if [[ "${a_pre}" == "${b_pre}" ]]; then return 1; fi # identical version
if [[ -z "${b_pre}" ]]; then return 0; fi # equal core: b is final, a is pre-release
if [[ -z "${a_pre}" ]]; then return 1; fi # equal core: a is final, b is pre-release
if [[ "$(printf '%s\n%s\n' "${a_pre}" "${b_pre}" | sort -V | tail -n1)" == "${b_pre}" ]]; then
return 0
fi
return 1
}

bumped=false
if [[ -n "${OLD_VERSION}" && -n "${NEW_VERSION}" ]] && version_gt "${OLD_VERSION}" "${NEW_VERSION}"; then
bumped=true
fi

echo "Previous version: '${OLD_VERSION:-<none>}'"
echo "Current version: '${NEW_VERSION:-<none>}'"
echo "Bumped: ${bumped}"

# Multiline delimiter form so a newline in a value can't forge extra output entries.
delim="__VERSION_BUMP_EOF_${RANDOM}_${RANDOM}__"
while [[ "${OLD_VERSION}${NEW_VERSION}" == *"${delim}"* ]]; do
delim="__VERSION_BUMP_EOF_${RANDOM}_${RANDOM}__"
done
{
echo "bumped=${bumped}"
echo "previous-version<<${delim}"
printf '%s\n' "${OLD_VERSION}"
echo "${delim}"
echo "current-version<<${delim}"
printf '%s\n' "${NEW_VERSION}"
echo "${delim}"
} >>"${GITHUB_OUTPUT}"
93 changes: 93 additions & 0 deletions version-bump/version-bump.test.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
TMP_DIR=$(mktemp -d)

trap 'rm -rf "${TMP_DIR}"' EXIT

# Stub `git show <ref>:<path>` -> composer.json fixture for that ref (STUB_OLD/STUB_NEW; empty = missing).
cat > "${TMP_DIR}/git" <<'EOF'
#!/usr/bin/env bash
if [[ "${1}" == "show" ]]; then
ref="${2%%:*}"
case "${ref}" in
old) content="${STUB_OLD-}" ;;
new) content="${STUB_NEW-}" ;;
*) content="" ;;
esac
if [[ -z "${content}" ]]; then
exit 128
fi
printf '%s' "${content}"
exit 0
fi
exit 1
EOF
chmod +x "${TMP_DIR}/git"

run_case() {
local name="${1}"
local old_json="${2}"
local new_json="${3}"
local expected_bumped="${4}"
local out
out=$(mktemp)

local output code
set +e
output=$(PATH="${TMP_DIR}:${PATH}" \
STUB_OLD="${old_json}" \
STUB_NEW="${new_json}" \
OLD_REF=old NEW_REF=new EXT_PATH=. \
GITHUB_OUTPUT="${out}" \
bash "${SCRIPT_DIR}/version-bump.bash" 2>&1)
code=$?
set -e

if [[ "${code}" -ne 0 ]]; then
echo "FAIL ${name}: version-bump.bash exited with ${code}"
echo "${output}"
cat "${out}"
rm -f "${out}"
exit 1
fi

local got
got=$(grep -m1 '^bumped=' "${out}" | cut -d= -f2 || true)
if [[ -z "${got}" ]]; then
echo "FAIL ${name}: did not write bumped=... to GITHUB_OUTPUT"
echo "${output}"
cat "${out}"
rm -f "${out}"
exit 1
fi
if [[ "${got}" != "${expected_bumped}" ]]; then
echo "FAIL ${name}: expected bumped=${expected_bumped}, got '${got}'"
echo "${output}"
cat "${out}"
rm -f "${out}"
exit 1
fi

echo "ok - ${name} (bumped=${got})"
rm -f "${out}"
}

run_case "minor bump" '{"version":"5.3.0"}' '{"version":"5.4.0"}' true
run_case "patch bump" '{"version":"5.3.0"}' '{"version":"5.3.1"}' true
run_case "no change" '{"version":"5.3.0"}' '{"version":"5.3.0"}' false
run_case "downgrade" '{"version":"5.4.0"}' '{"version":"5.3.0"}' false
run_case "four-segment bump" '{"version":"6.6.10.0"}' '{"version":"6.6.10.1"}' true
run_case "version field removed" '{"version":"5.3.0"}' '{"name":"swag/foo"}' false
run_case "version field added" '{"name":"swag/foo"}' '{"version":"5.3.0"}' false
run_case "missing old ref" '' '{"version":"5.3.0"}' false
run_case "leading v prefix" '{"version":"v1.0.0"}' '{"version":"v1.1.0"}' true
run_case "v prefix only" '{"version":"v1.0.0"}' '{"version":"1.0.0"}' false
run_case "rc to final" '{"version":"1.0.0-rc1"}' '{"version":"1.0.0"}' true
run_case "final to rc" '{"version":"1.0.0"}' '{"version":"1.0.0-rc1"}' false
run_case "rc to newer rc" '{"version":"1.0.0-rc1"}' '{"version":"1.0.0-rc2"}' true
run_case "rc to older rc" '{"version":"1.0.0-rc2"}' '{"version":"1.0.0-rc1"}' false
run_case "final to next rc" '{"version":"1.0.0"}' '{"version":"1.1.0-rc1"}' true

echo "All version-bump tests passed."
Loading