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
65 changes: 61 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
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
Expand Down Expand Up @@ -147,7 +148,7 @@
# ─────────────────────────────────────────────────────────────────────────────
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
Expand Down Expand Up @@ -198,6 +199,15 @@
--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'
Expand Down Expand Up @@ -404,9 +414,19 @@
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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}
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
Expand Down Expand Up @@ -492,6 +512,9 @@
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 }}
Expand All @@ -508,6 +531,21 @@
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'
Expand Down Expand Up @@ -536,6 +574,15 @@
--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'
Expand Down Expand Up @@ -758,9 +805,19 @@
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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}
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
Expand Down
166 changes: 166 additions & 0 deletions .github/workflows/smoke-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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-*-<channel> 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'
- 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: flat
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"
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) —

---

## [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`).
- 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).

---

## [0.4.0] — 2026-11-02

### Security
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir> $(cat scripts/smoke_samples.txt)`. To work on this workflow without publishing a beta, push a branch named `ci/<something>`: 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 <version> <samples dir> <sidecar command...>`.

---

## 🔧 Dependency Updates
Expand Down
2 changes: 2 additions & 0 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ 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.
- **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.

---

Expand Down
4 changes: 2 additions & 2 deletions electron/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "metalens",
"version": "0.4.0",
"version": "0.4.1",
"description": "Universal File Metadata Manager",
"author": "Graziano Mariella",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "metalens-frontend",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion python/config.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
1 change: 1 addition & 0 deletions scripts/smoke_samples.txt
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading