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
169 changes: 169 additions & 0 deletions .github/workflows/playground-preview.yml
Original file line number Diff line number Diff line change
@@ -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-<N>) 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 = '<!-- playground-preview -->';
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."
36 changes: 35 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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|<version>[^<]*</version>|<version>$(PACKAGE_VERSION)</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

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
49 changes: 49 additions & 0 deletions blueprint.json
Original file line number Diff line number Diff line change
@@ -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"] }
]
}
Binary file added tests/fixtures/propiedades.elpx
Binary file not shown.
Loading