From 9aa2205689b3df13506bb1018faf3dbe646a5d11 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 20:57:45 -0300 Subject: [PATCH 1/2] feat: manage nightly release workflow Signed-off-by: Vitor Mattos --- tests/test_nightly_release_template.py | 48 +++ workflow-catalog.json | 1 + .../nightly-release.properties.json | 5 + workflow-templates/nightly-release.yml | 297 ++++++++++++++++++ 4 files changed, 351 insertions(+) create mode 100644 tests/test_nightly_release_template.py create mode 100644 workflow-templates/nightly-release.properties.json create mode 100644 workflow-templates/nightly-release.yml diff --git a/tests/test_nightly_release_template.py b/tests/test_nightly_release_template.py new file mode 100644 index 0000000..482fa1a --- /dev/null +++ b/tests/test_nightly_release_template.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import json +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "workflow-templates" / "nightly-release.yml" +CATALOG = ROOT / "workflow-catalog.json" + + +class NightlyReleaseTemplateTest(unittest.TestCase): + def test_template_is_published(self) -> None: + catalog = json.loads(CATALOG.read_text(encoding="utf-8")) + self.assertIn("nightly-release", catalog["templates"]) + + def test_only_latest_exact_stable_can_publish(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + sha = "0bac49723213a5aa8e779d365a7b684608cf6989" + + self.assertIn(f"actions/release-stable-select@{sha} # v0.6.0", content) + self.assertIn("needs.check-latest-stable.outputs.is-latest == 'true'", content) + self.assertIn("current-branch:", content) + self.assertIn("latest-branch:", content) + self.assertIn("latest-major:", content) + + def test_nightly_runs_full_package_and_publication_path(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + + self.assertIn("make appstore", content) + self.assertIn("verify-appstore-package", content) + self.assertIn("integrity:sign-app", content) + self.assertIn("actions/release-artifact-validate@", content) + self.assertIn("gh release create nightly", content) + self.assertIn("nextcloud-libraries/nextcloud-appstore-push-action@", content) + self.assertIn("nightly: true", content) + + def test_checkout_credentials_are_not_persisted(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + + self.assertNotIn("persist-credentials: true", content) + self.assertIn("permissions:\n contents: write", content) + self.assertNotIn("actions: write", content) + + +if __name__ == "__main__": + unittest.main() diff --git a/workflow-catalog.json b/workflow-catalog.json index 8d9e40f..24ff745 100644 --- a/workflow-catalog.json +++ b/workflow-catalog.json @@ -8,6 +8,7 @@ "lint-php-cs", "lint-stylelint", "lint-typescript", + "nightly-release", "node-test", "npm-build", "openapi", diff --git a/workflow-templates/nightly-release.properties.json b/workflow-templates/nightly-release.properties.json new file mode 100644 index 0000000..8b8864e --- /dev/null +++ b/workflow-templates/nightly-release.properties.json @@ -0,0 +1,5 @@ +{ + "name": "Publish nightly release", + "description": "Build, sign, validate and publish a nightly Nextcloud app package only from the highest numeric stable branch.", + "iconName": "octicon moon" +} diff --git a/workflow-templates/nightly-release.yml b/workflow-templates/nightly-release.yml new file mode 100644 index 0000000..f0a0f78 --- /dev/null +++ b/workflow-templates/nightly-release.yml @@ -0,0 +1,297 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Nightly release + +on: + push: + branches: + - 'stable*' + paths: + - '**' + - '!**.md' + - '!.github/**' + - '.github/workflows/nightly-release.yml' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: nightly-release-${{ github.repository }} + cancel-in-progress: true + +jobs: + check-latest-stable: + name: Check latest stable + runs-on: ubuntu-latest + outputs: + is-latest: ${{ steps.stable.outputs.is-latest }} + current-branch: ${{ steps.stable.outputs.current-branch }} + current-major: ${{ steps.stable.outputs.current-major }} + latest-branch: ${{ steps.stable.outputs.latest-branch }} + latest-major: ${{ steps.stable.outputs.latest-major }} + steps: + - id: stable + name: Resolve stable release line + uses: LibreCodeCoop/github-workflows/actions/release-stable-select@0bac49723213a5aa8e779d365a7b684608cf6989 # v0.6.0 + + nightly-release: + name: Build and publish nightly + needs: check-latest-stable + if: needs.check-latest-stable.outputs.is-latest == 'true' + 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 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + fetch-depth: 0 + submodules: true + 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: Set app version + shell: bash + env: + APP_VERSION: ${{ fromJSON(steps.app-version.outputs.result).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@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + 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 + 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 }} + if: steps.versions.outputs.npmVersion + shell: bash + 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 }} + if: steps.versions.outputs.nodeVersion + env: + CYPRESS_INSTALL_BINARY: 0 + shell: bash + run: | + 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: | + 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 with Krankerl + if: steps.krankerl.outputs.files_exists == 'true' + shell: bash + run: | + cd "${APP_NAME}" + krankerl package + + - name: Package with Makefile + if: steps.krankerl.outputs.files_exists != 'true' + shell: bash + run: | + cd "${APP_NAME}" + make appstore + + - name: Verify app store package + if: steps.krankerl.outputs.files_exists != 'true' + working-directory: ${{ env.APP_NAME }} + shell: bash + run: | + if make -qp 2>/dev/null | grep -q '^verify-appstore-package:'; then + make verify-appstore-package + fi + + - name: Resolve Nextcloud server download + id: server-download + continue-on-error: true + shell: bash + env: + NEXTCLOUD_VERSION: ${{ fromJSON(steps.appinfo.outputs.result).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" + wget -q "${download_url}" -O nextcloud.zip + unzip -q nextcloud.zip + + - name: Checkout server master fallback + if: steps.server-download.outcome != 'success' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + path: nextcloud + + - name: Sign app + shell: bash + env: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + run: | + cd "${APP_NAME}/build/artifacts" + tar -xzf "${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 -zcf "${APP_NAME}.tar.gz" "${APP_NAME}" + + - name: Validate release artifact + uses: LibreCodeCoop/github-workflows/actions/release-artifact-validate@0bac49723213a5aa8e779d365a7b684608cf6989 # v0.6.0 + with: + artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + app-name: ${{ env.APP_NAME }} + version: ${{ env.APP_VERSION }} + + - name: Define nightly metadata + id: nightly + shell: bash + run: | + echo "tag=nightly" >> "${GITHUB_OUTPUT}" + echo "branch=${{ needs.check-latest-stable.outputs.current-branch }}" >> "${GITHUB_OUTPUT}" + + - name: Move nightly tag to this commit + shell: bash + env: + 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 + else + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref='refs/tags/nightly' -f sha="${GITHUB_SHA}" >/dev/null + fi + + - name: Build nightly release notes + working-directory: ${{ env.APP_NAME }} + shell: bash + env: + NIGHTLY_BRANCH: ${{ steps.nightly.outputs.branch }} + run: | + last_tag="$(git tag --list --sort=-version:refname | grep -v '^nightly$' | head -1 || true)" + if [ -n "${last_tag}" ]; then + commits="$(git log "${last_tag}..HEAD" --pretty=format:'- %s (%h)' --no-merges)" + else + commits="$(git log -10 --pretty=format:'- %s (%h)' --no-merges)" + fi + { + echo "## 🌙 Nightly Build - ${APP_VERSION}" + echo + echo "Automated nightly build from `${NIGHTLY_BRANCH}`." + echo + echo "⚠️ **Development version** - may contain bugs or unstable features." + echo + echo "### Recent Changes" + echo + printf '%s\n' "${commits}" + echo + echo "---" + echo "Generated from commit `${GITHUB_SHA:0:7}`." + } > release-notes.md + + - name: Create or update GitHub release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + NIGHTLY_BRANCH: ${{ steps.nightly.outputs.branch }} + run: | + title="Nightly ${APP_VERSION} (${NIGHTLY_BRANCH})" + notes="${APP_NAME}/release-notes.md" + if gh release view nightly --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then + gh release edit nightly --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file "${notes}" --prerelease + else + gh release create nightly --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file "${notes}" --prerelease + fi + + - name: Attach tarball to GitHub release + id: attach-to-release + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2.11.5 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + asset_name: ${{ env.APP_NAME }}-nightly.tar.gz + tag: nightly + overwrite: true + + - name: Upload nightly to Nextcloud App Store + 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 }} + nightly: true From 86baa0a5bcc2a13b1c4b33a6584eafa3b8e362fb Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 20:58:25 -0300 Subject: [PATCH 2/2] chore: add REUSE metadata for nightly template Signed-off-by: Vitor Mattos --- workflow-templates/nightly-release.properties.json.license | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 workflow-templates/nightly-release.properties.json.license diff --git a/workflow-templates/nightly-release.properties.json.license b/workflow-templates/nightly-release.properties.json.license new file mode 100644 index 0000000..a7eb745 --- /dev/null +++ b/workflow-templates/nightly-release.properties.json.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later