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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.2
0.11.3
37 changes: 37 additions & 0 deletions actions/artifact-validate/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: Validate release artifact
description: Validate a packaged Nextcloud app artifact using release-tool.

inputs:
artifact:
description: Path to the packaged tar.gz artifact.
required: true
app-name:
description: Expected Nextcloud app id.
required: true
version:
description: Expected app version.
required: true

runs:
using: composite
steps:
- id: setup
name: Setup release-tool
shell: bash
run: bash "${GITHUB_ACTION_PATH}/../_internal/setup.sh"

- name: Validate packaged app
shell: bash
env:
RELEASE_TOOL_PATH: ${{ steps.setup.outputs.path }}
RELEASE_ARTIFACT: ${{ inputs.artifact }}
RELEASE_APP_NAME: ${{ inputs.app-name }}
RELEASE_VERSION: ${{ inputs.version }}
run: >-
php "${RELEASE_TOOL_PATH}" artifact:validate-package
--artifact "${RELEASE_ARTIFACT}"
--app-name "${RELEASE_APP_NAME}"
--expected-version "${RELEASE_VERSION}"
69 changes: 69 additions & 0 deletions actions/metadata-inspect/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: Inspect release metadata
description: Validate and expose release metadata for a consumer repository.

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:
changelog-path:
description: Resolved changelog path.
value: ${{ steps.inspect.outputs.changelog-path }}
version:
description: Resolved application version.
value: ${{ steps.inspect.outputs.version }}
major:
description: Resolved application major version.
value: ${{ steps.inspect.outputs.major }}
development:
description: Whether the inspected version is a development version.
value: ${{ steps.inspect.outputs.development }}

runs:
using: composite
steps:
- id: setup
name: Setup release-tool
shell: bash
run: bash "${GITHUB_ACTION_PATH}/../_internal/setup.sh"

- id: inspect
name: Inspect release metadata
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: |
set -euo pipefail
metadata="${RUNNER_TEMP}/release-metadata.json"
php "${RELEASE_TOOL_PATH}" metadata:inspect --config "${RELEASE_CONFIG_PATH}" --root "${RELEASE_ROOT}" --ref "${RELEASE_REF}" --json > "${metadata}"
php -r '
$m=json_decode(file_get_contents($argv[1]), true, 512, JSON_THROW_ON_ERROR);
$out=getenv("GITHUB_OUTPUT");
$values=[
"changelog-path" => $m["changelog_path"] ?? "",
"version" => $m["version"] ?? "",
"major" => $m["major"] ?? "",
"development" => !empty($m["development"]) ? "true" : "false",
];
$fh=fopen($out, "ab");
foreach ($values as $key => $value) {
fwrite($fh, $key . "=" . $value . PHP_EOL);
}
fclose($fh);
' "${metadata}"
74 changes: 74 additions & 0 deletions actions/release-notes/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: Build release note changes
description: Build release-note entries from commits and associated pull requests.

inputs:
repository:
description: Repository in owner/name form.
required: true
branch:
description: Release branch.
required: true
working-directory:
description: Git working directory used to resolve commits and tags.
required: false
default: .
from-ref:
description: Optional starting ref. When omitted, the latest non-nightly tag is used.
required: false
default: ''
to-ref:
description: Ending ref.
required: false
default: HEAD
fallback-limit:
description: Number of direct commits to include when no starting tag exists.
required: false
default: '10'
github-token:
description: GitHub token used to resolve pull requests.
required: true

outputs:
changes-file:
description: Markdown file containing generated change entries.
value: ${{ steps.notes.outputs.changes-file }}
change-count:
description: Total number of change entries.
value: ${{ steps.notes.outputs.change-count }}
pull-request-count:
description: Number of pull-request-backed entries.
value: ${{ steps.notes.outputs.pull-request-count }}
commit-fallback-count:
description: Number of direct-commit fallback entries.
value: ${{ steps.notes.outputs.commit-fallback-count }}

runs:
using: composite
steps:
- id: setup
name: Setup release-tool
shell: bash
run: bash "${GITHUB_ACTION_PATH}/../_internal/setup.sh"

- id: notes
name: Build release note changes
shell: bash
env:
RELEASE_TOOL_PATH: ${{ steps.setup.outputs.path }}
RELEASE_NOTES_GITHUB_TOKEN: ${{ inputs.github-token }}
RELEASE_REPOSITORY: ${{ inputs.repository }}
RELEASE_BRANCH: ${{ inputs.branch }}
RELEASE_WORKING_DIRECTORY: ${{ inputs.working-directory }}
RELEASE_FROM_REF: ${{ inputs.from-ref }}
RELEASE_TO_REF: ${{ inputs.to-ref }}
RELEASE_FALLBACK_LIMIT: ${{ inputs.fallback-limit }}
run: |
set -euo pipefail
from_ref="${RELEASE_FROM_REF}"
if [[ -z "${from_ref}" ]]; then
from_ref="$(git -C "${RELEASE_WORKING_DIRECTORY}" tag --list --sort=-version:refname | grep -v '^nightly$' | head -1 || true)"
fi
php "${RELEASE_TOOL_PATH}" release:notes --repository "${RELEASE_REPOSITORY}" --branch "${RELEASE_BRANCH}" --working-directory "${RELEASE_WORKING_DIRECTORY}" --from-ref "${from_ref}" --to-ref "${RELEASE_TO_REF}" --fallback-limit "${RELEASE_FALLBACK_LIMIT}"
57 changes: 57 additions & 0 deletions actions/stable-select/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: Resolve stable release line
description: Resolve whether the current stable<N> branch is the latest supported stable release line.

inputs:
repository:
description: Repository in owner/name form. Defaults to the current repository.
required: false
default: ''
branch:
description: Stable branch to compare. Defaults to the current ref name.
required: false
default: ''
github-token:
description: GitHub token used to inspect repository branches.
required: true

outputs:
is-latest:
description: Whether the selected branch is the latest stable release line.
value: ${{ steps.select.outputs.is_latest }}
current-branch:
description: Current branch.
value: ${{ steps.select.outputs.current_branch }}
current-major:
description: Current stable major.
value: ${{ steps.select.outputs.current_major }}
latest-branch:
description: Latest stable branch.
value: ${{ steps.select.outputs.latest_branch }}
latest-major:
description: Latest stable major.
value: ${{ steps.select.outputs.latest_major }}

runs:
using: composite
steps:
- id: setup
name: Setup release-tool
shell: bash
run: bash "${GITHUB_ACTION_PATH}/../_internal/setup.sh"

- id: select
name: Resolve stable release line
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
RELEASE_TOOL_PATH: ${{ steps.setup.outputs.path }}
RELEASE_REPOSITORY: ${{ inputs.repository }}
RELEASE_BRANCH: ${{ inputs.branch }}
run: |
set -euo pipefail
repository="${RELEASE_REPOSITORY:-${GITHUB_REPOSITORY}}"
branch="${RELEASE_BRANCH:-${GITHUB_REF_NAME}}"
php "${RELEASE_TOOL_PATH}" release:stable-select --repository "${repository}" --branch "${branch}"
13 changes: 6 additions & 7 deletions docs/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,14 @@ Do not run mutation logic for arbitrary pull requests.

## Actions

The main building blocks are:
The public building blocks are:

- `actions/prepare`;
- `actions/post-merge`;
- `actions/publication`.
- lifecycle: `actions/prepare`, `actions/post-merge`, `actions/publication`;
- supporting release operations: `actions/stable-select`, `actions/artifact-validate`, `actions/metadata-inspect`, `actions/release-notes`.

Pin them to an immutable commit SHA and keep a comment with the corresponding release version.
Consumers should call these Actions instead of downloading the PHAR or duplicating release-tool command bootstrap in workflow YAML. The Actions resolve the matching product version from this repository's `VERSION` file and verify the published PHAR checksum internally.

Do not reference `main` or a floating tag in production release automation.
Pin Actions to an immutable commit SHA. Do not duplicate the release-tool semantic version in consumer shell, and do not reference `main` or a floating tag in production release automation.

## Consumer responsibilities

Expand Down Expand Up @@ -93,4 +92,4 @@ 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 the three public lifecycle 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.
`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.
26 changes: 23 additions & 3 deletions tests/Acceptance/ReleaseActionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

final class ReleaseActionsTest extends TestCase
{
private const array PUBLIC_ACTIONS = ['post-merge', 'prepare', 'publication'];
private const array PUBLIC_ACTIONS = ['artifact-validate', 'metadata-inspect', 'post-merge', 'prepare', 'publication', 'release-notes', 'stable-select'];

public function testPublicActionSurfaceContainsOnlyLifecycleStages(): void
public function testPublicActionSurfaceIsExplicit(): void
{
$paths = glob($this->root() . '/actions/*/action.yml');
self::assertIsArray($paths);
Expand All @@ -27,7 +27,7 @@ public function testPublicActionSurfaceContainsOnlyLifecycleStages(): void
}

#[DataProvider('publicActions')]
public function testPublicActionsArePhpOrchestrationWithoutLegacyHelpers(string $name): void
public function testPublicActionsAreReleaseToolOrchestrationWithoutLegacyHelpers(string $name): void
{
$path = $this->root() . '/actions/' . $name . '/action.yml';
$content = (string) file_get_contents($path);
Expand Down Expand Up @@ -104,6 +104,16 @@ public function testInternalBootstrapUsesExactVersionAndVerifiedChecksum(): void
/** @return iterable<string, array{string, list<string>, list<string>}> */
public static function publicActionContracts(): iterable
{
yield 'artifact-validate' => [
'artifact-validate',
['artifact', 'app-name', 'version'],
[],
];
yield 'metadata-inspect' => [
'metadata-inspect',
['config-path', 'root', 'ref'],
['changelog-path', 'version', 'major', 'development'],
];
yield 'prepare' => [
'prepare',
[
Expand Down Expand Up @@ -162,6 +172,16 @@ public static function publicActionContracts(): iterable
'verification-artifact-name',
],
];
yield 'release-notes' => [
'release-notes',
['repository', 'branch', 'working-directory', 'from-ref', 'to-ref', 'fallback-limit', 'github-token'],
['changes-file', 'change-count', 'pull-request-count', 'commit-fallback-count'],
];
yield 'stable-select' => [
'stable-select',
['repository', 'branch', 'github-token'],
['is-latest', 'current-branch', 'current-major', 'latest-branch', 'latest-major'],
];
}

/** @return iterable<string, array{string}> */
Expand Down
Loading