From b9f8cdb9ebe81d4f0fb582dc44dfd1dc6bb490cf Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Sat, 17 Oct 2026 20:43:04 +0000 Subject: [PATCH 1/4] release: one release "latest" titled "Cerberus ", published on a version bump; cerberus 0.3.1 (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci.yml: the release job runs on master (pushes and manual runs) after lint, test and build; it reads CERBERUS_VERSION and publishes only when it differs from the version in the title of the current "latest": the binaries just built and tested, the installers, the license and SHA256SUMS (ci/assemble.sh), "latest" deleted with its tag and created again on the built commit, titled "Cerberus ". A failed lookup of "latest" (other than not found) stops the job. The v* tag trigger is gone; on master a newer push waits instead of cancelling. - ci/build.sh: the compiled version is CERBERUS_VERSION in src/cerberus.c (VERSION overrides it for a test build only); no git describe, so the build no longer needs the tags or a full clone. - CERBERUS_VERSION 0.3.1: the first release under this scheme. - READMEs: "Releases" and "CI and releases" describe the one release and how to publish (bump the version with its CHANGELOG.md section). - CHANGELOG.md: 0.3.1. The v0.1.0...v0.3.0 releases and tags are removed after the first latest is checked (their notes are in CHANGELOG.md, #35). Tested: ci/build.sh (all four arches, compot cache): no warnings, ELF and TLS checks under QEMU pass, sizes as in 0.3.0, cerberus --help says 0.3.1; tests/run.sh 172 passed, 8 failed — the WireGuard data-plane checks that cannot run in this sandbox (no NET_ADMIN), as on master; shellcheck v0.10.0 -S warning clean. Closes #32 --- .github/workflows/ci.yml | 69 ++++++++++++++++++++++++++++++++-------- CHANGELOG.md | 13 ++++++++ README.md | 14 ++++---- README.ru.md | 14 ++++---- README.zh-CN.md | 13 ++++---- ci/build.sh | 8 ++--- src/cerberus.c | 2 +- 7 files changed, 94 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28e54b6..d42ea98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,13 +3,19 @@ name: ci on: push: branches: [master] - tags: ['v*'] pull_request: workflow_dispatch: permissions: contents: read +# One run per branch: on pull requests a newer push cancels the older run; on master it waits +# instead (only the newest waits), so a release is never cut off between deleting the old "latest" +# and creating the new one. A manual run shares the push group. +concurrency: + group: ci-${{ github.event_name == 'pull_request' && 'pull_request' || 'push' }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + jobs: # Pull requests: a change to src/, the installers, the Makefile or ci/ comes with a new # CERBERUS_VERSION (MAJOR.MINOR.PATCH) and its CHANGELOG.md section (#33). The checkout is the @@ -76,10 +82,8 @@ jobs: matrix: arch: [arm, mipsel, aarch64, x86_64] steps: + # The version compiled into the binary is CERBERUS_VERSION in src/cerberus.c. - uses: actions/checkout@v4 - with: - # git describe needs the tags: the version compiled into the binary comes from them. - fetch-depth: 0 - name: Toolchain and mbedTLS cache uses: actions/cache@v4 @@ -98,10 +102,12 @@ jobs: release: name: release - # A v* tag publishes the binaries that were just built and tested — the same files, not a rebuild — - # together with the installer, the uninstaller (now a wrapper around it) and SHA256SUMS over all of them. - # Asset names carry no version: cameras fetch releases/latest/download/. - if: startsWith(github.ref, 'refs/tags/v') + # The one release, "latest" (#32): published from master when CERBERUS_VERSION differs from the + # version in the title of the current "latest", with the binaries that were just built and tested + # (the same files, not a rebuild), the installer, the uninstaller (a wrapper around it), the license + # and SHA256SUMS over all of them. Asset names carry no version: cameras fetch + # releases/latest/download/. A push without a new version publishes nothing. + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' needs: [lint, test, build] runs-on: ubuntu-latest permissions: @@ -109,25 +115,62 @@ jobs: steps: - uses: actions/checkout@v4 + - name: A new version? + id: ver + env: + GH_TOKEN: ${{ github.token }} + run: | + ver=$(sed -n 's/^#define CERBERUS_VERSION "\([^"]*\)"[[:space:]]*$/\1/p' src/cerberus.c) + [ "$(printf '%s\n' "$ver" | grep -c .)" = 1 ] || { echo "::error::expected one CERBERUS_VERSION"; exit 1; } + # "Not found" means there is no "latest" yet; any other failure stops here. + if prev=$(gh release view latest --repo "$GITHUB_REPOSITORY" --json name -q .name 2>err.txt); then + echo "exists=1" >> "$GITHUB_OUTPUT" + elif grep -qi 'not found' err.txt; then + prev="" + echo "exists=0" >> "$GITHUB_OUTPUT" + else + cat err.txt; echo "::error::cannot read the current \"latest\" release"; exit 1 + fi + echo "latest is \"${prev:-none}\", src/cerberus.c says $ver" + echo "ver=$ver" >> "$GITHUB_OUTPUT" + [ "$prev" = "Cerberus $ver" ] || echo "new=true" >> "$GITHUB_OUTPUT" + - uses: actions/download-artifact@v4 + if: steps.ver.outputs.new == 'true' with: pattern: cerberus-* path: dist merge-multiple: true - - name: The binaries were built as this tag + - name: The binaries were built as this version + if: steps.ver.outputs.new == 'true' + env: + VER: ${{ steps.ver.outputs.ver }} run: | chmod +x dist/cerberus-* got=$(dist/cerberus-x86_64 --help 2>&1 | head -1 | cut -d' ' -f2) - echo "binary says $got, tag says ${GITHUB_REF_NAME#v}" - [ "$got" = "${GITHUB_REF_NAME#v}" ] + echo "binary says $got, src/cerberus.c says $VER" + [ "$got" = "$VER" ] - name: Lay out the release + if: steps.ver.outputs.new == 'true' run: ci/assemble.sh - name: Publish + if: steps.ver.outputs.new == 'true' env: GH_TOKEN: ${{ github.token }} + VER: ${{ steps.ver.outputs.ver }} + EXISTS: ${{ steps.ver.outputs.exists }} run: | - gh release create "$GITHUB_REF_NAME" dist/* \ - --repo "$GITHUB_REPOSITORY" --title "cerberus ${GITHUB_REF_NAME#v}" --generate-notes --verify-tag + url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" + cat > notes.md <\`. Changes: [CHANGELOG.md](${url}/blob/master/CHANGELOG.md); details: [README](${url}#readme). + NOTES + if [ "$EXISTS" = 1 ]; then + gh release delete latest --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes + fi + gh release create latest dist/* --repo "$GITHUB_REPOSITORY" --latest \ + --target "$GITHUB_SHA" --title "Cerberus ${VER}" --notes-file notes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e0438..6eb8c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ The version lives in `CERBERUS_VERSION` in `src/cerberus.c` (`MAJOR.MINOR.PATCH` published only as the [`latest`](https://github.com/OpenIPC/cerberus/releases/latest) release (see the README); this file is the change history, newest first. +## 0.3.1 — 2026-10-17 + +One release, `latest` (#32), and a version check (#33). + +- Releases: one release, `latest`, titled `Cerberus `, published from `master` when + `CERBERUS_VERSION` changes; the versioned releases `v0.1.0`…`v0.3.0` and their tags are gone, + their notes are in this file. The download links (`releases/latest/download/`) and the + files stay the same. +- The version compiled into the binary is `CERBERUS_VERSION` in `src/cerberus.c`; it no longer + comes from a `v*` tag. +- CI: a pull request that changes the code or the build raises the version and adds its section + here (#33). + ## 0.3.0 — 2026-10-15 - Review fixes: the snapshot URL, the probe subnet, the enrolment JSON, `--help` (PR #12). diff --git a/README.md b/README.md index c6c3fdf..1b4abbe 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ the camera. ## Releases -Every release carries one **static** binary per CPU architecture — static with musl and mbedTLS +There is one release, [`latest`](https://github.com/OpenIPC/cerberus/releases/latest), titled +`Cerberus `; it is published again whenever the version changes. It carries one **static** binary per CPU architecture — static with musl and mbedTLS inside, so it does not matter which libc or which mbedTLS a given firmware has, or whether it has one at all: @@ -306,12 +307,11 @@ makes its absence a failure, which is how CI runs it. ## CI and releases Every push and pull request runs shellcheck, the suite on both mbedTLS generations, and the four -release builds. A `v*` tag publishes those same binaries, with the installers, the init script and -`SHA256SUMS`, as a GitHub release; the version compiled into the binaries comes from the tag. - -```sh -git tag v0.2.0 && git push origin v0.2.0 -``` +release builds. When a push to `master` brings a new `CERBERUS_VERSION` (`src/cerberus.c`), CI +replaces the one release, `latest`, with those same binaries, the installers, the license and +`SHA256SUMS`, titled `Cerberus `; a push without a new version publishes nothing. The +version compiled into the binaries is `CERBERUS_VERSION` itself; there are no `v*` tags. To release: +bump the version and add its `CHANGELOG.md` section in the pull request, then merge it. Versions are `MAJOR.MINOR.PATCH` (`CERBERUS_VERSION` in `src/cerberus.c`): PATCH for fixes and small changes, MINOR for a new feature, MAJOR for an incompatible change or a large update, only diff --git a/README.ru.md b/README.ru.md index 22d2bee..d2a8642 100644 --- a/README.ru.md +++ b/README.ru.md @@ -69,7 +69,8 @@ fw_setenv cerberus_enrollment_token tok_... ## Релизы -В каждом релизе — по одному **статическому** бинарю на архитектуру процессора: musl и mbedTLS внутри, +Релиз один — [`latest`](https://github.com/OpenIPC/cerberus/releases/latest), с заголовком +`Cerberus <версия>`; он публикуется заново при каждой смене версии. В нём — по одному **статическому** бинарю на архитектуру процессора: musl и mbedTLS внутри, поэтому неважно, какая libc и какой mbedTLS в конкретной прошивке и есть ли он там вообще: | файл | на чём работает | @@ -304,12 +305,11 @@ kernel-WireGuard туннель до хаба в отдельном network name ## CI и релизы На каждый push и pull request — shellcheck, тесты на обоих поколениях mbedTLS и четыре сборки релиза. -Тег `v*` публикует эти же бинари вместе с установщиками, init-скриптом и `SHA256SUMS` как GitHub -release; версия, вкомпилированная в бинари, берётся из тега. - -```sh -git tag v0.2.0 && git push origin v0.2.0 -``` +Когда push в `master` приносит новую `CERBERUS_VERSION` (`src/cerberus.c`), CI заменяет единственный +релиз `latest` этими же бинарями вместе с установщиками, лицензией и `SHA256SUMS`, с заголовком +`Cerberus <версия>`; push без новой версии ничего не публикует. Версия, вкомпилированная в бинари, — +сама `CERBERUS_VERSION`, тегов `v*` нет. Чтобы выпустить релиз, поднимите версию и добавьте её раздел +в `CHANGELOG.md` в pull request, затем смержите его. Версия — `MAJOR.MINOR.PATCH` (`CERBERUS_VERSION` в `src/cerberus.c`): PATCH — исправления и мелкие правки, MINOR — новая возможность, MAJOR — несовместимое изменение или крупное обновление, только по diff --git a/README.zh-CN.md b/README.zh-CN.md index 933a22f..873f65c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -63,7 +63,8 @@ fw_setenv cerberus_enrollment_token tok_... ## 发布 -每个发布都为每种 CPU 架构提供一个**静态**二进制文件:musl 和 mbedTLS 已编译在内,因此无论固件使用哪种 libc、 +只有一个发布:[`latest`](https://github.com/OpenIPC/cerberus/releases/latest),标题为 `Cerberus <版本>`;每当版本号变化时都会重新发布。 +它为每种 CPU 架构提供一个**静态**二进制文件:musl 和 mbedTLS 已编译在内,因此无论固件使用哪种 libc、 带有哪个版本的 mbedTLS,甚至没有 mbedTLS,都没有关系: | 文件 | 适用于 | @@ -255,12 +256,10 @@ CERBERUS_TEST_IMAGE=debian:bookworm-slim tests/run.sh # glibc + mbedTLS 2.2 ## CI 与发布 -每次 push 和 pull request 都会运行 shellcheck、两代 mbedTLS 上的测试套件以及四个发布构建。推送 `v*` 标签后, -这些二进制文件会连同安装脚本、启动脚本和 `SHA256SUMS` 一起作为 GitHub release 发布;编译进二进制文件的版本号取自标签。 - -```sh -git tag v0.2.0 && git push origin v0.2.0 -``` +每次 push 和 pull request 都会运行 shellcheck、两代 mbedTLS 上的测试套件以及四个发布构建。当推送到 `master` 带来新的 +`CERBERUS_VERSION`(`src/cerberus.c`)时,CI 会用这些二进制文件连同安装脚本、许可证和 `SHA256SUMS` 替换唯一的发布 +`latest`,标题为 `Cerberus <版本>`;不带新版本号的推送不会发布任何内容。编译进二进制文件的版本号就是 +`CERBERUS_VERSION` 本身,不再使用 `v*` 标签。发布方法:在 pull request 中提升版本号并添加其 `CHANGELOG.md` 章节,然后合并。 版本号格式为 `MAJOR.MINOR.PATCH`(`src/cerberus.c` 中的 `CERBERUS_VERSION`):PATCH 表示修复和小改动,MINOR 表示新功能,MAJOR 表示不兼容的变更或大规模更新,仅由维护者决定。修改 `src/`、安装脚本、`Makefile` 或 `ci/` diff --git a/ci/build.sh b/ci/build.sh index a1c2e29..e601da6 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -22,8 +22,9 @@ # ci/build.sh arm mipsel # a subset # VERSION=1.2.0 OUT=dist ci/build.sh # -# VERSION defaults to the nearest v* tag, without the v. It is compiled into the binary — it is what -# the agent reports to the coordinator. With no tag in reach the binary keeps the version in src/cerberus.c. +# The version compiled into the binary — what the agent reports to the coordinator — is +# CERBERUS_VERSION in src/cerberus.c. VERSION overrides it for a test build only; releases never set it. +# (It used to come from the nearest v* tag; there are no such tags any more, #32.) # # Output: $OUT/cerberus-. Names carry no version on purpose: the installer fetches # releases/latest/download/cerberus-, and that address must not change between releases. @@ -41,8 +42,7 @@ TOOLCHAIN_RELEASE=stable-2025.08-1 MBEDTLS_VERSION=3.6.7 MBEDTLS_SHA256=a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6 -VERSION=${VERSION:-$(git describe --tags --match 'v[0-9]*' --dirty 2>/dev/null || true)} -VERSION=${VERSION#v} +VERSION=${VERSION:-} mkdir -p "$OUT" "$CACHE" diff --git a/src/cerberus.c b/src/cerberus.c index cf7e477..7a584d9 100644 --- a/src/cerberus.c +++ b/src/cerberus.c @@ -70,7 +70,7 @@ /* Release builds pass the tag in (`make VERSION=...`); a plain `make` reports the source version. */ #ifndef CERBERUS_VERSION -#define CERBERUS_VERSION "0.3.0" +#define CERBERUS_VERSION "0.3.1" #endif /* Where the errors are decoded. Printed after every fatal one: the reader is on a camera From f596de5adcbd012da824979456a0e9e05cebf6a6 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Sat, 17 Oct 2026 20:45:29 +0000 Subject: [PATCH 2/4] release: only the head of master publishes, and latest is updated in place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qodo on #37: - a rerun of an older master workflow could replace a newer latest and roll the cameras back: the job publishes only when the built commit is the head of master (git ls-remote), otherwise it says so and stops. - deleting latest before creating it again left the installer's releases/latest/download links without files if the create failed: an existing latest is now updated in place — the files replaced (--clobber), the tag moved to the built commit, then the title and notes. Only the very first publication creates it (moving a left-over latest tag first). Refs #32 --- .github/workflows/ci.yml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d42ea98..1920601 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,8 @@ permissions: contents: read # One run per branch: on pull requests a newer push cancels the older run; on master it waits -# instead (only the newest waits), so a release is never cut off between deleting the old "latest" -# and creating the new one. A manual run shares the push group. +# instead (only the newest waits), so a release is never cut off halfway. A manual run shares the +# push group. concurrency: group: ci-${{ github.event_name == 'pull_request' && 'pull_request' || 'push' }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} @@ -106,7 +106,9 @@ jobs: # version in the title of the current "latest", with the binaries that were just built and tested # (the same files, not a rebuild), the installer, the uninstaller (a wrapper around it), the license # and SHA256SUMS over all of them. Asset names carry no version: cameras fetch - # releases/latest/download/. A push without a new version publishes nothing. + # releases/latest/download/, so "latest" is updated in place, never deleted: the files are + # replaced, the tag is moved to the built commit, then the title and notes follow. A push without + # a new version publishes nothing, and only the head of master is published. if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' needs: [lint, test, build] runs-on: ubuntu-latest @@ -120,6 +122,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + # Only the head of master: a rerun of an older run must not roll the cameras back. + head=$(git ls-remote origin refs/heads/master | cut -f 1) + if [ "$head" != "$GITHUB_SHA" ]; then + echo "::notice::master is at ${head:0:7} now, not ${GITHUB_SHA::7}: not published"; exit 0 + fi ver=$(sed -n 's/^#define CERBERUS_VERSION "\([^"]*\)"[[:space:]]*$/\1/p' src/cerberus.c) [ "$(printf '%s\n' "$ver" | grep -c .)" = 1 ] || { echo "::error::expected one CERBERUS_VERSION"; exit 1; } # "Not found" means there is no "latest" yet; any other failure stops here. @@ -170,7 +177,14 @@ jobs: Install: \`curl -fsSL ${url}/releases/latest/download/install.sh | sh -s -- \`. Changes: [CHANGELOG.md](${url}/blob/master/CHANGELOG.md); details: [README](${url}#readme). NOTES if [ "$EXISTS" = 1 ]; then - gh release delete latest --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes + # in place: files first, then the tag, then the title and notes + gh release upload latest dist/* --repo "$GITHUB_REPOSITORY" --clobber + gh api -X PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true >/dev/null + gh release edit latest --repo "$GITHUB_REPOSITORY" --latest --title "Cerberus ${VER}" --notes-file notes.md + else + # a "latest" tag left without its release would keep its old commit under --target + gh api -X PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true \ + >/dev/null 2>&1 || true + gh release create latest dist/* --repo "$GITHUB_REPOSITORY" --latest \ + --target "$GITHUB_SHA" --title "Cerberus ${VER}" --notes-file notes.md fi - gh release create latest dist/* --repo "$GITHUB_REPOSITORY" --latest \ - --target "$GITHUB_SHA" --title "Cerberus ${VER}" --notes-file notes.md From 0bf8925c7b03b38870a5b880a42353bb5fef7315 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Sat, 17 Oct 2026 20:48:45 +0000 Subject: [PATCH 3/4] release: publish again when latest has the version but lacks a file Qodo on #37: if a publication was cut off after the title was set, later runs at the same version skipped it and left files missing for the installer. With the same version in the title the job now checks the release's files (the four binaries, the installers, LICENSE.md, SHA256SUMS) and publishes again when one is missing. Refs #32 --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1920601..96226bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,7 +140,15 @@ jobs: fi echo "latest is \"${prev:-none}\", src/cerberus.c says $ver" echo "ver=$ver" >> "$GITHUB_OUTPUT" - [ "$prev" = "Cerberus $ver" ] || echo "new=true" >> "$GITHUB_OUTPUT" + if [ "$prev" != "Cerberus $ver" ]; then + echo "new=true" >> "$GITHUB_OUTPUT" + else + # same version: publish again only if a publication was cut off and files are missing + have=$(gh release view latest --repo "$GITHUB_REPOSITORY" --json assets -q '.assets[].name') + for f in cerberus-arm cerberus-mipsel cerberus-aarch64 cerberus-x86_64 install.sh uninstall.sh LICENSE.md SHA256SUMS; do + printf '%s\n' "$have" | grep -qx "$f" || { echo "latest lacks $f: publishing again"; echo "new=true" >> "$GITHUB_OUTPUT"; break; } + done + fi - uses: actions/download-artifact@v4 if: steps.ver.outputs.new == 'true' From 661f338c44ccd0173a11657e7ecb39fb0dc3e0c8 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Sat, 17 Oct 2026 20:53:47 +0000 Subject: [PATCH 4/4] release: a left-over latest tag must move before the release is created Qodo on #37: the first-publication path hid a failure to move a latest tag that survived without its release, and --target does not move an existing tag. The tag is now moved only if it exists, and a failure stops the job before the release is created. Refs #32 --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96226bf..f2fb494 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,9 +190,11 @@ jobs: gh api -X PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true >/dev/null gh release edit latest --repo "$GITHUB_REPOSITORY" --latest --title "Cerberus ${VER}" --notes-file notes.md else - # a "latest" tag left without its release would keep its old commit under --target - gh api -X PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true \ - >/dev/null 2>&1 || true + # a "latest" tag left without its release would keep its old commit under --target: + # move it (a failure stops the job), or create the release when there is no such tag + if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/latest" >/dev/null 2>&1; then + gh api -X PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true >/dev/null + fi gh release create latest dist/* --repo "$GITHUB_REPOSITORY" --latest \ --target "$GITHUB_SHA" --title "Cerberus ${VER}" --notes-file notes.md fi