diff --git a/actions/validate-release-consumer/action.yml b/actions/validate-release-consumer/action.yml new file mode 100644 index 0000000..7bee24f --- /dev/null +++ b/actions/validate-release-consumer/action.yml @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Validate release consumer +description: Validate a release-tool consumer configuration and expose normalized release metadata. + +inputs: + config-path: + description: Consumer release configuration path. + required: false + default: .nextcloud-release.yml + root: + description: Consumer repository root. + required: false + default: . + ref: + description: Git ref to inspect. + required: false + default: HEAD + +outputs: + version: + description: Current application version. + value: ${{ steps.validate.outputs.version }} + major: + description: Current application major version. + value: ${{ steps.validate.outputs.major }} + development: + description: Whether the current application version is a development version. + value: ${{ steps.validate.outputs.development }} + changelog-path: + description: Per-major changelog path selected by release-tool. + value: ${{ steps.validate.outputs.changelog-path }} + tool-version: + description: Exact release-tool version used for validation. + value: ${{ steps.setup.outputs.version }} + +runs: + using: composite + steps: + - id: setup + name: Setup release-tool + uses: $/actions/setup-release-tool + + - id: validate + name: Validate release consumer + shell: bash + env: + RELEASE_TOOL_PATH: ${{ steps.setup.outputs.path }} + RELEASE_CONFIG_PATH: ${{ inputs.config-path }} + RELEASE_ROOT: ${{ inputs.root }} + RELEASE_REF: ${{ inputs.ref }} + run: bash "${{ github.action_path }}/validate.sh" diff --git a/actions/validate-release-consumer/validate.sh b/actions/validate-release-consumer/validate.sh new file mode 100644 index 0000000..260fe4e --- /dev/null +++ b/actions/validate-release-consumer/validate.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -euo pipefail + +: "${RELEASE_TOOL_PATH:?release-tool path is required}" +: "${RELEASE_CONFIG_PATH:?release config path is required}" +: "${RELEASE_ROOT:?release root is required}" +: "${RELEASE_REF:?release ref is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +php "${RELEASE_TOOL_PATH}" config:validate --config "${RELEASE_CONFIG_PATH}" --root "${RELEASE_ROOT}" + +metadata_file="${RUNNER_TEMP:-/tmp}/release-consumer-metadata-${RANDOM}-${RANDOM}.json" +trap 'rm -f "${metadata_file}"' EXIT + +php "${RELEASE_TOOL_PATH}" metadata:inspect --config "${RELEASE_CONFIG_PATH}" --root "${RELEASE_ROOT}" --ref "${RELEASE_REF}" --json > "${metadata_file}" + +php -r ' +$metadata = json_decode(file_get_contents($argv[1]), true, 512, JSON_THROW_ON_ERROR); +foreach (["version", "major", "development", "changelog_path"] as $key) { + if (!array_key_exists($key, $metadata)) { + fwrite(STDERR, "Missing metadata field: {$key}\n"); + exit(2); + } +} +printf("version=%s\n", $metadata["version"]); +printf("major=%s\n", $metadata["major"]); +printf("development=%s\n", $metadata["development"] ? "true" : "false"); +printf("changelog-path=%s\n", $metadata["changelog_path"]); +' "${metadata_file}" >> "${GITHUB_OUTPUT}" diff --git a/tests/test_validate_release_consumer_action.py b/tests/test_validate_release_consumer_action.py new file mode 100644 index 0000000..1c440da --- /dev/null +++ b/tests/test_validate_release_consumer_action.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +ACTION = ROOT / "actions" / "validate-release-consumer" / "action.yml" +SCRIPT = ROOT / "actions" / "validate-release-consumer" / "validate.sh" + + +class ValidateReleaseConsumerActionTest(unittest.TestCase): + def test_action_wraps_setup_and_exposes_normalized_outputs(self) -> None: + content = ACTION.read_text(encoding="utf-8") + + self.assertIn("uses: $/actions/setup-release-tool", content) + for output in ( + "version:", + "major:", + "development:", + "changelog-path:", + "tool-version:", + ): + self.assertIn(output, content) + + def test_script_validates_config_and_metadata_once(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + fake_bin = root / "bin" + fake_bin.mkdir() + output = root / "output" + calls = root / "calls" + + php = fake_bin / "php" + php.write_text( + "#!/usr/bin/env bash\n" + "set -euo pipefail\n" + "printf '%s\\n' \"$*\" >> \"${CALLS_FILE}\"\n" + "if [[ \"$*\" == *'config:validate'* ]]; then exit 0; fi\n" + "if [[ \"$*\" == *'metadata:inspect'* ]]; then\n" + " cat <<'JSON'\n" + "{\"schema\":1,\"valid\":true,\"version\":\"16.0.0-dev.2\",\"major\":16,\"development\":true,\"changelog_path\":\"docs/changelogs/changelog-16.md\"}\n" + "JSON\n" + " exit 0\n" + "fi\n" + "exec /usr/bin/php \"$@\"\n", + encoding="utf-8", + ) + php.chmod(0o755) + + env = os.environ | { + "PATH": str(fake_bin) + os.pathsep + os.environ["PATH"], + "CALLS_FILE": str(calls), + "RELEASE_TOOL_PATH": "/tmp/release-tool.phar", + "RELEASE_CONFIG_PATH": ".nextcloud-release.yml", + "RELEASE_ROOT": ".", + "RELEASE_REF": "HEAD", + "RUNNER_TEMP": str(root), + "GITHUB_OUTPUT": str(output), + } + + result = subprocess.run( + ["bash", str(SCRIPT)], + env=env, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(0, result.returncode, result.stderr) + called = calls.read_text(encoding="utf-8") + self.assertIn("config:validate", called) + self.assertIn("metadata:inspect", called) + + outputs = output.read_text(encoding="utf-8") + self.assertIn("version=16.0.0-dev.2", outputs) + self.assertIn("major=16", outputs) + self.assertIn("development=true", outputs) + self.assertIn( + "changelog-path=docs/changelogs/changelog-16.md", + outputs, + ) + + +if __name__ == "__main__": + unittest.main()