The deb and rpm aarch64 workflows asked for a bare self-hosted runner.
x64 self-hosted runners now also serve this repository, so those jobs
could land on one and publish x64 packages to the aarch64 repos.
Require the ARM64 label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EPEL 10 has no arp-scan package, so the EL10 job failed at "Install
build tools" with "Unable to find a match: arp-scan".
The install was added in 283fd7da6 so cmake's find_program could pick
up the path. Since 52c5af93f the spec passes
-DZM_PATH_ARP_SCAN=/usr/sbin/arp-scan, which skips that search, and
the spec has no BuildRequires or Requires on arp-scan. The binary is
not used during the build on any distro.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fedora 45 releases on 2026-10-27. The fedora:45 image and
rpmfusion-free-release-45 are already published, so nightlies can
build against it now and the repo is ready on release day.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fedora 41 reached end of life on 2025-12-15 and Fedora 42 on
2026-05-27. EL10 had no entry in the matrix at all.
The official rockylinux image stops at 9, so EL10 uses
rockylinux/rockylinux:10. utils/zmrepo_mkdirs.sh mirrors the matrix
and gets the same change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RPM workflows rsync packages to rpm/master/<family>/<rel>/<arch>/
on zmrepo but never run createrepo_c, so that tree has no repodata and
dnf can't use it.
Add utils/zmrepo_createrepo.sh and run it over ssh after the publish
step. It serializes runs per directory with flock, removes a .repodata/
temp dir left by a run that died (createrepo_c refuses to start while
one exists; that is what froze el/10/x86_64 and fc/42/x86_64 in the
older tree on 2026-06-18), and ignores hangups so cancelling the job
doesn't kill createrepo_c halfway and leave the same mess behind.
The zmrepo host needs createrepo_c and flock installed.
tests/python/test_zmrepo_createrepo.py drives the script through
bash -s with a stub createrepo_c: stale temp dir cleanup, exit codes,
lock timeout, and surviving SIGHUP mid-run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-source-tarball pasted ${{ inputs.tag }},
${{ github.event.release.tag_name }} and the step outputs derived from
them straight into the run: scripts. GitHub substitutes expressions
into the script text before bash parses it, so a tag containing shell
syntax ran as code in a job with contents: write. Only people who can
publish a release or dispatch the workflow can set the tag, so this is
hardening, not an exposure.
Pass each value through env: and read it as a shell variable, as
GitHub's security hardening guide recommends. Normal tags produce the
same outputs and tarball name as before; the `with:` inputs are not
shell and are unchanged.
The package workflows' publish steps ran easingthemes/ssh-deploy@main with
the zmrepo SSH key in its environment, and imported the package signing key
with crazy-max/ghaction-import-gpg@v7. A branch or tag is resolved when the
workflow runs, so whoever could move it upstream could run code with those
keys on our next push to master or release-1.38, and publish packages to the
apt and rpm repositories.
Pin each `uses:` to the commit its ref names today, with the ref as a comment
so dependabot keeps updating them. ssh-deploy is pinned to v6.0.1, which main
differs from only in release metadata. GitHub's own actions are pinned too.
utils/check-pinned-actions.py and the CI Pinned Actions workflow fail when a
workflow uses an action by anything but a full commit SHA.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing links or dlopens libvlc any more, so the dev package is no
longer a build requirement anywhere: the debian, beowulf, ubuntu and
split-package control files, the podman Containerfile, the redhat and
opensuse specs, the centos CI image, and the five workflows that
installed libvlc-dev. The redhat spec also required vlc-core at
runtime, which was the dlopen target.
Documentation follows: the libVLC source tab section, the macos note
about libVLC not being found, and the WEB_FILTER_SOURCE source-type
list.
Two stale statements nearby are corrected while the files are open. The
Remote source tab still said its RTSP method "may be removed in the
future"; it was removed earlier on this branch. AGENTS.md still listed
RemoteCameraRTSP and LibVLCCamera among the camera classes, both of
which are now gone.
All workflow yaml parses, the three edited rst files render, and the
debian control field structure is unchanged apart from the removed
entries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
* fix: give each migrated password its own random bcrypt salt
migratePasswords() stored the Bytes::Random::Secure object rather than
bytes from it, so en_base64() encoded the string
"Bytes::Random::Secure=HASH(0x...)". bcrypt only reads the first 22
characters of the salt, all from the constant class name, so every user
on every install got the same salt. With neither Bytes::Random::Secure
nor Data::Entropy installed the salt was empty and bcrypt() died with
"bad bcrypt settings", aborting zmupdate.pl. Even the Data::Entropy path
reused a single salt for every user in the run.
Read 16 bytes per user from /dev/urandom, as generateAuthHashSecret()
in ZoneMinder::Config already does. If it can't be read, leave that
user's legacy hash in place, which auth.php still verifies, instead of
writing a weak one. This drops the need for Bytes::Random::Secure and
the deprecated Data::Entropy. refs #4333
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: drop the Data::Entropy dependency from packaging
zmupdate.pl no longer uses Data::Entropy, which upstream has deprecated
(CVE-2025-1860). refs #4333
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: rehash legacy passwords on every password login
Only the login form called migrateHash(), so accounts that sign in with
user=/pass= or through the API kept their mysql or zmupdate.pl
(mysql+bcrypt) hash, including those with the fixed salt. Call it from
those paths too, and have it check the password type itself.
migrateHash() also regenerated the auth hash from the in-memory user,
which still held the old password, so getAuthUser(), which checks
against the new one in the database, rejected it. Update the in-memory
password first, and update the row by Id rather than by the username
as typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: note passwords still on a zmupdate.pl-migrated hash
Every salt migratePasswords() used before this branch was shared by all
users in a run: from Data::Entropy, which before 0.008 keys its generator
from rand() (CVE-2025-1860), or, since 38c0f743 with Bytes::Random::Secure
installed, a constant. The original hash input is gone, so zmupdate.pl
can't rehash these, and nothing distinguishes them from properly salted
ones. Log a warning listing every user still on a -ZM- hash, noting that
it is upgraded at their next login or password reset, and that unused
accounts can be disabled or deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Three merges landed within 45 seconds on 2026-09-12 and each started its own
run of both package workflows. Every run publishes, so a burst of merges puts
several uploads into the repo for what is effectively one state of the tree.
Add a concurrency group per workflow and branch with cancel-in-progress, so a
newer push supersedes a build still in flight rather than racing it into the
incoming directory. This also stops the matrix rebuilding fourteen containers
for a commit that is already stale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkQwahn9pi1y4wJe9BTxjM
The snapshot component is date plus the number of commits since version.txt
last changed, so every build of the same tip on the same day produces the same
version string. The repo operator reports 96% of version+dist pairs published
more than once, and 1.39.34~20260912.22-bookworm1 uploaded six times in about
half an hour, each upload overwriting the last with different bytes.
Nothing on the repo server can close that. Reindexing shrinks the window but a
client that read Packages before a republish and fetched the .deb after it still
gets a hash mismatch, because the version no longer identifies the content.
Append GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT, which are unique per build and
never reused. Outside Actions the string is unchanged, so a local build still
produces the version it always did.
Checked that the longer string still sorts after the versions already published
and before the next version.txt bump, with dpkg --compare-versions and
rpm.vercmp.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkQwahn9pi1y4wJe9BTxjM
Nothing in CI compiles for anything but Linux, so Linux-only APIs reach
master unnoticed. dep/RtspServer calling pipe2() sat in master until a
native build was attempted by hand.
Builds on macos-latest with Homebrew deps and runs the Catch2 suite.
Notes on the choices:
- macos-latest, so the job follows whatever macOS GitHub ships (arm64,
macOS 26 at time of writing). The bare label is a standard runner and
free on public repos; -large and -xlarge are billed even here.
- mysql-client is keg-only, so the bare find_library in CMakeLists.txt
misses it and the build dies on "'mysql/mysql.h' file not found".
Hence the explicit -I alongside CMAKE_PREFIX_PATH.
- gsoap is installed although optional, so ONVIF is covered on macOS.
Without it the suite runs 138 cases instead of 144.
- Runs the test binary rather than ctest, matching ci-cpp-tests.yml:
catch_discover_tests does not map Catch2 tags onto ctest labels, so
ctest cannot skip [notCI], and zm_font.cpp loads fixtures by relative
path.
- push filter is '**' rather than '*', which does not match branch names
containing a slash.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5KL9Xbi7K5aGsauLtd8tG
Nothing in CI built tests/ at all. The packaging workflows and the -Werror job
all configure with BUILD_TEST_SUITE off, which is why the two Catch2 v2 leftovers
fixed in the previous commit sat there breaking the build unnoticed.
Pinned to ubuntu-24.04 rather than ubuntu-latest because the suite needs Catch2
v3 and noble is what packages it.
Runs the binary rather than ctest. catch_discover_tests does not carry Catch2
tags through as ctest labels, so ctest cannot skip the [notCI] cases that want a
reachable database or a listening socket, and zm_font.cpp loads its fixtures by
relative path so the working directory has to be the one the build copied
tests/data into.
Verified with a fresh out-of-source configure and build using the same commands
as the workflow: 144 test cases, 12480 assertions, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests/js holds eight files and 154 assertions covering auth-helpers,
EventStream, MonitorStream, table-helpers, the encoder templates and the zone
object size tool. No workflow ran any of them, so they only ever executed if
someone thought to run them by hand, and ci-eslint only checks that the source
parses and conforms to style.
The tests need nothing installed: they use node builtins (assert, fs, path,
vm) and files from web/js, so there is no npm install step and no submodule
to fetch.
The loop keeps going after a failure and fails at the end, so one CI run
reports every broken test rather than stopping at the first.
Verified by extracting the run script from the YAML and executing it: exit 0
with all eight files run, and after deliberately failing an assertion in the
first file, exit 1 with all eight still run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
The amd64 and aarch64 deb workflows build the same distro matrix, so both
produced zoneminder-doc_<version>_all.deb and uploaded it to the same
mini-dinstall incoming directory at the same time, racing each other and
offering mini-dinstall the same file twice.
Add a binary-arch build type to do_debian_package.sh that runs debuild -B,
and use it from the aarch64 workflow. The arm .changes then references only
arm64 packages and the arch-independent debs come from amd64 alone.
mini-dinstall moves files out of incoming/ while a job is uploading, so the
rsync receiver can generate block checksums for a basis file that disappears
before the transfer completes, failing with "got a block match with no basis
file". .deb payloads are already compressed, so delta transfer buys nothing
here anyway.
Every CI runner is 64bit, so nothing in CI exercised the 32bit SharedData
layout that zmc/zma/zms, ZoneMinder::Memory and web/includes/Monitor.php
all have to agree on. The i386 alignment divergence that broke the 32bit
build was only caught downstream.
utils/check-shareddata-abi.py lifts the struct body and its static_asserts
out of zm_monitor.h, compiles them standalone at -m32 and -m64, and
requires the size and every member offset to match. Compiling only the
struct avoids needing a 32bit copy of the ffmpeg, mysql and curl headers;
gcc-multilib alone is enough and the check takes about a second, so it
runs before the build rather than after it.
The expected size is never duplicated in the script. It comes from the
assertions in the header, so there is one source of truth and the script
cannot drift from it. Removing the assertions is itself reported as a
failure, since deleting the guard is the tempting way to silence a 32bit
build error.
Verified by reverting the epadding members, which reproduces the original
downstream failure (880 == 888) and additionally names capture_fps,
startup_time and control_state, and by deleting the assertions, which is
reported as a missing guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GCC emits a whole class of warnings only from its optimizer passes, so
they cannot appear in the -O0 Debug trees used for local development:
-Wclobbered, -Wmaybe-uninitialized, -Wstringop-truncation,
-Wstringop-overflow, -Warray-bounds, -Wdangling-pointer, -Wuse-after-free.
Nothing in CI closed that gap. The deb/rpm workflows build Release and
did print these warnings, but never set ENABLE_WERROR, so they scrolled
past as log noise. The only -Werror build is .cirrus.yml, which is
FreeBSD/clang, and clang does not implement -Wclobbered at all. The three
conditions therefore intersected only in the downstream Docker build,
which is where such warnings first broke a build.
Third-party diagnostics are demoted to warnings so they cannot fail the
build: system mosquittopp.h carries a #warning, and gsoap's wsseapi.c
casts between function and object pointers. Warnings in ZoneMinder's own
sources stay hard errors.
Pinned to ubuntu-24.04 rather than ubuntu-latest because -Wclobbered is
sensitive to compiler version and inlining, so a GCC bump should be a
deliberate change rather than a surprise CI failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four package workflows upload every built .deb/.rpm as a run artifact
and then rsync the same files to ZMREPO in the next step. The only consumer
of the artifacts is the `release` job, which is gated on
startsWith(github.ref, 'refs/tags/'). On branch pushes to master and
release-1.38 nothing ever reads them, so each push was parking ~1.3 GB of
artifacts against the org Actions storage quota for their full one-day
retention. Three pushes in a day held ~3.9 GB at once.
Gate each upload step on the same tag condition the `release` job uses, so
the artifacts exist exactly when something consumes them. Branch pushes
still publish to ZMREPO unchanged; tag builds are unaffected.
This also skips the artifact upload itself on branch pushes, which saves
transfer time on the self-hosted aarch64 runners.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179XFS73S5t4PM4L8zomZHX
Fedora 44 images come with Fedora's stripped ffmpeg libraries installed
(libavutil-free, libswresample-free). RPMFusion's ffmpeg-libs, which
satisfies the spec's BuildRequires on ffmpeg-devel, declares Conflicts
against those, so builddep fails to depsolve:
installed package libswresample-free-8.1.2-1.fc44.x86_64 conflicts
with libswresample-free provided by ffmpeg-libs-8.1.2-2.fc44.x86_64
from rpmfusion-free-updates
Pass --allowerasing so dnf swaps the -free libraries for the RPMFusion
ffmpeg-libs, matching what the systemd install in the build tools step
already does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZhAPb2indRrLWSEJ87kf8
Add fedora:44 to the build matrix in build-rpm-packages.yml and
build-rpm-packages-aarch64.yml, and to the DISTROS list in
utils/zmrepo_mkdirs.sh so the repo tree gets an rpm/master/fedora/44
directory for the deploy rsync.
Fall back to rpmfusion-free-release-rawhide.noarch.rpm in the x86_64
workflow when the versioned release RPM is missing, matching what the
aarch64 workflow already does. Fedora 44 is rawhide, so
rpmfusion-free-release-44.noarch.rpm does not exist yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZhAPb2indRrLWSEJ87kf8
Cyrillic/Greek characters that look like ASCII letters keep landing in the
tree from pasted contributions (discussions #4993, #4678, PR #4678), breaking
lookups and getting corrupted by reverse proxies. utils/check-homoglyphs.py
scans the git-tracked source for characters in the Cyrillic (U+0400-U+04FF)
and Greek (U+0370-U+03FF) blocks and exits non-zero on any hit, excluding
translations (web/lang/) and vendored/minified assets where they are
legitimate. A CI Homoglyphs workflow runs it on push and pull request, and it
can be run locally with python3 utils/check-homoglyphs.py.
refs https://github.com/ZoneMinder/zoneminder/discussions/4993
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQipwf632JGgNH4W7p8cqs
The build-rpm-packages workflows deploy with easingthemes/ssh-deploy using
rsync args -rltgoDzvO, which does not create missing parent directories on
the remote. When the zmrepo directory tree was deleted the deploy step
failed.
--mkpath is not a viable fix: it is parsed by the local rsync in the build
container, and Rocky 8 ships rsync 3.1.3 which predates the flag (3.2.3+).
Add a pre-deploy step that creates rpm/master/<family>/<releasever>/<arch>/
over ssh with mkdir -p, which has no rsync version dependency. Also add
utils/zmrepo_mkdirs.sh to recreate the full tree manually.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GitHub's auto-generated 'Source code' release assets are plain git
archive output without submodules, so cmake fails immediately on the
submodule check and the release cannot be built standalone.
On every published release (or manual dispatch with a tag, to backfill
existing releases) build zoneminder-<tag>.tar.gz from a recursive
checkout and attach it plus a sha256 to the release. The tarball is
reproducible (commit mtime, sorted entries, no owner, no gzip
timestamp) and is sanity-checked for the same file CMakeLists.txt
requires before upload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>