Merge main into poc/certificate-posture

This commit is contained in:
Viktor Liu
2026-10-05 19:09:43 +02:00
390 changed files with 28443 additions and 14545 deletions
+22
View File
@@ -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"
+338
View File
@@ -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::"
+3 -3
View File
@@ -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') }}
+1 -1
View File
@@ -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
+46
View File
@@ -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/...
+199
View File
@@ -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
+46 -11
View File
@@ -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
+46
View File
@@ -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
+1 -1
View File
@@ -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"