no-bug: Run update verification before releasing (gh-13353)

This commit is contained in:
mr. m
2026-04-22 15:49:43 +02:00
committed by GitHub
parent 3166d50412
commit 91f276dd0a
9 changed files with 319 additions and 13 deletions

View File

@@ -514,6 +514,13 @@ jobs:
run: |
bash scripts/mar_sign.sh -s
- name: Verify MARs and update manifests
env:
SIGNMAR: ${{ github.workspace }}/linux-bin-x86_64/signmar
RELEASE_BRANCH: ${{ inputs.update_branch }}
run: |
bash scripts/mar_verify.sh
- name: Copy update manifests
env:
RELEASE_BRANCH: ${{ inputs.update_branch }}

View File

@@ -26,7 +26,7 @@ on:
jobs:
mac-build:
name: Build macOS - ${{ matrix.arch }}
runs-on: ${{ (inputs.release-branch == 'release' && matrix.arch == 'aarch64') && 'blacksmith-6vcpu-macos-latest' || 'macos-26' }}
runs-on: ${{ (inputs.release-branch == 'release') && 'blacksmith-6vcpu-macos-latest' || 'macos-26' }}
strategy:
fail-fast: false

280
scripts/mar_verify.sh Executable file
View File

@@ -0,0 +1,280 @@
#!/usr/bin/env bash
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Pre-release verification for MAR artifacts.
#
# Run this AFTER `scripts/mar_sign.sh -s` and BEFORE publishing a release.
# It confirms that every MAR we are about to ship is present, non-empty,
# signed with the cert whose public half lives at build/signing/public_key.der,
# and that the accompanying update.xml manifest reflects the signed file's
# current sha512 / size. Exits non-zero on the first verification failure so
# CI halts before we overwrite a good release with a broken one.
set -euo pipefail
CERT_PATH_DIR="build/signing"
PUBLIC_KEY_DER="$CERT_PATH_DIR/public_key.der"
VERIFY_NSS_DIR="$CERT_PATH_DIR/nss_verify"
LINUX_EXTRACT_DIR="$CERT_PATH_DIR/extracted_linux"
LINUX_ARCHIVE="zen.linux-x86_64.tar.xz/zen.linux-x86_64.tar.xz"
FAILURES=0
fail() {
echo " [FAIL] $*" >&2
FAILURES=$((FAILURES + 1))
}
ok() {
echo " [ OK ] $*"
}
cleanup_verify_db() {
rm -rf "$VERIFY_NSS_DIR"
rm -rf "$LINUX_EXTRACT_DIR"
}
trap cleanup_verify_db EXIT
if [ -z "${SIGNMAR:-}" ]; then
echo "Error: SIGNMAR environment variable is not set." >&2
exit 1
fi
if [ ! -f "$SIGNMAR" ]; then
echo "Error: signmar not found at $SIGNMAR." >&2
exit 1
fi
chmod +x "$SIGNMAR"
if [ ! -f "$PUBLIC_KEY_DER" ]; then
echo "Error: $PUBLIC_KEY_DER not found. Run 'mar_sign.sh -g' first." >&2
exit 1
fi
EXPECTED_MAR_CHANNEL="${RELEASE_BRANCH:-}"
if [ -z "$EXPECTED_MAR_CHANNEL" ]; then
echo "Error: RELEASE_BRANCH environment variable is not set (expected 'release' or 'twilight')." >&2
exit 1
fi
# Build a throwaway NSS database that trusts only the signing cert, so
# signmar -v can verify signatures without the private key being present.
setup_verify_db() {
rm -rf "$VERIFY_NSS_DIR"
mkdir -p "$VERIFY_NSS_DIR"
local pass="$VERIFY_NSS_DIR/password.txt"
: > "$pass"
certutil -N -d "$VERIFY_NSS_DIR" -f "$pass"
certutil -A -d "$VERIFY_NSS_DIR" -n "mar_sig" -t "CT,C,C" -i "$PUBLIC_KEY_DER"
}
# Each entry: "<mar_path>|<manifest_dir>|<platform_label>"
declare -a pairs=(
"linux.mar/linux.mar|linux_update_manifest_x86_64|linux-x86_64"
"linux-aarch64.mar/linux-aarch64.mar|linux_update_manifest_aarch64|linux-aarch64"
"macos.mar/macos.mar|macos_update_manifest|macos"
)
if [ -d ".github/workflows/object/windows-x64-signed-x86_64" ]; then
pairs+=(
".github/workflows/object/windows-x64-signed-x86_64/windows.mar|.github/workflows/object/windows-x64-signed-x86_64/update_manifest|windows-x86_64"
".github/workflows/object/windows-x64-signed-arm64/windows-arm64.mar|.github/workflows/object/windows-x64-signed-arm64/update_manifest|windows-arm64"
)
else
pairs+=(
"windows.mar/windows.mar|windows_update_manifest_x86_64|windows-x86_64"
"windows-arm64.mar/windows-arm64.mar|windows_update_manifest_arm64|windows-arm64"
)
fi
hash_file() {
if command -v sha512sum >/dev/null 2>&1; then
sha512sum "$1" | awk '{print $1}'
else
shasum -a 512 "$1" | awk '{print $1}'
fi
}
size_file() {
wc -c < "$1" | tr -d ' '
}
verify_signature() {
local mar="$1"
if "$SIGNMAR" -d "$VERIFY_NSS_DIR" -n "mar_sig" -v "$mar" >/dev/null 2>&1; then
ok "Signature valid: $mar"
else
fail "Signature INVALID (or missing) for $mar"
fi
}
# Cache signmar -T output per MAR so we only pay for it once per file.
mar_info() {
local mar="$1"
"$SIGNMAR" -T "$mar" 2>&1
}
verify_signature_count() {
local mar="$1"
local info count
info=$(mar_info "$mar")
# signmar -T prints one "Signature <n>:" line per signature block.
count=$(echo "$info" | grep -cE '^[[:space:]]*Signature[[:space:]]+[0-9]+:' || true)
if [ "$count" != "1" ]; then
fail "$mar has $count signatures, expected exactly 1"
else
ok "$mar has exactly 1 signature"
fi
}
verify_mar_channel() {
local mar="$1"
local info channel
info=$(mar_info "$mar")
# Accept either "MAR channel name:" or "MAR channel ID:" — the label
# has drifted between Mozilla releases.
channel=$(echo "$info" \
| grep -iE 'MAR channel (name|id)[[:space:]]*:' \
| head -1 \
| sed -E 's/.*MAR channel (name|id)[[:space:]]*:[[:space:]]*//I' \
| tr -d '[:space:]')
if [ -z "$channel" ]; then
fail "$mar: could not read MAR channel from product info block"
return
fi
if [ "$channel" != "$EXPECTED_MAR_CHANNEL" ]; then
fail "$mar: MAR channel is '$channel', expected '$EXPECTED_MAR_CHANNEL' (RELEASE_BRANCH)"
else
ok "$mar: MAR channel = $channel"
fi
}
verify_update_settings() {
echo ""
echo "Checking update-settings.ini in $LINUX_ARCHIVE..."
if [ ! -f "$LINUX_ARCHIVE" ]; then
fail "Linux build archive not found at $LINUX_ARCHIVE"
return
fi
rm -rf "$LINUX_EXTRACT_DIR"
mkdir -p "$LINUX_EXTRACT_DIR"
if ! tar -xf "$LINUX_ARCHIVE" -C "$LINUX_EXTRACT_DIR"; then
fail "Failed to extract $LINUX_ARCHIVE"
return
fi
local ini
ini=$(find "$LINUX_EXTRACT_DIR" -type f -name "update-settings.ini" | head -1)
if [ -z "$ini" ]; then
fail "update-settings.ini not found inside $LINUX_ARCHIVE"
return
fi
local accepted
accepted=$(grep -oP '^ACCEPTED_MAR_CHANNEL_IDS[[:space:]]*=[[:space:]]*\K.*' "$ini" \
| head -1 | tr -d '\r')
if [ -z "$accepted" ]; then
fail "ACCEPTED_MAR_CHANNEL_IDS not set in $ini"
return
fi
# ACCEPTED_MAR_CHANNEL_IDS is a comma-separated list; membership is what
# the updater enforces, so we check membership rather than strict equality.
local found=0 entry
IFS=',' read -ra entries <<< "$accepted"
for entry in "${entries[@]}"; do
entry=$(echo "$entry" | tr -d '[:space:]')
if [ "$entry" = "$EXPECTED_MAR_CHANNEL" ]; then
found=1
break
fi
done
if [ "$found" = "1" ]; then
ok "update-settings.ini accepts MAR channel '$EXPECTED_MAR_CHANNEL' (ACCEPTED_MAR_CHANNEL_IDS=$accepted)"
else
fail "update-settings.ini ACCEPTED_MAR_CHANNEL_IDS='$accepted' does not include '$EXPECTED_MAR_CHANNEL'"
fi
}
verify_manifest() {
local mar="$1" manifest_dir="$2" label="$3"
if [ ! -d "$manifest_dir" ]; then
fail "$label: manifest directory $manifest_dir not found"
return
fi
local xmls
xmls=$(find "$manifest_dir" -type f -name "update.xml")
if [ -z "$xmls" ]; then
fail "$label: no update.xml files found under $manifest_dir"
return
fi
local actual_hash actual_size
actual_hash=$(hash_file "$mar")
actual_size=$(size_file "$mar")
while IFS= read -r xml; do
local xhash xsize xurl
xhash=$(grep -oP 'hashValue="\K[^"]+' "$xml" | head -1 || true)
xsize=$(grep -oP 'size="\K[^"]+' "$xml" | head -1 || true)
xurl=$(grep -oP 'URL="\K[^"]+' "$xml" | head -1 || true)
if [ -z "$xhash" ] || [ -z "$xsize" ]; then
fail "$label: $xml is missing hashValue or size"
continue
fi
if [ -z "$xurl" ]; then
fail "$label: $xml is missing URL attribute"
continue
fi
if ! grep -q 'hashFunction="sha512"' "$xml"; then
fail "$label: $xml hashFunction is not sha512"
continue
fi
if [ "$xhash" != "$actual_hash" ]; then
fail "$label: hashValue mismatch in $xml"
echo " manifest: $xhash" >&2
echo " actual: $actual_hash" >&2
continue
fi
if [ "$xsize" != "$actual_size" ]; then
fail "$label: size mismatch in $xml (manifest=$xsize, actual=$actual_size)"
continue
fi
ok "$label: $(basename "$xml") matches $mar (size=$actual_size)"
done <<< "$xmls"
}
setup_verify_db
verify_update_settings
for entry in "${pairs[@]}"; do
IFS='|' read -r mar manifest label <<< "$entry"
echo ""
echo "Verifying $label: $mar"
if [ ! -f "$mar" ]; then
fail "$label: MAR file $mar not found"
continue
fi
if [ ! -s "$mar" ]; then
fail "$label: MAR file $mar is empty"
continue
fi
verify_signature "$mar"
verify_signature_count "$mar"
verify_mar_channel "$mar"
verify_manifest "$mar" "$manifest" "$label"
done
echo ""
if [ "$FAILURES" -gt 0 ]; then
echo "Pre-release verification FAILED with $FAILURES issue(s)." >&2
exit 1
fi
echo "Pre-release verification passed."

View File

@@ -0,0 +1,13 @@
diff --git a/tools/update-packaging/common.sh b/tools/update-packaging/common.sh
index be2061b98cbba0d472e5f4d03cab2b058b249cb8..caa3379cd09efc2ded9686929fc658985a9dda51 100755
--- a/tools/update-packaging/common.sh
+++ b/tools/update-packaging/common.sh
@@ -94,8 +94,6 @@ check_for_add_if_not_update() {
if [[ "$(basename "$add_if_not_file_chk")" = "channel-prefs.js" || \
"$add_if_not_file_chk" =~ (^|/)ChannelPrefs\.framework/ || \
- "$(basename "$add_if_not_file_chk")" = "update-settings.ini" || \
- "$add_if_not_file_chk" =~ (^|/)UpdateSettings\.framework/ || \
"$(basename "$add_if_not_file_chk")" = "distribution.ini" ]]; then
## "true"
return 0;

View File

@@ -586,6 +586,10 @@
transform-origin: right;
opacity: 0.7;
.urlbarView-row[selected] & {
opacity: 1;
}
&::after {
content: "";
display: inline-block;
@@ -636,7 +640,7 @@
}
&[selected] {
--zen-selected-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2)));
--zen-selected-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.1)));
--zen-selected-color: color-mix(in srgb, var(--zen-selected-bg), black 30%);
background-color: var(--zen-selected-bg) !important;
@@ -654,7 +658,7 @@
color: var(--zen-selected-color) !important;
}
&:where([type="switchtab"], [type="dynamic"], [type^="history"], [type^="autofill_"], [type="top_site"]) .urlbarView-favicon,
&:where([type="switchtab"], [type="dynamic"], [type^="history"], [type^="autofill_"], [type="top_site"], [type="bookmark"]) .urlbarView-favicon,
& .urlbarView-shortcutContent {
fill: var(--zen-selected-color) !important;
background-color: rgba(255, 255, 255, 0.9) !important;

View File

@@ -214,7 +214,7 @@
--urlbar-margin-inline: 1px !important;
--tab-icon-overlay-stroke: light-dark(white, black) !important;
--tab-close-button-padding: 5px !important;
--tab-close-button-padding: 4px !important;
--input-border-color: var(--zen-input-border-color) !important;
--zen-themed-toolbar-bg-transparent: light-dark(var(--zen-branding-bg), #171717);
@@ -263,7 +263,7 @@
);
--zen-sidebar-notification-shadow: 0 0 6px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3));
--zen-sidebar-themed-icon-fill: color-mix(in srgb, var(--zen-primary-color) 40%, light-dark(black, white));
--zen-sidebar-themed-icon-fill: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
--zen-native-inner-radius: var(
--zen-webview-border-radius,

View File

@@ -119,10 +119,12 @@
init() {
super.init();
this.handle_windowDragEnter = this.handle_windowDragEnter.bind(this);
gZenWorkspaces.workspaceIcons.addEventListener(
"dragover",
this.handle_spaceIconDragOver.bind(this)
);
if (gZenWorkspaces.workspaceEnabled) {
gZenWorkspaces.workspaceIcons.addEventListener(
"dragover",
this.handle_spaceIconDragOver.bind(this)
);
}
window.addEventListener(
"dragleave",
this.handle_windowDragLeave.bind(this),

View File

@@ -131,15 +131,14 @@ export class ZenSpacesSwipe {
event.preventDefault();
event.stopPropagation();
const delta = event.delta * 300;
const stripWidth =
window.windowUtils.getBoundsWithoutFlushing(
document.getElementById("navigator-toolbox")
).width +
window.windowUtils.getBoundsWithoutFlushing(
document.getElementById("zen-sidebar-splitter")
).width *
2;
).width;
const delta = event.delta * stripWidth;
let translateX = this._swipeState.lastDelta + delta;
// Add a force multiplier as we are translating the strip depending on how close to the edge we are
let forceMultiplier = Math.min(

View File

@@ -75,6 +75,7 @@
&[active='true'] .zen-workspace-icon {
fill: var(--zen-sidebar-themed-icon-fill);
fill-opacity: 1;
}
&[active='true'],
@@ -216,7 +217,7 @@
height: 16px;
justify-content: center;
align-items: center;
fill-opacity: 0.6;
fill-opacity: 1;
-moz-context-properties: fill-opacity, fill;
fill: var(--zen-sidebar-themed-icon-fill);
font-size: 16px;