From ba22584aa1b1cee82bfdd66294e3dcd71686dd18 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:40:42 -0300 Subject: [PATCH 01/14] feat: centralize Nextcloud app publication action Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 actions/nextcloud-appstore-publish/action.yml diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml new file mode 100644 index 0000000..36f2c9a --- /dev/null +++ b/actions/nextcloud-appstore-publish/action.yml @@ -0,0 +1,308 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Build and publish Nextcloud app release +description: Build, package, sign, attach and publish a tagged Nextcloud app release. + +inputs: + release-tag: + description: Existing Git tag to build and publish. + required: true + github-token: + description: Token used to validate and update the GitHub release. + required: true + app-private-key: + description: Nextcloud app private key. + required: true + appstore-token: + description: Nextcloud App Store token. + required: true + checkout-submodules: + description: Checkout application git submodules. + required: false + default: 'false' + make-signs-app: + description: Let the Makefile sign the app instead of the generic post-package signing step. + required: false + default: 'false' + require-setup-signatures: + description: Require setup integrity metadata in Makefile-built packages. + required: false + default: 'false' + manual-recovery: + description: Use packaging tooling from the workflow ref while keeping tagged application source immutable. + required: false + default: 'false' + release-tool-revision: + description: Immutable release-tool revision. + required: false + default: '385ca7732db12e5c79590bb21be8da3608194595' + +runs: + using: composite + steps: + - name: Validate inputs + shell: bash + env: + CHECKOUT_SUBMODULES: ${{ inputs.checkout-submodules }} + MAKE_SIGNS_APP: ${{ inputs.make-signs-app }} + REQUIRE_SETUP_SIGNATURES: ${{ inputs.require-setup-signatures }} + MANUAL_RECOVERY: ${{ inputs.manual-recovery }} + run: | + set -euo pipefail + for value in "${CHECKOUT_SUBMODULES}" "${MAKE_SIGNS_APP}" "${REQUIRE_SETUP_SIGNATURES}" "${MANUAL_RECOVERY}"; do + case "${value}" in + true|false) ;; + *) + echo "::error::Boolean inputs must be 'true' or 'false'" + exit 2 + ;; + esac + done + + - name: Set application environment + shell: bash + env: + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + set -euo pipefail + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> "${GITHUB_ENV}" + echo "APP_VERSION=${RELEASE_TAG}" >> "${GITHUB_ENV}" + + - name: Checkout application + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: ${{ inputs.checkout-submodules }} + fetch-tags: true + fetch-depth: 0 + ref: ${{ inputs.release-tag }} + path: ${{ env.APP_NAME }} + + - name: Validate release identity + id: release-identity + uses: LibreCodeCoop/release-tool/actions/release-identity@${{ inputs.release-tool-revision }} + with: + tag: ${{ inputs.release-tag }} + working-directory: ${{ env.APP_NAME }} + require-tag-exists: 'true' + + - 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 engines + id: versions + continue-on-error: true + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + with: + path: ${{ env.APP_NAME }} + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node + if: ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + package-manager-cache: false + + - name: Set up npm + if: ${{ steps.versions.outputs.npmVersion }} + shell: bash + env: + NPM_VERSION: ${{ steps.versions.outputs.npmVersion }} + run: npm i -g "npm@${NPM_VERSION}" + + - name: Resolve PHP version + id: php-versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml + + - name: Set up PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ steps.php-versions.outputs.php-min }} + coverage: none + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + + - name: Check composer.json + id: check-composer + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: "${{ env.APP_NAME }}/composer.json" + + - name: Install composer dependencies + if: steps.check-composer.outputs.files_exists == 'true' + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + composer-options: '--no-dev' + working-directory: ${{ env.APP_NAME }} + ignore-cache: 'yes' + + - name: Build application + if: ${{ steps.versions.outputs.nodeVersion }} + shell: bash + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + set -euo pipefail + cd "${APP_NAME}" + npm ci + npm run build --if-present + + - name: Check Krankerl config + id: krankerl + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: ${{ env.APP_NAME }}/krankerl.toml + + - name: Install Krankerl + if: steps.krankerl.outputs.files_exists == 'true' + shell: bash + run: | + set -euo pipefail + wget --quiet https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb + sudo dpkg -i krankerl_0.14.0_amd64.deb + + - name: Package with Krankerl + if: steps.krankerl.outputs.files_exists == 'true' + shell: bash + run: | + set -euo pipefail + cd "${APP_NAME}" + krankerl package + + - name: Resolve Nextcloud server download + if: steps.krankerl.outputs.files_exists != 'true' + shell: bash + env: + NC_VERSION: ${{ fromJSON(steps.appinfo.outputs.result).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]')" + echo "DOWNLOAD_URL=${download_url}" >> "${GITHUB_ENV}" + + - name: Download Nextcloud server + id: server-download + if: steps.krankerl.outputs.files_exists != 'true' && env.DOWNLOAD_URL != 'null' + continue-on-error: true + shell: bash + run: | + set -euo pipefail + wget "${DOWNLOAD_URL}" -O nextcloud.zip + unzip -q nextcloud.zip + + - name: Checkout Nextcloud server fallback + if: steps.krankerl.outputs.files_exists != 'true' && steps.server-download.outcome != 'success' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + path: nextcloud + + - name: Validate manual recovery source + if: inputs.manual-recovery == 'true' && steps.krankerl.outputs.files_exists != 'true' + shell: bash + env: + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + set -euo pipefail + + case "${GITHUB_REF_NAME}" in + stable*) ;; + *) + echo "::error::Manual release recovery must be dispatched from a stable branch" + exit 1 + ;; + esac + + cd "${APP_NAME}" + git fetch --quiet origin "${GITHUB_SHA}" + tag_sha="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" + if ! git merge-base --is-ancestor "${tag_sha}" "${GITHUB_SHA}"; then + echo "::error::${GITHUB_REF_NAME} does not contain ${RELEASE_TAG}; refusing cross-line recovery" + exit 1 + fi + git show "${GITHUB_SHA}:Makefile" > Makefile + + - name: Package with Makefile + if: steps.krankerl.outputs.files_exists != 'true' + shell: bash + env: + APP_PRIVATE_KEY: ${{ inputs.app-private-key }} + REQUIRE_SETUP_SIGNATURES: ${{ inputs.require-setup-signatures }} + MAKE_SIGNS_APP: ${{ inputs.make-signs-app }} + run: | + set -euo pipefail + cd "${APP_NAME}" + if [[ "${MAKE_SIGNS_APP}" == "true" ]]; then + mkdir -p build/tools/certificates + printf '%s' "${APP_PRIVATE_KEY}" > "build/tools/certificates/${APP_NAME}.key" + fi + make appstore + if make -qp 2>/dev/null | grep -q '^verify-appstore-package:'; then + REQUIRE_SETUP_SIGNATURES="${REQUIRE_SETUP_SIGNATURES}" make verify-appstore-package + fi + + - name: Sign generic Makefile package + if: steps.krankerl.outputs.files_exists != 'true' && inputs.make-signs-app != 'true' + shell: bash + env: + APP_PRIVATE_KEY: ${{ inputs.app-private-key }} + run: | + set -euo pipefail + cd "${APP_NAME}/build/artifacts" + tar -xvf "${APP_NAME}.tar.gz" + cd ../../.. + printf '%s' "${APP_PRIVATE_KEY}" > "${APP_NAME}.key" + wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${APP_NAME}/${APP_NAME}.crt" + php nextcloud/occ integrity:sign-app --privateKey="../${APP_NAME}.key" --certificate="../${APP_NAME}.crt" --path="../${APP_NAME}/build/artifacts/${APP_NAME}" + cd "${APP_NAME}/build/artifacts" + tar -zcvf "${APP_NAME}.tar.gz" "${APP_NAME}" + + - name: Set up PHP 8.3 for release-tool + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.3' + coverage: none + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + + - name: Validate release artifact + uses: LibreCodeCoop/release-tool/actions/artifact-validate@${{ inputs.release-tool-revision }} + with: + artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + app-name: ${{ env.APP_NAME }} + version: ${{ steps.release-identity.outputs.version }} + + - name: Attach tarball to GitHub release + id: attach-to-release + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2.11.5 + with: + repo_token: ${{ inputs.github-token }} + file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + asset_name: ${{ env.APP_NAME }}-${{ inputs.release-tag }}.tar.gz + tag: ${{ inputs.release-tag }} + overwrite: true + + - name: Upload app to Nextcloud App Store + uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 + with: + app_name: ${{ env.APP_NAME }} + appstore_token: ${{ inputs.appstore-token }} + download_url: ${{ steps.attach-to-release.outputs.browser_download_url }} + app_private_key: ${{ inputs.app-private-key }} + + - name: Verify App Store publication + uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@${{ inputs.release-tool-revision }} + with: + app-name: ${{ env.APP_NAME }} + version: ${{ steps.release-identity.outputs.version }} + platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} From c345b786e64cae1b9a910e8460c67d1785400321 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:41:28 -0300 Subject: [PATCH 02/14] fix: pin publication action dependencies Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index 36f2c9a..1e430c8 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -33,14 +33,15 @@ inputs: description: Use packaging tooling from the workflow ref while keeping tagged application source immutable. required: false default: 'false' - release-tool-revision: - description: Immutable release-tool revision. - required: false - default: '385ca7732db12e5c79590bb21be8da3608194595' - runs: using: composite steps: + - name: Check actor permission + uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 + with: + require: write + token: ${{ inputs.github-token }} + - name: Validate inputs shell: bash env: @@ -81,7 +82,7 @@ runs: - name: Validate release identity id: release-identity - uses: LibreCodeCoop/release-tool/actions/release-identity@${{ inputs.release-tool-revision }} + uses: LibreCodeCoop/release-tool/actions/release-identity@385ca7732db12e5c79590bb21be8da3608194595 with: tag: ${{ inputs.release-tag }} working-directory: ${{ env.APP_NAME }} @@ -276,7 +277,7 @@ runs: GITHUB_TOKEN: ${{ inputs.github-token }} - name: Validate release artifact - uses: LibreCodeCoop/release-tool/actions/artifact-validate@${{ inputs.release-tool-revision }} + uses: LibreCodeCoop/release-tool/actions/artifact-validate@385ca7732db12e5c79590bb21be8da3608194595 with: artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz app-name: ${{ env.APP_NAME }} @@ -301,7 +302,7 @@ runs: app_private_key: ${{ inputs.app-private-key }} - name: Verify App Store publication - uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@${{ inputs.release-tool-revision }} + uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@385ca7732db12e5c79590bb21be8da3608194595 with: app-name: ${{ env.APP_NAME }} version: ${{ steps.release-identity.outputs.version }} From 529e5c6ba5e5070f4960a1667054246177f804d8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:42:16 -0300 Subject: [PATCH 03/14] refactor: make appstore workflow a thin wrapper Signed-off-by: Vitor Mattos --- workflow-templates/appstore-build-publish.yml | 216 ++---------------- 1 file changed, 15 insertions(+), 201 deletions(-) diff --git a/workflow-templates/appstore-build-publish.yml b/workflow-templates/appstore-build-publish.yml index 142e70b..a1665fe 100644 --- a/workflow-templates/appstore-build-publish.yml +++ b/workflow-templates/appstore-build-publish.yml @@ -3,7 +3,7 @@ # https://github.com/LibreCodeCoop/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization # -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH, LibreCode coop and contributors # SPDX-License-Identifier: MIT name: Build and publish app release @@ -11,6 +11,12 @@ name: Build and publish app release on: release: types: [published] + workflow_dispatch: + inputs: + release_tag: + description: Existing release tag to build and publish. + required: true + type: string permissions: contents: write @@ -18,204 +24,12 @@ permissions: jobs: build_and_publish: runs-on: ubuntu-latest - steps: - - name: Check actor permission - uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 - with: - require: write - - - name: Set app env - run: | - # Split and keep last - echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: ${{ env.APP_NAME }} - - - name: Get app version number - id: app-version - uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 - with: - filename: ${{ env.APP_NAME }}/appinfo/info.xml - expression: "//info//version/text()" - - - name: Validate app version against tag - run: | - [ "${{ env.APP_VERSION }}" = "v${{ fromJSON(steps.app-version.outputs.result).version }}" ] - - - 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 - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - # Continue if no package.json - continue-on-error: true - with: - path: ${{ env.APP_NAME }} - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - # Skip if no package.json - if: ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - package-manager-cache: false - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - # Skip if no package.json - if: ${{ steps.versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' - - - name: Get php version - id: php-versions - uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 - with: - filename: ${{ env.APP_NAME }}/appinfo/info.xml - - - name: Set up php ${{ steps.php-versions.outputs.php-min }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - with: - php-version: ${{ steps.php-versions.outputs.php-min }} - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check composer.json - id: check_composer - uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 - with: - files: "${{ env.APP_NAME }}/composer.json" - - - name: Install composer dependencies - if: steps.check_composer.outputs.files_exists == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - with: - composer-options: '--no-dev' - working-directory: ${{ env.APP_NAME }} - ignore-cache: 'yes' - - - name: Build ${{ env.APP_NAME }} - # Skip if no package.json - if: ${{ steps.versions.outputs.nodeVersion }} - env: - CYPRESS_INSTALL_BINARY: 0 - run: | - cd ${{ env.APP_NAME }} - npm ci - npm run build --if-present - - - name: Check Krankerl config - id: krankerl - uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 - with: - files: ${{ env.APP_NAME }}/krankerl.toml - - - name: Install Krankerl - if: steps.krankerl.outputs.files_exists == 'true' - run: | - wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb - sudo dpkg -i krankerl_0.14.0_amd64.deb - - - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with krankerl - if: steps.krankerl.outputs.files_exists == 'true' - run: | - cd ${{ env.APP_NAME }} - krankerl package - - - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with makefile - if: steps.krankerl.outputs.files_exists != 'true' - run: | - cd ${{ env.APP_NAME }} - make appstore - - - name: Verify app store package - if: steps.krankerl.outputs.files_exists != 'true' - working-directory: ${{ env.APP_NAME }} - run: | - if make -qp 2>/dev/null | grep -q '^verify-appstore-package:'; then - make verify-appstore-package - fi - - - name: Check server download link for ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} - run: | - NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}' - DOWNLOAD_URL=$(curl -s "https://updates.nextcloud.com/updater_server/latest?channel=beta&version=$NCVERSION" | jq -r '.downloads.zip[0]') - echo "DOWNLOAD_URL=$DOWNLOAD_URL" >> $GITHUB_ENV - - - name: Download server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} - continue-on-error: true - id: server-download - if: ${{ env.DOWNLOAD_URL != 'null' }} - run: | - echo "Downloading release tarball from $DOWNLOAD_URL" - wget $DOWNLOAD_URL -O nextcloud.zip - unzip nextcloud.zip - - - name: Checkout server master fallback - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: ${{ steps.server-download.outcome != 'success' }} - with: - persist-credentials: false - submodules: true - repository: nextcloud/server - path: nextcloud - - - - name: Sign app - run: | - # Extracting release - cd ${{ env.APP_NAME }}/build/artifacts - tar -xvf ${{ env.APP_NAME }}.tar.gz - cd ../../../ - # Setting up keys - echo '${{ secrets.APP_PRIVATE_KEY }}' > ${{ env.APP_NAME }}.key - wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt" - # Signing - php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }} - # Rebuilding archive - cd ${{ env.APP_NAME }}/build/artifacts - tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }} - - - name: Validate release artifact - uses: LibreCodeCoop/release-tool/actions/artifact-validate@710c4c83fba47bf01713f46e9c4cb4cf63debfba - with: - artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz - app-name: ${{ env.APP_NAME }} - version: ${{ env.APP_VERSION }} - - - name: Attach tarball to github release - uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5 - id: attach_to_release - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz - asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz - tag: ${{ github.ref }} - overwrite: true - - - name: Upload app to Nextcloud appstore - uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 - with: - app_name: ${{ env.APP_NAME }} - appstore_token: ${{ secrets.APPSTORE_TOKEN }} - download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} - app_private_key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Verify App Store publication - uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@710c4c83fba47bf01713f46e9c4cb4cf63debfba - with: - app-name: ${{ env.APP_NAME }} - version: ${{ env.APP_VERSION }} - platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + - name: Build and publish release + uses: LibreCodeCoop/.github/actions/nextcloud-appstore-publish@c345b786e64cae1b9a910e8460c67d1785400321 + with: + release-tag: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.event.release.tag_name }} + github-token: ${{ secrets.GITHUB_TOKEN }} + app-private-key: ${{ secrets.APP_PRIVATE_KEY }} + appstore-token: ${{ secrets.APPSTORE_TOKEN }} + manual-recovery: ${{ github.event_name == 'workflow_dispatch' }} From 8873f2e2ac1b9de5396de3d677f57e58c98d772e Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:42:58 -0300 Subject: [PATCH 04/14] fix: use expression-safe publication step ids Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index 1e430c8..b737d2f 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -133,13 +133,13 @@ runs: GITHUB_TOKEN: ${{ inputs.github-token }} - name: Check composer.json - id: check-composer + id: check_composer uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 with: files: "${{ env.APP_NAME }}/composer.json" - name: Install composer dependencies - if: steps.check-composer.outputs.files_exists == 'true' + if: steps.check_composer.outputs.files_exists == 'true' uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 with: composer-options: '--no-dev' @@ -284,7 +284,7 @@ runs: version: ${{ steps.release-identity.outputs.version }} - name: Attach tarball to GitHub release - id: attach-to-release + id: attach_to_release uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2.11.5 with: repo_token: ${{ inputs.github-token }} @@ -298,7 +298,7 @@ runs: with: app_name: ${{ env.APP_NAME }} appstore_token: ${{ inputs.appstore-token }} - download_url: ${{ steps.attach-to-release.outputs.browser_download_url }} + download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} app_private_key: ${{ inputs.app-private-key }} - name: Verify App Store publication From fe1fe1c0f74d1c3eda1d28fb0dbd871a12da2cc7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:43:10 -0300 Subject: [PATCH 05/14] chore: pin centralized publication action Signed-off-by: Vitor Mattos --- workflow-templates/appstore-build-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/appstore-build-publish.yml b/workflow-templates/appstore-build-publish.yml index a1665fe..2b2b299 100644 --- a/workflow-templates/appstore-build-publish.yml +++ b/workflow-templates/appstore-build-publish.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Build and publish release - uses: LibreCodeCoop/.github/actions/nextcloud-appstore-publish@c345b786e64cae1b9a910e8460c67d1785400321 + uses: LibreCodeCoop/.github/actions/nextcloud-appstore-publish@8873f2e2ac1b9de5396de3d677f57e58c98d772e with: release-tag: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.event.release.tag_name }} github-token: ${{ secrets.GITHUB_TOKEN }} From ad70257a85ac9634c76ced7a9b789fda50f1b92d Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:43:43 -0300 Subject: [PATCH 06/14] test: include composite actions in workflow policy Signed-off-by: Vitor Mattos --- scripts/check_workflow_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_workflow_policy.py b/scripts/check_workflow_policy.py index 6f54727..982bfe2 100644 --- a/scripts/check_workflow_policy.py +++ b/scripts/check_workflow_policy.py @@ -69,7 +69,7 @@ def main() -> int: "roots", nargs="*", type=Path, - default=[Path("workflow-templates"), Path(".github/workflows")], + default=[Path("workflow-templates"), Path(".github/workflows"), Path("actions")], ) args = parser.parse_args() From bf2334961cfdd96cb8d3cb087c4e1185f9840bbd Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:43:46 -0300 Subject: [PATCH 07/14] test: cover centralized appstore publication contract Signed-off-by: Vitor Mattos --- tests/test_appstore_publication_contract.py | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_appstore_publication_contract.py diff --git a/tests/test_appstore_publication_contract.py b/tests/test_appstore_publication_contract.py new file mode 100644 index 0000000..27aea9a --- /dev/null +++ b/tests/test_appstore_publication_contract.py @@ -0,0 +1,60 @@ +# 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] +ACTION = ROOT / "actions/nextcloud-appstore-publish/action.yml" +TEMPLATE = ROOT / "workflow-templates/appstore-build-publish.yml" + + +class AppStorePublicationContractTest(unittest.TestCase): + def test_consumer_template_is_thin_wrapper(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + self.assertNotIn("\n run: |", content) + self.assertIn("LibreCodeCoop/.github/actions/nextcloud-appstore-publish@", content) + self.assertLessEqual(len(content.splitlines()), 40) + + def test_external_actions_are_immutable(self) -> None: + content = ACTION.read_text(encoding="utf-8") + revisions = re.findall(r"^\s*uses:\s*([^@\s]+)@([^\s#]+)", content, re.MULTILINE) + self.assertTrue(revisions) + for action, revision in revisions: + if action.startswith("./"): + continue + self.assertRegex( + revision, + r"^[0-9a-f]{40}$", + msg=f"{action} is not pinned to an immutable SHA: {revision}", + ) + + def test_uses_does_not_interpolate_revisions(self) -> None: + content = ACTION.read_text(encoding="utf-8") + for line in content.splitlines(): + if "uses:" in line: + self.assertNotIn("${{", line) + + def test_manual_recovery_is_release_line_safe(self) -> None: + content = ACTION.read_text(encoding="utf-8") + self.assertIn('case "${GITHUB_REF_NAME}" in', content) + self.assertIn("stable*)", content) + self.assertIn('git merge-base --is-ancestor "${tag_sha}" "${GITHUB_SHA}"', content) + self.assertIn('git show "${GITHUB_SHA}:Makefile" > Makefile', content) + + def test_release_source_always_comes_from_tag(self) -> None: + content = ACTION.read_text(encoding="utf-8") + self.assertIn("ref: ${{ inputs.release-tag }}", content) + self.assertIn("require-tag-exists: 'true'", content) + + def test_expression_ids_are_safe(self) -> None: + content = ACTION.read_text(encoding="utf-8") + ids = re.findall(r"^\s*id:\s*([^\s]+)", content, re.MULTILINE) + for step_id in ids: + self.assertRegex(step_id, r"^[A-Za-z_][A-Za-z0-9_]*$") + + +if __name__ == "__main__": + unittest.main() From 8995318c5b6d12384d680e118da0f5c49cfecbfc Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:44:30 -0300 Subject: [PATCH 08/14] ci: validate composite action changes Signed-off-by: Vitor Mattos --- .github/workflows/workflow-policy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/workflow-policy.yml b/.github/workflows/workflow-policy.yml index 903dd17..9087d02 100644 --- a/.github/workflows/workflow-policy.yml +++ b/.github/workflows/workflow-policy.yml @@ -8,6 +8,7 @@ on: paths: - '.github/workflows/**' - 'workflow-templates/**' + - 'actions/**' - 'scripts/check_workflow_policy.py' - 'tests/test_workflow_policy.py' push: @@ -16,6 +17,7 @@ on: paths: - '.github/workflows/**' - 'workflow-templates/**' + - 'actions/**' - 'scripts/check_workflow_policy.py' - 'tests/test_workflow_policy.py' From e3ade4672f661a3e4639a976c8f06455a616a12f Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:47:26 -0300 Subject: [PATCH 09/14] fix: harden centralized publication action Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index b737d2f..5445d83 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -5,6 +5,9 @@ name: Build and publish Nextcloud app release description: Build, package, sign, attach and publish a tagged Nextcloud app release. inputs: + app-name: + description: Nextcloud app id and repository directory name. + required: true release-tag: description: Existing Git tag to build and publish. required: true @@ -33,6 +36,7 @@ inputs: description: Use packaging tooling from the workflow ref while keeping tagged application source immutable. required: false default: 'false' + runs: using: composite steps: @@ -42,7 +46,7 @@ runs: require: write token: ${{ inputs.github-token }} - - name: Validate inputs + - name: Validate boolean inputs shell: bash env: CHECKOUT_SUBMODULES: ${{ inputs.checkout-submodules }} @@ -61,15 +65,6 @@ runs: esac done - - name: Set application environment - shell: bash - env: - RELEASE_TAG: ${{ inputs.release-tag }} - run: | - set -euo pipefail - echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> "${GITHUB_ENV}" - echo "APP_VERSION=${RELEASE_TAG}" >> "${GITHUB_ENV}" - - name: Checkout application uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -78,21 +73,21 @@ runs: fetch-tags: true fetch-depth: 0 ref: ${{ inputs.release-tag }} - path: ${{ env.APP_NAME }} + path: ${{ inputs.app-name }} - name: Validate release identity - id: release-identity + id: release_identity uses: LibreCodeCoop/release-tool/actions/release-identity@385ca7732db12e5c79590bb21be8da3608194595 with: tag: ${{ inputs.release-tag }} - working-directory: ${{ env.APP_NAME }} + working-directory: ${{ inputs.app-name }} require-tag-exists: 'true' - name: Get appinfo data id: appinfo uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 with: - filename: ${{ env.APP_NAME }}/appinfo/info.xml + filename: ${{ inputs.app-name }}/appinfo/info.xml expression: "//info//dependencies//nextcloud/@min-version" - name: Read package engines @@ -100,7 +95,7 @@ runs: continue-on-error: true uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 with: - path: ${{ env.APP_NAME }} + path: ${{ inputs.app-name }} fallbackNode: '^24' fallbackNpm: '^11.3' @@ -119,15 +114,15 @@ runs: run: npm i -g "npm@${NPM_VERSION}" - name: Resolve PHP version - id: php-versions + id: php_versions uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 with: - filename: ${{ env.APP_NAME }}/appinfo/info.xml + filename: ${{ inputs.app-name }}/appinfo/info.xml - name: Set up PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: - php-version: ${{ steps.php-versions.outputs.php-min }} + php-version: ${{ steps.php_versions.outputs.php-min }} coverage: none env: GITHUB_TOKEN: ${{ inputs.github-token }} @@ -136,20 +131,21 @@ runs: id: check_composer uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 with: - files: "${{ env.APP_NAME }}/composer.json" + files: "${{ inputs.app-name }}/composer.json" - name: Install composer dependencies if: steps.check_composer.outputs.files_exists == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 with: composer-options: '--no-dev' - working-directory: ${{ env.APP_NAME }} + working-directory: ${{ inputs.app-name }} ignore-cache: 'yes' - name: Build application if: ${{ steps.versions.outputs.nodeVersion }} shell: bash env: + APP_NAME: ${{ inputs.app-name }} CYPRESS_INSTALL_BINARY: 0 run: | set -euo pipefail @@ -161,7 +157,7 @@ runs: id: krankerl uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 with: - files: ${{ env.APP_NAME }}/krankerl.toml + files: ${{ inputs.app-name }}/krankerl.toml - name: Install Krankerl if: steps.krankerl.outputs.files_exists == 'true' @@ -174,12 +170,15 @@ runs: - name: Package with Krankerl if: steps.krankerl.outputs.files_exists == 'true' shell: bash + env: + APP_NAME: ${{ inputs.app-name }} run: | set -euo pipefail cd "${APP_NAME}" krankerl package - name: Resolve Nextcloud server download + id: server_url if: steps.krankerl.outputs.files_exists != 'true' shell: bash env: @@ -187,20 +186,22 @@ runs: 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]')" - echo "DOWNLOAD_URL=${download_url}" >> "${GITHUB_ENV}" + printf 'download-url=%s\n' "${download_url}" >> "${GITHUB_OUTPUT}" - name: Download Nextcloud server - id: server-download - if: steps.krankerl.outputs.files_exists != 'true' && env.DOWNLOAD_URL != 'null' + id: server_download + if: steps.krankerl.outputs.files_exists != 'true' && steps.server_url.outputs.download-url != 'null' continue-on-error: true shell: bash + env: + DOWNLOAD_URL: ${{ steps.server_url.outputs.download-url }} run: | set -euo pipefail wget "${DOWNLOAD_URL}" -O nextcloud.zip unzip -q nextcloud.zip - name: Checkout Nextcloud server fallback - if: steps.krankerl.outputs.files_exists != 'true' && steps.server-download.outcome != 'success' + if: steps.krankerl.outputs.files_exists != 'true' && steps.server_download.outcome != 'success' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -212,10 +213,10 @@ runs: if: inputs.manual-recovery == 'true' && steps.krankerl.outputs.files_exists != 'true' shell: bash env: + APP_NAME: ${{ inputs.app-name }} RELEASE_TAG: ${{ inputs.release-tag }} run: | set -euo pipefail - case "${GITHUB_REF_NAME}" in stable*) ;; *) @@ -223,7 +224,6 @@ runs: exit 1 ;; esac - cd "${APP_NAME}" git fetch --quiet origin "${GITHUB_SHA}" tag_sha="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" @@ -237,6 +237,7 @@ runs: if: steps.krankerl.outputs.files_exists != 'true' shell: bash env: + APP_NAME: ${{ inputs.app-name }} APP_PRIVATE_KEY: ${{ inputs.app-private-key }} REQUIRE_SETUP_SIGNATURES: ${{ inputs.require-setup-signatures }} MAKE_SIGNS_APP: ${{ inputs.make-signs-app }} @@ -256,6 +257,7 @@ runs: if: steps.krankerl.outputs.files_exists != 'true' && inputs.make-signs-app != 'true' shell: bash env: + APP_NAME: ${{ inputs.app-name }} APP_PRIVATE_KEY: ${{ inputs.app-private-key }} run: | set -euo pipefail @@ -269,7 +271,7 @@ runs: tar -zcvf "${APP_NAME}.tar.gz" "${APP_NAME}" - name: Set up PHP 8.3 for release-tool - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 with: php-version: '8.3' coverage: none @@ -279,31 +281,32 @@ runs: - name: Validate release artifact uses: LibreCodeCoop/release-tool/actions/artifact-validate@385ca7732db12e5c79590bb21be8da3608194595 with: - artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz - app-name: ${{ env.APP_NAME }} - version: ${{ steps.release-identity.outputs.version }} + artifact: ${{ inputs.app-name }}/build/artifacts/${{ inputs.app-name }}.tar.gz + app-name: ${{ inputs.app-name }} + version: ${{ steps.release_identity.outputs.version }} - name: Attach tarball to GitHub release - id: attach_to_release - uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2.11.5 - with: - repo_token: ${{ inputs.github-token }} - file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz - asset_name: ${{ env.APP_NAME }}-${{ inputs.release-tag }}.tar.gz - tag: ${{ inputs.release-tag }} - overwrite: true + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + APP_NAME: ${{ inputs.app-name }} + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + set -euo pipefail + asset="${APP_NAME}/build/artifacts/${APP_NAME}.tar.gz" + gh release upload "${RELEASE_TAG}" "${asset}#${APP_NAME}-${RELEASE_TAG}.tar.gz" --clobber --repo "${GITHUB_REPOSITORY}" - name: Upload app to Nextcloud App Store uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 with: - app_name: ${{ env.APP_NAME }} + app_name: ${{ inputs.app-name }} appstore_token: ${{ inputs.appstore-token }} - download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} + download_url: https://github.com/${{ github.repository }}/releases/download/${{ inputs.release-tag }}/${{ inputs.app-name }}-${{ inputs.release-tag }}.tar.gz app_private_key: ${{ inputs.app-private-key }} - name: Verify App Store publication uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@385ca7732db12e5c79590bb21be8da3608194595 with: - app-name: ${{ env.APP_NAME }} - version: ${{ steps.release-identity.outputs.version }} + app-name: ${{ inputs.app-name }} + version: ${{ steps.release_identity.outputs.version }} platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} From e4cb45e1f28d05cb98963c24680f4a0ded0139c6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:47:52 -0300 Subject: [PATCH 10/14] fix: use expression-safe action output name Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index 5445d83..257592a 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -186,15 +186,15 @@ runs: 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]')" - printf 'download-url=%s\n' "${download_url}" >> "${GITHUB_OUTPUT}" + printf 'download_url=%s\n' "${download_url}" >> "${GITHUB_OUTPUT}" - name: Download Nextcloud server id: server_download - if: steps.krankerl.outputs.files_exists != 'true' && steps.server_url.outputs.download-url != 'null' + if: steps.krankerl.outputs.files_exists != 'true' && steps.server_url.outputs.download_url != 'null' continue-on-error: true shell: bash env: - DOWNLOAD_URL: ${{ steps.server_url.outputs.download-url }} + DOWNLOAD_URL: ${{ steps.server_url.outputs.download_url }} run: | set -euo pipefail wget "${DOWNLOAD_URL}" -O nextcloud.zip From b61b583223f3a24d5e1eeaaae651718a089999a0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:48:58 -0300 Subject: [PATCH 11/14] fix: preserve release asset filename Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index 257592a..ae8ef59 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -293,8 +293,11 @@ runs: RELEASE_TAG: ${{ inputs.release-tag }} run: | set -euo pipefail - asset="${APP_NAME}/build/artifacts/${APP_NAME}.tar.gz" - gh release upload "${RELEASE_TAG}" "${asset}#${APP_NAME}-${RELEASE_TAG}.tar.gz" --clobber --repo "${GITHUB_REPOSITORY}" + source_asset="${APP_NAME}/build/artifacts/${APP_NAME}.tar.gz" + asset_name="${APP_NAME}-${RELEASE_TAG}.tar.gz" + publish_asset="${RUNNER_TEMP}/${asset_name}" + cp "${source_asset}" "${publish_asset}" + gh release upload "${RELEASE_TAG}" "${publish_asset}" --clobber --repo "${GITHUB_REPOSITORY}" - name: Upload app to Nextcloud App Store uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 From 27f55ccc8b991f27f6188fd3fe03f5862365a6a2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:49:04 -0300 Subject: [PATCH 12/14] chore: stage publication action before template adoption Signed-off-by: Vitor Mattos --- workflow-templates/appstore-build-publish.yml | 216 ++++++++++++++++-- 1 file changed, 201 insertions(+), 15 deletions(-) diff --git a/workflow-templates/appstore-build-publish.yml b/workflow-templates/appstore-build-publish.yml index 2b2b299..142e70b 100644 --- a/workflow-templates/appstore-build-publish.yml +++ b/workflow-templates/appstore-build-publish.yml @@ -3,7 +3,7 @@ # https://github.com/LibreCodeCoop/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization # -# SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH, LibreCode coop and contributors +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT name: Build and publish app release @@ -11,12 +11,6 @@ name: Build and publish app release on: release: types: [published] - workflow_dispatch: - inputs: - release_tag: - description: Existing release tag to build and publish. - required: true - type: string permissions: contents: write @@ -24,12 +18,204 @@ permissions: jobs: build_and_publish: runs-on: ubuntu-latest + steps: - - name: Build and publish release - uses: LibreCodeCoop/.github/actions/nextcloud-appstore-publish@8873f2e2ac1b9de5396de3d677f57e58c98d772e - with: - release-tag: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.event.release.tag_name }} - github-token: ${{ secrets.GITHUB_TOKEN }} - app-private-key: ${{ secrets.APP_PRIVATE_KEY }} - appstore-token: ${{ secrets.APPSTORE_TOKEN }} - manual-recovery: ${{ github.event_name == 'workflow_dispatch' }} + - name: Check actor permission + uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 + with: + require: write + + - name: Set app env + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: ${{ env.APP_NAME }} + + - name: Get app version number + id: app-version + uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml + expression: "//info//version/text()" + + - name: Validate app version against tag + run: | + [ "${{ env.APP_VERSION }}" = "v${{ fromJSON(steps.app-version.outputs.result).version }}" ] + + - 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 + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + # Continue if no package.json + continue-on-error: true + with: + path: ${{ env.APP_NAME }} + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + # Skip if no package.json + if: ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + package-manager-cache: false + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + # Skip if no package.json + if: ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Get php version + id: php-versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml + + - name: Set up php ${{ steps.php-versions.outputs.php-min }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ steps.php-versions.outputs.php-min }} + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check composer.json + id: check_composer + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: "${{ env.APP_NAME }}/composer.json" + + - name: Install composer dependencies + if: steps.check_composer.outputs.files_exists == 'true' + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + composer-options: '--no-dev' + working-directory: ${{ env.APP_NAME }} + ignore-cache: 'yes' + + - name: Build ${{ env.APP_NAME }} + # Skip if no package.json + if: ${{ steps.versions.outputs.nodeVersion }} + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + cd ${{ env.APP_NAME }} + npm ci + npm run build --if-present + + - name: Check Krankerl config + id: krankerl + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: ${{ env.APP_NAME }}/krankerl.toml + + - name: Install Krankerl + if: steps.krankerl.outputs.files_exists == 'true' + run: | + wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb + sudo dpkg -i krankerl_0.14.0_amd64.deb + + - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with krankerl + if: steps.krankerl.outputs.files_exists == 'true' + run: | + cd ${{ env.APP_NAME }} + krankerl package + + - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with makefile + if: steps.krankerl.outputs.files_exists != 'true' + run: | + cd ${{ env.APP_NAME }} + make appstore + + - name: Verify app store package + if: steps.krankerl.outputs.files_exists != 'true' + working-directory: ${{ env.APP_NAME }} + run: | + if make -qp 2>/dev/null | grep -q '^verify-appstore-package:'; then + make verify-appstore-package + fi + + - name: Check server download link for ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + run: | + NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}' + DOWNLOAD_URL=$(curl -s "https://updates.nextcloud.com/updater_server/latest?channel=beta&version=$NCVERSION" | jq -r '.downloads.zip[0]') + echo "DOWNLOAD_URL=$DOWNLOAD_URL" >> $GITHUB_ENV + + - name: Download server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + continue-on-error: true + id: server-download + if: ${{ env.DOWNLOAD_URL != 'null' }} + run: | + echo "Downloading release tarball from $DOWNLOAD_URL" + wget $DOWNLOAD_URL -O nextcloud.zip + unzip nextcloud.zip + + - name: Checkout server master fallback + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: ${{ steps.server-download.outcome != 'success' }} + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + path: nextcloud + + + - name: Sign app + run: | + # Extracting release + cd ${{ env.APP_NAME }}/build/artifacts + tar -xvf ${{ env.APP_NAME }}.tar.gz + cd ../../../ + # Setting up keys + echo '${{ secrets.APP_PRIVATE_KEY }}' > ${{ env.APP_NAME }}.key + wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt" + # Signing + php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }} + # Rebuilding archive + cd ${{ env.APP_NAME }}/build/artifacts + tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }} + + - name: Validate release artifact + uses: LibreCodeCoop/release-tool/actions/artifact-validate@710c4c83fba47bf01713f46e9c4cb4cf63debfba + with: + artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + app-name: ${{ env.APP_NAME }} + version: ${{ env.APP_VERSION }} + + - name: Attach tarball to github release + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5 + id: attach_to_release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz + tag: ${{ github.ref }} + overwrite: true + + - name: Upload app to Nextcloud appstore + uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 + with: + app_name: ${{ env.APP_NAME }} + appstore_token: ${{ secrets.APPSTORE_TOKEN }} + download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Verify App Store publication + uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@710c4c83fba47bf01713f46e9c4cb4cf63debfba + with: + app-name: ${{ env.APP_NAME }} + version: ${{ env.APP_VERSION }} + platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} From 53a5c372d82949b4a9e5a21b71e83ffacdccda5e Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:49:44 -0300 Subject: [PATCH 13/14] test: harden publication action regression coverage Signed-off-by: Vitor Mattos --- tests/test_appstore_publication_contract.py | 24 +++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_appstore_publication_contract.py b/tests/test_appstore_publication_contract.py index 27aea9a..8524918 100644 --- a/tests/test_appstore_publication_contract.py +++ b/tests/test_appstore_publication_contract.py @@ -8,16 +8,9 @@ ROOT = Path(__file__).resolve().parents[1] ACTION = ROOT / "actions/nextcloud-appstore-publish/action.yml" -TEMPLATE = ROOT / "workflow-templates/appstore-build-publish.yml" class AppStorePublicationContractTest(unittest.TestCase): - def test_consumer_template_is_thin_wrapper(self) -> None: - content = TEMPLATE.read_text(encoding="utf-8") - self.assertNotIn("\n run: |", content) - self.assertIn("LibreCodeCoop/.github/actions/nextcloud-appstore-publish@", content) - self.assertLessEqual(len(content.splitlines()), 40) - def test_external_actions_are_immutable(self) -> None: content = ACTION.read_text(encoding="utf-8") revisions = re.findall(r"^\s*uses:\s*([^@\s]+)@([^\s#]+)", content, re.MULTILINE) @@ -55,6 +48,23 @@ def test_expression_ids_are_safe(self) -> None: for step_id in ids: self.assertRegex(step_id, r"^[A-Za-z_][A-Za-z0-9_]*$") + def test_does_not_write_untrusted_values_to_github_env(self) -> None: + content = ACTION.read_text(encoding="utf-8") + self.assertNotIn("GITHUB_ENV", content) + + def test_release_upload_uses_runner_cli(self) -> None: + content = ACTION.read_text(encoding="utf-8") + self.assertNotIn("svenstaro/upload-release-action", content) + self.assertIn('gh release upload "${RELEASE_TAG}"', content) + + def test_asset_name_matches_appstore_download_url(self) -> None: + content = ACTION.read_text(encoding="utf-8") + self.assertIn('asset_name="${APP_NAME}-${RELEASE_TAG}.tar.gz"', content) + self.assertIn( + "releases/download/${{ inputs.release-tag }}/${{ inputs.app-name }}-${{ inputs.release-tag }}.tar.gz", + content, + ) + if __name__ == "__main__": unittest.main() From 39f9e19cc650a647b4aa5e2a1da3ec76c17a1c24 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 29 Sep 2026 10:57:47 -0300 Subject: [PATCH 14/14] refactor: rely on Node-bundled npm for reproducible builds Signed-off-by: Vitor Mattos --- actions/nextcloud-appstore-publish/action.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/actions/nextcloud-appstore-publish/action.yml b/actions/nextcloud-appstore-publish/action.yml index ae8ef59..e2ebb5a 100644 --- a/actions/nextcloud-appstore-publish/action.yml +++ b/actions/nextcloud-appstore-publish/action.yml @@ -97,7 +97,6 @@ runs: with: path: ${{ inputs.app-name }} fallbackNode: '^24' - fallbackNpm: '^11.3' - name: Set up node if: ${{ steps.versions.outputs.nodeVersion }} @@ -106,13 +105,6 @@ runs: node-version: ${{ steps.versions.outputs.nodeVersion }} package-manager-cache: false - - name: Set up npm - if: ${{ steps.versions.outputs.npmVersion }} - shell: bash - env: - NPM_VERSION: ${{ steps.versions.outputs.npmVersion }} - run: npm i -g "npm@${NPM_VERSION}" - - name: Resolve PHP version id: php_versions uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3