Files
frigate/.github/workflows/ci.yml
T

393 lines
16 KiB
YAML

name: CI
on:
workflow_dispatch:
push:
branches:
- dev
- master
paths-ignore:
- "docs/**"
# only run the latest commit to avoid cache overwrites
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: 3.11
jobs:
amd64_build:
runs-on: ubuntu-22.04
name: AMD64 Build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push amd64 standard build
uses: docker/build-push-action@v7
with:
context: .
file: docker/main/Dockerfile
push: true
platforms: linux/amd64
target: frigate
tags: ${{ steps.setup.outputs.image-name }}-amd64
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64
cache-to: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64,mode=max
smoke_test:
runs-on: ubuntu-22.04
name: AMD64 Smoke Test
needs:
- amd64_build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Start container
run: |
mkdir -p /tmp/frigate-config /tmp/frigate-media
printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config/config.yml
# simulate a root-era install: root-owned 0600 jwt secret pre-exists
docker run --rm -v /tmp/frigate-config:/config --entrypoint bash \
${{ steps.setup.outputs.image-name }}-amd64 \
-c "python3 -c 'import secrets; open(\"/config/.jwt_secret\",\"w\").write(secrets.token_hex(64))' && chmod 600 /config/.jwt_secret && chown 0:0 /config/.jwt_secret"
docker run -d --name frigate --shm-size 256m \
-v /tmp/frigate-config:/config \
-v /tmp/frigate-media:/media/frigate \
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=100000000 \
-p 5000:5000 -p 8971:8971 \
${{ steps.setup.outputs.image-name }}-amd64
- name: Wait for API
run: |
for i in $(seq 1 60); do
curl -fs http://127.0.0.1:5000/api/version && exit 0
sleep 5
done
echo "API never came up"; docker logs frigate; exit 1
- name: Assert security headers and permissions
run: |
headers=$(curl -ksI https://127.0.0.1:8971/)
echo "$headers"
echo "$headers" | grep -qi "x-content-type-options: nosniff"
echo "$headers" | grep -qi "referrer-policy: strict-origin-when-cross-origin"
# server_tokens off: Server header must not include a version.
# written as an if rather than "! grep", because bash exempts a
# negated command from set -e and the assertion would never fail
if echo "$headers" | grep -qiE "^server: nginx/[0-9]"; then
echo "Server header leaks the nginx version; server_tokens is not off"
exit 1
fi
# Frigate never ships frame-ancestors: HA's Webpage card and iframe
# panels frame it cross-origin and it would break them silently
if echo "$headers" | grep -qi "frame-ancestors"; then
echo "response carries frame-ancestors, which breaks cross-origin iframe embedding"
exit 1
fi
# -t must NOT run as root: ngx_create_paths would chown the live cache
# and temp dirs to the `user root` directive user, breaking the workers.
# stdout goes to /dev/null because -t reopens the config's
# error_log/access_log /dev/stdout by path, and the docker exec pipe
# is root-owned; -t reports on stderr, so nothing is lost
docker exec frigate /command/s6-setuidgid frigate bash -c '/usr/local/nginx/sbin/nginx -e stderr -t -c /tmp/nginx/conf/nginx.conf >/dev/null'
docker exec frigate stat -c %a /etc/letsencrypt/live/frigate/privkey.pem | grep -qx 600
docker exec frigate stat -c %a /dev/shm/go2rtc.yaml | grep -qx 640
- name: Assert services run as non-root
run: |
ps_out=$(docker exec frigate ps -eo user=,comm=)
echo "$ps_out"
assert_nonroot() {
# the process must exist AND no instance of it may run as root
echo "$ps_out" | grep -qw "$1" || { echo "$1 is not running"; exit 1; }
if echo "$ps_out" | grep -w "$1" | grep -q '^root'; then
echo "$1 is running as root"; exit 1
fi
}
assert_nonroot python3
assert_nonroot go2rtc
assert_nonroot nginx
# root-era jwt secret must have been captured by the sweep and the
# auth stack must be functional: wrong creds => clean 401, not 500
docker exec frigate stat -c %u /config/.jwt_secret | grep -qx "$(docker exec frigate id -u frigate)"
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:5000/api/login \
-H 'content-type: application/json' -d '{"user":"admin","password":"definitely-wrong"}')
[ "$code" = "401" ] || { echo "login endpoint returned $code"; exit 1; }
# nginx runtime state must belong to the runtime user (a root nginx -t
# in the step above would have chowned it to root)
owners=$(docker exec frigate stat -c %U /tmp/nginx /dev/shm/nginx_cache)
echo "$owners"
if echo "$owners" | grep -qvx frigate; then
echo "nginx runtime dirs are not owned by frigate"; exit 1
fi
# runtime user can write recordings storage
docker exec frigate /command/s6-setuidgid frigate touch /media/frigate/.write-probe
docker exec frigate rm /media/frigate/.write-probe
# /tmp/cache is a tmpfs mount here, as the docs recommend: it arrives
# root-owned, and the ZMQ IPC sockets live in it
docker exec frigate /command/s6-setuidgid frigate touch /tmp/cache/.write-probe
docker exec frigate rm /tmp/cache/.write-probe
# bundled models must be readable by the runtime user: they are baked
# in as root, and archive members can carry root-only modes
docker exec frigate /command/s6-setuidgid frigate sh -c '
for f in /cpu_model.tflite /edgetpu_model.tflite /cpu_audio_model.tflite \
/labelmap.txt /audio-labelmap.txt /openvino-model/*; do
[ -e "$f" ] || continue
test -r "$f" || { echo "$f is not readable by the runtime user"; exit 1; }
done'
- name: Assert escape hatch restores root
run: |
mkdir -p /tmp/frigate-config-root
printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config-root/config.yml
# pre-seed a sentinel: the assertion below is that the escape hatch
# DELETES it. Against a fresh dir the absence check passes vacuously
# and proves nothing about the rm -f in the prepare script.
echo "2:1000:1000" > /tmp/frigate-config-root/.permissions_version
docker run -d --name frigate-root --shm-size 256m \
-e FRIGATE_RUN_AS_ROOT=true \
-v /tmp/frigate-config-root:/config \
${{ steps.setup.outputs.image-name }}-amd64
up=0
for i in $(seq 1 60); do
docker exec frigate-root curl -fs http://127.0.0.1:5000/api/version && up=1 && break
sleep 5
done
if [ "$up" -ne 1 ]; then echo "escape hatch container never healthy"; docker logs frigate-root; exit 1; fi
ps_out=$(docker exec frigate-root ps -eo user=,comm=)
echo "$ps_out"
echo "$ps_out" | grep -w python3 | grep -q '^root'
echo "$ps_out" | grep -w go2rtc | grep -q '^root'
echo "$ps_out" | grep -w nginx | grep -q '^root'
# escape hatch must have DELETED the pre-seeded sentinel. written as
# an if because bash exempts a negated command from set -e
if docker exec frigate-root test -f /config/.permissions_version; then
echo "escape hatch did not delete the sweep sentinel"; exit 1
fi
docker rm -f frigate-root
- name: Assert PUID/PGID remapping
run: |
mkdir -p /tmp/frigate-config-puid /tmp/frigate-media-puid
printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config-puid/config.yml
docker run -d --name frigate-puid --shm-size 256m \
-e PUID=1500 -e PGID=1500 \
-v /tmp/frigate-config-puid:/config \
-v /tmp/frigate-media-puid:/media/frigate \
${{ steps.setup.outputs.image-name }}-amd64
up=0
for i in $(seq 1 60); do
docker exec frigate-puid curl -fs http://127.0.0.1:5000/api/version && up=1 && break
sleep 5
done
if [ "$up" -ne 1 ]; then echo "PUID container never became healthy"; docker logs frigate-puid; exit 1; fi
docker exec frigate-puid id -u frigate | grep -qx 1500
docker exec frigate-puid id -g frigate | grep -qx 1500
docker exec frigate-puid cat /config/.permissions_version | grep -qx "2:1500:1500"
# second boot must skip the sweep (sentinel hit). Poll rather than
# sleep: the string can only come from the second boot (the first
# had no sentinel), so grepping the full log is unambiguous.
docker restart frigate-puid
ok=0
for i in $(seq 1 30); do
docker logs frigate-puid 2>&1 | grep -q "already applied" && ok=1 && break
sleep 2
done
if [ "$ok" -ne 1 ]; then echo "sentinel skip never logged"; docker logs frigate-puid; exit 1; fi
docker rm -f frigate-puid
- name: Teardown
if: always()
run: docker rm -f frigate || true
arm64_build:
runs-on: ubuntu-22.04-arm
name: ARM Build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push arm64 standard build
uses: docker/build-push-action@v7
with:
context: .
file: docker/main/Dockerfile
push: true
platforms: linux/arm64
target: frigate
tags: |
${{ steps.setup.outputs.image-name }}-standard-arm64
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
- name: Build and push RPi build
uses: docker/bake-action@v7
with:
source: .
push: true
targets: rpi
files: docker/rpi/rpi.hcl
set: |
rpi.tags=${{ steps.setup.outputs.image-name }}-rpi
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max
jetson_jp6_build:
runs-on: ubuntu-22.04-arm
name: Jetson Jetpack 6
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push TensorRT (Jetson, Jetpack 6)
env:
ARCH: arm64
BASE_IMAGE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
SLIM_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
TRT_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
uses: docker/bake-action@v7
with:
source: .
push: true
targets: tensorrt
files: docker/tensorrt/trt.hcl
set: |
tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp6
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6,mode=max
amd64_extra_builds:
runs-on: ubuntu-22.04
name: AMD64 Extra Build
needs:
- amd64_build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push TensorRT (x86 GPU)
env:
COMPUTE_LEVEL: "50 60 70 80 90"
uses: docker/bake-action@v7
with:
source: .
push: true
targets: tensorrt
files: docker/tensorrt/trt.hcl
set: |
tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt,mode=max
- name: AMD/ROCm general build
env:
HSA_OVERRIDE: 0
uses: docker/bake-action@v7
with:
source: .
push: true
targets: rocm
files: docker/rocm/rocm.hcl
set: |
rocm.tags=${{ steps.setup.outputs.image-name }}-rocm
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm,mode=max
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm
arm64_extra_builds:
runs-on: ubuntu-22.04-arm
name: ARM Extra Build
needs:
- arm64_build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Rockchip build
uses: docker/bake-action@v7
with:
source: .
push: true
targets: rk
files: docker/rockchip/rk.hcl
set: |
rk.tags=${{ steps.setup.outputs.image-name }}-rk
*.cache-from=type=gha
synaptics_build:
runs-on: ubuntu-22.04-arm
name: Synaptics Build
needs:
- arm64_build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Synaptics build
uses: docker/bake-action@v7
with:
source: .
push: true
targets: synaptics
files: docker/synaptics/synaptics.hcl
set: |
synaptics.tags=${{ steps.setup.outputs.image-name }}-synaptics
*.cache-from=type=gha
# The majority of users running arm64 are rpi users, so the rpi
# build should be the primary arm64 image
assemble_default_build:
runs-on: ubuntu-22.04
name: Assemble and push default build
needs:
- amd64_build
- arm64_build
steps:
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create short sha
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- uses: int128/docker-manifest-create-action@v2
with:
tags: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}
sources: |
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi