From 062aa4800fa6ae14a829287f69c11e0173aca366 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:51:06 +0000
Subject: [PATCH 1/3] feat(build): inspect app metadata and package engines in
tested PHP service
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
src/Application/Build/BuildMetadata.php | 30 +++++
.../Build/BuildMetadataInspector.php | 81 ++++++++++++
.../Build/Port/BuildFileReader.php | 10 ++
.../Build/LocalBuildFileReader.php | 26 ++++
.../Build/BuildMetadataInspectorTest.php | 122 ++++++++++++++++++
5 files changed, 269 insertions(+)
create mode 100644 src/Application/Build/BuildMetadata.php
create mode 100644 src/Application/Build/BuildMetadataInspector.php
create mode 100644 src/Application/Build/Port/BuildFileReader.php
create mode 100644 src/Infrastructure/Build/LocalBuildFileReader.php
create mode 100644 tests/Unit/Application/Build/BuildMetadataInspectorTest.php
diff --git a/src/Application/Build/BuildMetadata.php b/src/Application/Build/BuildMetadata.php
new file mode 100644
index 0000000..a7ea6cd
--- /dev/null
+++ b/src/Application/Build/BuildMetadata.php
@@ -0,0 +1,30 @@
+ 1,
+ 'version' => $this->version,
+ 'nextcloud_min_version' => $this->nextcloudMinVersion,
+ 'node_version' => $this->nodeVersion,
+ 'npm_version' => $this->npmVersion,
+ 'has_package_json' => $this->hasPackageJson,
+ ];
+ }
+}
diff --git a/src/Application/Build/BuildMetadataInspector.php b/src/Application/Build/BuildMetadataInspector.php
new file mode 100644
index 0000000..49c095b
--- /dev/null
+++ b/src/Application/Build/BuildMetadataInspector.php
@@ -0,0 +1,81 @@
+reader->read($root, 'appinfo/info.xml');
+ if ($xmlContent === null) {
+ throw new DomainException('Missing appinfo/info.xml in application checkout.');
+ }
+
+ $xml = @simplexml_load_string($xmlContent, SimpleXMLElement::class, LIBXML_NONET);
+ if ($xml === false) {
+ throw new DomainException('Invalid appinfo/info.xml XML.');
+ }
+
+ $version = trim((string) ($xml->version ?? ''));
+ $nextcloud = $xml->dependencies->nextcloud ?? null;
+ $minimum = $nextcloud === null ? '' : trim((string) ($nextcloud['min-version'] ?? ''));
+ if ($version === '' || preg_match('/[\x00-\x1f\x7f]/', $version) === 1) {
+ throw new DomainException('appinfo/info.xml is missing a safe version.');
+ }
+ if (preg_match('/^[1-9][0-9]*$/', $minimum) !== 1) {
+ throw new DomainException('appinfo/info.xml is missing a numeric Nextcloud min-version.');
+ }
+
+ $json = $this->reader->read($root, 'package.json');
+ if ($json === null) {
+ return new BuildMetadata($version, $minimum, '', '', false);
+ }
+
+ try {
+ $package = json_decode($json, false, 512, JSON_THROW_ON_ERROR);
+ } catch (JsonException $exception) {
+ throw new DomainException('Invalid package.json JSON.', 0, $exception);
+ }
+ if (!$package instanceof \stdClass) {
+ throw new DomainException('package.json must be an object.');
+ }
+ $engines = $package->engines ?? new \stdClass();
+ if (!$engines instanceof \stdClass) {
+ throw new DomainException('package.json engines must be an object.');
+ }
+
+ return new BuildMetadata(
+ $version,
+ $minimum,
+ $this->engine($engines, 'node', '^24'),
+ $this->engine($engines, 'npm', '^11.3'),
+ true,
+ );
+ }
+
+ /** @param \stdClass $engines */
+ private function engine(\stdClass $engines, string $name, string $fallback): string
+ {
+ $value = $engines->{$name} ?? '';
+ if (!is_string($value)) {
+ throw new DomainException(sprintf('package.json engines.%s must be a string.', $name));
+ }
+ $value = trim($value);
+ if (strlen($value) > 128 || preg_match('/[\x00-\x1f\x7f]/', $value) === 1) {
+ throw new DomainException(sprintf('package.json engines.%s contains invalid control characters.', $name));
+ }
+
+ return $value !== '' ? $value : $fallback;
+ }
+}
diff --git a/src/Application/Build/Port/BuildFileReader.php b/src/Application/Build/Port/BuildFileReader.php
new file mode 100644
index 0000000..58778d7
--- /dev/null
+++ b/src/Application/Build/Port/BuildFileReader.php
@@ -0,0 +1,10 @@
+15.1.2';
+
+ public function testReadsVersionCompatibilityAndDeclaredEngines(): void
+ {
+ $result = $this->inspect([
+ 'appinfo/info.xml' => self::XML,
+ 'package.json' => '{"engines":{"node":">=22 <25","npm":"^11.4"}}',
+ ]);
+
+ self::assertSame('15.1.2', $result->version);
+ self::assertSame('35', $result->nextcloudMinVersion);
+ self::assertSame('>=22 <25', $result->nodeVersion);
+ self::assertSame('^11.4', $result->npmVersion);
+ self::assertTrue($result->hasPackageJson);
+ }
+
+ public function testMissingPackageJsonSkipsNodeBuild(): void
+ {
+ $result = $this->inspect(['appinfo/info.xml' => self::XML]);
+ self::assertFalse($result->hasPackageJson);
+ self::assertSame('', $result->nodeVersion);
+ self::assertSame('', $result->npmVersion);
+ }
+
+ public function testPresentPackageWithoutEnginesUsesFallbacks(): void
+ {
+ foreach (['{}', '{"engines":{}}', '{"engines":{"node":"","npm":""}}'] as $json) {
+ $result = $this->inspect(['appinfo/info.xml' => self::XML, 'package.json' => $json]);
+ self::assertTrue($result->hasPackageJson);
+ self::assertSame('^24', $result->nodeVersion);
+ self::assertSame('^11.3', $result->npmVersion);
+ }
+ }
+
+ public function testMissingXmlFails(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('Missing appinfo/info.xml');
+ $this->inspect([]);
+ }
+
+ public function testMalformedXmlFails(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('Invalid appinfo/info.xml');
+ $this->inspect(['appinfo/info.xml' => '1']);
+ }
+
+ public function testMissingCompatibilityFails(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('numeric Nextcloud min-version');
+ $this->inspect(['appinfo/info.xml' => '15.1.2']);
+ }
+
+ public function testMalformedPackageJsonFails(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('Invalid package.json');
+ $this->inspect(['appinfo/info.xml' => self::XML, 'package.json' => '{']);
+ }
+
+ public function testPackageJsonMustBeObject(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('package.json must be an object');
+ $this->inspect(['appinfo/info.xml' => self::XML, 'package.json' => '[]']);
+ }
+
+ public function testEnginesMustBeObject(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('engines must be an object');
+ $this->inspect(['appinfo/info.xml' => self::XML, 'package.json' => '{"engines":[]}']);
+ }
+
+ public function testEngineVersionMustBeString(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('engines.node must be a string');
+ $this->inspect(['appinfo/info.xml' => self::XML, 'package.json' => '{"engines":{"node":24}}']);
+ }
+
+ public function testNewlineInEngineIsRejectedForGithubOutputs(): void
+ {
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage('control characters');
+ $this->inspect(['appinfo/info.xml' => self::XML, 'package.json' => '{"engines":{"npm":"11.0\nmalicious=yes"}}']);
+ }
+
+ /** @param array $files */
+ private function inspect(array $files): BuildMetadata
+ {
+ $reader = new class($files) implements BuildFileReader {
+ /** @param array $files */
+ public function __construct(private array $files)
+ {
+ }
+
+ public function read(string $root, string $path): ?string
+ {
+ return $this->files[$path] ?? null;
+ }
+ };
+
+ return (new BuildMetadataInspector($reader))->inspect('.');
+ }
+}
From 7a370bdc8022b77d4ddf0178c30f8bc6c37f7387 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:51:17 +0000
Subject: [PATCH 2/3] feat(actions): expose build inspection and permission
adapters
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
actions/authorization-check/action.yml | 43 ++++++++++++
actions/build-inspect/action.yml | 49 +++++++++++++
bin/release-tool | 3 +
docs/github-actions.md | 7 ++
.../Console/ApplicationFactory.php | 7 ++
.../Console/Command/BuildInspectCommand.php | 68 +++++++++++++++++++
tests/Acceptance/ReleaseActionsTest.php | 12 +++-
.../Build/BuildInspectCommandTest.php | 67 ++++++++++++++++++
8 files changed, 255 insertions(+), 1 deletion(-)
create mode 100644 actions/authorization-check/action.yml
create mode 100644 actions/build-inspect/action.yml
create mode 100644 src/Application/Console/Command/BuildInspectCommand.php
create mode 100644 tests/Unit/Application/Build/BuildInspectCommandTest.php
diff --git a/actions/authorization-check/action.yml b/actions/authorization-check/action.yml
new file mode 100644
index 0000000..de395ca
--- /dev/null
+++ b/actions/authorization-check/action.yml
@@ -0,0 +1,43 @@
+# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+name: Check repository actor authorization
+description: Validate repository permission using the release-tool CLI.
+
+inputs:
+ repository:
+ description: Repository in owner/name form.
+ required: true
+ actor:
+ description: GitHub actor to authorize.
+ required: true
+ minimum:
+ description: Minimum repository permission.
+ required: false
+ default: write
+ github-token:
+ description: Token for GitHub permission lookup.
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - id: setup
+ name: Set up release-tool
+ shell: bash
+ run: bash "${GITHUB_ACTION_PATH}/../_internal/setup.sh"
+
+ - name: Check authorization
+ shell: bash
+ env:
+ RELEASE_TOOL_PATH: ${{ steps.setup.outputs.path }}
+ GITHUB_TOKEN: ${{ inputs.github-token }}
+ RELEASE_REPOSITORY: ${{ inputs.repository }}
+ RELEASE_ACTOR: ${{ inputs.actor }}
+ RELEASE_MINIMUM: ${{ inputs.minimum }}
+ run: |
+ set -euo pipefail
+ php "${RELEASE_TOOL_PATH}" release:authorization \
+ --repository "${RELEASE_REPOSITORY}" \
+ --actor "${RELEASE_ACTOR}" \
+ --minimum "${RELEASE_MINIMUM}"
diff --git a/actions/build-inspect/action.yml b/actions/build-inspect/action.yml
new file mode 100644
index 0000000..728f08d
--- /dev/null
+++ b/actions/build-inspect/action.yml
@@ -0,0 +1,49 @@
+# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+name: Inspect application build metadata
+description: Read Nextcloud compatibility and optional Node/npm engines using release-tool.
+
+inputs:
+ root:
+ description: Checked-out application directory.
+ required: false
+ default: .
+
+outputs:
+ version:
+ description: Version from appinfo/info.xml.
+ value: ${{ steps.inspect.outputs.version }}
+ nextcloud-min-version:
+ description: Minimum supported Nextcloud major.
+ value: ${{ steps.inspect.outputs.nextcloud-min-version }}
+ node-version:
+ description: Node engine, or ^24 when package.json exists without a Node engine.
+ value: ${{ steps.inspect.outputs.node-version }}
+ npm-version:
+ description: npm engine, or ^11.3 when package.json exists without an npm engine.
+ value: ${{ steps.inspect.outputs.npm-version }}
+ has-package-json:
+ description: Whether the consumer has package.json.
+ value: ${{ steps.inspect.outputs.has-package-json }}
+
+runs:
+ using: composite
+ steps:
+ - id: setup
+ name: Set up release-tool
+ shell: bash
+ run: bash "${GITHUB_ACTION_PATH}/../_internal/setup.sh"
+
+ - id: inspect
+ name: Inspect application build metadata
+ shell: bash
+ env:
+ RELEASE_TOOL_PATH: ${{ steps.setup.outputs.path }}
+ APPLICATION_ROOT: ${{ inputs.root }}
+ run: |
+ set -euo pipefail
+ php "${RELEASE_TOOL_PATH}" build:inspect \
+ --root "${APPLICATION_ROOT}" \
+ --github-output "${GITHUB_OUTPUT}" \
+ --json
diff --git a/bin/release-tool b/bin/release-tool
index e4ffe8b..8a18df1 100755
--- a/bin/release-tool
+++ b/bin/release-tool
@@ -6,6 +6,7 @@ declare(strict_types=1);
use LibreCode\ReleaseTool\Application\Artifact\ArtifactRestorer;
use LibreCode\ReleaseTool\Application\Artifact\ArtifactValidator;
use LibreCode\ReleaseTool\Application\Artifact\NextcloudPackageValidator;
+use LibreCode\ReleaseTool\Application\Build\BuildMetadataInspector;
use LibreCode\ReleaseTool\Application\Console\ApplicationFactory;
use LibreCode\ReleaseTool\Application\Publication\AppStorePublicationWaiter;
use LibreCode\ReleaseTool\Application\Publication\PublicationVerifier;
@@ -22,6 +23,7 @@ use LibreCode\ReleaseTool\Application\Release\StableBranchSelector;
use LibreCode\ReleaseTool\Application\ReleaseNotes\ReleaseNotesGenerator;
use LibreCode\ReleaseTool\Application\Security\RepositoryAuthorizationChecker;
use LibreCode\ReleaseTool\Infrastructure\AppStore\NextcloudAppStoreRepository;
+use LibreCode\ReleaseTool\Infrastructure\Build\LocalBuildFileReader;
use LibreCode\ReleaseTool\Infrastructure\Archive\PharArchiveReaderFactory;
use LibreCode\ReleaseTool\Infrastructure\Archive\SafeZipExtractor;
use LibreCode\ReleaseTool\Infrastructure\Configuration\LocalConsumerConfigContextValidator;
@@ -90,4 +92,5 @@ exit(ApplicationFactory::create(
new ReleasePreflight(GitHubReleasePreflightRepository::fromEnvironment()),
new ReleaseIdentityValidator($git, new GitReleaseMetadataReader($git)),
new ReleaseResumeContextResolver($finalizationRepository),
+ new BuildMetadataInspector(new LocalBuildFileReader()),
)->run());
diff --git a/docs/github-actions.md b/docs/github-actions.md
index 3fba553..75a76df 100644
--- a/docs/github-actions.md
+++ b/docs/github-actions.md
@@ -93,3 +93,10 @@ These properties are part of the reference integration and should not be weakene
## Reference
`LibreCodeCoop/release-tool` is the source of truth for release behavior and its public release Actions. `LibreCodeCoop/.github` owns LibreCode's managed workflow templates that orchestrate those Actions. LibreSign's `.github/workflows/prepare-release.yml` is the reference consumer and should be materialized from that catalog rather than becoming a second implementation.
+
+Build consumers can use the `actions/build-inspect` adapter for validated appinfo
+version, Nextcloud minimum major, and optional Node/npm engines. When a
+package.json is absent, the Action reports `has-package-json=false` and empty
+engine values; when present without engine declarations, it reports defaults
+Node `^24` and npm `^11.3`. The `actions/authorization-check` adapter
+reuses the tested PHP permission policy instead of separate GitHub API calls.
diff --git a/src/Application/Console/ApplicationFactory.php b/src/Application/Console/ApplicationFactory.php
index 0f4bc44..240a8d0 100644
--- a/src/Application/Console/ApplicationFactory.php
+++ b/src/Application/Console/ApplicationFactory.php
@@ -7,6 +7,7 @@
use LibreCode\ReleaseTool\Application\Artifact\ArtifactRestorer;
use LibreCode\ReleaseTool\Application\Artifact\ArtifactValidator;
use LibreCode\ReleaseTool\Application\Artifact\NextcloudPackageValidator;
+use LibreCode\ReleaseTool\Application\Build\BuildMetadataInspector;
use LibreCode\ReleaseTool\Application\Configuration\ConsumerConfigContextValidator;
use LibreCode\ReleaseTool\Application\Configuration\NoopConsumerConfigContextValidator;
use LibreCode\ReleaseTool\Application\Console\Command\AppStorePublicationWaitCommand;
@@ -14,6 +15,7 @@
use LibreCode\ReleaseTool\Application\Console\Command\ArtifactValidateCommand;
use LibreCode\ReleaseTool\Application\Console\Command\ArtifactValidatePackageCommand;
use LibreCode\ReleaseTool\Application\Console\Command\AuthorizationCheckCommand;
+use LibreCode\ReleaseTool\Application\Console\Command\BuildInspectCommand;
use LibreCode\ReleaseTool\Application\Console\Command\ConfigValidateCommand;
use LibreCode\ReleaseTool\Application\Console\Command\MetadataInspectCommand;
use LibreCode\ReleaseTool\Application\Console\Command\MilestoneTransitionCommand;
@@ -74,6 +76,7 @@ public static function create(
?ReleasePreflight $releasePreflight = null,
?ReleaseIdentityValidator $releaseIdentityValidator = null,
?ReleaseResumeContextResolver $releaseResumeContextResolver = null,
+ ?BuildMetadataInspector $buildMetadataInspector = null,
): Application
{
$application = new Application('release-tool', self::version());
@@ -82,6 +85,10 @@ public static function create(
contextValidator: $configValidator ?? new NoopConsumerConfigContextValidator(),
));
+ if ($buildMetadataInspector !== null) {
+ $application->add(new BuildInspectCommand($buildMetadataInspector));
+ }
+
if ($authorizationChecker !== null) {
$application->add(new AuthorizationCheckCommand($authorizationChecker));
}
diff --git a/src/Application/Console/Command/BuildInspectCommand.php b/src/Application/Console/Command/BuildInspectCommand.php
new file mode 100644
index 0000000..f63b65e
--- /dev/null
+++ b/src/Application/Console/Command/BuildInspectCommand.php
@@ -0,0 +1,68 @@
+addOption('root', null, InputOption::VALUE_REQUIRED, 'Application checkout path.', '.')
+ ->addOption('github-output', null, InputOption::VALUE_REQUIRED, 'Append validated values to a GitHub Actions output file.')
+ ->addOption('json', null, InputOption::VALUE_NONE, 'Print JSON metadata.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $metadata = $this->inspector->inspect((string) $input->getOption('root'));
+ $destination = $input->getOption('github-output');
+ if (is_string($destination) && $destination !== '') {
+ $values = [
+ 'version' => $metadata->version,
+ 'nextcloud-min-version' => $metadata->nextcloudMinVersion,
+ 'node-version' => $metadata->nodeVersion,
+ 'npm-version' => $metadata->npmVersion,
+ 'has-package-json' => $metadata->hasPackageJson ? 'true' : 'false',
+ ];
+ $lines = '';
+ foreach ($values as $name => $value) {
+ $lines .= $name.'='.$value."\n";
+ }
+ if (@file_put_contents($destination, $lines, FILE_APPEND | LOCK_EX) === false) {
+ throw new \DomainException('Unable to write GitHub Actions output file.');
+ }
+ }
+ } catch (\Throwable $exception) {
+ $output->writeln(''.$exception->getMessage().'');
+ return Command::FAILURE;
+ }
+
+ if ((bool) $input->getOption('json')) {
+ $output->writeln((string) json_encode($metadata->toArray(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES));
+ } else {
+ $output->writeln(sprintf(
+ 'Build metadata: %s (Nextcloud >= %s)',
+ $metadata->version,
+ $metadata->nextcloudMinVersion,
+ ));
+ }
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/tests/Acceptance/ReleaseActionsTest.php b/tests/Acceptance/ReleaseActionsTest.php
index d1be8aa..10b2edb 100644
--- a/tests/Acceptance/ReleaseActionsTest.php
+++ b/tests/Acceptance/ReleaseActionsTest.php
@@ -10,7 +10,7 @@
final class ReleaseActionsTest extends TestCase
{
- private const array PUBLIC_ACTIONS = ['appstore-publication-wait', 'artifact-validate', 'metadata-inspect', 'post-merge', 'prepare', 'publication', 'release-identity', 'release-notes', 'release-preflight', 'resume-context', 'stable-select'];
+ private const array PUBLIC_ACTIONS = ['appstore-publication-wait', 'artifact-validate', 'authorization-check', 'build-inspect', 'metadata-inspect', 'post-merge', 'prepare', 'publication', 'release-identity', 'release-notes', 'release-preflight', 'resume-context', 'stable-select'];
public function testPublicActionSurfaceIsExplicit(): void
{
@@ -164,6 +164,16 @@ public static function publicActionContracts(): iterable
['artifact', 'app-name', 'version'],
[],
];
+ yield 'authorization-check' => [
+ 'authorization-check',
+ ['repository', 'actor', 'minimum', 'github-token'],
+ [],
+ ];
+ yield 'build-inspect' => [
+ 'build-inspect',
+ ['root'],
+ ['version', 'nextcloud-min-version', 'node-version', 'npm-version', 'has-package-json'],
+ ];
yield 'metadata-inspect' => [
'metadata-inspect',
['config-path', 'root', 'ref'],
diff --git a/tests/Unit/Application/Build/BuildInspectCommandTest.php b/tests/Unit/Application/Build/BuildInspectCommandTest.php
new file mode 100644
index 0000000..724493c
--- /dev/null
+++ b/tests/Unit/Application/Build/BuildInspectCommandTest.php
@@ -0,0 +1,67 @@
+15.0.8');
+ file_put_contents($root.'/package.json', '{}');
+ $outputPath = $root.'/github-output';
+
+ try {
+ $application = ApplicationFactory::create(
+ buildMetadataInspector: new BuildMetadataInspector(new LocalBuildFileReader()),
+ );
+ $command = $application->find('build:inspect');
+ $tester = new CommandTester($command);
+ $exit = $tester->execute([
+ '--root' => $root,
+ '--github-output' => $outputPath,
+ '--json' => true,
+ ]);
+
+ self::assertSame(0, $exit, $tester->getDisplay());
+ $data = json_decode(trim($tester->getDisplay()), true, 512, JSON_THROW_ON_ERROR);
+ self::assertSame('15.0.8', $data['version']);
+ self::assertSame('^24', $data['node_version']);
+ self::assertSame('35', $data['nextcloud_min_version']);
+ self::assertSame(
+ "version=15.0.8\nnextcloud-min-version=35\nnode-version=^24\nnpm-version=^11.3\nhas-package-json=true\n",
+ file_get_contents($outputPath),
+ );
+ } finally {
+ @unlink($outputPath);
+ @unlink($root.'/package.json');
+ @unlink($root.'/appinfo/info.xml');
+ @rmdir($root.'/appinfo');
+ @rmdir($root);
+ }
+ }
+
+ public function testRejectsMissingCheckoutWithoutWritingOutputs(): void
+ {
+ $path = sys_get_temp_dir().'/release-missing-'.bin2hex(random_bytes(8));
+ $application = ApplicationFactory::create(
+ buildMetadataInspector: new BuildMetadataInspector(new LocalBuildFileReader()),
+ );
+ $tester = new CommandTester($application->find('build:inspect'));
+
+ self::assertNotSame(0, $tester->execute([
+ '--root' => $path,
+ '--github-output' => $path.'.output',
+ ]));
+ self::assertFileDoesNotExist($path.'.output');
+ }
+}
From 195f7dd48affe3c9721a3f520ef3230bbfe2d7f5 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:51:27 +0000
Subject: [PATCH 3/3] chore(release): prepare release-tool 0.11.21
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
VERSION | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/VERSION b/VERSION
index 7809b80..a9546b6 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.11.20
+0.11.21