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
48 changes: 48 additions & 0 deletions tests/test_nightly_release_template.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions workflow-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"lint-php-cs",
"lint-stylelint",
"lint-typescript",
"nightly-release",
"node-test",
"npm-build",
"openapi",
Expand Down
5 changes: 5 additions & 0 deletions workflow-templates/nightly-release.properties.json
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions workflow-templates/nightly-release.properties.json.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
297 changes: 297 additions & 0 deletions workflow-templates/nightly-release.yml
Original file line number Diff line number Diff line change
@@ -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
Loading