diff --git a/.github/workflows/version-bump-notify.yml b/.github/workflows/version-bump-notify.yml index 8dc425e..9f2f36a 100644 --- a/.github/workflows/version-bump-notify.yml +++ b/.github/workflows/version-bump-notify.yml @@ -1,27 +1,28 @@ -# 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.: +# Reusable workflow: posts a Slack alert when an extension's version is bumped — an early +# "a release is being prepared" signal. Works for plugins (composer.json) and apps +# (manifest.xml). Only a strict increase notifies — no-op edits, downgrades and reverts stay +# silent. Add a caller to the extension repo, using the version file it ships: # # name: Release prep alert # on: # pull_request: # types: [opened, reopened, synchronize] # branches: [main, master, trunk] # your release branch(es) -# paths: ['composer.json'] +# paths: ['composer.json'] # apps: ['manifest.xml'] # push: # branches: [main, master, trunk] -# paths: ['composer.json'] +# paths: ['composer.json'] # apps: ['manifest.xml'] # 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 +# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # secret stays in the extension repo name: Version Bump Notify on: workflow_call: inputs: path: - description: "Path to the extension root containing composer.json" + description: "Path to the extension root containing composer.json (plugin) or manifest.xml (app)" required: false type: string default: "." @@ -84,7 +85,7 @@ jobs: type: "section", text: { type: "mrkdwn", - text: "*\($product)* is preparing a release\n`composer.json` version bumped *\($prev)* → *\($cur)*" + text: "*\($product)* is preparing a release\nversion bumped *\($prev)* → *\($cur)*" } } | if $url != "" then .accessory = { type: "button", diff --git a/version-bump/README.md b/version-bump/README.md index 0d340fa..e09bf17 100644 --- a/version-bump/README.md +++ b/version-bump/README.md @@ -1,10 +1,10 @@ # Version Bump -Detects whether the root `version` field in `composer.json` strictly increased between two git references. +Detects whether an extension's `version` strictly increased between two git references. Works for plugins (`composer.json`) and apps (`manifest.xml`). ## What it does -1. Reads the `version` field from `composer.json` at `old-ref` and at `new-ref` +1. Reads the version at `old-ref` and at `new-ref` — from `composer.json` (`.version`) for plugins, or from `manifest.xml` (`/manifest/meta/version`) for apps. `composer.json` takes precedence when both are present. 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) @@ -14,7 +14,7 @@ Detects whether the root `version` field in `composer.json` strictly increased b |-------|-------------|----------|---------| | `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 | `.` | +| `path` | Path to the extension root containing `composer.json` (plugin) or `manifest.xml` (app) | No | `.` | ## Outputs @@ -26,7 +26,7 @@ Detects whether the root `version` field in `composer.json` strictly increased b > **Note:** the caller must check out the repository with `fetch-depth: 0` so both refs are available to `git show`. > -> **Note:** this action requires `jq` and GNU `sort` (for `sort -V`) to be available on the runner. +> **Note:** this action requires `jq` and GNU `sort` (for `sort -V`) on the runner; apps additionally need `python3` (preinstalled on GitHub-hosted runners) to read `manifest.xml`. ## Usage diff --git a/version-bump/action.yml b/version-bump/action.yml index aa6e9d4..6d0653e 100644 --- a/version-bump/action.yml +++ b/version-bump/action.yml @@ -1,5 +1,5 @@ name: version-bump -description: "Detects whether the root version field in composer.json strictly increased between two git refs." +description: "Detects whether an extension's version strictly increased between two git refs (composer.json for plugins, manifest.xml for apps)." author: "shopware AG" branding: color: "yellow" @@ -13,13 +13,13 @@ inputs: 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 + description: Path to the extension root containing composer.json (plugin) or manifest.xml (app) required: false default: "." outputs: bumped: - description: "'true' if the composer.json version strictly increased, otherwise 'false'" + description: "'true' if the extension version strictly increased, otherwise 'false'" value: ${{ steps.detect.outputs.bumped }} previous-version: description: The version at old-ref (empty if not found) diff --git a/version-bump/version-bump.bash b/version-bump/version-bump.bash index bfce9ea..ba70d17 100755 --- a/version-bump/version-bump.bash +++ b/version-bump/version-bump.bash @@ -1,21 +1,23 @@ #!/usr/bin/env bash -# Detects whether the root `version` field in composer.json strictly increased -# between two git references. +# Detects whether the extension version strictly increased between two git references. +# Plugins carry the version in composer.json, apps in manifest.xml (). # # 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: .) +# EXT_PATH - path to the extension root containing composer.json or manifest.xml (default: .) set -euo pipefail EXT_PATH="${EXT_PATH:-.}" if [[ "${EXT_PATH}" == "." || -z "${EXT_PATH}" ]]; then - COMPOSER_PATH="composer.json" + prefix="" else - COMPOSER_PATH="${EXT_PATH%/}/composer.json" + prefix="${EXT_PATH%/}/" fi +COMPOSER_PATH="${prefix}composer.json" +MANIFEST_PATH="${prefix}manifest.xml" if ! command -v jq >/dev/null 2>&1; then echo "Error: jq is required but was not found on PATH" >&2 @@ -24,11 +26,29 @@ 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 + # composer.json (plugin) takes precedence over manifest.xml (app) when both exist. + local ref="${1}" content + if content=$(git show "${ref}:${COMPOSER_PATH}" 2>/dev/null); then + jq -r '.version // empty' <<<"${content}" return 0 fi - jq -r '.version // empty' <<<"${json}" + if content=$(git show "${ref}:${MANIFEST_PATH}" 2>/dev/null); then + if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required to read manifest.xml but was not found on PATH" >&2 + exit 1 + fi + # Apps keep the version in ; there is no default namespace, + # so a plain element path works. A missing node prints empty (no notification); a + # malformed manifest raises and propagates (fail loud), matching the composer.json path. + printf '%s' "${content}" | python3 -c ' +import sys, xml.etree.ElementTree as ET +root = ET.fromstring(sys.stdin.read()) +el = root.find("./meta/version") +sys.stdout.write((el.text or "").strip() if el is not None else "") +' + return 0 + fi + return 0 } OLD_VERSION=$(read_version "${OLD_REF}") diff --git a/version-bump/version-bump.test.bash b/version-bump/version-bump.test.bash index 6785362..3b7efc8 100644 --- a/version-bump/version-bump.test.bash +++ b/version-bump/version-bump.test.bash @@ -6,15 +6,21 @@ TMP_DIR=$(mktemp -d) trap 'rm -rf "${TMP_DIR}"' EXIT -# Stub `git show :` -> composer.json fixture for that ref (STUB_OLD/STUB_NEW; empty = missing). +# Stub `git show :` -> fixture keyed on both ref and file: +# composer.json fixtures come from STUB_OLD/STUB_NEW, manifest.xml from STUB_OLD_XML/STUB_NEW_XML +# (empty = missing file at that ref -> exit 128, so the script falls through / reports empty). 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="" ;; + spec="${2}" + ref="${spec%%:*}" + file="${spec#*:}" + content="" + case "${file}" in + *composer.json) + case "${ref}" in old) content="${STUB_OLD-}" ;; new) content="${STUB_NEW-}" ;; esac ;; + *manifest.xml) + case "${ref}" in old) content="${STUB_OLD_XML-}" ;; new) content="${STUB_NEW_XML-}" ;; esac ;; esac if [[ -z "${content}" ]]; then exit 128 @@ -26,54 +32,64 @@ 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 +# Asserts the script wrote `bumped=` to GITHUB_OUTPUT. Args: name code output out expected. +_assert_bumped() { + local name="${1}" code="${2}" output="${3}" out="${4}" expected="${5}" if [[ "${code}" -ne 0 ]]; then echo "FAIL ${name}: version-bump.bash exited with ${code}" - echo "${output}" - cat "${out}" - rm -f "${out}" - exit 1 + 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 + 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 + if [[ "${got}" != "${expected}" ]]; then + echo "FAIL ${name}: expected bumped=${expected}, got '${got}'" + echo "${output}"; cat "${out}"; rm -f "${out}"; exit 1 fi echo "ok - ${name} (bumped=${got})" rm -f "${out}" } +# Plugin case: composer.json fixtures at old/new. +run_case() { + local name="${1}" old_json="${2}" new_json="${3}" 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 + _assert_bumped "${name}" "${code}" "${output}" "${out}" "${expected_bumped}" +} + +# App case: manifest.xml fixtures at old/new (no composer.json present -> app path taken). +run_case_app() { + local name="${1}" old_xml="${2}" new_xml="${3}" expected_bumped="${4}" + local out; out=$(mktemp) + local output code + set +e + output=$(PATH="${TMP_DIR}:${PATH}" \ + STUB_OLD_XML="${old_xml}" \ + STUB_NEW_XML="${new_xml}" \ + OLD_REF=old NEW_REF=new EXT_PATH=. \ + GITHUB_OUTPUT="${out}" \ + bash "${SCRIPT_DIR}/version-bump.bash" 2>&1) + code=$? + set -e + _assert_bumped "${name}" "${code}" "${output}" "${out}" "${expected_bumped}" +} + 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 @@ -90,4 +106,33 @@ run_case "rc to newer rc" '{"version":"1.0.0-rc1"}' '{"version":"1.0.0-rc 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 +# --- app (manifest.xml) cases --- +manifest() { printf 'swag/foo%s' "${1}"; } +manifest_nover='swag/foo' + +run_case_app "app minor bump" "$(manifest 1.0.0)" "$(manifest 1.1.0)" true +run_case_app "app patch bump" "$(manifest 4.0.0)" "$(manifest 4.0.1)" true +run_case_app "app no change" "$(manifest 3.1.0)" "$(manifest 3.1.0)" false +run_case_app "app downgrade" "$(manifest 4.0.0)" "$(manifest 3.9.9)" false +run_case_app "app version missing" "${manifest_nover}" "$(manifest 1.0.0)" false +run_case_app "app new manifest" '' "$(manifest 1.0.0)" false + +# real manifests carry xmlns:xsi / xsi:noNamespaceSchemaLocation on the root — must not break the XPath +run_case_app "app with xsi namespace attrs" \ + '1.0.0' \ + '1.1.0' \ + true + +# composer.json takes precedence when both files exist: no composer change -> false, despite a manifest bump +out=$(mktemp) +set +e +output=$(PATH="${TMP_DIR}:${PATH}" \ + STUB_OLD='{"version":"5.3.0"}' STUB_NEW='{"version":"5.3.0"}' \ + STUB_OLD_XML="$(manifest 1.0.0)" STUB_NEW_XML="$(manifest 2.0.0)" \ + OLD_REF=old NEW_REF=new EXT_PATH=. GITHUB_OUTPUT="${out}" \ + bash "${SCRIPT_DIR}/version-bump.bash" 2>&1) +code=$? +set -e +_assert_bumped "composer.json precedence over manifest.xml" "${code}" "${output}" "${out}" false + echo "All version-bump tests passed." \ No newline at end of file