From 074b21bc038fedab81985fa5ab3013a683871ddc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:57:31 +0000 Subject: [PATCH 1/3] refactor(publication): delegate metadata and actor policy to Release Tool Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- actions/nextcloud-appstore-publish/action.yml | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index 70c3add..04dd422 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -41,10 +41,12 @@ runs: using: composite steps: - name: Check actor permission - uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 + uses: LibreCodeCoop/release-tool/actions/authorization-check@30ba5d8a33089331d829f586546d34cd45a529d7 with: - require: write - token: ${{ inputs.github-token }} + github-token: ${{ inputs.github-token }} + repository: ${{ github.repository }} + actor: ${{ github.actor }} + minimum: write - name: Validate boolean inputs shell: bash @@ -77,32 +79,23 @@ runs: - name: Validate release identity id: release_identity - uses: LibreCodeCoop/release-tool/actions/release-identity@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/release-identity@30ba5d8a33089331d829f586546d34cd45a529d7 with: tag: ${{ inputs.release-tag }} working-directory: ${{ inputs.app-name }} require-tag-exists: 'true' - - name: Get appinfo data - id: appinfo - uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 + - name: Inspect application build metadata + id: build_metadata + uses: LibreCodeCoop/release-tool/actions/build-inspect@30ba5d8a33089331d829f586546d34cd45a529d7 with: - filename: ${{ inputs.app-name }}/appinfo/info.xml - expression: "//info//dependencies//nextcloud/@min-version" - - - name: Read package engines - id: versions - continue-on-error: true - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - with: - path: ${{ inputs.app-name }} - fallbackNode: '^24' + root: ${{ inputs.app-name }} - name: Set up node - if: ${{ steps.versions.outputs.nodeVersion }} + if: steps.build_metadata.outputs.has-package-json == 'true' uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: ${{ steps.versions.outputs.nodeVersion }} + node-version: ${{ steps.build_metadata.outputs.node-version }} package-manager-cache: false - name: Resolve PHP version @@ -134,7 +127,7 @@ runs: ignore-cache: 'yes' - name: Build application - if: ${{ steps.versions.outputs.nodeVersion }} + if: steps.build_metadata.outputs.has-package-json == 'true' shell: bash env: APP_NAME: ${{ inputs.app-name }} @@ -174,7 +167,7 @@ runs: if: steps.krankerl.outputs.files_exists != 'true' shell: bash env: - NC_VERSION: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + NC_VERSION: ${{ steps.build_metadata.outputs.nextcloud-min-version }} run: | set -euo pipefail download_url="$(curl --fail --silent --show-error "https://updates.nextcloud.com/updater_server/latest?channel=beta&version=${NC_VERSION}" | jq -r '.downloads.zip[0]')" @@ -263,7 +256,7 @@ runs: tar -zcvf "${APP_NAME}.tar.gz" "${APP_NAME}" - name: Set up PHP 8.3 for release-tool - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: '8.3' coverage: none @@ -271,7 +264,7 @@ runs: GITHUB_TOKEN: ${{ inputs.github-token }} - name: Validate release artifact - uses: LibreCodeCoop/release-tool/actions/artifact-validate@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/artifact-validate@30ba5d8a33089331d829f586546d34cd45a529d7 with: artifact: ${{ inputs.app-name }}/build/artifacts/${{ inputs.app-name }}.tar.gz app-name: ${{ inputs.app-name }} @@ -300,8 +293,8 @@ runs: app_private_key: ${{ inputs.app-private-key }} - name: Verify App Store publication - uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@30ba5d8a33089331d829f586546d34cd45a529d7 with: app-name: ${{ inputs.app-name }} version: ${{ steps.release_identity.outputs.version }} - platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + platform: ${{ steps.build_metadata.outputs.nextcloud-min-version }} From 1f6d487dc332cd7789594dfdc6f948c50432292a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:57:40 +0000 Subject: [PATCH 2/3] refactor(nightly): build latest stable on schedule via tested metadata adapter Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/test_release_tool_pins.py | 2 +- workflow-templates/nightly-release.yml | 94 +++++++++++--------------- workflow-templates/prepare-release.yml | 6 +- 3 files changed, 42 insertions(+), 60 deletions(-) diff --git a/tests/test_release_tool_pins.py b/tests/test_release_tool_pins.py index 2cdd1c7..73f916b 100644 --- a/tests/test_release_tool_pins.py +++ b/tests/test_release_tool_pins.py @@ -7,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] -RELEASE_TOOL_RELEASE_SHA = "9b33a67a7b1e37a45b0a3c252370899effcdfdc2" +RELEASE_TOOL_RELEASE_SHA = "30ba5d8a33089331d829f586546d34cd45a529d7" SURFACES = ( ROOT / "workflow-templates/prepare-release.yml", ROOT / "workflow-templates/nightly-release.yml", diff --git a/workflow-templates/nightly-release.yml b/workflow-templates/nightly-release.yml index fb905f4..5fe008e 100644 --- a/workflow-templates/nightly-release.yml +++ b/workflow-templates/nightly-release.yml @@ -4,14 +4,8 @@ name: Nightly release on: - push: - branches: - - 'stable*' - paths: - - '**' - - '!**.md' - - '!.github/**' - - '.github/workflows/nightly-release.yml' + schedule: + - cron: '17 3 * * *' workflow_dispatch: permissions: @@ -19,12 +13,12 @@ permissions: pull-requests: read concurrency: - group: nightly-release-${{ github.repository }}-${{ github.ref_name }} + group: nightly-release-${{ github.repository }} cancel-in-progress: true jobs: - check-latest-stable: - name: Check latest stable + resolve-latest-stable: + name: Resolve latest stable runs-on: ubuntu-latest outputs: is-latest: ${{ steps.stable.outputs.is-latest }} @@ -35,75 +29,62 @@ jobs: steps: - id: stable name: Resolve stable release line - uses: LibreCodeCoop/release-tool/actions/stable-select@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/stable-select@30ba5d8a33089331d829f586546d34cd45a529d7 with: github-token: ${{ github.token }} nightly-release: name: Build and publish nightly - needs: check-latest-stable - if: needs.check-latest-stable.outputs.is-latest == 'true' + needs: resolve-latest-stable + if: needs.resolve-latest-stable.outputs.latest-branch != '' runs-on: ubuntu-latest timeout-minutes: 30 steps: - - name: Check actor permission - uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 - with: - require: write - - name: Set app environment shell: bash run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> "${GITHUB_ENV}" - - name: Checkout + - name: Checkout latest stable uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 submodules: true + ref: ${{ needs.resolve-latest-stable.outputs.latest-branch }} path: ${{ env.APP_NAME }} - - name: Get app version number - id: app-version - uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 + - name: Resolve checked out commit + id: source + working-directory: ${{ env.APP_NAME }} + shell: bash + run: echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + + - name: Inspect checked-out stable metadata + id: build_metadata + uses: LibreCodeCoop/release-tool/actions/build-inspect@30ba5d8a33089331d829f586546d34cd45a529d7 with: - filename: ${{ env.APP_NAME }}/appinfo/info.xml - expression: "//info//version/text()" + root: ${{ env.APP_NAME }} - name: Set app version shell: bash env: - APP_VERSION: ${{ fromJSON(steps.app-version.outputs.result).version }} + APP_VERSION: ${{ steps.build_metadata.outputs.version }} run: echo "APP_VERSION=${APP_VERSION}" >> "${GITHUB_ENV}" - - name: Get appinfo data - id: appinfo - uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 - with: - filename: ${{ env.APP_NAME }}/appinfo/info.xml - expression: "//info//dependencies//nextcloud/@min-version" - - - name: Read package.json node and npm engines version - id: versions - uses: skjnldsv/read-package-engines-version-actions@06d6db44aa62f4609ba070f556ec24bc99fb85cb # v4 - continue-on-error: true - with: - path: ${{ env.APP_NAME }} - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - if: steps.versions.outputs.nodeVersion + - name: Set up node ${{ steps.build_metadata.outputs.node-version }} + if: steps.build_metadata.outputs.has-package-json == 'true' uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: ${{ steps.versions.outputs.nodeVersion }} + node-version: ${{ steps.build_metadata.outputs.node-version }} package-manager-cache: false - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - if: steps.versions.outputs.npmVersion + - name: Set up npm ${{ steps.build_metadata.outputs.npm-version }} + if: steps.build_metadata.outputs.has-package-json == 'true' shell: bash - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + env: + NPM_VERSION: ${{ steps.build_metadata.outputs.npm-version }} + run: npm i -g "npm@${NPM_VERSION}" - name: Get PHP version id: php-versions @@ -134,7 +115,7 @@ jobs: ignore-cache: 'yes' - name: Build ${{ env.APP_NAME }} - if: steps.versions.outputs.nodeVersion + if: steps.build_metadata.outputs.has-package-json == 'true' env: CYPRESS_INSTALL_BINARY: 0 shell: bash @@ -168,7 +149,7 @@ jobs: continue-on-error: true shell: bash env: - NEXTCLOUD_VERSION: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + NEXTCLOUD_VERSION: ${{ steps.build_metadata.outputs.nextcloud-min-version }} run: | download_url="$(curl -fsSL "https://updates.nextcloud.com/updater_server/latest?channel=beta&version=${NEXTCLOUD_VERSION}" | jq -r '.downloads.zip[0]')" test -n "${download_url}" && test "${download_url}" != "null" @@ -215,7 +196,7 @@ jobs: tar -zcf "${APP_NAME}.tar.gz" "${APP_NAME}" - name: Validate release artifact - uses: LibreCodeCoop/release-tool/actions/artifact-validate@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/artifact-validate@30ba5d8a33089331d829f586546d34cd45a529d7 with: artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz app-name: ${{ env.APP_NAME }} @@ -226,7 +207,7 @@ jobs: shell: bash run: | echo "tag=nightly" >> "${GITHUB_OUTPUT}" - echo "branch=${{ needs.check-latest-stable.outputs.current-branch }}" >> "${GITHUB_OUTPUT}" + echo "branch=${{ needs.resolve-latest-stable.outputs.latest-branch }}" >> "${GITHUB_OUTPUT}" - name: Move nightly tag to this commit shell: bash @@ -234,14 +215,14 @@ jobs: GH_TOKEN: ${{ github.token }} run: | if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/nightly" >/dev/null 2>&1; then - gh api --method PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/nightly" -f sha="${GITHUB_SHA}" -F force=true >/dev/null + gh api --method PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/nightly" -f sha="${{ steps.source.outputs.sha }}" -F force=true >/dev/null else - gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref='refs/tags/nightly' -f sha="${GITHUB_SHA}" >/dev/null + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref='refs/tags/nightly' -f sha="${{ steps.source.outputs.sha }}" >/dev/null fi - name: Build nightly change list id: release-note-changes - uses: LibreCodeCoop/release-tool/actions/release-notes@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/release-notes@30ba5d8a33089331d829f586546d34cd45a529d7 with: repository: ${{ github.repository }} branch: ${{ steps.nightly.outputs.branch }} @@ -267,7 +248,8 @@ jobs: cat "${CHANGES_FILE}" echo echo "---" - printf 'Generated from commit `%s`.\n' "${GITHUB_SHA:0:7}" + source_sha='${{ steps.source.outputs.sha }}' + printf 'Generated from commit `%s`.\n' "${source_sha:0:7}" } > release-notes.md - name: Create or update GitHub release diff --git a/workflow-templates/prepare-release.yml b/workflow-templates/prepare-release.yml index da96674..b07ef93 100644 --- a/workflow-templates/prepare-release.yml +++ b/workflow-templates/prepare-release.yml @@ -87,7 +87,7 @@ jobs: git fetch --quiet origin "+refs/heads/main:refs/remotes/origin/main" - name: Prepare release - uses: LibreCodeCoop/release-tool/actions/prepare@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/prepare@30ba5d8a33089331d829f586546d34cd45a529d7 with: branch: ${{ inputs.branch }} ref: ${{ inputs.ref }} @@ -132,7 +132,7 @@ jobs: git fetch --quiet --unshallow --prune origin "+refs/heads/${RELEASE_BRANCH}:refs/remotes/origin/${RELEASE_BRANCH}" "+refs/tags/*:refs/tags/*" - name: Finalize merged release - uses: LibreCodeCoop/release-tool/actions/post-merge@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/post-merge@30ba5d8a33089331d829f586546d34cd45a529d7 with: pull-request-number: ${{ github.event.pull_request.number }} merger: ${{ github.event.pull_request.merged_by.login }} @@ -158,7 +158,7 @@ jobs: ref: ${{ github.event.release.tag_name }} - name: Verify publication - uses: LibreCodeCoop/release-tool/actions/publication@9b33a67a7b1e37a45b0a3c252370899effcdfdc2 + uses: LibreCodeCoop/release-tool/actions/publication@30ba5d8a33089331d829f586546d34cd45a529d7 with: github-release-id: ${{ github.event.release.id }} config-path: .nextcloud-release.yml From 465142e019beb76eef5a51d3a3bb37fbc6f396ae Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:57:52 +0000 Subject: [PATCH 3/3] test(release): guard stable targeting and architectural boundaries Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- AGENTS.md | 1 + docs/release-workflow-architecture.md | 41 +++++++++++++++ tests/test_release_workflow_architecture.py | 57 +++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 docs/release-workflow-architecture.md create mode 100644 tests/test_release_workflow_architecture.py diff --git a/AGENTS.md b/AGENTS.md index 9434fc6..8d54f75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ This repository is the LibreCode Coop organization configuration repository. It - `governance.config.json` is the declarative source for LibreCodeCoop repository governance and supported GitHub metadata. - The governance engine itself lives in `LibreCodeCoop/github-governance`. - Organization workflow templates and helper Actions live in this repository. Product-specific release behavior belongs in its owning product repository, such as `LibreCodeCoop/release-tool`. +- Release workflows must consume testable, versioned release-tool Actions for authorization and build metadata; do not introduce duplicate inline Python or shell parsers in workflow YAML. - `workflow-templates/**` is the organization catalog consumed by repositories; do not add product-specific business logic here. - Repository-local licenses, README files and `AGENTS.md` stay in their own repositories. diff --git a/docs/release-workflow-architecture.md b/docs/release-workflow-architecture.md new file mode 100644 index 0000000..9f017be --- /dev/null +++ b/docs/release-workflow-architecture.md @@ -0,0 +1,41 @@ + + +# Release workflow implementation boundaries + +GitHub Actions workflows orchestrate reusable, versioned Release Tool Actions. +They must not independently implement XML/JSON parsing, permission policies, +or stable-branch selection with inline Python or shell programs. + +The organization publication Action delegates permission validation to the +release-tool authorization-check adapter and checkout metadata to build-inspect. +When package.json is missing, build-inspect returns no Node/npm versions and +Node build steps must not run. A present package.json without engines uses +Node ^24 and npm ^11.3. + +The nightly workflow runs daily or on manual dispatch. It selects the highest +stable branch using stable-select and checks out that exact ref. The tag and +release notes always refer to the checked-out commit, never the main branch +commit that triggered the scheduled run. When no stable branch is available, +the publishing job is skipped. + +The release finalization workflow uses pull_request_target only for closed, +merged release preparation PRs with verified branch and marker metadata. +Do not filter these events using changed file paths. + +Validation boundaries: + +1. Release Tool PHPUnit tests validate parsed build metadata, default values, + malformed or missing files, injection attempts and GitHub outputs. +2. Release Tool public Action acceptance tests pin adapter contracts. +3. Organization workflow tests validate orchestration, immutable pins, + branch selection and sensitive event guards; actionlint and zizmor + validate workflow structure and security. +4. Live App Store publishing and nightly tag mutations require a separate + controlled integration run, not an ad hoc CI dry run. + +Both repositories pin shared Actions to a single immutable release-tool commit. +Verify that the corresponding versioned PHAR is published and its checksum +valid before consuming the release. diff --git a/tests/test_release_workflow_architecture.py b/tests/test_release_workflow_architecture.py new file mode 100644 index 0000000..265c999 --- /dev/null +++ b/tests/test_release_workflow_architecture.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +NIGHTLY = ROOT / "workflow-templates/nightly-release.yml" +PUBLISH = ROOT / "actions/nextcloud-appstore-publish/action.yml" +PREPARE = ROOT / "workflow-templates/prepare-release.yml" + + +class ReleaseWorkflowArchitectureTest(unittest.TestCase): + def test_metadata_and_authorization_delegate_to_testable_php_adapters(self) -> None: + for path in (NIGHTLY, PUBLISH): + with self.subTest(path=path.name): + content = path.read_text() + self.assertIn("release-tool/actions/build-inspect@", content) + self.assertIn("outputs.has-package-json == 'true'", content) + self.assertNotIn("python3", content) + self.assertNotIn("skjnldsv/", content) + self.assertNotIn("read-package-engines-version-actions", content) + self.assertNotIn("xpath-action", content) + self.assertIn("release-tool/actions/authorization-check@", PUBLISH.read_text()) + + def test_nightly_only_uses_latest_stable_and_checked_out_sha(self) -> None: + content = NIGHTLY.read_text() + self.assertIn("cron: '17 3 * * *'", content) + self.assertNotIn("\n push:\n", content) + self.assertIn("if: needs.resolve-latest-stable.outputs.latest-branch != ''", content) + self.assertIn("ref: ${{ needs.resolve-latest-stable.outputs.latest-branch }}", content) + self.assertIn('sha=$(git rev-parse HEAD)', content) + self.assertIn('sha="${{ steps.source.outputs.sha }}"', content) + self.assertIn('node-version: ${{ steps.build_metadata.outputs.node-version }}', content) + self.assertIn('NPM_VERSION: ${{ steps.build_metadata.outputs.npm-version }}', content) + self.assertIn('run: npm i -g "npm@${NPM_VERSION}"', content) + + def test_prepare_release_preserves_privileged_event_guards(self) -> None: + content = PREPARE.read_text() + self.assertIn("github.event.pull_request.merged == true", content) + self.assertIn("startsWith(github.event.pull_request.head.ref, 'release-tool/')", content) + self.assertIn("contains(github.event.pull_request.body, '