21 Commits

Author SHA1 Message Date
dca08bb6c3 feat(nextcloud): add min-php input to php matrix jobs 2026-05-17 23:23:03 +03:00
808077ef6b feat(release-please): per-scope routing for fastlane changelogs 2026-05-15 23:45:27 +03:00
c8c1609cba feat: add release-please+fastlane workflow 2026-04-12 23:06:05 +03:00
ca85c41ef4 fix(nextcloud): show all issues in php tests 2026-04-06 00:09:01 +03:00
a69ee802a6 feat(homebrew): add standalone homebrew release workflow 2026-03-19 00:39:37 +02:00
70151d294e fix: nextcloud path filter 2026-03-10 00:27:05 +02:00
dd6e138c7a docs: add input examples & required column 2026-01-24 00:17:11 +02:00
94a677ab82 docs: add TOC to README.md 2026-01-24 00:00:08 +02:00
5755a7bfd8 build: remove global tag from makefile 2026-01-23 23:44:20 +02:00
388711cc86 feat: manual-homebrew-release workflow 2026-01-23 23:44:20 +02:00
e203813f6f docs: update README.md 2026-01-23 18:47:53 +02:00
0808232cc6 fix(nextcloud): improve filtering 2026-01-23 02:19:25 +02:00
1df942d283 build: update makefile 2026-01-23 01:10:51 +02:00
fdc3b6d89f fix: nextcloud phpunit matrix deps 2026-01-23 01:06:06 +02:00
f708347ed3 fix: nextcloud workflows permissions 2026-01-23 00:55:02 +02:00
6d06d8bbc5 fix: nextcloud workflows path filters 2026-01-23 00:48:49 +02:00
2a0c1c7c22 build: add tag tasks 2026-01-23 00:45:54 +02:00
b61dbc6d15 feat: add nextcloud workflows 2026-01-23 00:30:40 +02:00
135a943972 docs: update README.md 2026-01-22 23:42:02 +02:00
ed9ea64941 docs: add LICENSE 2026-01-22 23:38:30 +02:00
50c6be783b docs: add README.md 2026-01-22 23:38:30 +02:00
20 changed files with 2739 additions and 0 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
# EditorConfig for Makefile
root = true
[Makefile]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

89
.github/workflows/homebrew-release.yml vendored Normal file
View File

@@ -0,0 +1,89 @@
# Reusable Homebrew Release Workflow
#
# Dispatches a homebrew formula update to a tap repo.
# Does not include any build steps — suitable for shell-only packages.
#
# Call from other repos:
# jobs:
# homebrew:
# uses: chenasraf/workflows/.github/workflows/homebrew-release.yml@master
# with:
# homebrew-tap-repo: owner/homebrew-tap
# tag: ${{ needs.release.outputs.tag_name }}
# secrets:
# REPO_DISPATCH_PAT: ${{ secrets.REPO_DISPATCH_PAT }}
name: Homebrew Release
on:
workflow_call:
inputs:
homebrew-tap-repo:
description: 'Homebrew tap repo to dispatch to (e.g., owner/homebrew-tap)'
required: true
type: string
tag:
description: 'Release tag to dispatch (e.g., v1.0.0). If empty, uses the latest release tag.'
required: false
type: string
default: ''
secrets:
REPO_DISPATCH_PAT:
description: 'PAT for dispatching to homebrew tap repo'
required: true
permissions:
contents: read
jobs:
release-homebrew:
name: Trigger Homebrew Formula Update
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get release info
id: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${{ inputs.tag }}"
if [[ -z "$tag" ]]; then
tag=$(gh release view --json tagName -q .tagName)
echo "Using latest release tag: $tag"
else
echo "Using provided tag: $tag"
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
body=$(gh release view "$tag" --json body -q .body)
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$body" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Send dispatch to homebrew-tap
env:
GH_TOKEN: ${{ secrets.REPO_DISPATCH_PAT }}
run: |
tag="${{ steps.release.outputs.tag }}"
repo="${{ github.event.repository.name }}"
body=$(cat <<'BODY_EOF'
${{ steps.release.outputs.body }}
BODY_EOF
)
data=$(jq -n \
--arg tag "$tag" \
--arg repo "$repo" \
--arg body "$body" \
'{event_type: "trigger-from-release", client_payload: {tag: $tag, repo: $repo, body: $body}}')
echo "Dispatching tag $tag from $repo"
echo "Data: $data"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${{ inputs.homebrew-tap-repo }}/dispatches" \
-d "$data"
echo "Dispatched tag $tag from $repo"
echo "Created job on https://github.com/${{ inputs.homebrew-tap-repo }}/actions"

View File

@@ -0,0 +1,75 @@
# Reusable Manual Homebrew Release Workflow
#
# Call from other repos:
# jobs:
# homebrew:
# uses: chenasraf/workflows/.github/workflows/manual-homebrew-release.yml@master
# with:
# homebrew-tap-repo: owner/homebrew-tap
# secrets:
# REPO_DISPATCH_PAT: ${{ secrets.REPO_DISPATCH_PAT }}
name: Manual Homebrew Release
on:
workflow_call:
inputs:
homebrew-tap-repo:
description: 'Homebrew tap repo to dispatch to (e.g., owner/homebrew-tap)'
required: true
type: string
secrets:
REPO_DISPATCH_PAT:
description: 'PAT for dispatching to homebrew tap repo'
required: true
permissions:
contents: read
jobs:
release-homebrew:
name: Trigger Homebrew Formula Update
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get latest release info
id: latest
run: |
tag=$(gh release view --json tagName -q .tagName)
echo "Latest release tag: $tag"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
body=$(gh release view --json body -q .body)
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$body" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Send dispatch to homebrew-tap
env:
GH_TOKEN: ${{ secrets.REPO_DISPATCH_PAT }}
run: |
tag="${{ steps.latest.outputs.tag }}"
repo="${{ github.event.repository.name }}"
body=$(cat <<'BODY_EOF'
${{ steps.latest.outputs.body }}
BODY_EOF
)
data=$(jq -n \
--arg tag "$tag" \
--arg repo "$repo" \
--arg body "$body" \
'{event_type: "trigger-from-release", client_payload: {tag: $tag, repo: $repo, body: $body}}')
echo "Dispatching tag $tag from $repo"
echo "Data: $data"
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/${{ inputs.homebrew-tap-repo }}/dispatches" \
-d "$data"
echo "Dispatched tag $tag from $repo"
echo "Created job on https://github.com/${{ inputs.homebrew-tap-repo }}/actions"

View File

@@ -0,0 +1,33 @@
# Reusable workflow for blocking unconventional commits
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Block unconventional commits
on:
workflow_call:
inputs:
allowed-types:
description: 'Comma-separated list of allowed commit types (leave empty for defaults: feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert)'
required: false
type: string
default: ''
permissions:
pull-requests: read
contents: read
jobs:
block-unconventional-commits:
name: Block unconventional commits
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: webiny/action-conventional-commits@v1.3.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,94 @@
# Reusable workflow for NPM build verification
#
# SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Build NPM
on:
workflow_call:
inputs:
build-command:
description: 'Command to run build'
required: false
type: string
default: 'pnpm build'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '.github/workflows/**'
- 'src/**'
- '*.ts'
- '*.js'
- '*.json'
- '*.yaml'
- 'pnpm-lock.yaml'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
build:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: PNPM Build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_DOWNLOAD: true
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Build
run: ${{ inputs.build-command }}
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, build]
if: always()
name: build-npm-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.build.result != 'success' }}; then exit 1; fi

View File

@@ -0,0 +1,90 @@
# Reusable workflow for Nextcloud appinfo.xml linting
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint appinfo.xml
on:
workflow_call:
inputs:
xml-file:
description: 'Path to the info.xml file'
required: false
type: string
default: './appinfo/info.xml'
schema-url:
description: 'URL to the XSD schema file'
required: false
type: string
default: 'https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- 'appinfo/info.xml'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
xml-linters:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: info.xml lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download schema
run: wget ${{ inputs.schema-url }} -O info.xsd
- name: Lint info.xml
uses: ChristophWurst/xmllint-action@v1
with:
xml-file: ${{ inputs.xml-file }}
xml-schema-file: ./info.xsd
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, xml-linters]
if: always()
name: appinfo-xml-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.xml-linters.result != 'success' }}; then exit 1; fi

View File

@@ -0,0 +1,94 @@
# Reusable workflow for ESLint linting
#
# SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
# SPDX-License-Identifier: AGPL-3.0-or-later
name: Lint eslint
on:
workflow_call:
inputs:
lint-command:
description: 'Command to run lint'
required: false
type: string
default: 'pnpm lint'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '.github/workflows/**'
- 'src/**'
- '*.ts'
- '*.js'
- '*.json'
- '*.yaml'
- 'pnpm-lock.yaml'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
lint:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: NPM lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_DOWNLOAD: true
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Lint
run: ${{ inputs.lint-command }}
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, lint]
if: always()
name: eslint
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.lint.result != 'success' }}; then exit 1; fi

View File

@@ -0,0 +1,126 @@
# Reusable workflow for OpenAPI spec linting
#
# SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint OpenAPI
on:
workflow_call:
inputs:
openapi-command:
description: 'Composer command to regenerate OpenAPI'
required: false
type: string
default: 'composer run openapi'
typescript-types-pattern:
description: 'Glob pattern to check for TypeScript OpenAPI types'
required: false
type: string
default: 'src/types/openapi/openapi*.ts'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- 'lib/**/*.php'
- 'openapi.json'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
openapi:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true' && github.repository_owner != 'nextcloud-gmbh'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get php version
id: php_versions
uses: icewind1991/nextcloud-version-matrix@v1
- name: Set up php
uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.php_versions.outputs.php-available }}
extensions: xml
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check Typescript OpenApi types
id: check_typescript_openapi
uses: andstor/file-existence-action@v3
with:
files: ${{ inputs.typescript-types-pattern }}
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_DOWNLOAD: true
run: |
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Set up dependencies
run: composer i
- name: Regenerate OpenAPI
run: ${{ inputs.openapi-command }}
- name: Check openapi*.json and typescript changes
run: |
bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please run \"composer run openapi\" and commit the openapi*.json files and (if applicable) src/types/openapi/openapi*.ts, see the section \"Show changes on failure\" for details' && exit 1)"
- name: Show changes on failure
if: failure()
run: |
git status
git --no-pager diff
exit 1
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, openapi]
if: always()
name: openapi-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.openapi.result != 'success' && needs.openapi.result != 'skipped' }}; then exit 1; fi

View File

@@ -0,0 +1,103 @@
# Reusable workflow for PHP code style linting
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint php-cs
on:
workflow_call:
inputs:
cs-check-command:
description: 'Composer command to check code style'
required: false
type: string
default: "composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 )"
php-extensions:
description: 'PHP extensions to install'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '**.php'
- '.php-cs-fixer.dist.php'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
lint:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: php-cs
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get php version
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
- name: Set up php${{ steps.versions.outputs.php-available }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.versions.outputs.php-available }}
extensions: ${{ inputs.php-extensions }}
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: composer i
- name: Lint
run: ${{ inputs.cs-check-command }}
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, lint]
if: always()
name: php-cs-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.lint.result != 'success' }}; then exit 1; fi

122
.github/workflows/nextcloud-lint-php.yml vendored Normal file
View File

@@ -0,0 +1,122 @@
# Reusable workflow for PHP syntax linting
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint php
on:
workflow_call:
inputs:
lint-command:
description: 'Composer command to run lint'
required: false
type: string
default: 'composer run lint'
php-versions-min:
description: 'Minimum PHP version to include in the lint matrix (filters out anything lower than this from the auto-detected matrix)'
required: false
type: string
default: '8.1'
php-extensions:
description: 'PHP extensions to install'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '**.php'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
matrix:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
outputs:
php-versions: ${{ steps.filter.outputs.php-versions }}
steps:
- name: Checkout app
uses: actions/checkout@v4
- name: Get version matrix
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
- name: Filter php versions by minimum
id: filter
run: |
FILTERED=$(echo '${{ steps.versions.outputs.php-versions }}' | jq -c --arg min "${{ inputs.php-versions-min }}" '[.[] | select(. >= $min)]')
echo "php-versions=$FILTERED" >> $GITHUB_OUTPUT
echo "Filtered php matrix (>= ${{ inputs.php-versions-min }}): $FILTERED"
php-lint:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src == 'true'
strategy:
matrix:
php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}}
name: php-lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ inputs.php-extensions }}
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Lint
run: ${{ inputs.lint-command }}
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, php-lint]
if: always()
name: php-lint-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.php-lint.result != 'success' }}; then exit 1; fi

View File

@@ -0,0 +1,380 @@
# Reusable workflow for incremental migration tests
#
# This workflow tests that migrations work correctly when upgrading from an older version.
#
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit Incremental Migration
on:
workflow_call:
inputs:
baseline-version:
description: 'Git tag/ref for the baseline version to upgrade from'
required: true
type: string
php-version:
description: 'PHP version to test'
required: false
type: string
default: '8.3'
php-extensions-mysql:
description: 'PHP extensions for MySQL tests'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql'
php-extensions-pgsql:
description: 'PHP extensions for PostgreSQL tests'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql'
validation-query:
description: 'SQL query to validate migration (e.g., "SELECT COUNT(*) FROM oc_forum_users")'
required: false
type: string
default: ''
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'tests/**'
- 'composer.json'
- 'composer.lock'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
incremental-pgsql:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: Incremental Migration (PostgreSQL)
services:
postgres:
image: ghcr.io/nextcloud/continuous-integration-postgres-16:latest
ports:
- 4444:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout app (current)
uses: actions/checkout@v4
with:
persist-credentials: false
path: app-current
fetch-depth: 0
- name: Detect app ID from appinfo/info.xml
run: |
APP_ID=$(grep -oP '(?<=<id>)[^<]+' app-current/appinfo/info.xml | head -1)
echo "APP_NAME=$APP_ID" >> $GITHUB_ENV
echo "Detected app ID: $APP_ID"
- name: Get supported server versions
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
with:
filename: app-current/appinfo/info.xml
- name: Save current app for later
run: |
mkdir -p /tmp/app-backup
cp -r app-current /tmp/app-backup/
- name: Checkout server
uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ fromJson(steps.versions.outputs.branches)[0] }}
- name: Checkout app at baseline version
uses: actions/checkout@v4
with:
persist-credentials: false
ref: ${{ inputs.baseline-version }}
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ inputs.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ inputs.php-version }}
extensions: ${{ inputs.php-extensions-pgsql }}
coverage: none
ini-file: development
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up dependencies (baseline)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i --no-scripts || composer i
- name: Set up Nextcloud and install app at baseline
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
echo "::group::Installing app at baseline"
./occ app:enable --force ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after baseline install"
./occ db:convert-filecache-bigint --no-interaction || true
PGPASSWORD=rootpassword psql -h 127.0.0.1 -p $DB_PORT -U root -d nextcloud -c "\dt oc_${{ env.APP_NAME }}_*"
echo "::endgroup::"
- name: Upgrade app to current version
run: |
echo "::group::Replacing app with current version"
rm -rf apps/${{ env.APP_NAME }}
cp -r /tmp/app-backup/app-current apps/${{ env.APP_NAME }}
echo "::endgroup::"
- name: Set up dependencies (current)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i
- name: Run migrations to current version
env:
DB_PORT: 4444
run: |
echo "::group::Running upgrade migrations"
./occ maintenance:mode --off || true
./occ app:disable ${{ env.APP_NAME }}
./occ app:enable ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after upgrade"
PGPASSWORD=rootpassword psql -h 127.0.0.1 -p $DB_PORT -U root -d nextcloud -c "\dt oc_${{ env.APP_NAME }}_*"
echo "::endgroup::"
- name: Validate migration
if: inputs.validation-query != ''
env:
DB_PORT: 4444
run: |
echo "::group::Running validation query"
PGPASSWORD=rootpassword psql -h 127.0.0.1 -p $DB_PORT -U root -d nextcloud -c "${{ inputs.validation-query }}" || exit 1
echo "::endgroup::"
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
incremental-mysql:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: Incremental Migration (MySQL)
services:
mysql:
image: ghcr.io/nextcloud/continuous-integration-mariadb-10.11:latest
ports:
- 4444:3306/tcp
env:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: nextcloud
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout app (current)
uses: actions/checkout@v4
with:
persist-credentials: false
path: app-current
fetch-depth: 0
- name: Detect app ID from appinfo/info.xml
run: |
APP_ID=$(grep -oP '(?<=<id>)[^<]+' app-current/appinfo/info.xml | head -1)
echo "APP_NAME=$APP_ID" >> $GITHUB_ENV
echo "Detected app ID: $APP_ID"
- name: Get supported server versions
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
with:
filename: app-current/appinfo/info.xml
- name: Save current app for later
run: |
mkdir -p /tmp/app-backup
cp -r app-current /tmp/app-backup/
- name: Checkout server
uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ fromJson(steps.versions.outputs.branches)[0] }}
- name: Checkout app at baseline version
uses: actions/checkout@v4
with:
persist-credentials: false
ref: ${{ inputs.baseline-version }}
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ inputs.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ inputs.php-version }}
extensions: ${{ inputs.php-extensions-mysql }}
coverage: none
ini-file: development
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up dependencies (baseline)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i --no-scripts || composer i
- name: Set up Nextcloud and install app at baseline
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
echo "::group::Installing app at baseline"
./occ app:enable --force ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after baseline install"
mysql -h 127.0.0.1 -P $DB_PORT -u root -prootpassword nextcloud -e "SHOW TABLES LIKE 'oc_${{ env.APP_NAME }}_%'"
echo "::endgroup::"
- name: Upgrade app to current version
run: |
echo "::group::Replacing app with current version"
rm -rf apps/${{ env.APP_NAME }}
cp -r /tmp/app-backup/app-current apps/${{ env.APP_NAME }}
echo "::endgroup::"
- name: Set up dependencies (current)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i
- name: Run migrations to current version
env:
DB_PORT: 4444
run: |
echo "::group::Running upgrade migrations"
./occ maintenance:mode --off || true
./occ app:disable ${{ env.APP_NAME }}
./occ app:enable ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after upgrade"
mysql -h 127.0.0.1 -P $DB_PORT -u root -prootpassword nextcloud -e "SHOW TABLES LIKE 'oc_${{ env.APP_NAME }}_%'"
echo "::endgroup::"
- name: Validate migration
if: inputs.validation-query != ''
env:
DB_PORT: 4444
run: |
echo "::group::Running validation query"
mysql -h 127.0.0.1 -P $DB_PORT -u root -prootpassword nextcloud -e "${{ inputs.validation-query }}" || exit 1
echo "::endgroup::"
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, incremental-pgsql, incremental-mysql]
if: always()
name: incremental-migration-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && (needs.incremental-pgsql.result != 'success' || needs.incremental-mysql.result != 'success') }}; then exit 1; fi

View File

@@ -0,0 +1,243 @@
# Reusable workflow for PHPUnit tests with MySQL
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit MySQL
on:
workflow_call:
inputs:
php-versions-min:
description: 'Minimum PHP version to test'
required: false
type: string
default: '8.2'
php-versions-max:
description: 'Maximum PHP version to test'
required: false
type: string
default: '8.3'
mysql-version:
description: 'MySQL version to use'
required: false
type: string
default: '8.4'
php-extensions:
description: 'PHP extensions to install'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql'
path-filters:
description: 'JSON array of paths to filter on'
required: false
type: string
default: |
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'vendor/**'
- 'vendor-bin/**'
- '.php-cs-fixer.dist.php'
- 'composer.json'
- 'composer.lock'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
matrix:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout app
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Get supported server versions
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
- name: Build test matrix
id: set-matrix
run: |
BRANCHES='${{ steps.versions.outputs.branches }}'
MATRIX=$(jq -nc \
--argjson branches "$BRANCHES" \
--arg phpMin "${{ inputs.php-versions-min }}" \
--arg phpMax "${{ inputs.php-versions-max }}" \
--arg mysql "${{ inputs.mysql-version }}" \
'{include: [{"php-versions": $phpMin, "mysql-versions": $mysql, "server-versions": $branches[0]}, {"php-versions": $phpMax, "mysql-versions": $mysql, "server-versions": $branches[-1]}]}'
)
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
echo "Generated matrix: $MATRIX"
phpunit-mysql:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src == 'true'
strategy:
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }}
services:
mysql:
image: ghcr.io/nextcloud/continuous-integration-mysql-${{ matrix.mysql-versions }}:latest
ports:
- 4444:3306/tcp
env:
MYSQL_ROOT_PASSWORD: rootpassword
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10
steps:
- name: Checkout app
uses: actions/checkout@v4
with:
persist-credentials: false
path: app-checkout
- name: Detect app ID from appinfo/info.xml
run: |
APP_ID=$(grep -oP '(?<=<id>)[^<]+' app-checkout/appinfo/info.xml | head -1)
echo "APP_NAME=$APP_ID" >> $GITHUB_ENV
echo "Detected app ID: $APP_ID"
- name: Checkout server
uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout app
uses: actions/checkout@v4
with:
persist-credentials: false
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ inputs.php-extensions }}
coverage: none
ini-file: development
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable ONLY_FULL_GROUP_BY MySQL option
run: |
echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword
echo 'SELECT @@sql_mode;' | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword
- name: Check composer file existence
id: check_composer
uses: andstor/file-existence-action@v3
with:
files: apps/${{ env.APP_NAME }}/composer.json
- name: Set up dependencies
if: steps.check_composer.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts
composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
- name: Check PHPUnit script is defined
id: check_phpunit
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:unit ' | wc -l | grep 1
- name: PHPUnit
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit -- --display-all-issues
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
- name: Skipped
if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
run: |
echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
exit 1
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, phpunit-mysql]
if: always()
name: phpunit-mysql-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.phpunit-mysql.result != 'success' }}; then exit 1; fi

View File

@@ -0,0 +1,228 @@
# Reusable workflow for PHPUnit tests with PostgreSQL
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit PostgreSQL
on:
workflow_call:
inputs:
php-version:
description: 'PHP version to test'
required: false
type: string
default: '8.3'
php-extensions:
description: 'PHP extensions to install'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'vendor/**'
- 'vendor-bin/**'
- '.php-cs-fixer.dist.php'
- 'composer.json'
- 'composer.lock'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
matrix:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout app
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Get supported server versions
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
- name: Build test matrix
id: set-matrix
run: |
BRANCHES='${{ steps.versions.outputs.branches }}'
MATRIX=$(jq -nc \
--argjson branches "$BRANCHES" \
--arg php "${{ inputs.php-version }}" \
'{include: [{"php-versions": $php, "server-versions": $branches[-1]}]}'
)
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
echo "Generated matrix: $MATRIX"
phpunit-pgsql:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src == 'true'
strategy:
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }}
services:
postgres:
image: ghcr.io/nextcloud/continuous-integration-postgres-16:latest
ports:
- 4444:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout app
uses: actions/checkout@v4
with:
persist-credentials: false
path: app-checkout
- name: Detect app ID from appinfo/info.xml
run: |
APP_ID=$(grep -oP '(?<=<id>)[^<]+' app-checkout/appinfo/info.xml | head -1)
echo "APP_NAME=$APP_ID" >> $GITHUB_ENV
echo "Detected app ID: $APP_ID"
- name: Checkout server
uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout app
uses: actions/checkout@v4
with:
persist-credentials: false
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ inputs.php-extensions }}
coverage: none
ini-file: development
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check composer file existence
id: check_composer
uses: andstor/file-existence-action@v3
with:
files: apps/${{ env.APP_NAME }}/composer.json
- name: Set up dependencies
if: steps.check_composer.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts
composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
- name: Check PHPUnit script is defined
id: check_phpunit
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:unit ' | wc -l | grep 1
- name: PHPUnit
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit -- --display-all-issues
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
- name: Skipped
if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
run: |
echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
exit 1
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, phpunit-pgsql]
if: always()
name: phpunit-pgsql-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.phpunit-pgsql.result != 'success' }}; then exit 1; fi

126
.github/workflows/nextcloud-psalm.yml vendored Normal file
View File

@@ -0,0 +1,126 @@
# Reusable workflow for Psalm static analysis
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Static analysis
on:
workflow_call:
inputs:
psalm-command:
description: 'Composer command to run Psalm'
required: false
type: string
default: 'composer run psalm'
php-versions-min:
description: 'Minimum PHP version to include in the analysis matrix (filters out any auto-detected ocp-matrix entries below this)'
required: false
type: string
default: '8.1'
php-extensions:
description: 'PHP extensions to install'
required: false
type: string
default: 'bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '**.php'
- 'psalm.xml'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
matrix:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
outputs:
ocp-matrix: ${{ steps.filter.outputs.ocp-matrix }}
steps:
- name: Checkout app
uses: actions/checkout@v4
- name: Get version matrix
id: versions
uses: icewind1991/nextcloud-version-matrix@v1
- name: Filter ocp-matrix by minimum php version
id: filter
run: |
FILTERED=$(echo '${{ steps.versions.outputs.ocp-matrix }}' | jq -c --arg min "${{ inputs.php-versions-min }}" '.include |= [.[] | select(.["php-versions"] >= $min)]')
echo "ocp-matrix=$FILTERED" >> $GITHUB_OUTPUT
echo "Filtered ocp-matrix (php >= ${{ inputs.php-versions-min }}): $FILTERED"
static-analysis:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src == 'true'
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.matrix.outputs.ocp-matrix) }}
name: static-psalm-analysis ${{ matrix.ocp-version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up php${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ inputs.php-extensions }}
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: composer i
- name: Install dependencies
run: composer require --dev nextcloud/ocp:${{ matrix.ocp-version }} --ignore-platform-reqs --with-dependencies
- name: Run coding standards check
run: ${{ inputs.psalm-command }}
summary:
runs-on: ubuntu-latest
needs: [changes, static-analysis]
if: always()
name: static-psalm-analysis-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.static-analysis.result != 'success' }}; then exit 1; fi

120
.github/workflows/nextcloud-vitest.yml vendored Normal file
View File

@@ -0,0 +1,120 @@
# Reusable workflow for Vitest frontend tests
#
# SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
# SPDX-License-Identifier: AGPL-3.0-or-later
name: Vitest
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version to use'
required: false
type: string
default: '22'
test-command:
description: 'Command to run tests'
required: false
type: string
default: 'pnpm test:run'
path-filters:
description: 'Paths to filter on (YAML format)'
required: false
type: string
default: |
- '.github/workflows/**'
- 'src/**'
- 'tests/**'
- '*.ts'
- '*.js'
- '*.json'
- '*.yaml'
- 'pnpm-lock.yaml'
permissions:
pull-requests: read
contents: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build filters
id: build-filters
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
id: changes
continue-on-error: true
with:
filters: ${{ steps.build-filters.outputs.yaml }}
vitest:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src == 'true'
name: Vitest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: ${{ inputs.test-command }}
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, vitest]
if: always()
name: vitest-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src == 'true' && needs.vitest.result != 'success' }}; then exit 1; fi

View File

@@ -0,0 +1,250 @@
# Reusable Release Please + Fastlane Changelog Workflow
#
# Runs release-please, then if a PR was created/updated, generates a
# fastlane changelog from CHANGELOG.md and amends it into the release
# commit. The changelog is truncated to 500 chars (Play Store limit).
#
# Two complementary ways to configure output directories:
#
# 1. `fastlane-changelog-dirs` — newline-separated paths. These are
# "catch-all" dirs: every commit (regardless of scope) is written
# to them, unfiltered.
#
# 2. `scopes` — YAML map of `scope: dir(s)`. Dirs listed here are
# scope-filtered: they only receive commits whose scope either:
# - is not present anywhere in the map (treated as "applies
# to all"), or
# - is present and lists that dir among its targets.
#
# Example:
# scopes: |
# ios: fastlane/metadata/ios/en-US/changelogs
# apple: |
# fastlane/metadata/ios/en-US/changelogs
# fastlane/metadata/macos/en-US/changelogs
# android: fastlane/metadata/android/en-US/changelogs
#
# With this, `feat(ios): …` lands only in the iOS dir,
# `feat(apple): …` lands in iOS and macOS, `feat(android): …`
# lands only in the Android dir, and unscoped commits land in all.
#
# A dir present in both inputs is treated as a catch-all.
#
# Outputs are forwarded from release-please so downstream jobs can
# use release_created, tag_name, and version.
name: Release Please + Fastlane Changelog
on:
workflow_call:
inputs:
release-type:
description: 'Release Please release type (e.g., dart, node, python, go)'
required: true
type: string
version-file:
description: 'File containing the version string'
required: false
type: string
default: 'pubspec.yaml'
changelog-file:
description: 'Path to the CHANGELOG.md file'
required: false
type: string
default: 'CHANGELOG.md'
fastlane-changelog-dirs:
description: |
Newline-separated list of catch-all directories. Every commit
is written here unfiltered, regardless of scope.
required: false
type: string
default: ''
scopes:
description: |
YAML mapping of commit scope to one-or-more output directories.
Dirs listed here are scope-filtered: they only receive commits
whose scope is unmapped or lists that dir among its targets.
required: false
type: string
default: ''
max-length:
description: 'Maximum changelog length (Play Store limit is 500)'
required: false
type: number
default: 500
truncation-trailer:
description: 'Text appended when changelog is truncated'
required: false
type: string
default: "\n\n… see full notes on GitHub."
secrets:
token:
description: 'GitHub token for release-please (defaults to GITHUB_TOKEN)'
required: false
outputs:
release_created:
description: 'Whether a release was created'
value: ${{ jobs.release-please.outputs.release_created }}
tag_name:
description: 'The release tag name'
value: ${{ jobs.release-please.outputs.tag_name }}
version:
description: 'The release version'
value: ${{ jobs.release-please.outputs.version }}
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
steps:
- name: Release Please
id: release
uses: googleapis/release-please-action@v4
with:
release-type: ${{ inputs.release-type }}
token: ${{ secrets.token || github.token }}
- name: Update fastlane changelog on release PR
if: ${{ steps.release.outputs.pr && !steps.release.outputs.release_created }}
env:
PR_JSON: ${{ steps.release.outputs.pr }}
VERSION_FILE: ${{ inputs.version-file }}
CHANGELOG_FILE: ${{ inputs.changelog-file }}
OUT_DIRS: ${{ inputs.fastlane-changelog-dirs }}
SCOPES_INPUT: ${{ inputs.scopes }}
MAX_LENGTH: ${{ inputs.max-length }}
TRAILER: ${{ inputs.truncation-trailer }}
run: |
set -euo pipefail
PR_BRANCH=$(echo "$PR_JSON" | jq -r .headBranchName)
git clone --depth=5 --branch "$PR_BRANCH" \
https://x-access-token:${{ secrets.token || github.token }}@github.com/${{ github.repository }}.git _pr
cd _pr
# Extract version name and code
VERSION_LINE=$(grep '^version:' "$VERSION_FILE")
VERSION=$(echo "$VERSION_LINE" | sed 's/version: *//;s/+.*//')
CODE=$(echo "$VERSION_LINE" | sed 's/.*+//')
# Extract raw release notes from CHANGELOG.md (unfiltered, pre-cosmetics)
if [ ! -f "$CHANGELOG_FILE" ]; then
RAW_NOTES=""
echo "No $CHANGELOG_FILE found, will use fallback."
else
RAW_NOTES=$(awk "
/^## \\[${VERSION//./\\.}\\]/ { found=1; next }
/^## / { if (found) exit }
found { print }
" "$CHANGELOG_FILE")
fi
# Parse scopes map (if provided) → SCOPE_NAMES + SCOPE_DIRS
SCOPE_NAMES=()
declare -A SCOPE_DIRS=()
SCOPED_DIRS=""
if [ -n "${SCOPES_INPUT//[[:space:]]/}" ]; then
mapfile -t SCOPE_NAMES < <(printf '%s' "$SCOPES_INPUT" | yq 'keys | .[]')
for s in "${SCOPE_NAMES[@]}"; do
raw=$(printf '%s' "$SCOPES_INPUT" | yq ".\"$s\"")
[ "$raw" = "null" ] && raw=""
clean=$(printf '%s\n' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk 'NF')
SCOPE_DIRS["$s"]="$clean"
SCOPED_DIRS=$(printf '%s\n%s' "$SCOPED_DIRS" "$clean")
done
fi
SCOPED_DIRS=$(printf '%s\n' "$SCOPED_DIRS" | awk 'NF' | sort -u)
# Catch-all dirs from fastlane-changelog-dirs receive all commits unfiltered.
# A dir present in both inputs is treated as a catch-all.
CATCHALL_DIRS=$(printf '%s\n' "$OUT_DIRS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk 'NF' | sort -u)
ALL_DIRS=$(printf '%s\n%s\n' "$SCOPED_DIRS" "$CATCHALL_DIRS" | awk 'NF' | sort -u)
echo "Catch-all dirs:"
[ -n "$CATCHALL_DIRS" ] && printf ' %s\n' $CATCHALL_DIRS || echo " (none)"
echo "Scope-filtered dirs:"
if [ ${#SCOPE_NAMES[@]} -gt 0 ]; then
for s in "${SCOPE_NAMES[@]}"; do
echo " scope '$s':"
printf ' %s\n' ${SCOPE_DIRS[$s]}
done
else
echo " (none)"
fi
format_notes() {
local input="$1"
local out
out=$(printf '%s\n' "$input" \
| sed -E 's/ *\(\[[0-9a-f]+\]\([^)]+\)\)//g' \
| sed -E 's/^\* \*\*[^*]+:\*\* */- /; s/^\* /- /' \
| sed 's/^### //' \
| sed '/^[[:space:]]*$/d')
# strip leading + trailing blank lines
out=$(printf '%s' "$out" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba;}')
printf '%s' "$out"
}
truncate_notes() {
local notes="$1"
if [ ${#notes} -gt "$MAX_LENGTH" ]; then
local budget=$((MAX_LENGTH - ${#TRAILER}))
notes="${notes:0:$budget}"
notes=$(printf '%s' "$notes" | sed '$d')
notes="${notes}${TRAILER}"
fi
printf '%s' "$notes"
}
# Write per-directory
while IFS= read -r dir; do
[ -z "$dir" ] && continue
is_catchall=0
if printf '%s\n' "$CATCHALL_DIRS" | grep -Fxq "$dir"; then
is_catchall=1
fi
exclude_alt=""
if [ "$is_catchall" -eq 0 ] && [ ${#SCOPE_NAMES[@]} -gt 0 ]; then
for s in "${SCOPE_NAMES[@]}"; do
if ! printf '%s\n' "${SCOPE_DIRS[$s]}" | grep -Fxq "$dir"; then
if [ -z "$exclude_alt" ]; then
exclude_alt="$s"
else
exclude_alt="$exclude_alt|$s"
fi
fi
done
fi
filtered="$RAW_NOTES"
if [ -n "$exclude_alt" ]; then
filtered=$(printf '%s\n' "$RAW_NOTES" | grep -vE "^\* \*\*($exclude_alt)[^*]*:\*\*" || true)
fi
notes=$(format_notes "$filtered")
[ -z "$notes" ] && notes="Release $VERSION"
notes=$(truncate_notes "$notes")
mkdir -p "$dir"
printf '%s\n' "$notes" > "$dir/$CODE.txt"
if [ "$is_catchall" -eq 1 ]; then
echo "Wrote $dir/$CODE.txt (${#notes} chars, catch-all)"
else
echo "Wrote $dir/$CODE.txt (${#notes} chars, excluded scopes: ${exclude_alt:-none})"
fi
done <<< "$ALL_DIRS"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
if git diff --cached --quiet; then
echo "No changelog changes."
else
git commit --amend --no-edit
git push --force-with-lease
fi

15
.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"printWidth": 100,
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"overrides": [
{
"files": "*.md",
"options": {
"printWidth": 100,
"proseWrap": "always"
}
}
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright © 2026 Chen Asraf
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.

49
Makefile Normal file
View File

@@ -0,0 +1,49 @@
.PHONY: tag help
help:
@echo "Usage: make tag [TAG=<name>] [VERSION=<number>]"
@echo ""
@echo " TAG - Tag prefix name (e.g., 'go-release', 'nextcloud')"
@echo " Can also be set via TAG env var"
@echo " VERSION - Version number (optional, auto-increments if omitted)"
@echo ""
@echo "Examples:"
@echo " make tag TAG=go-release"
@echo " make tag TAG=go-release VERSION=3"
@echo " TAG=nextcloud make tag"
tag:
ifndef TAG
@read -p "Enter tag name: " tag_input; \
if [ -z "$$tag_input" ]; then \
echo "Error: TAG is required"; \
exit 1; \
fi; \
$(MAKE) tag TAG=$$tag_input $(if $(VERSION),VERSION=$(VERSION),)
else
@# Auto-detect version if not provided
$(eval _detected_version := $(shell \
latest=$$(git tag --list '$(TAG)-v*' | sed 's/$(TAG)-v//' | sort -n | tail -1); \
if [ -z "$$latest" ]; then echo 1; else echo $$((latest + 1)); fi \
))
$(eval VERSION := $(or $(VERSION),$(_detected_version)))
@echo "Tagging with:"
@echo " Tag version: $(TAG)-v$(VERSION)"
@echo " Latest tag: $(TAG)-latest"
@echo ""
@# Create tag-specific version
git tag -f $(TAG)-v$(VERSION)
@# Remove old latest tag and re-create
-git tag -d $(TAG)-latest 2>/dev/null || true
git tag $(TAG)-latest
@echo ""
@echo "Tags created successfully!"
@read -p "Push tags to remote? [Y/n] " answer; \
answer=$${answer:-y}; \
if echo "$$answer" | grep -iq "^y"; then \
echo "Pushing tags to remote..."; \
git push origin $(TAG)-v$(VERSION) $(TAG)-latest --force; \
else \
echo "To push tags later, run: git push origin $(TAG)-v$(VERSION) $(TAG)-latest --force"; \
fi
endif

471
README.md Normal file
View File

@@ -0,0 +1,471 @@
# Reusable GitHub Workflows
A collection of reusable GitHub Actions workflows.
## Table of Contents
- [Workflows](#workflows)
- [Go Release](#go-release-go-releaseyml)
- [Manual Homebrew Release](#manual-homebrew-release-manual-homebrew-releaseyml)
- [Release Please + Fastlane Changelog](#release-please--fastlane-changelog-release-please-fastlane-changelogyml)
- [Nextcloud Workflows](#nextcloud-workflows)
- [PHPUnit MySQL](#phpunit-mysql-nextcloud-phpunit-mysqlyml)
- [PHPUnit PostgreSQL](#phpunit-postgresql-nextcloud-phpunit-pgsqlyml)
- [PHPUnit Incremental Migration](#phpunit-incremental-migration-nextcloud-phpunit-incrementalyml)
- [Psalm Static Analysis](#psalm-static-analysis-nextcloud-psalmyml)
- [PHP Lint](#php-lint-nextcloud-lint-phpyml)
- [PHP-CS-Fixer](#php-cs-fixer-nextcloud-lint-php-csyml)
- [ESLint](#eslint-nextcloud-lint-eslintyml)
- [OpenAPI Lint](#openapi-lint-nextcloud-lint-openapiyml)
- [AppInfo XML Lint](#appinfo-xml-lint-nextcloud-lint-appinfo-xmlyml)
- [NPM Build](#npm-build-nextcloud-build-npmyml)
- [Vitest](#vitest-nextcloud-vitestyml)
- [Block Unconventional Commits](#block-unconventional-commits-nextcloud-block-unconventional-commitsyml)
- [License](#license)
## Workflows
### Go Release (`go-release.yml`)
A complete CI/CD workflow for Go projects that handles testing, cross-platform builds, releases, and
Homebrew tap updates.
#### Features
- Runs tests with configurable test command
- Cross-platform builds (Linux, macOS, Windows)
- Automated releases via [release-please](https://github.com/googleapis/release-please)
- Automatic Homebrew tap updates via repository dispatch
#### Usage
```yaml
name: Release
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
release:
uses: chenasraf/workflows/.github/workflows/go-release.yml@master
with:
name: my-cli
go-version: '1.24'
platforms: '["linux/amd64", "darwin/arm64"]'
main-branch: main
homebrew-tap-repo: myorg/homebrew-tap
secrets:
REPO_DISPATCH_PAT: ${{ secrets.REPO_DISPATCH_PAT }}
```
#### Inputs
| Input | Description | Required | Default |
| ------------------- | ---------------------------------------------------- | -------- | ------------------------------------------------------------------ |
| `name` | Binary/project name | Yes | - |
| `go-version` | Go version to use | No | `1.24` |
| `platforms` | JSON array of platforms to build | No | `["linux/amd64", "darwin/amd64", "darwin/arm64", "windows/amd64"]` |
| `package` | Go package path (empty for root) | No | `""` |
| `compress` | Compress build artifacts | No | `true` |
| `test-command` | Test command to run | No | `go test -v ./...` |
| `skip-tests` | Skip running tests | No | `false` |
| `main-branch` | Main branch name for releases | No | `master` |
| `homebrew-tap-repo` | Homebrew tap repo for dispatch (leave empty to skip) | No | `` |
#### Secrets
| Secret | Description | Required |
| ------------------- | ---------------------------------------- | -------- |
| `REPO_DISPATCH_PAT` | PAT for dispatching to homebrew tap repo | No |
---
### Manual Homebrew Release (`manual-homebrew-release.yml`)
Manually triggers a Homebrew tap update for the latest release. Useful when you need to re-trigger a Homebrew formula update without creating a new release.
#### Features
- Fetches the latest release tag and body from the repository
- Sends a repository dispatch event to your Homebrew tap repo
- Works with any Homebrew tap that listens for `trigger-from-release` events with payload: `{ tag, repo, body }`
#### Usage
```yaml
name: Manual Homebrew Release
on:
workflow_dispatch:
jobs:
homebrew:
uses: chenasraf/workflows/.github/workflows/manual-homebrew-release.yml@master
with:
homebrew-tap-repo: myorg/homebrew-tap
secrets:
REPO_DISPATCH_PAT: ${{ secrets.REPO_DISPATCH_PAT }}
```
#### Inputs
| Input | Description | Required | Default |
| ------------------- | ------------------------------------------------ | -------- | ------- |
| `homebrew-tap-repo` | Homebrew tap repo to dispatch to (e.g., owner/homebrew-tap) | Yes | - |
#### Secrets
| Secret | Description | Required |
| ------------------- | ---------------------------------------- | -------- |
| `REPO_DISPATCH_PAT` | PAT for dispatching to homebrew tap repo | Yes |
---
### Release Please + Fastlane Changelog (`release-please-fastlane-changelog.yml`)
Runs [release-please](https://github.com/googleapis/release-please), then when a release PR is
created or updated, extracts the changelog for the current version from `CHANGELOG.md`, formats it
for the Play Store (stripped of markdown links, commit hashes, etc.), truncates to the 500-char
limit, and writes it to one or more fastlane metadata directories. The changelog is amended into
release-please's commit so the tagged release includes the file.
#### Features
- Runs release-please with configurable release type
- Extracts version-specific notes from `CHANGELOG.md`
- Strips commit links, reformats bullets, removes markdown headers
- Truncates to Play Store's 500-char limit with a configurable trailer
- Writes to multiple output directories (e.g. Android + iOS fastlane metadata)
- Amends the release-please commit (no extra commits in the PR)
- Forwards `release_created`, `tag_name`, and `version` outputs
#### Usage
```yaml
name: CI/CD
on:
push:
branches: [master]
jobs:
release-please:
uses: chenasraf/workflows/.github/workflows/release-please-fastlane-changelog.yml@master
with:
release-type: dart
```
With multiple output directories (Android + iOS):
```yaml
jobs:
release-please:
uses: chenasraf/workflows/.github/workflows/release-please-fastlane-changelog.yml@master
with:
release-type: dart
fastlane-changelog-dirs: |
fastlane/metadata/android/en-US/changelogs
fastlane/metadata/ios/en-US/changelogs
build:
needs: release-please
if: ${{ needs.release-please.outputs.release_created }}
# ...
```
#### Inputs
| Input | Description | Required | Default |
| -------------------------- | ------------------------------------------------------------------ | -------- | ------------------------------------------------ |
| `release-type` | Release Please release type (`dart`, `node`, `python`, `go`, etc.) | Yes | - |
| `version-file` | File containing the version string | No | `pubspec.yaml` |
| `changelog-file` | Path to the CHANGELOG.md file | No | `CHANGELOG.md` |
| `fastlane-changelog-dirs` | Newline-separated list of output directories | No | `fastlane/metadata/android/en-US/changelogs` |
| `max-length` | Maximum changelog length in characters | No | `500` |
| `truncation-trailer` | Text appended when the changelog is truncated | No | `\n\n… see full notes on GitHub.` |
#### Secrets
| Secret | Description | Required |
| ------- | -------------------------------------------------- | -------- |
| `token` | GitHub token for release-please (defaults to `GITHUB_TOKEN`) | No |
#### Outputs
| Output | Description |
| ----------------- | ---------------------------- |
| `release_created` | Whether a release was created |
| `tag_name` | The release tag name |
| `version` | The release version |
---
## Nextcloud Workflows
Reusable workflows for Nextcloud app development. These workflows include automatic path filtering to skip unnecessary runs when irrelevant files change.
### PHPUnit MySQL (`nextcloud-phpunit-mysql.yml`)
Runs PHPUnit tests with MySQL database.
```yaml
jobs:
phpunit:
uses: chenasraf/workflows/.github/workflows/nextcloud-phpunit-mysql.yml@nextcloud-latest
with:
php-versions-min: '8.1'
php-versions-max: '8.4'
mysql-version: '8.0'
path-filters: |
- 'lib/**'
- 'tests/**'
- 'composer.json'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `php-versions-min` | Minimum PHP version | No | `8.2` |
| `php-versions-max` | Maximum PHP version | No | `8.3` |
| `mysql-version` | MySQL version | No | `8.4` |
| `php-extensions` | PHP extensions to install | No | _(common extensions)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | _(lib, tests, etc.)_ |
### PHPUnit PostgreSQL (`nextcloud-phpunit-pgsql.yml`)
Runs PHPUnit tests with PostgreSQL database.
```yaml
jobs:
phpunit:
uses: chenasraf/workflows/.github/workflows/nextcloud-phpunit-pgsql.yml@nextcloud-latest
with:
php-version: '8.2'
path-filters: |
- 'lib/**'
- 'tests/**'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `php-version` | PHP version | No | `8.3` |
| `php-extensions` | PHP extensions to install | No | _(common extensions)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | _(lib, tests, etc.)_ |
### PHPUnit Incremental Migration (`nextcloud-phpunit-incremental.yml`)
Tests database migrations by upgrading from a baseline version.
```yaml
jobs:
incremental:
uses: chenasraf/workflows/.github/workflows/nextcloud-phpunit-incremental.yml@nextcloud-latest
with:
baseline-version: v1.0.0
php-version: '8.2'
validation-query: 'SELECT COUNT(*) FROM oc_myapp_users'
path-filters: |
- 'lib/Migration/**'
- 'appinfo/info.xml'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `baseline-version` | Git tag/ref to upgrade from | Yes | - |
| `php-version` | PHP version | No | `8.3` |
| `php-extensions-mysql` | PHP extensions for MySQL tests | No | _(common extensions)_ |
| `php-extensions-pgsql` | PHP extensions for PostgreSQL tests | No | _(common extensions)_ |
| `validation-query` | SQL query to validate migration | No | _(empty)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | _(lib, tests, etc.)_ |
### Psalm Static Analysis (`nextcloud-psalm.yml`)
Runs Psalm static analysis across supported Nextcloud versions.
```yaml
jobs:
psalm:
uses: chenasraf/workflows/.github/workflows/nextcloud-psalm.yml@nextcloud-latest
with:
psalm-command: 'composer run psalm -- --show-info=true'
path-filters: |
- 'lib/**/*.php'
- 'psalm.xml'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `psalm-command` | Command to run Psalm | No | `composer run psalm` |
| `php-extensions` | PHP extensions to install | No | _(common extensions)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | `**.php`, `psalm.xml` |
### PHP Lint (`nextcloud-lint-php.yml`)
Runs PHP syntax linting across supported PHP versions.
```yaml
jobs:
lint:
uses: chenasraf/workflows/.github/workflows/nextcloud-lint-php.yml@nextcloud-latest
with:
lint-command: 'composer run lint -- --colors'
path-filters: |
- 'lib/**/*.php'
- 'appinfo/**/*.php'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `lint-command` | Command to run lint | No | `composer run lint` |
| `php-extensions` | PHP extensions to install | No | _(common extensions)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | `**.php` |
### PHP-CS-Fixer (`nextcloud-lint-php-cs.yml`)
Checks PHP code style with PHP-CS-Fixer.
```yaml
jobs:
cs:
uses: chenasraf/workflows/.github/workflows/nextcloud-lint-php-cs.yml@nextcloud-latest
with:
cs-check-command: 'vendor/bin/php-cs-fixer fix --dry-run --diff'
path-filters: |
- 'lib/**/*.php'
- 'tests/**/*.php'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `cs-check-command` | Command to check code style | No | `composer run cs:check` |
| `php-extensions` | PHP extensions to install | No | _(common extensions)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | `**.php`, `.php-cs-fixer.dist.php` |
### ESLint (`nextcloud-lint-eslint.yml`)
Runs ESLint on frontend code.
```yaml
jobs:
eslint:
uses: chenasraf/workflows/.github/workflows/nextcloud-lint-eslint.yml@nextcloud-latest
with:
lint-command: 'pnpm lint --max-warnings 0'
path-filters: |
- 'src/**/*.ts'
- 'src/**/*.vue'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `lint-command` | Command to run lint | No | `pnpm lint` |
| `path-filters` | Paths to trigger on (YAML list) | No | `src/**`, `*.ts`, `*.js`, etc. |
### OpenAPI Lint (`nextcloud-lint-openapi.yml`)
Validates OpenAPI spec is up to date.
```yaml
jobs:
openapi:
uses: chenasraf/workflows/.github/workflows/nextcloud-lint-openapi.yml@nextcloud-latest
with:
openapi-command: 'composer run generate-openapi'
typescript-types-pattern: 'src/api/types/*.ts'
path-filters: |
- 'lib/Controller/**/*.php'
- 'openapi.json'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `openapi-command` | Command to regenerate OpenAPI | No | `composer run openapi` |
| `typescript-types-pattern` | Glob for TypeScript types | No | `src/types/openapi/openapi*.ts` |
| `path-filters` | Paths to trigger on (YAML list) | No | `lib/**/*.php`, `openapi.json` |
### AppInfo XML Lint (`nextcloud-lint-appinfo-xml.yml`)
Validates `appinfo/info.xml` against schema.
```yaml
jobs:
xml:
uses: chenasraf/workflows/.github/workflows/nextcloud-lint-appinfo-xml.yml@nextcloud-latest
with:
xml-file: './custom/path/info.xml'
path-filters: |
- 'custom/path/info.xml'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `xml-file` | Path to the info.xml file | No | `./appinfo/info.xml` |
| `schema-url` | URL to XML schema | No | _(Nextcloud schema)_ |
| `path-filters` | Paths to trigger on (YAML list) | No | `appinfo/info.xml` |
### NPM Build (`nextcloud-build-npm.yml`)
Builds frontend assets with pnpm.
```yaml
jobs:
build:
uses: chenasraf/workflows/.github/workflows/nextcloud-build-npm.yml@nextcloud-latest
with:
build-command: 'pnpm build:prod'
path-filters: |
- 'src/**'
- 'package.json'
- 'pnpm-lock.yaml'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `build-command` | Command to run build | No | `pnpm build` |
| `path-filters` | Paths to trigger on (YAML list) | No | `src/**`, `*.json`, etc. |
### Vitest (`nextcloud-vitest.yml`)
Runs Vitest frontend tests.
```yaml
jobs:
vitest:
uses: chenasraf/workflows/.github/workflows/nextcloud-vitest.yml@nextcloud-latest
with:
node-version: '20'
test-command: 'pnpm vitest run --coverage'
path-filters: |
- 'src/**'
- 'tests/**'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `node-version` | Node.js version to use | No | `22` |
| `test-command` | Command to run tests | No | `pnpm test:run` |
| `path-filters` | Paths to trigger on (YAML list) | No | `src/**`, `*.ts`, etc. |
### Block Unconventional Commits (`nextcloud-block-unconventional-commits.yml`)
Blocks commits that don't follow conventional commit format.
```yaml
jobs:
commits:
uses: chenasraf/workflows/.github/workflows/nextcloud-block-unconventional-commits.yml@nextcloud-latest
with:
allowed-types: 'feat,fix,docs,chore,refactor'
```
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `allowed-types` | Comma-separated list of allowed commit types | No | _(feat, fix, docs, etc.)_ |
---
## License
MIT