From 85ed1b6c641483a2458dd58ddfb4e00f7388ce05 Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:54:19 +0100 Subject: [PATCH 1/5] chore: bump develop to 0.4.1 after the 0.4.0 stable release --- CHANGELOG.md | 4 ++++ electron/package-lock.json | 4 ++-- electron/package.json | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- python/config.py | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8c3c8..152bb70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) — - [ ] **Batch Edit** — Apply a field change to multiple selected files at once - [ ] **Search/Filter Bar** — Filter the file list by name or extension +### Changed +- Version bumped to 0.4.1 on `develop` after the 0.4.0 stable release; beta builds of the next + release are numbered `0.4.1-beta.N`. + --- ## [0.4.0] — 2026-11-02 diff --git a/electron/package-lock.json b/electron/package-lock.json index 5c35056..f532f64 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "metalens", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metalens", - "version": "0.4.0", + "version": "0.4.1", "license": "MIT", "devDependencies": { "@electron-forge/cli": "^8.0.1", diff --git a/electron/package.json b/electron/package.json index 712a6c6..1954856 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "metalens", - "version": "0.4.0", + "version": "0.4.1", "description": "Universal File Metadata Manager", "author": "Graziano Mariella", "license": "MIT", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9069775..b40f7af 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "metalens-frontend", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metalens-frontend", - "version": "0.4.0", + "version": "0.4.1", "dependencies": { "@fontsource/inter": "^5.3.0", "@fontsource/jetbrains-mono": "^5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 9f1ed68..a9d0bc1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "metalens-frontend", - "version": "0.4.0", + "version": "0.4.1", "private": true, "type": "module", "scripts": { diff --git a/python/config.py b/python/config.py index e502896..fced168 100644 --- a/python/config.py +++ b/python/config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -VERSION = "0.4.0" +VERSION = "0.4.1" APP_NAME = "MetaLens" SIDECAR_HOST = "127.0.0.1" From f577b1f49ffc67dd6ba8824b8ff2e8cd585c5daf Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:07:53 +0100 Subject: [PATCH 2/5] ci: test-python-windows gates the beta; stable requires main to match the validated beta; docs --- .github/workflows/build.yml | 23 +++++++++++++++++++++-- CHANGELOG.md | 8 ++++++++ docs/REQUIREMENTS.md | 1 + 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 322a606..1b46c9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,8 @@ jobs: pytest tests/ -v # Same suite on Windows: atomic replace (ReplaceFileW), file attributes, DACL/ADS and - # pre-1970 timestamps are Windows-specific. Not a dependency of the beta/stable jobs. + # pre-1970 timestamps are Windows-specific. Gates the beta (get-version-beta); the stable + # is gated through find-latest-beta, which requires main to be the code of that beta. test-python-windows: if: github.ref == 'refs/heads/develop' || (github.event_name == 'pull_request' && github.base_ref == 'develop') runs-on: windows-latest @@ -147,7 +148,7 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── get-version-beta: if: github.ref == 'refs/heads/develop' - needs: [test-python, test-frontend, test-electron] + needs: [test-python, test-python-windows, test-frontend, test-electron] runs-on: ubuntu-latest permissions: contents: read @@ -492,6 +493,9 @@ jobs: outputs: beta_tag: ${{ steps.find.outputs.beta_tag }} steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # full history and tags, to compare main with the beta tag - id: find env: GH_TOKEN: ${{ github.token }} @@ -508,6 +512,21 @@ jobs: fi echo "beta_tag=$TAG" >> "$GITHUB_OUTPUT" echo "Validated matching beta $TAG for stable v${VERSION}" + - name: Verify main has the code that the beta tested + env: + TAG: ${{ steps.find.outputs.beta_tag }} + run: | + # The test jobs only run on develop and gate the beta; the stable runs no tests. + # So the stable is allowed only if everything outside the files the workflow + # ignores (docs, markdown) is identical to the validated beta. + CHANGED=$(git diff --name-only "$TAG" HEAD | grep -Ev '^(docs/|README\.md$|CHANGELOG\.md$|LICENSE$|[^/]+\.md$)' || true) + if [ -n "$CHANGED" ]; then + echo "::error::main differs from $TAG in files that were not validated by the beta pipeline:" + echo "$CHANGED" + echo "::error::Push these changes to develop first, wait for the new beta, then promote." + exit 1 + fi + echo "main matches $TAG (only documentation may differ)" build-windows-stable: if: github.ref == 'refs/heads/main' diff --git a/CHANGELOG.md b/CHANGELOG.md index 152bb70..90becf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) — - Version bumped to 0.4.1 on `develop` after the 0.4.0 stable release; beta builds of the next release are numbered `0.4.1-beta.N`. +### CI/CD +- `test-python-windows` now gates the beta (it is in the `needs` of `get-version-beta`). +- The stable pipeline runs no tests, so `find-latest-beta` now also checks that `main` has the same + code as the matching beta (only documentation may differ) and stops the release otherwise. + +### Documentation +- macOS: extended attributes stay read-only (stated in the requirements guide). + --- ## [0.4.0] — 2026-11-02 diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 31e6b80..ea3018a 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -32,6 +32,7 @@ Requirements for running the pre-built MetaLens application and, separately, for ### 🍎 macOS - **Status:** ❌ Not currently packaged — no macOS build is produced by CI. Building from source with `npm run make` is possible but unverified. +- **Extended attributes (xattr):** read-only on macOS. Writing them needs a macOS machine to be verified (the CI has no macOS runner and no Mac build is published), and an unverified write could corrupt user data, so it is not offered. Linux `user.` attributes are writable and covered by the CI. --- From a994343145f40e4645e64b74c33c7b469b5ba7ee Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:39:46 +0100 Subject: [PATCH 3/5] ci: smoke test of the installed packages gates the beta and the stable release --- .github/workflows/build.yml | 42 +++++++- .github/workflows/smoke-packages.yml | 156 +++++++++++++++++++++++++++ CHANGELOG.md | 6 ++ CONTRIBUTING.md | 2 + docs/REQUIREMENTS.md | 1 + scripts/smoke_samples.txt | 1 + scripts/smoke_sidecar.sh | 105 ++++++++++++++++++ 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/smoke-packages.yml create mode 100644 scripts/smoke_samples.txt create mode 100644 scripts/smoke_sidecar.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b46c9c..d6a1523 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -199,6 +199,15 @@ jobs: --hidden-import hachoir ` --hidden-import olefile ` --hidden-import pypdf + - name: Smoke test the packaged sidecar + shell: bash + run: | + # Starts the PyInstaller exe that will be shipped and exercises it (scripts/smoke_sidecar.sh). + cd python + pip install -r requirements-dev.txt -q + python -m tests.factories --out ../smoke-samples $(cat ../scripts/smoke_samples.txt) + cd .. + bash scripts/smoke_sidecar.sh "${{ needs.get-version-beta.outputs.base_version }}" "$PWD/smoke-samples" "$PWD/python/dist/metalens-sidecar.exe" - uses: actions/setup-node@v7 with: node-version: '24' @@ -405,9 +414,19 @@ jobs: path: electron/out/make/**/*.rpm if-no-files-found: error + # Installs the .deb/.rpm/.tar.gz on the supported distributions and exercises the installed + # sidecar (see smoke-packages.yml). Gates the beta: nothing is published if a package is broken. + smoke-linux-beta: + if: github.ref == 'refs/heads/develop' + needs: [get-version-beta, build-linux-beta, build-rpm-beta] + uses: ./.github/workflows/smoke-packages.yml + with: + expected_version: ${{ needs.get-version-beta.outputs.base_version }} + channel: beta + release-beta: if: github.ref == 'refs/heads/develop' - needs: [get-version-beta, build-windows-beta, build-linux-beta, build-rpm-beta] + needs: [get-version-beta, build-windows-beta, build-linux-beta, build-rpm-beta, smoke-linux-beta] runs-on: ubuntu-latest permissions: contents: write @@ -555,6 +574,15 @@ jobs: --hidden-import hachoir ` --hidden-import olefile ` --hidden-import pypdf + - name: Smoke test the packaged sidecar + shell: bash + run: | + # Starts the PyInstaller exe that will be shipped and exercises it (scripts/smoke_sidecar.sh). + cd python + pip install -r requirements-dev.txt -q + python -m tests.factories --out ../smoke-samples $(cat ../scripts/smoke_samples.txt) + cd .. + bash scripts/smoke_sidecar.sh "${{ needs.get-version.outputs.version }}" "$PWD/smoke-samples" "$PWD/python/dist/metalens-sidecar.exe" - uses: actions/setup-node@v7 with: node-version: '24' @@ -777,9 +805,19 @@ jobs: path: electron/out/make/**/*.rpm if-no-files-found: error + # Same smoke test on the packages the stable release will ship (they are rebuilt, not reused + # from the beta): nothing is published if one of them does not install or start. + smoke-linux-stable: + if: github.ref == 'refs/heads/main' + needs: [get-version, build-linux-stable, build-rpm-stable] + uses: ./.github/workflows/smoke-packages.yml + with: + expected_version: ${{ needs.get-version.outputs.version }} + channel: stable + release-stable: if: github.ref == 'refs/heads/main' - needs: [get-version, find-latest-beta, build-windows-stable, build-linux-stable, build-rpm-stable] + needs: [get-version, find-latest-beta, build-windows-stable, build-linux-stable, build-rpm-stable, smoke-linux-stable] runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/smoke-packages.yml b/.github/workflows/smoke-packages.yml new file mode 100644 index 0000000..1a7a537 --- /dev/null +++ b/.github/workflows/smoke-packages.yml @@ -0,0 +1,156 @@ +name: Smoke test packages + +# Installs the Linux packages (.deb, .rpm, .tar.gz) on the distributions we support and runs +# scripts/smoke_sidecar.sh against the INSTALLED sidecar: it starts, authenticates, reads a set +# of real sample files of every format family and does a write + undo round trip. +# +# - workflow_call: used by build.yml to gate the beta release (packages from this run's artifacts). +# - push on ci/** branches (no inputs): lets this workflow be developed without publishing a beta; it tests +# the packages of the latest published release. + +on: + workflow_call: + inputs: + expected_version: + description: Version the sidecar must report on /health (python/config.py VERSION) + required: true + type: string + channel: + description: beta or stable - selects the linux-*- artifacts of the calling run + required: true + type: string + push: + branches: ['ci/**'] + +permissions: + contents: read + +jobs: + samples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + cache: pip + cache-dependency-path: python/requirements*.txt + - name: Build the sample files + run: | + cd python + pip install -r requirements-dev.txt -q + python -m tests.factories --out ../smoke-samples $(cat ../scripts/smoke_samples.txt) + - uses: actions/upload-artifact@v7 + with: + name: smoke-samples + path: smoke-samples + if-no-files-found: error + + packages: + runs-on: ubuntu-latest + steps: + # Called from build.yml (inputs are set): take the packages built in the same run. + # NB: github.event_name is the CALLER's event (push), so it cannot tell the two modes apart. + - name: Download the packages built in this run + if: inputs.expected_version != '' + uses: actions/download-artifact@v8 + with: + pattern: linux-*-${{ inputs.channel }} + merge-multiple: true + path: pkgs + # Pushed on a ci/** branch: take the packages of the latest published release. + - name: Download the latest release packages + if: inputs.expected_version == '' + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 1 --json tagName --jq '.[0].tagName') + echo "Testing the packages of $TAG" + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir pkgs --pattern '*.deb' --pattern '*.rpm' --pattern '*.tar.gz' + - run: find pkgs -type f | sort + - uses: actions/upload-artifact@v7 + with: + name: smoke-packages + path: pkgs + if-no-files-found: error + + version: + runs-on: ubuntu-latest + outputs: + expected: ${{ steps.v.outputs.expected }} + steps: + - uses: actions/checkout@v7 + - id: v + env: + INPUT_VERSION: ${{ inputs.expected_version }} + run: | + V="$INPUT_VERSION" + if [ -z "$V" ]; then V=$(grep -oP 'VERSION\s*=\s*"[^"]+' python/config.py | grep -oP '[0-9][^"]*'); fi + echo "expected=$V" >> "$GITHUB_OUTPUT" + echo "Sidecar must report version $V" + + smoke: + needs: [samples, packages, version] + runs-on: ubuntu-24.04 + # Only distributions with long remaining support (LTS or recent). Review this list when one + # approaches its end of support (Ubuntu 22.04 and Debian 12 were dropped on 2026-11-02). + strategy: + fail-fast: false + matrix: + include: + - { name: ubuntu-24.04 deb, image: 'ubuntu:24.04', kind: deb } + - { name: ubuntu-26.04 deb, image: 'ubuntu:26.04', kind: deb } + - { name: debian-13 deb, image: 'debian:13', kind: deb } + - { name: rocky-9 rpm, image: 'rockylinux:9', kind: rpm } + - { name: rocky-10 rpm, image: 'rockylinux/rockylinux:10', kind: rpm } + - { name: fedora-latest rpm, image: 'fedora:latest', kind: rpm } + # Oldest glibc we promise (2.34, the one the sidecar is built against). + - { name: rocky-9 tar.gz, image: 'rockylinux:9', kind: tar } + name: ${{ matrix.name }} + container: ${{ matrix.image }} + steps: + - name: Install the tools the smoke test needs + run: | + if command -v apt-get >/dev/null; then + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates findutils procps >/dev/null + else + dnf install -y -q curl findutils procps-ng --allowerasing >/dev/null + fi + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: smoke-samples + path: smoke-samples + - uses: actions/download-artifact@v8 + with: + name: smoke-packages + path: pkgs + - name: Install the package + run: | + case "${{ matrix.kind }}" in + deb) + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ./pkgs/*.deb >/dev/null ;; + rpm) + dnf install -y -q ./pkgs/*.rpm >/dev/null ;; + tar) + mkdir -p /opt/metalens && tar -xzf pkgs/*.tar.gz -C /opt/metalens ;; + esac + - name: Locate the installed sidecar and the Electron binary + run: | + SIDECAR=$(find / -xdev -name metalens-sidecar -type f 2>/dev/null | head -1) + APP=$(find / -xdev -name metalens -type f -perm -u+x 2>/dev/null | head -1) + echo "sidecar: $SIDECAR"; echo "app: $APP" + [ -n "$SIDECAR" ] || { echo "::error::metalens-sidecar not found after installing the package"; exit 1; } + [ -n "$APP" ] || { echo "::error::the metalens executable was not found after installing the package"; exit 1; } + echo "SIDECAR=$SIDECAR" >> "$GITHUB_ENV" + echo "APP=$APP" >> "$GITHUB_ENV" + - name: The packaged app has all its shared libraries + if: matrix.kind != 'tar' + run: | + # Electron needs GTK, NSS, ... : the package dependencies must pull them in. + MISSING=$(ldd "$APP" 2>&1 | grep 'not found' || true) + if [ -n "$MISSING" ]; then echo "::error::missing libraries after installing the package:"; echo "$MISSING"; exit 1; fi + echo "all libraries of $APP resolve" + - name: Run the smoke test against the installed sidecar + run: bash scripts/smoke_sidecar.sh "${{ needs.version.outputs.expected }}" smoke-samples "$SIDECAR" diff --git a/CHANGELOG.md b/CHANGELOG.md index 90becf9..07b0305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) — - `test-python-windows` now gates the beta (it is in the `needs` of `get-version-beta`). - The stable pipeline runs no tests, so `find-latest-beta` now also checks that `main` has the same code as the matching beta (only documentation may differ) and stops the release otherwise. +- New smoke test of the packages (`scripts/smoke_sidecar.sh`, `.github/workflows/smoke-packages.yml`): the + `.deb`, `.rpm` and `.tar.gz` are installed on Ubuntu 24.04 and 26.04, Debian 13, Rocky Linux 9 and 10 and + current Fedora, and the installed sidecar must start, require its token, read sample files of every format + family and complete a write + undo round trip (the app's shared libraries must also resolve). The packaged + Windows sidecar is exercised the same way. It gates both the beta and the stable release: nothing is + published if a package is broken. The stable rebuilds its packages and tests those. ### Documentation - macOS: extended attributes stay read-only (stated in the requirements guide). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 738719f..9c372f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -194,6 +194,8 @@ Preview what a release will say with `python scripts/changelog_notes.py X.Y.Z`. The PyInstaller sidecar embeds the glibc of the machine that builds it, so the Linux jobs in `.github/workflows/build.yml` never use `ubuntu-latest` for it. `build-sidecar-linux-*` builds the sidecar once in a `rockylinux:9` container (glibc 2.34, Python 3.13 from `uv`); `build-linux-*` (`.deb` and `.tar.gz`, on `ubuntu-24.04`) and `build-rpm-*` (`.rpm`, in the same container) reuse it. Do not build the `.rpm` on `ubuntu-26.04`: RPM 6 breaks `electron-installer-redhat` 4.0.0. A workflow change can be tried without publishing a release by dispatching `build.yml` from a temporary branch whose job conditions are relaxed. +**Package smoke test.** `.github/workflows/smoke-packages.yml` (called by `smoke-linux-beta` / `smoke-linux-stable` in `build.yml`, which gate `release-beta` / `release-stable`) installs the `.deb`, `.rpm` and `.tar.gz` in containers of the supported distributions and runs `scripts/smoke_sidecar.sh` against the installed sidecar; the Windows jobs run the same script on the freshly built `metalens-sidecar.exe`. The sample files come from `python -m tests.factories --out $(cat scripts/smoke_samples.txt)`. To work on this workflow without publishing a beta, push a branch named `ci/`: the `push` trigger of `smoke-packages.yml` then tests the packages of the latest published release. Keep the distribution list to long-supported releases (LTS or recent) and review it when one nears its end of support. The script can be tried locally against any sidecar: `scripts/smoke_sidecar.sh `. + --- ## 🔧 Dependency Updates diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index ea3018a..70195c2 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -29,6 +29,7 @@ Requirements for running the pre-built MetaLens application and, separately, for - **Minimum C library:** glibc 2.34 — the bundled sidecar is built on RHEL 9. This covers Ubuntu 22.04+, Debian 12+, RHEL 9 / 10 (Rocky, AlmaLinux) and current Fedora - **Not supported:** RHEL 8 and older (glibc older than 2.34) - **Tested:** installing each package and starting the sidecar (not the full desktop window) was verified in containers on Ubuntu 22.04 / 24.04 / 26.04, Debian 12 / 13, Fedora 43 / 44, Rocky Linux 9 and AlmaLinux 10 (`.tar.gz` on Debian 12, Ubuntu 22.04 and Fedora 44) +- **Automated check (CI, every beta):** the packages are installed on Ubuntu 24.04 and 26.04, Debian 13, Rocky Linux 9 and 10 and current Fedora (the `.tar.gz` on Rocky Linux 9, which has the oldest supported glibc). The installed sidecar must start, require its token, read sample files of every format family and complete a write + undo round trip, and the app's shared libraries must resolve. Only distributions with long remaining support are in this list; older ones keep working as long as they meet the glibc baseline. ### 🍎 macOS - **Status:** ❌ Not currently packaged — no macOS build is produced by CI. Building from source with `npm run make` is possible but unverified. diff --git a/scripts/smoke_samples.txt b/scripts/smoke_samples.txt new file mode 100644 index 0000000..186ffe5 --- /dev/null +++ b/scripts/smoke_samples.txt @@ -0,0 +1 @@ +jpeg_camera jpeg_iptc png_text_exif webp_exif gif_comment tiff_dpi mp3_v24_full flac_vorbis_picture ogg_opus wav_id3 m4a_tags mp4_tags pdf_xmp docx xlsx pptx ole_doc_cp1252 avi_two_streams mkv_two_tracks random_xyz diff --git a/scripts/smoke_sidecar.sh b/scripts/smoke_sidecar.sh new file mode 100644 index 0000000..f08aa51 --- /dev/null +++ b/scripts/smoke_sidecar.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Smoke test of a PACKAGED (or any) sidecar: start it, then exercise it over HTTP. +# +# scripts/smoke_sidecar.sh +# +# The sidecar command gets the port as its last argument (as Electron does) and the token in +# METALENS_SIDECAR_TOKEN. holds files made by `python -m tests.factories --out`. +# Checks: /health (status + version), auth is enforced, /list, /read of every sample with no +# read errors (this loads every handler and library bundled by PyInstaller) and a /write + /undo +# round trip on a PDF. Needs only bash + curl, so it runs inside minimal containers. +set -u + +if [ "$#" -lt 3 ]; then + echo "usage: $0 " >&2 + exit 2 +fi +EXPECTED="$1"; SAMPLES="$2"; shift 2 + +PORT=$((20000 + RANDOM % 20000)) +TOKEN=$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n') +LOG=$(mktemp) +FAILS=0 + +fail() { echo "FAIL: $*"; FAILS=$((FAILS + 1)); } +ok() { echo "ok: $*"; } + +# Paths are sent to the sidecar as it understands them (native Windows paths under Git Bash). +native() { if command -v cygpath >/dev/null 2>&1; then cygpath -w "$1"; else printf '%s' "$1"; fi; } +BS=$'\134' # a single backslash +json_escape() { local s=${1//"$BS"/"$BS$BS"}; printf '%s' "${s//\"/$BS\"}"; } +api() { curl -s --max-time 30 -H "Authorization: Bearer $TOKEN" "$@"; } + +METALENS_SIDECAR_TOKEN="$TOKEN" "$@" "$PORT" >"$LOG" 2>&1 & +PID=$! +cleanup() { + kill "$PID" >/dev/null 2>&1 + # A PyInstaller --onefile sidecar is a bootstrap plus a child that serves: stop both. + # -x: match the process NAME only; -f would also match this script (its path contains the name). + pkill -x "metalens-sidecar" >/dev/null 2>&1 + command -v taskkill >/dev/null 2>&1 && taskkill //F //T //PID "$PID" >/dev/null 2>&1 + rm -f "$LOG" +} +trap cleanup EXIT + +# ── /health ────────────────────────────────────────────────────────────────────────────── +HEALTH="" +for _ in $(seq 1 60); do + HEALTH=$(api "http://127.0.0.1:$PORT/health" 2>/dev/null) + case "$HEALTH" in *'"status":"ok"'*) break ;; esac + kill -0 "$PID" >/dev/null 2>&1 || { echo "sidecar exited early:"; cat "$LOG"; exit 1; } + sleep 1 +done +case "$HEALTH" in + *'"status":"ok"'*) ok "health: $HEALTH" ;; + *) echo "sidecar never became healthy:"; cat "$LOG"; exit 1 ;; +esac +case "$HEALTH" in + *"\"version\":\"$EXPECTED\""*) ok "version is $EXPECTED" ;; + *) fail "expected version $EXPECTED, got: $HEALTH" ;; +esac + +# ── auth is enforced ───────────────────────────────────────────────────────────────────── +CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "http://127.0.0.1:$PORT/health") +[ "$CODE" = "401" ] && ok "request without token -> 401" || fail "request without token -> $CODE (expected 401)" + +# ── /list ──────────────────────────────────────────────────────────────────────────────── +CODE=$(api -o /dev/null -w '%{http_code}' --get --data-urlencode "path=$(native "$SAMPLES")" "http://127.0.0.1:$PORT/list") +[ "$CODE" = "200" ] && ok "/list of the samples folder -> 200" || fail "/list -> $CODE" + +# ── /read of every sample ──────────────────────────────────────────────────────────────── +COUNT=0 +while IFS= read -r FILE; do + COUNT=$((COUNT + 1)) + BODY=$(api -w '\n%{http_code}' --get --data-urlencode "path=$(native "$FILE")" "http://127.0.0.1:$PORT/read") + CODE=${BODY##*$'\n'}; BODY=${BODY%$'\n'*} + case "$CODE:$BODY" in + 200:*'"read_errors":[]'*) ok "read ${FILE#"$SAMPLES"/}" ;; + 200:*) fail "read ${FILE#"$SAMPLES"/}: read_errors not empty" ;; + *) fail "read ${FILE#"$SAMPLES"/}: HTTP $CODE ${BODY:0:200}" ;; + esac +done < <(find "$SAMPLES" -type f | sort) +[ "$COUNT" -gt 0 ] || fail "no sample files found in $SAMPLES" + +# ── /write + /undo round trip on a PDF ─────────────────────────────────────────────────── +PDF=$(find "$SAMPLES" -type f -name '*.pdf' | sort | head -1) +if [ -n "$PDF" ]; then + P=$(json_escape "$(native "$PDF")") + W=$(api -X POST -H 'Content-Type: application/json' \ + -d "{\"path\":\"$P\",\"fields\":[{\"key\":\"pdf:Title\",\"value\":\"MetaLens smoke test\"}],\"delete\":[]}" \ + "http://127.0.0.1:$PORT/write") + case "$W" in + *'"ok":true'*'"undo_token":"'*) + TK=${W#*\"undo_token\":\"}; TK=${TK%%\"*} + ok "write pdf:Title" + U=$(api -X POST -H 'Content-Type: application/json' -d "{\"token\":\"$TK\"}" "http://127.0.0.1:$PORT/undo") + case "$U" in *'"ok":true'*) ok "undo restored the PDF" ;; *) fail "undo: ${U:0:200}" ;; esac ;; + *) fail "write pdf:Title: ${W:0:300}" ;; + esac +else + fail "no PDF sample for the write/undo round trip" +fi + +echo +if [ "$FAILS" -eq 0 ]; then echo "SMOKE TEST PASSED ($COUNT files read)"; else echo "SMOKE TEST FAILED: $FAILS check(s)"; echo "--- sidecar log ---"; cat "$LOG"; fi +[ "$FAILS" -eq 0 ] From 2cac2bf49daa878c81e8d05d04ba69e860154f83 Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:58:29 +0100 Subject: [PATCH 4/5] ci: smoke test copies the packages into one flat folder --- .github/workflows/smoke-packages.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-packages.yml b/.github/workflows/smoke-packages.yml index 1a7a537..e2f9c6d 100644 --- a/.github/workflows/smoke-packages.yml +++ b/.github/workflows/smoke-packages.yml @@ -67,11 +67,21 @@ jobs: TAG=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 1 --json tagName --jq '.[0].tagName') echo "Testing the packages of $TAG" gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir pkgs --pattern '*.deb' --pattern '*.rpm' --pattern '*.tar.gz' - - run: find pkgs -type f | sort + - name: Put the packages in one flat folder + run: | + # Artifacts keep their sub-folders (pkgs/deb/..., pkgs/rpm/...); a release download does not. + mkdir flat + for pat in '*.deb' '*.rpm' '*.tar.gz'; do + find pkgs -type f -name "$pat" -exec cp -t flat {} + + done + ls -l flat + for ext in deb rpm tar.gz; do + ls flat/*.$ext >/dev/null 2>&1 || { echo "::error::no .$ext package found"; exit 1; } + done - uses: actions/upload-artifact@v7 with: name: smoke-packages - path: pkgs + path: flat if-no-files-found: error version: From ec031b4f5a467ce68cfe7876a804f79362c71dcd Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:15:19 +0100 Subject: [PATCH 5/5] docs: changelog for the 0.4.1 stable release --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b0305..9c0f311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) — - [ ] **Batch Edit** — Apply a field change to multiple selected files at once - [ ] **Search/Filter Bar** — Filter the file list by name or extension -### Changed -- Version bumped to 0.4.1 on `develop` after the 0.4.0 stable release; beta builds of the next - release are numbered `0.4.1-beta.N`. +--- + +## [0.4.1] — 2026-11-03 + +This release contains no change to the application itself: it hardens the build and release pipeline +(the program is the same as 0.4.0). ### CI/CD - `test-python-windows` now gates the beta (it is in the `needs` of `get-version-beta`).