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
93 changes: 80 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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' }}

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
Expand Down Expand Up @@ -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
Expand All @@ -98,36 +102,99 @@ 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/<name>.
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/<name>, 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
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: A new version?
id: ver
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
Comment on lines +126 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. A newer push can leave older binaries live 🐞 Bug ≡ Correctness

A new version? checks the head of master only once, before downloading artifacts and publishing
the release. If another commit with the same version lands after that check, the older run can
publish its binaries, while the newer run sees a complete release with the expected title and skips
publication.
Agent Prompt
## Issue description
A push can advance `master` after the release job's sole head check, allowing an older run to publish assets that a subsequent same-version run will not replace.

## Fix Focus Areas
- .github/workflows/ci.yml[125-129]
- .github/workflows/ci.yml[174-200]

## Recommended Fix
Check the remote head again immediately before mutating the release. If it has advanced, exit without publishing so the queued run can publish from the newer commit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.
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"
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'
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 <<NOTES
Cerberus ${VER} (\`${GITHUB_SHA::7}\`): static binaries for arm, mipsel, aarch64 and x86_64, the installer and \`SHA256SUMS\`.

Install: \`curl -fsSL ${url}/releases/latest/download/install.sh | sh -s -- <token>\`. Changes: [CHANGELOG.md](${url}/blob/master/CHANGELOG.md); details: [README](${url}#readme).
NOTES
if [ "$EXISTS" = 1 ]; then
# in place: files first, then the tag, then the title and notes
gh release upload latest dist/* --repo "$GITHUB_REPOSITORY" --clobber

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Camera installs can fail during an update 🐞 Bug ☼ Reliability

gh release upload latest dist/* --clobber replaces release assets separately, exposing binaries
and SHA256SUMS from different builds while publication is in progress. An installer that fetches
those files on opposite sides of a replacement fails checksum verification; an upload failure can
leave that mismatch in place until publication is retried.
Agent Prompt
## Issue description
Updating the live release replaces assets separately, so installers can fetch a binary and checksum from different builds. A failed upload can leave that state in place.
## Fix Focus Areas
- .github/workflows/ci.yml[179-183]
- install.sh[95-105]
## Recommended Fix
Make publication recover from a partially replaced asset set, and have the installer retry fetching both the binary and checksum when they disagree during a release update. Ensure a failed upload cannot leave the live release permanently inconsistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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
Comment on lines +187 to +191

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. The old latest release record remains 📎 Requirement gap ≡ Correctness

The Publish branch for an existing release uploads over latest and edits it instead of deleting
it with --cleanup-tag. Whenever latest already exists, its release record is retained and only
its assets, tag target, title, and notes are mutated, so the required delete-and-recreate
publication is not performed.
Agent Prompt
## Issue description
The existing `latest` release is edited in place rather than deleted with its tag and recreated on the built commit, which does not satisfy the required release replacement flow.

## Fix Focus Areas
- .github/workflows/ci.yml[179-183]

## Recommended Fix
Replace the existing-release upload and edit branch with a `gh release delete latest --cleanup-tag` operation followed by `gh release create latest` using the assembled assets, `--latest`, the built commit, the `Cerberus <version>` title, and the generated notes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

else
# 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
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
fi
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>`, 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/<name>`) 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).
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>`; 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:

Expand Down Expand Up @@ -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 <version>`; 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
Expand Down
14 changes: 7 additions & 7 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 в конкретной прошивке и есть ли он там вообще:

| файл | на чём работает |
Expand Down Expand Up @@ -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 — несовместимое изменение или крупное обновление, только по
Expand Down
13 changes: 6 additions & 7 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,都没有关系:

| 文件 | 适用于 |
Expand Down Expand Up @@ -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/`
Expand Down
8 changes: 4 additions & 4 deletions ci/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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-<arch>. Names carry no version on purpose: the installer fetches
# releases/latest/download/cerberus-<arch>, and that address must not change between releases.
Expand All @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion src/cerberus.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading