diff --git a/.github/workflows/playground-preview.yml b/.github/workflows/playground-preview.yml new file mode 100644 index 0000000..e4e63da --- /dev/null +++ b/.github/workflows/playground-preview.yml @@ -0,0 +1,169 @@ +name: Playground preview + +# Builds the app — plus the prebuilt eXeLearning static editor, *downloaded* +# from the editor's latest release rather than compiled here — into a ZIP the +# Nextcloud Playground installs in the browser via the blueprint `installApp` +# step. +# +# - push to main -> refresh the rolling `playground` prerelease asset +# (exelearning.zip) that blueprint.json points at, so the +# README badge always boots the latest main. +# - pull_request -> publish a per-PR prerelease (playground-pr-) and post +# a one-click playground link as a sticky PR comment. +# - PR closed -> delete that per-PR prerelease and its tag. + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened, closed] + +concurrency: + group: playground-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +env: + PLAYGROUND_URL: https://ateeducacion.github.io/nextcloud-playground/ + +jobs: + build: + # Build on push to main, and on open/sync/reopen of PRs from this repo. + # Fork PRs run with a read-only token (no release write), so skip them. + if: >- + github.event_name == 'push' || + (github.event.action != 'closed' && + github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Resolve preview identity + id: id + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "push" ]; then + echo "tag=playground" >> "$GITHUB_OUTPUT" + echo "version=playground" >> "$GITHUB_OUTPUT" + else + echo "tag=playground-pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" + echo "version=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" + fi + + # Pulls the prebuilt static editor from the latest exelearning/exelearning + # release (e.g. exelearning-static-vX.Y.Z.zip) into js/editor/ — no bun + # build. `make package-zip` then bundles it. `clean` keeps js/editor/. + - name: Download the eXeLearning static editor (latest release) + run: make download-editor + + - name: Build the playground ZIP + run: | + set -euo pipefail + make package-zip PACKAGE_VERSION=${{ steps.id.outputs.version }} + cp "build/artifacts/exelearning-${{ steps.id.outputs.version }}.zip" exelearning.zip + ls -lh exelearning.zip + + - name: Publish exelearning.zip to the preview release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ steps.id.outputs.tag }} + name: ${{ steps.id.outputs.tag }} + prerelease: true + make_latest: false + files: exelearning.zip + fail_on_unmatched_files: true + body: > + Automated Nextcloud Playground preview build (not a real release). + Installed in the browser via the blueprint `installApp` step. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Inline the blueprint (pointed at this PR's asset) as URL-safe base64 in + # ?blueprint= so we don't have to host a per-PR blueprint file. URLSearchParams + # turns '+' into a space, so '+/' must become '-_' and padding is dropped. + # The asset is fetched through the shared CORS proxy because GitHub release + # downloads are served from Azure Blob without Access-Control-Allow-Origin, + # which a cross-origin browser fetch from the playground would reject. + - name: Build PR playground link + if: github.event_name == 'pull_request' + id: link + run: | + set -euo pipefail + asset="https://zip-proxy.erseco.workers.dev/?repo=${{ github.repository }}&release=${{ steps.id.outputs.tag }}&asset=exelearning.zip" + jq --arg url "$asset" '(.steps[] | select(.step == "installApp")).url = $url' blueprint.json > blueprint.pr.json + # Sample fixtures are referenced from raw main; point them at this PR's + # commit so the preview reflects fixtures added/changed on the branch. + sed -i "s#/nextcloud-exelearning/main/tests/fixtures#/nextcloud-exelearning/${{ github.event.pull_request.head.sha }}/tests/fixtures#g" blueprint.pr.json + b64=$(base64 -w0 blueprint.pr.json | tr '+/' '-_' | tr -d '=') + echo "url=${PLAYGROUND_URL}?blueprint=${b64}" >> "$GITHUB_OUTPUT" + + - name: Upsert PR comment + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + env: + PREVIEW_URL: ${{ steps.link.outputs.url }} + PREVIEW_TAG: ${{ steps.id.outputs.tag }} + with: + script: | + const marker = ''; + const url = process.env.PREVIEW_URL; + const tag = process.env.PREVIEW_TAG; + const body = [ + marker, + '### ▶️ Preview this PR in the Nextcloud Playground', + '', + `[**Open this PR in the Nextcloud Playground**](${url})`, + '', + "A fresh Nextcloud boots in your browser with this branch's " + + '`exelearning` app installed and enabled (log in as `admin` / `admin`, ' + + 'then upload an `.elpx` in Files).', + '', + `Built artifact: \`exelearning.zip\` on the \`${tag}\` prerelease.`, + '', + '> The viewer relies on a scoped Service Worker; some viewer features ' + + "may be limited inside the playground's own Service Worker. Core " + + 'install, Files and the app UI work.', + ].join('\n'); + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, + }); + const existing = comments.find((c) => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + + cleanup: + if: >- + github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Delete the per-PR preview release and tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + tag="playground-pr-${{ github.event.pull_request.number }}" + gh release delete "$tag" --cleanup-tag --yes || echo "No release $tag to delete." diff --git a/Makefile b/Makefile index 2e85c8b..b4556b5 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,9 @@ RELEASE_DIR := $(BUILD_DIR)/$(APP_NAME) # from appinfo/info.xml, e.g. `make package PACKAGE_VERSION=0.2.0-rc1`. PACKAGE_VERSION ?= $(APP_VERSION) PACKAGE_NAME := $(APP_NAME)-$(PACKAGE_VERSION).tar.gz +# Same staged tree as PACKAGE_NAME but as a ZIP — the format the Nextcloud +# Playground `installApp` blueprint step extracts in the browser. +PACKAGE_ZIP := $(APP_NAME)-$(PACKAGE_VERSION).zip # --- PHP runtime selection ---------------------------------------------- # appinfo/info.xml declares PHP 8.2–8.5 as the supported range. Pick the @@ -104,7 +107,7 @@ NC_ADMIN_PASS ?= admin .PHONY: help install build dev watch-js lint typecheck test clean \ composer-install composer-test cs-check cs-fix php-version \ download-editor fetch-editor-source build-editor clean-editor \ - package appstore \ + package package-zip appstore \ check-docker \ up down restart logs shell occ-status status sync ci-matrix \ seed-fixtures @@ -299,6 +302,37 @@ package: clean build @echo " - version: $(PACKAGE_VERSION)" @echo "================================================================" +# Produce build/artifacts/$(APP_NAME)-$(PACKAGE_VERSION).zip with +# `$(APP_NAME)/` as the single top-level directory. Same staged tree as +# `package` (filtered through .distignore, version stamped) but zipped — +# this is what the Nextcloud Playground downloads and extracts in the +# browser via the blueprint `installApp` step. Run `make download-editor` +# first if you want the embedded eXeLearning editor bundled in. +package-zip: clean build + @if [ ! -f .distignore ]; then \ + echo "Error: .distignore is missing — cannot build a distribution package."; \ + exit 1; \ + fi + @command -v zip >/dev/null 2>&1 || { echo "Error: 'zip' is required for package-zip."; exit 1; } + @mkdir -p $(ARTIFACT_DIR) + @rm -rf $(RELEASE_DIR) + @mkdir -p $(RELEASE_DIR) + @echo ">> staging $(APP_NAME) $(PACKAGE_VERSION) into $(RELEASE_DIR)" + @rsync -a --delete --exclude-from=.distignore ./ $(RELEASE_DIR)/ + @echo ">> stamping version $(PACKAGE_VERSION) into staged appinfo/info.xml" + @sed -i.bak -E 's|[^<]*|$(PACKAGE_VERSION)|' $(RELEASE_DIR)/appinfo/info.xml + @rm -f $(RELEASE_DIR)/appinfo/info.xml.bak + @echo ">> creating $(ARTIFACT_DIR)/$(PACKAGE_ZIP)" + @rm -f $(ARTIFACT_DIR)/$(PACKAGE_ZIP) + @cd $(BUILD_DIR) && zip -qr -X $(ARTIFACT_DIR)/$(PACKAGE_ZIP) $(APP_NAME) + @rm -rf $(RELEASE_DIR) + @echo + @echo "================================================================" + @echo " Built $(ARTIFACT_DIR)/$(PACKAGE_ZIP)" + @echo " - app id : $(APP_NAME)" + @echo " - version: $(PACKAGE_VERSION)" + @echo "================================================================" + # Backwards-compatible alias for the historical target name. appstore: package diff --git a/README.md b/README.md index d1eede6..126764e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,30 @@ # nextcloud-exelearning [![CI](https://github.com/exelearning/nextcloud-exelearning/actions/workflows/ci.yml/badge.svg)](https://github.com/exelearning/nextcloud-exelearning/actions/workflows/ci.yml) +[![Open in Nextcloud Playground](https://img.shields.io/badge/Nextcloud%20Playground-Open-0082c9?logo=nextcloud&logoColor=white)](https://ateeducacion.github.io/nextcloud-playground/?blueprint-url=https://raw.githubusercontent.com/exelearning/nextcloud-exelearning/main/blueprint.json) Preview and edit [eXeLearning](https://exelearning.net/) `.elpx` packages directly inside Nextcloud Files. +## Quick test (no install) + +The fastest way to try this app is the **Nextcloud Playground** — a full +Nextcloud running in your browser via WebAssembly, no server required. Click +the badge at the top of this README and you'll get a fresh Nextcloud with: + +* The `exelearning` app installed and enabled. +* The Files app open, logged in as `admin` / `admin`. + +Upload an `.elpx` package in Files and click it to open the viewer. Nothing is +installed locally; everything runs in the browser and is discarded when you +close the tab. + +The instance is provisioned from [`blueprint.json`](blueprint.json), which +installs the app with the playground's `installApp` step from a built +`exelearning.zip` artifact. Every pull request gets its own one-click +playground link (posted as a PR comment) built from that branch — see +[`.github/workflows/playground-preview.yml`](.github/workflows/playground-preview.yml). + ## What this app does When a user clicks a `.elpx` file in Nextcloud Files: diff --git a/blueprint.json b/blueprint.json new file mode 100644 index 0000000..a1fa879 --- /dev/null +++ b/blueprint.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://raw.githubusercontent.com/ateeducacion/nextcloud-playground/main/assets/blueprints/blueprint-schema.json", + "meta": { + "title": "nextcloud-exelearning Playground", + "author": "eXeLearning", + "description": "A fresh Nextcloud in the browser with the eXeLearning app installed, ready to preview and edit .elpx packages in Files." + }, + "landingPage": "/index.php/apps/files/", + "siteOptions": { + "title": "eXeLearning on Nextcloud", + "locale": "en", + "timezone": "UTC" + }, + "admin": { + "username": "admin", + "password": "admin", + "email": "admin@example.com" + }, + "steps": [ + { + "step": "installApp", + "appId": "exelearning", + "url": "https://zip-proxy.erseco.workers.dev/?repo=exelearning/nextcloud-exelearning&release=playground&asset=exelearning.zip" + }, + { + "step": "writeFile", + "path": "config/mimetypemapping.json", + "content": "{\"elpx\":[\"application/vnd.exelearning.elpx\",\"application/zip\"],\"elp\":[\"application/vnd.exelearning.elpx\",\"application/zip\"]}" + }, + { + "step": "writeFile", + "path": "config/mimetypealiases.json", + "content": "{\"application/vnd.exelearning.elpx\":\"exelearning\",\"application/x-exelearning\":\"exelearning\"}" + }, + { "step": "runOcc", "args": ["maintenance:mimetype:update-js"] }, + { "step": "runOcc", "args": ["maintenance:mimetype:update-db", "--repair-filecache"] }, + { + "step": "writeFile", + "path": "data/admin/files/exelearning-samples/un-contenido-de-ejemplo-para-probar-estilos-y-catalogacion.elpx", + "url": "https://raw.githubusercontent.com/exelearning/nextcloud-exelearning/main/tests/fixtures/un-contenido-de-ejemplo-para-probar-estilos-y-catalogacion.elpx" + }, + { + "step": "writeFile", + "path": "data/admin/files/exelearning-samples/propiedades.elpx", + "url": "https://raw.githubusercontent.com/exelearning/nextcloud-exelearning/main/tests/fixtures/propiedades.elpx" + }, + { "step": "runOcc", "args": ["files:scan", "admin"] } + ] +} diff --git a/tests/fixtures/propiedades.elpx b/tests/fixtures/propiedades.elpx new file mode 100644 index 0000000..02d2525 Binary files /dev/null and b/tests/fixtures/propiedades.elpx differ