10 Commits

16 changed files with 858 additions and 36 deletions

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

@@ -46,7 +46,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -45,7 +45,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -46,7 +46,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -46,7 +46,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -46,7 +46,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -45,7 +45,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -66,7 +66,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
@@ -213,7 +215,7 @@ jobs:
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()
@@ -356,7 +358,7 @@ jobs:
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()

View File

@@ -64,7 +64,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
@@ -197,7 +199,7 @@ jobs:
- name: PHPUnit
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit
run: composer run test:unit -- --display-all-issues
- name: Check PHPUnit integration script is defined
id: check_integration
@@ -213,7 +215,7 @@ jobs:
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()

View File

@@ -54,7 +54,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3
@@ -182,7 +184,7 @@ jobs:
- name: PHPUnit
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit
run: composer run test:unit -- --display-all-issues
- name: Check PHPUnit integration script is defined
id: check_integration
@@ -198,7 +200,7 @@ jobs:
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
run: composer run test:integration -- --display-all-issues
- name: Print logs
if: always()

View File

@@ -46,7 +46,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

View File

@@ -52,7 +52,9 @@ jobs:
run: |
echo "yaml<<EOF" >> $GITHUB_OUTPUT
echo "src:" >> $GITHUB_OUTPUT
echo '${{ inputs.path-filters }}' | sed 's/^/ /' >> $GITHUB_OUTPUT
cat <<'INPUT_EOF' | sed 's/^/ /' >> $GITHUB_OUTPUT
${{ inputs.path-filters }}
INPUT_EOF
echo "EOF" >> $GITHUB_OUTPUT
- uses: dorny/paths-filter@v3

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

View File

@@ -27,18 +27,10 @@ else
if [ -z "$$latest" ]; then echo 1; else echo $$((latest + 1)); fi \
))
$(eval VERSION := $(or $(VERSION),$(_detected_version)))
@# Get latest global version
$(eval GLOBAL_VERSION := $(shell \
latest=$$(git tag --list 'v*' | grep -E '^v[0-9]+$$' | sed 's/v//' | sort -n | tail -1); \
if [ -z "$$latest" ]; then echo 1; else echo $$((latest + 1)); fi \
))
@echo "Tagging with:"
@echo " Global version: v$(GLOBAL_VERSION)"
@echo " Tag version: $(TAG)-v$(VERSION)"
@echo " Latest tag: $(TAG)-latest"
@echo ""
@# Create global version tag
git tag -f v$(GLOBAL_VERSION)
@# Create tag-specific version
git tag -f $(TAG)-v$(VERSION)
@# Remove old latest tag and re-create
@@ -50,8 +42,8 @@ else
answer=$${answer:-y}; \
if echo "$$answer" | grep -iq "^y"; then \
echo "Pushing tags to remote..."; \
git push origin v$(GLOBAL_VERSION) $(TAG)-v$(VERSION) $(TAG)-latest --force; \
git push origin $(TAG)-v$(VERSION) $(TAG)-latest --force; \
else \
echo "To push tags later, run: git push origin v$(GLOBAL_VERSION) $(TAG)-v$(VERSION) $(TAG)-latest --force"; \
echo "To push tags later, run: git push origin $(TAG)-v$(VERSION) $(TAG)-latest --force"; \
fi
endif

412
README.md
View File

@@ -2,6 +2,27 @@
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`)
@@ -29,9 +50,13 @@ on:
jobs:
release:
uses: chenasraf/workflows/.github/workflows/go-release.yml@main
uses: chenasraf/workflows/.github/workflows/go-release.yml@master
with:
name: my-binary
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 }}
```
@@ -56,22 +81,391 @@ jobs:
| ------------------- | ---------------------------------------- | -------- |
| `REPO_DISPATCH_PAT` | PAT for dispatching to homebrew tap repo | No |
#### Example with Custom Options
---
### 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:
release:
uses: chenasraf/workflows/.github/workflows/go-release.yml@master
homebrew:
uses: chenasraf/workflows/.github/workflows/manual-homebrew-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 |
| ------------------- | ------------------------------------------------ | -------- | ------- |
| `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