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
15 changes: 15 additions & 0 deletions LICENSES/MIT.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Copyright <YEAR> <COPYRIGHT HOLDER>

Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and associated documentation files (the “Software”),
to deal in the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 changes: 20 additions & 2 deletions scripts/sync_upstream.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse
from urllib.request import Request, urlopen


Expand Down Expand Up @@ -43,8 +44,7 @@ def load_sources(manifest_path: Path) -> list[Source]:

if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
raise ValueError(f"sources[{index}].sha256 must be 64 lowercase hex characters")
if "/refs/heads/" in url or url.endswith(("/main", "/master")):
raise ValueError(f"sources[{index}].url must be pinned to an immutable commit")
_validate_immutable_url(url, f"sources[{index}].url")

sources.append(
Source(
Expand Down Expand Up @@ -91,6 +91,24 @@ def check(sources: list[Source], root: Path) -> None:
raise ValueError("generated templates are out of date: " + ", ".join(drift))


def _validate_immutable_url(url: str, path: str) -> None:
parsed = urlparse(url)
if parsed.scheme != "https":
raise ValueError(f"{path} must use https")

if parsed.hostname == "raw.githubusercontent.com":
parts = [part for part in parsed.path.split("/") if part]
if len(parts) < 4:
raise ValueError(f"{path} is not a valid raw GitHub file URL")
revision = parts[2]
if len(revision) != 40 or any(
char not in "0123456789abcdefABCDEF" for char in revision
):
raise ValueError(f"{path} must pin a 40-character Git commit SHA")
elif "/refs/heads/" in parsed.path:
raise ValueError(f"{path} must not reference a mutable branch")


def _safe_destination(root: Path, destination: Path) -> Path:
if destination.is_absolute() or ".." in destination.parts:
raise ValueError(f"unsafe destination: {destination}")
Expand Down
22 changes: 22 additions & 0 deletions tests/test_sync_upstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ def test_rejects_invalid_hash(self) -> None:
with self.assertRaisesRegex(ValueError, "sha256"):
load_sources(manifest)

def test_rejects_mutable_raw_github_revision(self) -> None:
with tempfile.TemporaryDirectory() as directory:
manifest = Path(directory) / "sources.json"
manifest.write_text(
json.dumps(
{
"sources": [
{
"name": "workflow",
"url": "https://raw.githubusercontent.com/example/project/master/workflow.yml",
"sha256": "a" * 64,
"destination": "templates/workflow.yml",
}
]
}
),
encoding="utf-8",
)

with self.assertRaisesRegex(ValueError, "40-character Git commit SHA"):
load_sources(manifest)

def test_sync_writes_verified_content(self) -> None:
content = b"name: Example\n"
source = Source(
Expand Down
9 changes: 8 additions & 1 deletion upstream/sources.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
{
"sources": []
"sources": [
{
"name": "nextcloud-appstore-build-publish",
"url": "https://raw.githubusercontent.com/nextcloud/.github/cf6248d5ef28a328cde764daf80bc5fae4705ade/workflow-templates/appstore-build-publish.yml",
"sha256": "4710d78c576abd1c875d799770f67262988119404d58d379656f5932872fcba8",
"destination": "upstream/vendor/nextcloud/appstore-build-publish.yml"
}
]
}
202 changes: 202 additions & 0 deletions upstream/vendor/nextcloud/appstore-build-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.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-License-Identifier: MIT

name: Build and publish app release

on:
release:
types: [published]

permissions:
contents: write

jobs:
build_and_publish:
runs-on: ubuntu-latest

# Only allowed to be run on nextcloud-releases repositories
if: ${{ github.repository_owner == 'nextcloud-releases' }}

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: 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: 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 }}
Loading