mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-07 14:09:07 +02:00
Merge main into poc/certificate-posture
This commit is contained in:
@@ -46,3 +46,25 @@ updates:
|
||||
wireguard:
|
||||
patterns:
|
||||
- "golang.zx2c4.com/wireguard*"
|
||||
|
||||
# Base images of the source-build Dockerfiles, pinned by digest (Chainguard
|
||||
# publishes only :latest for free). Dockerfile.release files feed goreleaser
|
||||
# and keep the published images as they are, so their bases are left alone.
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
- "/upload-server"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
base-images:
|
||||
patterns:
|
||||
- "*"
|
||||
ignore:
|
||||
- dependency-name: "gcr.io/distroless/base"
|
||||
# Go minor and major versions move with the rest of the repository;
|
||||
# patch releases and new digests of the pinned tag still come through.
|
||||
- dependency-name: "golang"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
- "version-update:semver-major"
|
||||
|
||||
Executable
+338
@@ -0,0 +1,338 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "::error::$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ ${RUNNER_ENVIRONMENT:-} != github-hosted || ${RUNNER_OS:-} != macOS || $(uname -s) != Darwin ]]; then
|
||||
fail "This test installs a system daemon and must run on a disposable GitHub macOS runner."
|
||||
fi
|
||||
if [[ $EUID == 0 ]]; then
|
||||
fail "Run this script as the Homebrew user, not root."
|
||||
fi
|
||||
|
||||
readonly test_dir="${RUNNER_TEMP:?}/homebrew-cask"
|
||||
readonly results_dir="$test_dir/results"
|
||||
readonly app='/Applications/Netbird UI.app'
|
||||
readonly plist='/Library/LaunchDaemons/netbird.plist'
|
||||
readonly cask='netbirdio/tap/netbird-ui'
|
||||
readonly formula='netbirdio/tap/netbird'
|
||||
readonly published_cask="$test_dir/published-netbird-ui.rb"
|
||||
readonly legacy_cask="$test_dir/legacy-netbird-ui.rb"
|
||||
readonly rendered_cask="$test_dir/rendered-netbird-ui.rb"
|
||||
readonly fixture_dir="$test_dir/fixture"
|
||||
readonly serve_dir="$test_dir/serve"
|
||||
readonly fixture_zip="$serve_dir/netbird-ui.zip"
|
||||
readonly fixture_port=18080
|
||||
readonly fixture_url="http://127.0.0.1:$fixture_port/netbird-ui.zip"
|
||||
readonly marker="$test_dir/installer.marker"
|
||||
|
||||
mkdir -p "$results_dir" "$fixture_dir/netbird_ui_darwin" "$serve_dir" "$test_dir/downloads"
|
||||
exec > >(tee "$results_dir/test.log") 2>&1
|
||||
|
||||
sudo -n true
|
||||
if command -v netbird || [[ -e "$app" || -e "$plist" ]] || pgrep -x netbird-ui; then
|
||||
fail "The runner already has NetBird installed or running."
|
||||
fi
|
||||
if sudo launchctl print system/netbird > "$results_dir/initial-service.log" 2>&1; then
|
||||
fail "The runner already has a NetBird service loaded."
|
||||
fi
|
||||
|
||||
install_attempted=false
|
||||
server_pid=''
|
||||
daemon_pid=''
|
||||
version=''
|
||||
|
||||
stop_ui() {
|
||||
local status=0
|
||||
sudo pkill -x netbird-ui || status=$?
|
||||
# pkill returns 1 when the UI is already closed.
|
||||
[[ $status == 0 || $status == 1 ]]
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
set +e
|
||||
|
||||
if [[ $install_attempted == true ]]; then
|
||||
stop_ui || status=1
|
||||
if [[ -S /var/run/netbird.sock ]]; then
|
||||
sudo netbird down || status=1
|
||||
fi
|
||||
if brew list --cask "$cask" >/dev/null 2>&1 || [[ -e "$app" ]]; then
|
||||
brew uninstall --cask --force "$cask" || status=1
|
||||
fi
|
||||
# A failed cask install can leave a daemon even after Homebrew rolls back the app.
|
||||
if sudo launchctl print system/netbird > "$results_dir/cleanup-service.log" 2>&1; then
|
||||
sudo netbird service stop || status=1
|
||||
fi
|
||||
if [[ -e "$plist" ]]; then
|
||||
sudo netbird service uninstall || status=1
|
||||
fi
|
||||
fi
|
||||
if [[ -f /var/log/netbird/client.log ]]; then
|
||||
sudo cat /var/log/netbird/client.log > "$results_dir/client.log" || status=1
|
||||
fi
|
||||
if command -v netbird >/dev/null; then
|
||||
brew uninstall --formula "$formula" || status=1
|
||||
fi
|
||||
if [[ -n $server_pid ]]; then
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
fi
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
run_logged() {
|
||||
local name=$1
|
||||
shift
|
||||
"$@" 2>&1 | tee "$results_dir/$name.log"
|
||||
}
|
||||
|
||||
cask_field() {
|
||||
local stanza=$1 file=$2
|
||||
sed -nE "s/^[[:space:]]*$stanza \"([^\"]+)\".*/\\1/p" "$file"
|
||||
}
|
||||
|
||||
release_fields() {
|
||||
local file=$1
|
||||
grep -E '^[[:space:]]*(version|url|sha256|app) ' "$file"
|
||||
}
|
||||
|
||||
use_cask() {
|
||||
local file=$1
|
||||
cp "$file" "$tap_dir/Casks/netbird-ui.rb"
|
||||
}
|
||||
|
||||
# The released installer opens the UI as root, which never returns on a headless
|
||||
# runner. The cask only needs two script paths and a version argument, so the test
|
||||
# ships a stub bundle that records what it received and starts the daemon.
|
||||
build_fixture() {
|
||||
local bundle="$fixture_dir/netbird_ui_darwin"
|
||||
printf '#!/bin/sh\nexit 0\n' > "$bundle/netbird-ui"
|
||||
chmod 755 "$bundle/netbird-ui"
|
||||
# After a bootout launchd keeps tearing the previous daemon down for a couple of
|
||||
# seconds, and loading the same label again fails until that finishes.
|
||||
cat > "$bundle/installer.sh" <<EOF
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
export PATH=\$PATH:/usr/local/bin:/opt/homebrew/bin
|
||||
printf 'version=%s\\nuid=%s\\n' "\$1" "\$(id -u)" > '$marker'
|
||||
netbird service install
|
||||
attempt=0
|
||||
until netbird service start; do
|
||||
attempt=\$((attempt + 1))
|
||||
[ "\$attempt" -lt 15 ] || exit 1
|
||||
sleep 1
|
||||
done
|
||||
EOF
|
||||
printf '#!/bin/sh\nexit 0\n' > "$bundle/uninstaller.sh"
|
||||
# Shipped without the executable bit so the 0755 seen after install can only come from the cask.
|
||||
chmod 644 "$bundle/installer.sh" "$bundle/uninstaller.sh"
|
||||
rm -f "$fixture_zip"
|
||||
(cd "$fixture_dir" && zip -qr "$fixture_zip" netbird_ui_darwin)
|
||||
}
|
||||
|
||||
start_fixture_server() {
|
||||
python3 -m http.server "$fixture_port" --bind 127.0.0.1 --directory "$serve_dir" \
|
||||
> "$results_dir/fixture-server.log" 2>&1 &
|
||||
server_pid=$!
|
||||
local attempt
|
||||
for attempt in {1..20}; do
|
||||
if curl --silent --fail --output /dev/null "$fixture_url"; then
|
||||
return
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
fail "The fixture HTTP server did not come up on port $fixture_port."
|
||||
}
|
||||
|
||||
assert_published_layout() {
|
||||
local url archive script
|
||||
while read -r url; do
|
||||
archive="$test_dir/downloads/${url##*/}"
|
||||
curl --fail --location --silent --retry 3 --output "$archive" "$url"
|
||||
for script in installer.sh uninstaller.sh; do
|
||||
unzip -l "$archive" | grep -q " netbird_ui_darwin/$script\$" ||
|
||||
fail "The published archive ${url##*/} has no netbird_ui_darwin/$script."
|
||||
done
|
||||
done < <(cask_field url "$published_cask")
|
||||
}
|
||||
|
||||
assert_no_deprecations() {
|
||||
if grep -Ei '(postflight|uninstall_preflight).*deprecated|deprecated.*(postflight|uninstall_preflight)' "$@"; then
|
||||
fail "Homebrew reported a deprecated cask lifecycle hook."
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_daemon() {
|
||||
local attempt
|
||||
for attempt in {1..30}; do
|
||||
if sudo launchctl print system/netbird > "$results_dir/service.log" 2>&1 &&
|
||||
grep -Eq '^[[:space:]]*state = running$' "$results_dir/service.log"; then
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
cat "$results_dir/service.log"
|
||||
fail "The installed daemon did not reach the running state."
|
||||
}
|
||||
|
||||
wait_for_exit() {
|
||||
local pid=$1 attempt
|
||||
for attempt in {1..30}; do
|
||||
if ! sudo kill -0 "$pid" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fail "Daemon process $pid is still running after removal."
|
||||
}
|
||||
|
||||
assert_service_absent() {
|
||||
if sudo launchctl print system/netbird > "$results_dir/removed-service.log" 2>&1; then
|
||||
fail "The NetBird service is still loaded after removal."
|
||||
fi
|
||||
}
|
||||
|
||||
assert_installed() {
|
||||
local script
|
||||
[[ -f $marker ]] || fail "The cask did not run installer.sh."
|
||||
grep -qx "version=$version" "$marker" || fail "installer.sh did not receive the cask version: $(cat "$marker")"
|
||||
grep -qx 'uid=0' "$marker" || fail "installer.sh did not run as root: $(cat "$marker")"
|
||||
[[ -d "$app" && -x "$app/netbird-ui" ]] || fail "The UI was not installed."
|
||||
for script in installer.sh uninstaller.sh; do
|
||||
[[ $(stat -f '%Lp' "$app/$script") == 755 ]] || fail "Incorrect permissions on $script."
|
||||
done
|
||||
[[ -f "$plist" ]] || fail "The installer did not create the daemon plist."
|
||||
wait_for_daemon
|
||||
daemon_pid=$(awk '/^[[:space:]]*pid = / { print $3; exit }' "$results_dir/service.log")
|
||||
[[ $daemon_pid =~ ^[0-9]+$ ]] || fail "The running daemon has no PID."
|
||||
sudo kill -0 "$daemon_pid"
|
||||
}
|
||||
|
||||
assert_uninstalled() {
|
||||
local log=$1
|
||||
assert_no_deprecations "$log"
|
||||
[[ ! -e "$app" ]] || fail "The UI app remains after uninstall."
|
||||
[[ ! -e "$plist" ]] || fail "The daemon plist remains after uninstall."
|
||||
assert_service_absent
|
||||
wait_for_exit "$daemon_pid"
|
||||
[[ $(netbird version) == "$version" ]] || fail "Cask uninstall removed the CLI dependency."
|
||||
}
|
||||
|
||||
installed_caskfiles() {
|
||||
local extension=$1
|
||||
find "$(brew --caskroom)/netbird-ui/.metadata" -name "netbird-ui.$extension" 2>/dev/null
|
||||
}
|
||||
|
||||
assert_legacy_metadata() {
|
||||
installed_caskfiles rb | grep -q . || fail "The legacy cask did not leave a Ruby caskfile behind."
|
||||
}
|
||||
|
||||
assert_steps_metadata() {
|
||||
if installed_caskfiles rb | grep -q .; then
|
||||
fail "Homebrew still keeps the legacy Ruby caskfile after reinstall."
|
||||
fi
|
||||
installed_caskfiles json | grep -q . || fail "Homebrew did not save the reinstalled cask as JSON."
|
||||
}
|
||||
|
||||
brew --version
|
||||
sw_vers
|
||||
brew tap netbirdio/tap "${GITHUB_WORKSPACE:?}/.homebrew-cask-tap"
|
||||
tap_dir=$(brew --repository netbirdio/tap)
|
||||
readonly tap_dir
|
||||
|
||||
[[ -f "$tap_dir/Casks/netbird-ui.rb" ]] || fail "The tap has no Casks/netbird-ui.rb."
|
||||
cp "$tap_dir/Casks/netbird-ui.rb" "$published_cask"
|
||||
cp "$published_cask" "$results_dir/published-netbird-ui.rb"
|
||||
|
||||
version=$(brew info --json=v2 --formula "$formula" | jq -r '.formulae[0].versions.stable')
|
||||
readonly version
|
||||
[[ -n $version && $version != null ]] || fail "Could not read the formula version from the tap."
|
||||
|
||||
assert_published_layout
|
||||
|
||||
build_fixture
|
||||
fixture_sha=$(shasum -a 256 "$fixture_zip" | cut -d' ' -f1)
|
||||
readonly fixture_sha
|
||||
start_fixture_server
|
||||
|
||||
export PROJECT=netbird-ui VERSION="$version"
|
||||
export AMD="$fixture_zip" ARM="$fixture_zip" AMD_URL="$fixture_url" ARM_URL="$fixture_url"
|
||||
gomplate -f "$GITHUB_WORKSPACE/client/ui/netbird-ui.rb.tmpl" -o "$rendered_cask"
|
||||
cp "$rendered_cask" "$results_dir/rendered-netbird-ui.rb"
|
||||
|
||||
sed -E "s|^([[:space:]]*version) \"[^\"]+\"|\\1 \"$version\"|; s|^([[:space:]]*url) \"[^\"]+\"|\\1 \"$fixture_url\"|; s|^([[:space:]]*sha256) \"[^\"]+\"|\\1 \"$fixture_sha\"|" \
|
||||
"$published_cask" > "$legacy_cask"
|
||||
cp "$legacy_cask" "$results_dir/legacy-netbird-ui.rb"
|
||||
if ! diff <(release_fields "$legacy_cask") <(release_fields "$rendered_cask"); then
|
||||
fail "The rendered cask changes release data, not only lifecycle stanzas."
|
||||
fi
|
||||
|
||||
use_cask "$rendered_cask"
|
||||
brew info --json=v2 --cask "$cask" > "$results_dir/cask.json" 2> "$results_dir/load.log"
|
||||
cat "$results_dir/load.log"
|
||||
assert_no_deprecations "$results_dir/load.log"
|
||||
run_logged style brew style --cask --only-cops=Cask/InstallSteps "$cask"
|
||||
|
||||
run_logged install-cli brew install --formula "$formula"
|
||||
[[ $(netbird version) == "$version" ]] || fail "The installed CLI does not report the formula version."
|
||||
|
||||
for scenario in running stopped missing; do
|
||||
echo "::group::Uninstall with $scenario service"
|
||||
install_attempted=true
|
||||
sudo rm -f "$marker"
|
||||
run_logged "install-$scenario" brew install --cask "$cask"
|
||||
assert_no_deprecations "$results_dir/install-$scenario.log"
|
||||
assert_installed
|
||||
stop_ui
|
||||
|
||||
case "$scenario" in
|
||||
running) ;;
|
||||
stopped)
|
||||
run_logged stop-daemon sudo netbird service stop
|
||||
wait_for_exit "$daemon_pid"
|
||||
[[ -f "$plist" ]] || fail "Stopping the daemon unexpectedly removed its plist."
|
||||
;;
|
||||
missing)
|
||||
run_logged stop-missing-daemon sudo netbird service stop
|
||||
run_logged remove-daemon sudo netbird service uninstall
|
||||
wait_for_exit "$daemon_pid"
|
||||
[[ ! -e "$plist" ]] || fail "The missing-service scenario still has a plist."
|
||||
assert_service_absent
|
||||
;;
|
||||
*) fail "Unknown uninstall scenario: $scenario" ;;
|
||||
esac
|
||||
|
||||
run_logged "uninstall-$scenario" brew uninstall --cask "$cask"
|
||||
assert_uninstalled "$results_dir/uninstall-$scenario.log"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
# Every existing user first meets the new cask through an upgrade of the published
|
||||
# one, whose legacy flight blocks Homebrew replays from the saved Ruby caskfile.
|
||||
echo "::group::Reinstall over the published legacy cask"
|
||||
install_attempted=true
|
||||
use_cask "$legacy_cask"
|
||||
sudo rm -f "$marker"
|
||||
run_logged install-legacy brew install --cask "$cask"
|
||||
assert_installed
|
||||
assert_legacy_metadata
|
||||
stop_ui
|
||||
|
||||
use_cask "$rendered_cask"
|
||||
sudo rm -f "$marker"
|
||||
run_logged reinstall-legacy brew reinstall --cask "$cask"
|
||||
assert_installed
|
||||
assert_steps_metadata
|
||||
stop_ui
|
||||
|
||||
run_logged uninstall-legacy brew uninstall --cask "$cask"
|
||||
assert_uninstalled "$results_dir/uninstall-legacy.log"
|
||||
echo "::endgroup::"
|
||||
@@ -38,12 +38,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
version: 11
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('client/ui/frontend/pnpm-lock.yaml') }}
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
release: "15.0"
|
||||
release: "15.1"
|
||||
envs: "GO_VERSION"
|
||||
prepare: |
|
||||
pkg install -y curl pkgconf xorg
|
||||
|
||||
@@ -80,3 +80,49 @@ jobs:
|
||||
skip-save-cache: true
|
||||
cache-invalidation-interval: 0
|
||||
args: --timeout=20m
|
||||
|
||||
# Separate job rather than extra rows in the matrix above: those rows pick a
|
||||
# GOOS by picking a runner OS, while android/ios are cross-compiled from
|
||||
# ubuntu — an `include` entry with os: ubuntu-latest would merge into the
|
||||
# Linux row instead of adding one. The package path is restricted because a
|
||||
# whole-repo run under GOOS=android pulls *_linux.go files into packages that
|
||||
# have no android counterpart.
|
||||
golangci-mobile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
packages: ./client/android/...
|
||||
display_name: Android
|
||||
- goos: ios
|
||||
goarch: arm64
|
||||
packages: ./client/ios/...
|
||||
display_name: iOS
|
||||
name: ${{ matrix.display_name }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||
with:
|
||||
version: latest
|
||||
install-mode: binary
|
||||
skip-cache: true
|
||||
skip-save-cache: true
|
||||
cache-invalidation-interval: 0
|
||||
args: --timeout=20m ${{ matrix.packages }}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Mobile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "release-*"
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
android_build:
|
||||
name: "Android / Build"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goarch: [arm64, arm, amd64, "386"]
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: android
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Build Android bridge
|
||||
run: go build ./client/android/...
|
||||
- name: Vet Android bridge
|
||||
if: matrix.goarch == 'arm64'
|
||||
run: go vet ./client/android/...
|
||||
|
||||
ios_build:
|
||||
name: "iOS / Build"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goarch: [arm64, amd64]
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ios
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
# No `go vet` counterpart: every ios target requires external (cgo)
|
||||
# linking, which needs an Xcode toolchain the runner does not have.
|
||||
- name: Build iOS SDK
|
||||
run: go build ./client/ios/...
|
||||
@@ -0,0 +1,199 @@
|
||||
name: Red Hat Certification
|
||||
|
||||
# Certify published UBI images in the Red Hat Ecosystem Catalog. Called by
|
||||
# release.yml on stable tags, or run by hand to (re)certify any released
|
||||
# version. preflight submits every architecture of an image's manifest list
|
||||
# to Pyxis; auto-publish on the component makes it public once certified.
|
||||
#
|
||||
# Each component's Partner Connect ID comes from the REDHAT_CERT_ID_<NAME>
|
||||
# repository variable, e.g. REDHAT_CERT_ID_CLIENT_ROOTLESS. The run fails
|
||||
# before certifying anything if a selected component's variable is not set.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
component:
|
||||
type: string
|
||||
required: true
|
||||
version:
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
PYXIS_API_TOKEN:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
component:
|
||||
description: "Component to certify"
|
||||
type: choice
|
||||
required: true
|
||||
default: all
|
||||
options:
|
||||
- all
|
||||
- client-rootless
|
||||
- reverse-proxy
|
||||
version:
|
||||
description: "Released version, e.g. v0.80.0"
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
resolve:
|
||||
name: Resolve components
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
matrix: ${{ steps.resolve.outputs.matrix }}
|
||||
steps:
|
||||
- name: Resolve components and images
|
||||
id: resolve
|
||||
env:
|
||||
COMPONENT: ${{ inputs.component }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
REPO_VARS: ${{ toJSON(vars) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${INPUT_VERSION#v}"
|
||||
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::Only stable x.y.z versions are certified, got '${INPUT_VERSION}'"
|
||||
exit 1
|
||||
fi
|
||||
# name, image repository, tag suffix (must match .goreleaser.yaml).
|
||||
# Keep the names in sync with the workflow_dispatch options above.
|
||||
components=(
|
||||
"client-rootless ghcr.io/netbirdio/netbird -rootless-ubi"
|
||||
"reverse-proxy ghcr.io/netbirdio/reverse-proxy -ubi"
|
||||
)
|
||||
matrix="[]"
|
||||
missing=()
|
||||
for c in "${components[@]}"; do
|
||||
read -r name repo suffix <<< "$c"
|
||||
[[ "$COMPONENT" == all || "$COMPONENT" == "$name" ]] || continue
|
||||
var="REDHAT_CERT_ID_${name^^}"; var="${var//-/_}"
|
||||
id="$(jq -r --arg v "$var" '.[$v] // empty' <<< "$REPO_VARS")"
|
||||
if [[ -z "$id" ]]; then
|
||||
missing+=("$var")
|
||||
continue
|
||||
fi
|
||||
matrix="$(jq -c --arg n "$name" --arg t "${version}${suffix}" --arg r "${repo}:${version}${suffix}" --arg i "$id" \
|
||||
'. + [{component: $n, tag: $t, ref: $r, component_id: $i}]' <<< "$matrix")"
|
||||
done
|
||||
if (( ${#missing[@]} )); then
|
||||
echo "::error::Set these repository variables to the Partner Connect component IDs: ${missing[*]}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$matrix" == "[]" ]]; then
|
||||
echo "::error::No component to certify for '${COMPONENT}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Components to certify: ${matrix}"
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix=${matrix}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
certify:
|
||||
name: "Certify ${{ matrix.component }} UBI image"
|
||||
needs: resolve
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.resolve.outputs.matrix) }}
|
||||
env:
|
||||
PREFLIGHT_VERSION: "1.21.0"
|
||||
# sha256 of preflight-linux-amd64 from the 1.21.0 GitHub release.
|
||||
# Red Hat publishes no checksum file, so the value is pinned here.
|
||||
PREFLIGHT_SHA256: "5e653135503c72f8702bbe31d7643197d12937c68086879133dd6b9650a9a449"
|
||||
steps:
|
||||
- name: Verify the multi-arch image is on ghcr.io
|
||||
env:
|
||||
IMAGE_REF: ${{ matrix.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx imagetools inspect "$IMAGE_REF" --raw > manifest.json
|
||||
for arch in amd64 arm64; do
|
||||
if ! jq -e --arg a "$arch" '.manifests[] | select(.platform.architecture == $a)' manifest.json > /dev/null; then
|
||||
echo "::error::${IMAGE_REF} has no ${arch} manifest"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Manifest list for ${IMAGE_REF}:"
|
||||
jq -r '.manifests[] | "\(.platform.os)/\(.platform.architecture) \(.digest)"' manifest.json
|
||||
|
||||
- name: Install preflight
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL --proto '=https' --proto-redir '=https' -o preflight \
|
||||
"https://github.com/redhat-openshift-ecosystem/openshift-preflight/releases/download/${PREFLIGHT_VERSION}/preflight-linux-amd64"
|
||||
echo "${PREFLIGHT_SHA256} preflight" | sha256sum -c -
|
||||
chmod +x preflight
|
||||
./preflight --version
|
||||
|
||||
- name: Run preflight checks and submit to Red Hat
|
||||
env:
|
||||
IMAGE_REF: ${{ matrix.ref }}
|
||||
PFLT_PYXIS_API_TOKEN: ${{ secrets.PYXIS_API_TOKEN }}
|
||||
PFLT_CERTIFICATION_COMPONENT_ID: ${{ matrix.component_id }}
|
||||
PFLT_ARTIFACTS: artifacts
|
||||
PFLT_LOGFILE: artifacts/preflight.log
|
||||
PFLT_LOGLEVEL: info
|
||||
PFLT_JUNIT: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# No --platform: preflight walks the manifest list and submits every
|
||||
# architecture in one run, grouped under one manifest-list digest.
|
||||
# preflight does not create the PFLT_LOGFILE directory, and --submit
|
||||
# fails if the log file is missing.
|
||||
mkdir -p artifacts
|
||||
./preflight check container "$IMAGE_REF" --submit
|
||||
|
||||
- name: Fail if any check did not pass
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
results=(artifacts/results.json artifacts/*/results.json)
|
||||
if [[ ${#results[@]} -eq 0 ]]; then
|
||||
echo "::error::preflight produced no results.json"
|
||||
exit 1
|
||||
fi
|
||||
status=0
|
||||
for f in "${results[@]}"; do
|
||||
arch="$(basename "$(dirname "$f")")"
|
||||
passed="$(jq -r '.passed' "$f")"
|
||||
failed="$(jq -r '[.results.failed[]?.name] | join(", ")' "$f")"
|
||||
echo "${arch}: passed=${passed} ${failed:+failed checks: ${failed}}"
|
||||
[[ "$passed" == "true" ]] || status=1
|
||||
done
|
||||
exit $status
|
||||
|
||||
- name: Upload preflight artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: redhat-preflight-${{ matrix.component }}-${{ needs.resolve.outputs.version }}
|
||||
path: artifacts/
|
||||
retention-days: 30
|
||||
|
||||
- name: Wait for Pyxis to mark both architectures certified
|
||||
env:
|
||||
TAG: ${{ matrix.tag }}
|
||||
COMPONENT_ID: ${{ matrix.component_id }}
|
||||
PFLT_PYXIS_API_TOKEN: ${{ secrets.PYXIS_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Filter on the tag server-side so older versions are found past the first page.
|
||||
url="https://catalog.redhat.com/api/containers/v1/projects/certification/id/${COMPONENT_ID}/images?filter=repositories.tags.name==${TAG}&page_size=100"
|
||||
for attempt in $(seq 1 20); do
|
||||
certified="$(curl -fsS --proto '=https' --proto-redir '=https' -H "X-API-KEY: ${PFLT_PYXIS_API_TOKEN}" "$url" \
|
||||
| jq -r --arg t "$TAG" '[.data[] | select(.repositories[]?.tags[]?.name == $t) | select(.certified == true) | .architecture] | unique | join(",")')"
|
||||
echo "attempt ${attempt}: certified architectures for ${TAG}: ${certified:-none}"
|
||||
if [[ "$certified" == "amd64,arm64" ]]; then
|
||||
echo "Both architectures certified. Auto-publish is enabled on the component, so the catalog updates on its own."
|
||||
exit 0
|
||||
fi
|
||||
sleep 30
|
||||
done
|
||||
echo "::error::Pyxis has not marked both architectures certified after 10 minutes. Check https://connect.redhat.com/component/view/${COMPONENT_ID}/images"
|
||||
exit 1
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
release: "15.0"
|
||||
release: "15.1"
|
||||
envs: "GO_VERSION"
|
||||
prepare: |
|
||||
# Install required packages
|
||||
@@ -191,6 +191,17 @@ jobs:
|
||||
# requires a changelog. Generated, not committed (see .gitignore).
|
||||
# chglog is a go.mod tool directive, so go.sum pins it and its deps.
|
||||
run: bash release_files/rpm-changelog.sh
|
||||
- name: Fill the RPM ISA provide version
|
||||
# nfpm cannot emit rpmbuild's ISA provide and GoReleaser cannot template it.
|
||||
run: bash release_files/rpm-provides.sh
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Install proxy web dependencies for license collection
|
||||
# proxy/collect-licenses.sh reads the UI's license terms from node_modules.
|
||||
working-directory: proxy/web
|
||||
run: npm ci --ignore-scripts
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 #v4.1.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -230,14 +241,18 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||
with:
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --clean ${{ env.flags }}
|
||||
args: release --config .goreleaser.generated.yaml --clean ${{ env.flags }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||
NFPM_NETBIRD_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
# One per nfpm id: GoReleaser looks the passphrase up as NFPM_<ID>_PASSPHRASE.
|
||||
NFPM_NETBIRD_RPM_AMD64_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
NFPM_NETBIRD_RPM_ARM64_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
NFPM_NETBIRD_RPM_ARM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
NFPM_NETBIRD_RPM_386_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
||||
SKIP_DOCKER_PUSH: ${{ env.SKIP_DOCKER_PUSH }}
|
||||
- name: Verify RPM signatures
|
||||
@@ -294,10 +309,12 @@ jobs:
|
||||
tag_and_push() {
|
||||
local src="$1" img_name tag dst variant=""
|
||||
img_name="${src%%:*}"
|
||||
# Client variants share a repository, so keep their tag suffixes.
|
||||
# Variants share a repository with their default image, so keep
|
||||
# their tag suffixes. Order matters: the first matching pattern wins.
|
||||
case "$src" in
|
||||
*-rootless-ubi-amd64) variant="-rootless-ubi" ;;
|
||||
*-rootless-amd64) variant="-rootless" ;;
|
||||
*-ubi-amd64) variant="-ubi" ;;
|
||||
esac
|
||||
for tag in $(resolve_tags); do
|
||||
dst="${img_name}:${tag}${variant}"
|
||||
@@ -365,6 +382,24 @@ jobs:
|
||||
path: dist/netbird_darwin**
|
||||
retention-days: 7
|
||||
|
||||
# Certify the UBI images in the Red Hat Ecosystem Catalog on stable tags.
|
||||
# See redhat-certify.yml, which can also be run by hand for any released version.
|
||||
redhat_certification:
|
||||
name: "Red Hat"
|
||||
needs: release
|
||||
if: |
|
||||
github.repository == 'netbirdio/netbird' &&
|
||||
startsWith(github.ref, 'refs/tags/v') &&
|
||||
!contains(github.ref_name, '-')
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/redhat-certify.yml
|
||||
with:
|
||||
component: all
|
||||
version: ${{ github.ref_name }}
|
||||
secrets:
|
||||
PYXIS_API_TOKEN: ${{ secrets.PYXIS_API_TOKEN }}
|
||||
|
||||
release_ui:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
@@ -419,12 +454,12 @@ jobs:
|
||||
run: git --no-pager diff --exit-code
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
version: 11
|
||||
|
||||
@@ -556,12 +591,12 @@ jobs:
|
||||
run: git --no-pager diff --exit-code
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
version: 11
|
||||
|
||||
@@ -653,11 +688,11 @@ jobs:
|
||||
- name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
version: 11
|
||||
- name: Install wails3 CLI
|
||||
@@ -776,7 +811,7 @@ jobs:
|
||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||
|
||||
- name: Set up Go for wails3 CLI
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Test Homebrew cask
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "client/ui/netbird-ui.rb.tmpl"
|
||||
- ".github/scripts/test-homebrew-cask.sh"
|
||||
- ".github/workflows/test-homebrew-cask.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
install-uninstall:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Clone the Homebrew tap
|
||||
run: git clone https://github.com/netbirdio/homebrew-tap.git .homebrew-cask-tap
|
||||
|
||||
- name: Update Homebrew and install gomplate
|
||||
# The runner image disables auto-update; the cask steps DSL needs Homebrew 6.0.20 or newer.
|
||||
run: |
|
||||
brew update
|
||||
brew install gomplate
|
||||
|
||||
- name: Install and uninstall the cask
|
||||
run: .github/scripts/test-homebrew-cask.sh
|
||||
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||
with:
|
||||
name: homebrew-cask-results
|
||||
path: ${{ runner.temp }}/homebrew-cask/results
|
||||
if-no-files-found: ignore
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user