Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/workflow-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
paths:
- '.github/workflows/**'
- 'workflow-templates/**'
- 'actions/**'
- 'scripts/check_workflow_policy.py'
- 'tests/test_workflow_policy.py'
push:
Expand All @@ -16,6 +17,7 @@ on:
paths:
- '.github/workflows/**'
- 'workflow-templates/**'
- 'actions/**'
- 'scripts/check_workflow_policy.py'
- 'tests/test_workflow_policy.py'

Expand Down
307 changes: 307 additions & 0 deletions actions/nextcloud-appstore-publish/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: Build and publish Nextcloud app release
description: Build, package, sign, attach and publish a tagged Nextcloud app release.

inputs:
app-name:
description: Nextcloud app id and repository directory name.
required: true
release-tag:
description: Existing Git tag to build and publish.
required: true
github-token:
description: Token used to validate and update the GitHub release.
required: true
app-private-key:
description: Nextcloud app private key.
required: true
appstore-token:
description: Nextcloud App Store token.
required: true
checkout-submodules:
description: Checkout application git submodules.
required: false
default: 'false'
make-signs-app:
description: Let the Makefile sign the app instead of the generic post-package signing step.
required: false
default: 'false'
require-setup-signatures:
description: Require setup integrity metadata in Makefile-built packages.
required: false
default: 'false'
manual-recovery:
description: Use packaging tooling from the workflow ref while keeping tagged application source immutable.
required: false
default: 'false'

runs:
using: composite
steps:
- name: Check actor permission
uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0
with:
require: write
token: ${{ inputs.github-token }}

- name: Validate boolean inputs
shell: bash
env:
CHECKOUT_SUBMODULES: ${{ inputs.checkout-submodules }}
MAKE_SIGNS_APP: ${{ inputs.make-signs-app }}
REQUIRE_SETUP_SIGNATURES: ${{ inputs.require-setup-signatures }}
MANUAL_RECOVERY: ${{ inputs.manual-recovery }}
run: |
set -euo pipefail
for value in "${CHECKOUT_SUBMODULES}" "${MAKE_SIGNS_APP}" "${REQUIRE_SETUP_SIGNATURES}" "${MANUAL_RECOVERY}"; do
case "${value}" in
true|false) ;;
*)
echo "::error::Boolean inputs must be 'true' or 'false'"
exit 2
;;
esac
done

- name: Checkout application
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: ${{ inputs.checkout-submodules }}
fetch-tags: true
fetch-depth: 0
ref: ${{ inputs.release-tag }}
path: ${{ inputs.app-name }}

- name: Validate release identity
id: release_identity
uses: LibreCodeCoop/release-tool/actions/release-identity@385ca7732db12e5c79590bb21be8da3608194595
with:
tag: ${{ inputs.release-tag }}
working-directory: ${{ inputs.app-name }}
require-tag-exists: 'true'

- name: Get appinfo data
id: appinfo
uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0
with:
filename: ${{ inputs.app-name }}/appinfo/info.xml
expression: "//info//dependencies//nextcloud/@min-version"

- name: Read package engines
id: versions
continue-on-error: true
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
with:
path: ${{ inputs.app-name }}
fallbackNode: '^24'

- name: Set up node
if: ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
package-manager-cache: false

- name: Resolve PHP version
id: php_versions
uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3
with:
filename: ${{ inputs.app-name }}/appinfo/info.xml

- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ steps.php_versions.outputs.php-min }}
coverage: none
env:
GITHUB_TOKEN: ${{ inputs.github-token }}

- name: Check composer.json
id: check_composer
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
with:
files: "${{ inputs.app-name }}/composer.json"

- name: Install composer dependencies
if: steps.check_composer.outputs.files_exists == 'true'
uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0
with:
composer-options: '--no-dev'
working-directory: ${{ inputs.app-name }}
ignore-cache: 'yes'

- name: Build application
if: ${{ steps.versions.outputs.nodeVersion }}
shell: bash
env:
APP_NAME: ${{ inputs.app-name }}
CYPRESS_INSTALL_BINARY: 0
run: |
set -euo pipefail
cd "${APP_NAME}"
npm ci
npm run build --if-present

- name: Check Krankerl config
id: krankerl
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
with:
files: ${{ inputs.app-name }}/krankerl.toml

- name: Install Krankerl
if: steps.krankerl.outputs.files_exists == 'true'
shell: bash
run: |
set -euo pipefail
wget --quiet https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb
sudo dpkg -i krankerl_0.14.0_amd64.deb

- name: Package with Krankerl
if: steps.krankerl.outputs.files_exists == 'true'
shell: bash
env:
APP_NAME: ${{ inputs.app-name }}
run: |
set -euo pipefail
cd "${APP_NAME}"
krankerl package

- name: Resolve Nextcloud server download
id: server_url
if: steps.krankerl.outputs.files_exists != 'true'
shell: bash
env:
NC_VERSION: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
run: |
set -euo pipefail
download_url="$(curl --fail --silent --show-error "https://updates.nextcloud.com/updater_server/latest?channel=beta&version=${NC_VERSION}" | jq -r '.downloads.zip[0]')"
printf 'download_url=%s\n' "${download_url}" >> "${GITHUB_OUTPUT}"

- name: Download Nextcloud server
id: server_download
if: steps.krankerl.outputs.files_exists != 'true' && steps.server_url.outputs.download_url != 'null'
continue-on-error: true
shell: bash
env:
DOWNLOAD_URL: ${{ steps.server_url.outputs.download_url }}
run: |
set -euo pipefail
wget "${DOWNLOAD_URL}" -O nextcloud.zip
unzip -q nextcloud.zip

- name: Checkout Nextcloud server fallback
if: steps.krankerl.outputs.files_exists != 'true' && steps.server_download.outcome != 'success'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
path: nextcloud

- name: Validate manual recovery source
if: inputs.manual-recovery == 'true' && steps.krankerl.outputs.files_exists != 'true'
shell: bash
env:
APP_NAME: ${{ inputs.app-name }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
set -euo pipefail
case "${GITHUB_REF_NAME}" in
stable*) ;;
*)
echo "::error::Manual release recovery must be dispatched from a stable branch"
exit 1
;;
esac
cd "${APP_NAME}"
git fetch --quiet origin "${GITHUB_SHA}"
tag_sha="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")"
if ! git merge-base --is-ancestor "${tag_sha}" "${GITHUB_SHA}"; then
echo "::error::${GITHUB_REF_NAME} does not contain ${RELEASE_TAG}; refusing cross-line recovery"
exit 1
fi
git show "${GITHUB_SHA}:Makefile" > Makefile

- name: Package with Makefile
if: steps.krankerl.outputs.files_exists != 'true'
shell: bash
env:
APP_NAME: ${{ inputs.app-name }}
APP_PRIVATE_KEY: ${{ inputs.app-private-key }}
REQUIRE_SETUP_SIGNATURES: ${{ inputs.require-setup-signatures }}
MAKE_SIGNS_APP: ${{ inputs.make-signs-app }}
run: |
set -euo pipefail
cd "${APP_NAME}"
if [[ "${MAKE_SIGNS_APP}" == "true" ]]; then
mkdir -p build/tools/certificates
printf '%s' "${APP_PRIVATE_KEY}" > "build/tools/certificates/${APP_NAME}.key"
fi
make appstore
if make -qp 2>/dev/null | grep -q '^verify-appstore-package:'; then
REQUIRE_SETUP_SIGNATURES="${REQUIRE_SETUP_SIGNATURES}" make verify-appstore-package
fi

- name: Sign generic Makefile package
if: steps.krankerl.outputs.files_exists != 'true' && inputs.make-signs-app != 'true'
shell: bash
env:
APP_NAME: ${{ inputs.app-name }}
APP_PRIVATE_KEY: ${{ inputs.app-private-key }}
run: |
set -euo pipefail
cd "${APP_NAME}/build/artifacts"
tar -xvf "${APP_NAME}.tar.gz"
cd ../../..
printf '%s' "${APP_PRIVATE_KEY}" > "${APP_NAME}.key"
wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${APP_NAME}/${APP_NAME}.crt"
php nextcloud/occ integrity:sign-app --privateKey="../${APP_NAME}.key" --certificate="../${APP_NAME}.crt" --path="../${APP_NAME}/build/artifacts/${APP_NAME}"
cd "${APP_NAME}/build/artifacts"
tar -zcvf "${APP_NAME}.tar.gz" "${APP_NAME}"

- name: Set up PHP 8.3 for release-tool
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2
with:
php-version: '8.3'
coverage: none
env:
GITHUB_TOKEN: ${{ inputs.github-token }}

- name: Validate release artifact
uses: LibreCodeCoop/release-tool/actions/artifact-validate@385ca7732db12e5c79590bb21be8da3608194595
with:
artifact: ${{ inputs.app-name }}/build/artifacts/${{ inputs.app-name }}.tar.gz
app-name: ${{ inputs.app-name }}
version: ${{ steps.release_identity.outputs.version }}

- name: Attach tarball to GitHub release
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
APP_NAME: ${{ inputs.app-name }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
set -euo pipefail
source_asset="${APP_NAME}/build/artifacts/${APP_NAME}.tar.gz"
asset_name="${APP_NAME}-${RELEASE_TAG}.tar.gz"
publish_asset="${RUNNER_TEMP}/${asset_name}"
cp "${source_asset}" "${publish_asset}"
gh release upload "${RELEASE_TAG}" "${publish_asset}" --clobber --repo "${GITHUB_REPOSITORY}"

- name: Upload app to Nextcloud App Store
uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3
with:
app_name: ${{ inputs.app-name }}
appstore_token: ${{ inputs.appstore-token }}
download_url: https://github.com/${{ github.repository }}/releases/download/${{ inputs.release-tag }}/${{ inputs.app-name }}-${{ inputs.release-tag }}.tar.gz
app_private_key: ${{ inputs.app-private-key }}

- name: Verify App Store publication
uses: LibreCodeCoop/release-tool/actions/appstore-publication-wait@385ca7732db12e5c79590bb21be8da3608194595
with:
app-name: ${{ inputs.app-name }}
version: ${{ steps.release_identity.outputs.version }}
platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
2 changes: 1 addition & 1 deletion scripts/check_workflow_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def main() -> int:
"roots",
nargs="*",
type=Path,
default=[Path("workflow-templates"), Path(".github/workflows")],
default=[Path("workflow-templates"), Path(".github/workflows"), Path("actions")],
)
args = parser.parse_args()

Expand Down
Loading
Loading