diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index da944e3..5e320cc 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -15,26 +15,61 @@ jobs: name: Build installable ZIP runs-on: ubuntu-latest timeout-minutes: 20 + outputs: + version: ${{ steps.release.outputs.version }} + archive_name: ${{ steps.release.outputs.archive_name }} steps: - name: Checkout uses: actions/checkout@v7 - name: Build and validate release - run: scripts/build-release.sh + id: release + run: | + set -Eeuo pipefail + + scripts/build-release.sh + + version=$(awk -F"'" '/\$this->version[[:space:]]*=/{print $2; exit}' prettyblocks.php) + archive_name="prettyblocks-${version}.zip" + test -s "dist/${archive_name}" + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "archive_name=${archive_name}" >> "$GITHUB_OUTPUT" - - name: Upload release packages + - name: Upload release package uses: actions/upload-artifact@v7 with: - name: prettyblocks-${{ github.sha }} - path: dist/*.zip + name: prettyblocks-${{ steps.release.outputs.version }} + path: dist/${{ steps.release.outputs.archive_name }} if-no-files-found: error retention-days: 14 + test-release: + name: Install ZIP on PrestaShop + needs: build-release + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Download release package + uses: actions/download-artifact@v8 + with: + name: prettyblocks-${{ needs.build-release.outputs.version }} + path: dist + + - name: Test downloaded release package + run: scripts/test-release.sh "dist/${{ needs.build-release.outputs.archive_name }}" + publish-release: name: Publish GitHub release if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: build-release + needs: + - build-release + - test-release runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -49,24 +84,31 @@ jobs: - name: Download release package uses: actions/download-artifact@v8 with: - name: prettyblocks-${{ github.sha }} + name: prettyblocks-${{ needs.build-release.outputs.version }} path: dist - name: Prepare release metadata id: release env: GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.build-release.outputs.version }} + ARCHIVE_NAME: ${{ needs.build-release.outputs.archive_name }} run: | set -Eeuo pipefail - version=$(awk -F"'" '/\$this->version[[:space:]]*=/{print $2; exit}' prettyblocks.php) - if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.-]+)?$ ]]; then + if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.-]+)?$ ]]; then echo "Unable to read a valid semantic version from prettyblocks.php." >&2 exit 1 fi - test -s dist/prettyblocks.zip - sha256sum dist/prettyblocks.zip > dist/prettyblocks.zip.sha256 + archive_path="dist/${ARCHIVE_NAME}" + checksum_path="${archive_path}.sha256" + test -s "$archive_path" + ( + cd dist + sha256sum "$ARCHIVE_NAME" > "${ARCHIVE_NAME}.sha256" + sha256sum --check "${ARCHIVE_NAME}.sha256" + ) previous_tag=$(gh release list \ --exclude-drafts \ @@ -82,6 +124,10 @@ jobs: fi { + echo '## Installation' + echo + printf 'Download `%s` from this release assets. Do not use the automatically generated **Source code (zip)** archive, which is not an installable PrestaShop module.\n' "$ARCHIVE_NAME" + echo echo '## Contributors' echo git log --format='%aN%x09%aE' "$commit_range" \ @@ -92,23 +138,37 @@ jobs: done } > release-notes-prefix.md - echo "version=$version" >> "$GITHUB_OUTPUT" + echo "archive_path=${archive_path}" >> "$GITHUB_OUTPUT" + echo "checksum_path=${checksum_path}" >> "$GITHUB_OUTPUT" - name: Publish release env: GH_TOKEN: ${{ github.token }} - VERSION: ${{ steps.release.outputs.version }} + VERSION: ${{ needs.build-release.outputs.version }} + ARCHIVE_PATH: ${{ steps.release.outputs.archive_path }} + CHECKSUM_PATH: ${{ steps.release.outputs.checksum_path }} run: | set -Eeuo pipefail if gh release view "$VERSION" >/dev/null 2>&1; then - echo "Release $VERSION already exists; nothing to publish." + tag_commit=$(git rev-list -n 1 "$VERSION") + if [ "$tag_commit" != "$GITHUB_SHA" ]; then + echo "Release $VERSION points to $tag_commit instead of $GITHUB_SHA." >&2 + echo "Bump the module version before publishing a different commit." >&2 + exit 1 + fi + + gh release upload "$VERSION" \ + "$ARCHIVE_PATH#PrettyBlocks installable module ZIP" \ + "$CHECKSUM_PATH#SHA-256 checksum" \ + --clobber + echo "Release $VERSION already exists; its assets were validated and refreshed." exit 0 fi gh release create "$VERSION" \ - 'dist/prettyblocks.zip#PrettyBlocks module ZIP' \ - 'dist/prettyblocks.zip.sha256#SHA-256 checksum' \ + "$ARCHIVE_PATH#PrettyBlocks installable module ZIP" \ + "$CHECKSUM_PATH#SHA-256 checksum" \ --target "$GITHUB_SHA" \ --title "$VERSION" \ --generate-notes \ diff --git a/README.md b/README.md index faeea6d..d1c1ebc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,11 @@ cd ../../../ && php bin/console prestashop:module install prettyblocks ### Release -Téléchargez la [dernière release](https://github.com/PrestaSafe/prettyblocks/releases/latest), puis installez le module directement dans votre PrestaShop :) +Téléchargez `prettyblocks-{version}.zip` dans les assets de la +[dernière release](https://github.com/PrestaSafe/prettyblocks/releases/latest), +puis installez ce fichier directement dans votre PrestaShop. N'utilisez pas +l'archive automatique **Source code (zip)** de GitHub : elle ne contient pas le +module compilé et n'est pas installable. Vous pouvez également utiliser notre module ClassicBlocks, afin d'avoir 4 blocks à utiliser. Disponible ici: [Télécharger ClassicBlocks](https://github.com/PrestaSafe/classicblocks) @@ -64,7 +68,11 @@ cd ../../../ && php bin/console prestashop:module install prettyblocks ``` ### From Release -For installing PrettyBlocks, you can download our [latest release](https://github.com/PrestaSafe/prettyblocks/releases/latest) and install the module directly in your PrestaShop. +Download `prettyblocks-{version}.zip` from the assets of our +[latest release](https://github.com/PrestaSafe/prettyblocks/releases/latest) +and install that file directly in PrestaShop. Do not use GitHub's automatically +generated **Source code (zip)** archive: it does not contain the compiled module +and cannot be installed. You can also add our module ClassicBlocks for having 4 blocks to use [Download ClassicBlocks](https://github.com/PrestaSafe/classicblocks) diff --git a/_dev/vite.config.js b/_dev/vite.config.js index 2ab33bf..d85a183 100644 --- a/_dev/vite.config.js +++ b/_dev/vite.config.js @@ -5,7 +5,7 @@ import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], build:{ - manifest: true, + manifest: 'manifest.json', outDir: "../build/", emptyOutDir: true, }, diff --git a/docker/README.md b/docker/README.md index c7099b2..22c5ad6 100644 --- a/docker/README.md +++ b/docker/README.md @@ -65,15 +65,22 @@ with a top-level `prettyblocks/` directory, and validates its contents: scripts/build-release.sh ``` -The versioned archive and its stable `prettyblocks.zip` alias are written to -`dist/`. To prove that the ZIP is self-contained, run it against two fresh, -temporary shops. This test disables both the source asset build and the runtime -Composer install, so only files present in the archive can be used: +The installable `prettyblocks-{version}.zip` archive is written to `dist/`. Its +top-level directory is always `prettyblocks/`, without the version suffix. To +prove that the ZIP is self-contained, run it against two fresh, temporary +shops. This test disables both the source asset build and the runtime Composer +install, so only files present in the archive can be used: ```bash scripts/test-release.sh ``` +Pass an existing archive to test the exact package without rebuilding it: + +```bash +scripts/test-release.sh dist/prettyblocks-3.2.2.zip +``` + The release test validates and extracts the ZIP with PrestaShop's native ZIP handler, then installs that extracted artifact with the PrestaShop CLI. It uses ports `8182` and `8191` by default and deletes its shops, databases, and diff --git a/prettyblocks.php b/prettyblocks.php index 1f43a66..631ef91 100755 --- a/prettyblocks.php +++ b/prettyblocks.php @@ -73,7 +73,7 @@ public function __construct() { $this->name = 'prettyblocks'; $this->tab = 'administration'; - $this->version = '3.2.1'; + $this->version = '3.2.2'; $this->author = 'PrestaSafe'; $this->need_instance = 1; $this->js_path = $this->_path . 'views/js/'; diff --git a/scripts/build-release.sh b/scripts/build-release.sh index eb2d697..b1c3584 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -55,7 +55,7 @@ required_paths=( prettyblocks/prettyblocks.php prettyblocks/vendor/autoload.php prettyblocks/vendor/composer/installed.php - prettyblocks/build/.vite/manifest.json + prettyblocks/build/manifest.json prettyblocks/views/js/build/build.js prettyblocks/views/css/iframe.css prettyblocks/views/css/dist/main.css @@ -116,15 +116,9 @@ fi mkdir -p "$output_dir" cp "$built_archive" "$output_dir/$archive_name" -cp "$built_archive" "$output_dir/prettyblocks.zip" +rm -f "$output_dir/prettyblocks.zip" "$output_dir/prettyblocks.zip.sha256" versioned_checksum=$(sha256_file "$output_dir/$archive_name") -stable_checksum=$(sha256_file "$output_dir/prettyblocks.zip") -if [[ $versioned_checksum != "$stable_checksum" ]]; then - echo "Versioned and stable release archives differ." >&2 - exit 1 -fi echo "Release ready: $output_dir/$archive_name" -echo "Stable alias: $output_dir/prettyblocks.zip" echo "SHA-256: $versioned_checksum" diff --git a/scripts/test-release.sh b/scripts/test-release.sh index a2d2b6b..6e2e161 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -8,10 +8,34 @@ compose_files=( -f "$project_dir/docker/compose.release-test.yml" ) -"$script_dir/build-release.sh" - version=$(awk -F"'" '/\$this->version[[:space:]]*=/{print $2; exit}' "$project_dir/prettyblocks.php") -export PRETTYBLOCKS_RELEASE_ARCHIVE_PATH="$project_dir/dist/prettyblocks-${version}.zip" +expected_archive_name="prettyblocks-${version}.zip" + +if [ "$#" -gt 1 ]; then + echo "Usage: $0 [archive]" >&2 + exit 1 +fi + +if [ "$#" -eq 1 ]; then + archive_path=$1 + if [ ! -f "$archive_path" ]; then + echo "Release archive not found: $archive_path" >&2 + exit 1 + fi + + archive_dir=$(CDPATH='' cd -- "$(dirname -- "$archive_path")" && pwd) + archive_path="$archive_dir/$(basename -- "$archive_path")" +else + "$script_dir/build-release.sh" + archive_path="$project_dir/dist/$expected_archive_name" +fi + +if [ "$(basename -- "$archive_path")" != "$expected_archive_name" ]; then + echo "Expected release archive name: $expected_archive_name" >&2 + exit 1 +fi + +export PRETTYBLOCKS_RELEASE_ARCHIVE_PATH="$archive_path" export COMPOSE_PROJECT_NAME="prettyblocks-release-test-${$}" export PS82_PORT=${PS82_RELEASE_PORT:-8182} export PS91_PORT=${PS91_RELEASE_PORT:-8191} @@ -60,18 +84,39 @@ for service_and_port in "ps82:$PS82_PORT" "ps91:$PS91_PORT"; do port=${service_and_port##*:} asset_paths=$(docker compose "${compose_files[@]}" exec -T "$service" php -r ' + $manifestPath = "modules/prettyblocks/build/manifest.json"; $manifest = json_decode( - file_get_contents("modules/prettyblocks/build/.vite/manifest.json"), + file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR ); - foreach ($manifest as $entry) { - echo $entry["file"], "\n"; - foreach ($entry["css"] ?? [] as $css) { - echo $css, "\n"; + $entry = $manifest["index.html"] ?? null; + if (!is_array($entry) || true !== ($entry["isEntry"] ?? false)) { + throw new RuntimeException("The Vite manifest has no index.html entry."); + } + + $javascript = $entry["file"] ?? null; + $stylesheets = $entry["css"] ?? null; + if (!is_string($javascript) || !str_ends_with($javascript, ".js")) { + throw new RuntimeException("The Vite manifest has no entry JavaScript."); + } + if (!is_array($stylesheets) || [] === $stylesheets) { + throw new RuntimeException("The Vite manifest has no entry stylesheet."); + } + + foreach (array_merge([$javascript], $stylesheets) as $asset) { + if ( + !is_string($asset) + || str_starts_with($asset, "/") + || str_contains($asset, "..") + || str_contains($asset, "\\") + ) { + throw new RuntimeException("The Vite manifest contains an unsafe asset path."); } + + echo $asset, "\n"; } ' | tr -d '\r') diff --git a/src/Controller/AdminThemeManagerController.php b/src/Controller/AdminThemeManagerController.php index 26213c0..854e602 100644 --- a/src/Controller/AdminThemeManagerController.php +++ b/src/Controller/AdminThemeManagerController.php @@ -202,10 +202,16 @@ public function indexAction() $js_entry = ''; if ($filesystem->exists($build_dir)) { // load manifest.json - $manifest = $build_dir . '.vite/manifest.json'; + $manifest = $build_dir . 'manifest.json'; if (!$filesystem->exists($manifest)) { - throw new \Exception('manifest.json not exist'); + // Keep existing development builds working after moving the + // release manifest out of Vite's hidden .vite directory. + $manifest = $build_dir . '.vite/manifest.json'; + } + + if (!$filesystem->exists($manifest)) { + throw new \Exception('manifest.json does not exist'); } $json = \Tools::file_get_contents($manifest); $json = json_decode($json, true);