first commit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 11:05:58 +02:00
co-authored by Claude Opus 5
commit 99757361ef
15 changed files with 3020 additions and 0 deletions
+458
View File
@@ -0,0 +1,458 @@
#!/usr/bin/env bash
#
# Baut das Add-on "Swyx Tab Bridge" und installiert es per SSH auf einem
# Windows-Rechner mit Firefox.
#
# Ablauf:
# 1. Version in manifest.json hochzaehlen (immer, ausser --no-bump)
# 2. .xpi packen, optional per web-ext signieren
# 3. .xpi + Install-SwyxTabBridge.ps1 per scp uebertragen
# 4. Install-SwyxTabBridge.ps1 per ssh mit PowerShell ausfuehren
#
# Beispiele:
# ./deploy/deploy-windows.sh # signieren + ausrollen, ohne Parameter
# ./deploy/deploy-windows.sh --keep-firefox # laufenden Firefox nicht beenden
# ./deploy/deploy-windows.sh --uninstall
# ./deploy/deploy-windows.sh --xpi build/xyz-1.0.16.xpi --no-bump
# # bereits signiertes Paket erneut ausrollen
# ./deploy/deploy-windows.sh --host admin@ws-042 --firefox-dir auto
# # anderer Rechner, Firefox selbst suchen
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
MANIFEST="$SRC_DIR/manifest.json"
BUILD_DIR="$SRC_DIR/build"
PS_SCRIPT="$SCRIPT_DIR/Install-SwyxTabBridge.ps1"
EXTENSION_ID="swyx-tab-bridge@appcreation.de"
# Inhalte des Add-ons - bewusst explizit, damit weder test-server/ noch build/
# noch .DS_Store im Paket landen.
PACKAGE_FILES=(manifest.json background.js popup.html popup.js options.html options.js icons)
# Voreinstellungen fuer den Windows-Testrechner. Der Alias 'swyx-dev' steht in
# ~/.ssh/config und bringt Benutzer und Schluessel mit; --host bzw.
# --firefox-dir ueberschreiben das fuer andere Rechner.
DEFAULT_HOST="swyx-dev"
# Ziel ist die Release-Installation. Sie liegt als Benutzerinstallation unter
# %LOCALAPPDATA% und laesst sich damit ohne Administratorrechte bespielen. Der
# Pfad steht fest, weil auf dem Rechner zusaetzlich die Developer Edition
# installiert ist und die automatische Suche bei zwei Funden abbricht.
DEFAULT_FIREFOX_DIR='C:\Users\dev\AppData\Local\Mozilla Firefox'
# Firefox Release erzwingt die Signatur fest im Build. Jeder Rollout muss also
# ueber AMO signiert werden, und an der Signaturpruefung wird nichts gedreht.
DEFAULT_SIGN=true
DEFAULT_ALLOW_UNSIGNED=false
# Beendet wird nur, was aus DEFAULT_FIREFOX_DIR gestartet wurde; eine daneben
# laufende zweite Installation bleibt offen. Firefox stellt die Sitzung beim
# naechsten Start wieder her.
DEFAULT_STOP_FIREFOX=true
HOST="$DEFAULT_HOST"
SSH_PORT=""
MODE="Policy"
BUMP="patch"
FORCED_VERSION=""
XPI_OVERRIDE=""
REMOTE_DIR="swyx-deploy"
DO_SIGN=$DEFAULT_SIGN
ALLOW_UNSIGNED=$DEFAULT_ALLOW_UNSIGNED
STOP_FIREFOX=$DEFAULT_STOP_FIREFOX
UNINSTALL=false
FIREFOX_DIR="$DEFAULT_FIREFOX_DIR"
TARGET_USER=""
DRY_RUN=false
BUILD_ONLY=false
# ------------------------------------------------------------------ Ausgabe
c_step() { printf '\n\033[36m==> %s\033[0m\n' "$*"; }
c_info() { printf ' %s\n' "$*"; }
c_good() { printf ' \033[32m%s\033[0m\n' "$*"; }
c_note() { printf ' \033[33m%s\033[0m\n' "$*"; }
c_fail() { printf '\n\033[31mFEHLER: %s\033[0m\n' "$*" >&2; exit 1; }
usage() {
# Kopfkommentar ab Zeile 3 ausgeben, bis die erste Nicht-Kommentarzeile kommt.
awk 'NR > 2 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"
cat <<'EOF'
Optionen:
--host USER@HOST Ziel-Rechner (Default: swyx-dev aus ~/.ssh/config)
--port N SSH-Port
--mode policy|profile Installationsart (Default: policy; braucht Schreibrecht
im Firefox-Installationsverzeichnis)
--bump LEVEL major | minor | patch | build | none (Default: patch)
--set-version X.Y.Z Version fest vorgeben statt hochzuzaehlen
--no-bump Version unveraendert lassen (Kurzform fuer --bump none)
--xpi DATEI Fertiges (z.B. signiertes) .xpi verwenden, nicht bauen
--sign Per 'web-ext sign --channel=unlisted' bei AMO signieren.
Voreingestellt an, weil Firefox Release nur signierte
Add-ons installiert. Jeder Lauf verbraucht dabei eine
Versionsnummer bei AMO
--no-sign Nicht signieren (nur fuer ESR/Developer Edition sinnvoll,
dann zusammen mit --allow-unsigned)
--allow-unsigned Signaturpflicht auf dem Ziel abschalten (nur ESR/Dev).
Voreingestellt aus
--require-signed Signaturpflicht auf dem Ziel unangetastet lassen (Default)
--stop-firefox Laufenden Firefox auf dem Ziel beenden, damit die
Installation sofort greift. Voreingestellt an
--keep-firefox Laufenden Firefox nicht anfassen
--firefox-dir PFAD Firefox-Installationsverzeichnis auf dem Ziel
(Default: C:\Users\dev\AppData\Local\Mozilla Firefox;
"auto" laesst das Skript selbst suchen)
--target-user NAME Windows-Benutzer fuer --mode profile
--remote-dir NAME Uebertragungsordner im Home des SSH-Benutzers
--uninstall Add-on und Policy auf dem Ziel entfernen
--build-only Nur bauen, nichts uebertragen
--dry-run Nur anzeigen, was passieren wuerde
-h, --help Diese Hilfe
Umgebungsvariablen:
SSH_CMD / SCP_CMD Eigener Aufrufer statt 'ssh'/'scp', z.B. fuer
Passwort-Login: SSH_CMD="sshpass -e ssh"
SCP_CMD="sshpass -e scp" SSHPASS=geheim ...
WEB_EXT_API_KEY / WEB_EXT_API_SECRET AMO-Zugangsdaten fuer --sign. Werden
sonst aus deploy/.amo-credentials gelesen
EOF
exit 0
}
# ------------------------------------------------------------------ Argumente
while [[ $# -gt 0 ]]; do
case "$1" in
--host) HOST="${2:?}"; shift 2 ;;
--port) SSH_PORT="${2:?}"; shift 2 ;;
--mode) MODE="${2:?}"; shift 2 ;;
--bump) BUMP="${2:?}"; shift 2 ;;
--set-version) FORCED_VERSION="${2:?}"; shift 2 ;;
--no-bump) BUMP="none"; shift ;;
--xpi) XPI_OVERRIDE="${2:?}"; shift 2 ;;
--sign) DO_SIGN=true; shift ;;
--no-sign) DO_SIGN=false; shift ;;
--allow-unsigned) ALLOW_UNSIGNED=true; shift ;;
--require-signed) ALLOW_UNSIGNED=false; shift ;;
--stop-firefox) STOP_FIREFOX=true; shift ;;
--keep-firefox) STOP_FIREFOX=false; shift ;;
--firefox-dir) FIREFOX_DIR="${2:?}"; shift 2
[[ "$FIREFOX_DIR" == "auto" ]] && FIREFOX_DIR="" ;;
--target-user) TARGET_USER="${2:?}"; shift 2 ;;
--remote-dir) REMOTE_DIR="${2:?}"; shift 2 ;;
--uninstall) UNINSTALL=true; shift ;;
--build-only) BUILD_ONLY=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage ;;
*) c_fail "Unbekannte Option: $1 (--help fuer die Uebersicht)" ;;
esac
done
case "$(printf '%s' "$MODE" | tr '[:upper:]' '[:lower:]')" in
policy) MODE="Policy" ;;
profile) MODE="Profile" ;;
*) c_fail "--mode muss 'policy' oder 'profile' sein." ;;
esac
case "$BUMP" in
major|minor|patch|build|none) ;;
*) c_fail "--bump muss major, minor, patch, build oder none sein." ;;
esac
if ! $BUILD_ONLY && [[ -z "$HOST" ]]; then
c_fail "Kein Ziel-Rechner: --host angeben."
fi
SSH_OPTS=()
SCP_OPTS=()
if [[ -n "$SSH_PORT" ]]; then
SSH_OPTS+=(-p "$SSH_PORT")
SCP_OPTS+=(-P "$SSH_PORT")
fi
# AMO-Zugangsdaten fuer --sign. Bereits gesetzte Umgebungsvariablen haben
# Vorrang, damit sich der Schluessel fuer einen einzelnen Lauf uebersteuern laesst.
AMO_CREDENTIALS="$SCRIPT_DIR/.amo-credentials"
if [[ -f "$AMO_CREDENTIALS" ]]; then
if [[ -z "${WEB_EXT_API_KEY:-}" || -z "${WEB_EXT_API_SECRET:-}" ]]; then
set -a
# shellcheck source=/dev/null
. "$AMO_CREDENTIALS"
set +a
fi
# Der Schluessel gilt fuer das ganze AMO-Konto - er hat auf einer
# mitlesbaren Datei nichts verloren.
CRED_PERMS="$(stat -f '%Lp' "$AMO_CREDENTIALS" 2>/dev/null || stat -c '%a' "$AMO_CREDENTIALS" 2>/dev/null || echo '')"
if [[ -n "$CRED_PERMS" && "${CRED_PERMS: -2}" != "00" ]]; then
c_note "$AMO_CREDENTIALS ist auch fuer andere lesbar - 'chmod 600' empfohlen."
fi
fi
# Normalfall ist Anmeldung per Schluessel. Wer Passwort-Login oder einen
# Jumphost braucht, haengt ueber SSH_CMD/SCP_CMD einen eigenen Aufrufer davor:
# SSH_CMD="sshpass -e ssh" SCP_CMD="sshpass -e scp" SSHPASS=... ./deploy-windows.sh ...
read -r -a SSH_BIN <<< "${SSH_CMD:-ssh}"
read -r -a SCP_BIN <<< "${SCP_CMD:-scp}"
# ------------------------------------------------------------------ Hilfsfunktionen
# PowerShell-Kommando als UTF-16LE-Base64 uebergeben. Damit ist voellig egal,
# ob auf dem Ziel cmd.exe oder PowerShell die Standard-Shell ist - es gibt kein
# Quoting-Problem mit Leerzeichen, Backslashes oder Anfuehrungszeichen.
encode_ps() {
printf '%s' "$1" | iconv -f UTF-8 -t UTF-16LE | base64 | tr -d '\n'
}
run_remote_ps() {
local command="$1"
local encoded
# Ohne das schiebt Windows PowerShell beim Nachladen von Modulen einen
# Progress-Record als CLIXML in den Ausgabestrom.
encoded="$(encode_ps "\$ProgressPreference = 'SilentlyContinue'
$command")"
if $DRY_RUN; then
c_info "[dry-run] ${SSH_BIN[*]} ${SSH_OPTS[*]:-} $HOST powershell -EncodedCommand <${#encoded} Zeichen>"
c_info "[dry-run] ${command//$'\n'/ }"
return 0
fi
"${SSH_BIN[@]}" ${SSH_OPTS[@]+"${SSH_OPTS[@]}"} "$HOST" powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand "$encoded"
}
# Ein Argument fuer die PowerShell-Kommandozeile in einfache Anfuehrungszeichen setzen.
ps_quote() {
printf "'%s'" "${1//\'/\'\'}"
}
read_manifest_version() {
python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST"
}
# Version in manifest.json ersetzen, ohne die Formatierung der Datei anzutasten.
write_manifest_version() {
python3 - "$MANIFEST" "$1" <<'PY'
import re, sys
path, version = sys.argv[1], sys.argv[2]
with open(path, encoding="utf-8") as handle:
text = handle.read()
new_text, count = re.subn(
r'("version"\s*:\s*)"[^"]*"',
lambda m: '%s"%s"' % (m.group(1), version),
text,
count=1,
)
if count != 1:
sys.exit("Feld 'version' nicht in %s gefunden." % path)
with open(path, "w", encoding="utf-8") as handle:
handle.write(new_text)
PY
}
# Toolkit-Versionen duerfen bis zu vier Zahlen haben (1.0.0.7). 'build' zaehlt
# den vierten Teil hoch und laesst die eigentliche Release-Nummer in Ruhe.
next_version() {
python3 - "$1" "$2" <<'PY'
import sys
version, level = sys.argv[1], sys.argv[2]
parts = [int(p) for p in version.split(".")]
while len(parts) < 3:
parts.append(0)
if level == "major":
parts = [parts[0] + 1, 0, 0]
elif level == "minor":
parts = [parts[0], parts[1] + 1, 0]
elif level == "patch":
parts = [parts[0], parts[1], parts[2] + 1]
elif level == "build":
parts = parts[:3] + [(parts[3] if len(parts) > 3 else 0) + 1]
print(".".join(str(p) for p in parts))
PY
}
# ------------------------------------------------------------------ Version
VERSION=""
if ! $UNINSTALL && [[ -z "$XPI_OVERRIDE" ]]; then
c_step "Version"
CURRENT="$(read_manifest_version)"
if [[ -n "$FORCED_VERSION" ]]; then
VERSION="$FORCED_VERSION"
elif [[ "$BUMP" == "none" ]]; then
VERSION="$CURRENT"
else
VERSION="$(next_version "$CURRENT" "$BUMP")"
fi
if [[ "$VERSION" == "$CURRENT" ]]; then
c_info "unveraendert: $CURRENT"
c_note "Firefox installiert ein Paket mit gleicher oder kleinerer Version nicht erneut."
elif $DRY_RUN; then
c_info "[dry-run] $CURRENT -> $VERSION (manifest.json bliebe unveraendert)"
else
write_manifest_version "$VERSION"
c_good "$CURRENT -> $VERSION (manifest.json aktualisiert)"
fi
fi
# ------------------------------------------------------------------ Paket bauen
XPI=""
if $UNINSTALL; then
:
elif [[ -n "$XPI_OVERRIDE" ]]; then
[[ -f "$XPI_OVERRIDE" ]] || c_fail "Datei nicht gefunden: $XPI_OVERRIDE"
XPI="$(cd "$(dirname "$XPI_OVERRIDE")" && pwd)/$(basename "$XPI_OVERRIDE")"
c_step "Paket"
c_info "verwende vorhandenes Paket: $XPI"
else
c_step "Paket bauen"
for entry in "${PACKAGE_FILES[@]}"; do
[[ -e "$SRC_DIR/$entry" ]] || c_fail "Im Add-on fehlt: $entry"
done
mkdir -p "$BUILD_DIR"
if $DO_SIGN; then
# Signieren erzeugt das Paket selbst - ein vorher gebautes Zip waere nur
# eine zweite, unsignierte Datei mit demselben Namen.
if [[ -z "${WEB_EXT_API_KEY:-}" || -z "${WEB_EXT_API_SECRET:-}" ]]; then
c_fail "Keine AMO-Zugangsdaten: WEB_EXT_API_KEY/WEB_EXT_API_SECRET setzen oder in $AMO_CREDENTIALS hinterlegen."
fi
if $DRY_RUN; then
c_info "[dry-run] npx web-ext sign --channel=unlisted (Version $VERSION)"
XPI="$BUILD_DIR/swyx_tab_bridge-$VERSION.xpi"
else
SIGN_MARKER="$BUILD_DIR/.sign-marker"
: > "$SIGN_MARKER"
( cd "$SRC_DIR" && npx --yes web-ext sign \
--channel=unlisted \
--source-dir="$SRC_DIR" \
--artifacts-dir="$BUILD_DIR" \
--ignore-files 'test-server/**' 'deploy/**' 'build/**' '**/.DS_Store' )
# Nur Dateien akzeptieren, die nach dem Start des Signierlaufs entstanden sind.
XPI="$(find "$BUILD_DIR" -maxdepth 1 -name '*.xpi' -newer "$SIGN_MARKER" | head -n1)"
rm -f "$SIGN_MARKER"
[[ -n "$XPI" ]] || c_fail "web-ext hat kein signiertes .xpi in $BUILD_DIR hinterlassen."
c_good "signiert: $XPI"
fi
else
XPI="$BUILD_DIR/swyx_tab_bridge-$VERSION.xpi"
if $DRY_RUN; then
c_info "[dry-run] wuerde bauen: $XPI"
else
rm -f "$XPI"
# -X: keine macOS-Metadaten; gezippt wird der *Inhalt*, nicht der Ordner.
( cd "$SRC_DIR" && zip -q -r -X "$XPI" "${PACKAGE_FILES[@]}" -x '*.DS_Store' '*/__MACOSX/*' )
c_good "$XPI ($(du -h "$XPI" | cut -f1 | tr -d ' '))"
fi
if ! $ALLOW_UNSIGNED; then
c_note "Nicht signiert - Firefox Release/Beta verweigert die Installation. Entweder --sign verwenden oder auf ESR/Dev die Signaturpflicht abschalten."
fi
fi
fi
if $BUILD_ONLY; then
c_step "Fertig"
c_good "${XPI:-kein Paket gebaut}"
exit 0
fi
# ------------------------------------------------------------------ Uebertragen
c_step "Verbindung zu $HOST"
if $DRY_RUN; then
c_info "[dry-run] ssh-Test uebersprungen"
else
# Auch der Test laeuft ueber -EncodedCommand: die Standard-Shell des
# SSH-Benutzers ist unter Windows mal cmd.exe, mal PowerShell, und ein
# blankes $env:COMPUTERNAME wuerde von der aeusseren Shell aufgeloest.
REMOTE_NAME="$(run_remote_ps 'Write-Output "$env:COMPUTERNAME|$($PSVersionTable.PSVersion)"' | tr -d '\r')" \
|| c_fail "SSH-Verbindung zu $HOST fehlgeschlagen."
c_good "erreichbar: ${REMOTE_NAME:-unbekannt}"
fi
c_step "Dateien uebertragen"
run_remote_ps "New-Item -ItemType Directory -Force -Path \"\$HOME\\$REMOTE_DIR\" | Out-Null"
if $DRY_RUN; then
c_info "[dry-run] scp $PS_SCRIPT -> $HOST:$REMOTE_DIR/"
[[ -n "$XPI" ]] && c_info "[dry-run] scp $XPI -> $HOST:$REMOTE_DIR/"
else
"${SCP_BIN[@]}" ${SCP_OPTS[@]+"${SCP_OPTS[@]}"} -q "$PS_SCRIPT" "$HOST:$REMOTE_DIR/Install-SwyxTabBridge.ps1"
c_good "Install-SwyxTabBridge.ps1"
if [[ -n "$XPI" ]]; then
"${SCP_BIN[@]}" ${SCP_OPTS[@]+"${SCP_OPTS[@]}"} -q "$XPI" "$HOST:$REMOTE_DIR/$(basename "$XPI")"
c_good "$(basename "$XPI")"
fi
fi
# ------------------------------------------------------------------ Ausfuehren
c_step "Installation auf $HOST"
PS_ARGS=""
add_arg() { PS_ARGS="$PS_ARGS $1"; }
add_arg "-ExtensionId $(ps_quote "$EXTENSION_ID")"
if $UNINSTALL; then
add_arg "-Uninstall"
else
add_arg "-XpiPath \"\$base\\$(basename "$XPI")\""
add_arg "-Mode $(ps_quote "$MODE")"
fi
[[ -n "$FIREFOX_DIR" ]] && add_arg "-FirefoxDir $(ps_quote "$FIREFOX_DIR")"
[[ -n "$TARGET_USER" ]] && add_arg "-TargetUser $(ps_quote "$TARGET_USER")"
$ALLOW_UNSIGNED && add_arg "-AllowUnsigned"
$STOP_FIREFOX && add_arg "-StopFirefox"
REMOTE_COMMAND="\$ErrorActionPreference = 'Stop'
\$ProgressPreference = 'SilentlyContinue'
\$base = Join-Path \$HOME '$REMOTE_DIR'
try {
& (Join-Path \$base 'Install-SwyxTabBridge.ps1')$PS_ARGS
exit \$LASTEXITCODE
} catch {
Write-Host \"FEHLER: \$(\$_.Exception.Message)\" -ForegroundColor Red
exit 1
}"
REMOTE_LOG="$(mktemp -t swyx-deploy)"
trap 'rm -f "$REMOTE_LOG"' EXIT
set +e
run_remote_ps "$REMOTE_COMMAND" 2>&1 | tee "$REMOTE_LOG"
STATUS=${PIPESTATUS[0]}
set -e
# Die Login-Shell des Windows-SSH-Dienstes reicht nur 0 und "ungleich 0" durch;
# aus 2 wird unterwegs 1. Massgeblich ist deshalb die Markerzeile des Skripts,
# der Exitcode dient nur als Rueckfallebene.
MARKER="$(grep -o 'SWYX-STATUS: [A-Z]*' "$REMOTE_LOG" | tail -n1 | awk '{print $2}')"
[[ -z "$MARKER" && "$STATUS" -eq 0 ]] && MARKER="OK"
c_step "Ergebnis"
case "$MARKER" in
OK) c_good "Erfolgreich abgeschlossen." ;;
RESTART) c_good "Erfolgreich abgeschlossen."
c_note "Auf $HOST muss Firefox noch neu gestartet werden." ;;
*) c_fail "Das Installationsskript auf $HOST ist abgebrochen (Exitcode $STATUS)." ;;
esac
if [[ -n "$VERSION" ]]; then
c_info "Ausgerollte Version: $VERSION"
fi