release infra: build scripts, Firefox signing, plugin fixes

- .gitignore: release/, .env, *.xpi, node_modules/
- .env.example: template for AMO credentials
- extension-firefox/package.json: web-ext scripts (lint, sign)
- extension-firefox/manifest.json: gecko.id + update_url + data_collection_permissions
- scripts/build.sh: renamed binaries (verstak, verstak-server), release target
- scripts/sign-firefox-xpi.sh: AMO signing with --self-hosted
- scripts/release-firefox-xpi.sh: signed XPI + updates.json generation
- scripts/release.sh: full release pipeline (DEB/RPM/checksums/git tag)
- VERSION: 0.1.0
- README.md: Firefox extension & release sections

Plugin fixes:
- internal/core/plugins/manager.go: auto-create .verstak/plugins/ dir
- frontend/src/lib/SettingsSidebar.svelte: remove 'plugins' from disabled + active left border
- frontend/src/lib/SettingsPlugins.svelte: force ListPlugins refresh on toggle
- frontend/src/lib/SettingsWindow.svelte: onPluginToggle callback
- frontend/src/App.svelte: refreshSystemViews on plugin toggle
- frontend/src/lib/CalendarPluginPage.svelte: visible error icon
This commit is contained in:
mirivlad 2026-06-08 11:07:29 +08:00
parent 3b79754f45
commit b1d1defebe
22 changed files with 4792 additions and 30 deletions

22
.env.example Normal file
View File

@ -0,0 +1,22 @@
# Verstak Firefox Extension — environment template
# Copy to .env and fill in real values.
# DO NOT commit .env — it contains AMO secrets.
# AMO API credentials (from https://addons.mozilla.org/developers/)
WEB_EXT_API_KEY=
WEB_EXT_API_SECRET=
# Build channel: "unlisted" for self-distributed builds
WEB_EXT_CHANNEL=unlisted
# Source directory for the Firefox extension
WEB_EXT_SOURCE_DIR=extension-firefox
# Output directory for signed XPI artifacts
WEB_EXT_ARTIFACTS_DIR=web-ext-artifacts
# Base URL for Firefox self-hosted updates
VERSTAK_FIREFOX_UPDATE_BASE_URL=https://mirv.top/verstak/firefox
# Optional: HTTP proxy for AMO API requests (e.g. http://localhost:12334)
# WEB_EXT_API_PROXY=

14
.gitignore vendored
View File

@ -49,3 +49,17 @@ server-data/
# Build output # Build output
build/ build/
build.log build.log
# Release artifacts
release/
# Environment / secrets
.env
.env.*
!.env.example
# Firefox extension
web-ext-artifacts/
*.xpi
*.zip
node_modules/

View File

@ -70,22 +70,88 @@
Дополнительно запускается headless Chromium smoke через Wails-mock: проверяются first-run, recovery, основное окно, Settings, workspace, вкладки дела, файлы, журнал, активность и мобильный viewport. Smoke выполняет реальные UI-действия: создание заметки, запись worklog, создание узла, вход в папку и возврат назад, а также Sync Now с предупреждениями о conflicts/applyErrors. Скриншоты пишутся в `/tmp/verstak-gui-smoke`. Дополнительно запускается headless Chromium smoke через Wails-mock: проверяются first-run, recovery, основное окно, Settings, workspace, вкладки дела, файлы, журнал, активность и мобильный viewport. Smoke выполняет реальные UI-действия: создание заметки, запись worklog, создание узла, вход в папку и возврат назад, а также Sync Now с предупреждениями о conflicts/applyErrors. Скриншоты пишутся в `/tmp/verstak-gui-smoke`.
Бинарники попадают в `build/`: Бинарники попадают в `build/`:
- `verstak-gui-linux-amd64` — GUI-приложение - `verstak` — GUI-приложение
- `verstak-server-linux-amd64` — опциональный сервер синхронизации - `verstak-server` — опциональный сервер синхронизации
### Запуск ### Запуск
```bash ```bash
# GUI (после сборки) # GUI (после сборки)
./build/verstak-gui-linux-amd64 ./build/verstak
# Сервер (после сборки) # Сервер (после сборки)
./build/verstak-server-linux-amd64 --help ./build/verstak-server --help
# CLI # CLI
go run ./cmd/verstak/ --help go run ./cmd/verstak/ --help
``` ```
## Firefox Extension
Расширение для Firefox — `extension-firefox/`. Распространяется как signed XPI: **Mozilla только подписывает** XPI через AMO (unlisted channel), а мы самостоятельно хостим signed XPI и управляем обновлениями через `updates.json`.
### Сборка (unsigned)
```bash
./scripts/build.sh firefox
# → build/verstak-bridge-firefox-unsigned.zip
```
### Подпись (требуются AMO-токены)
```bash
# 1. Скопировать .env.example → .env и заполнить WEB_EXT_API_KEY / WEB_EXT_API_SECRET
cp .env.example .env
# 2. Установить зависимости
cd extension-firefox && npm install && cd ..
# 3. Подписать XPI
./scripts/sign-firefox-xpi.sh
# → web-ext-artifacts/*.xpi
# 4. Полный релиз: подпись + release/firefox/ + updates.json
./scripts/release-firefox-xpi.sh
# → release/firefox/verstak-firefox-VERSION.xpi
# → release/firefox/updates.json
```
### Firefox Release Artifacts
```
release/firefox/verstak-firefox-VERSION.xpi
release/firefox/updates.json
```
Обновления: Firefox проверяет `update_url` из manifest.json, указывающий на наш `updates.json`. При выходе новой версии достаточно:
1. Подписать новый XPI
2. Заменить файл на сервере
3. Обновить `updates.json`
## Release
```bash
# Полная сборка с упаковкой в DEB/RPM
./scripts/release.sh # dry-run (без git tag); требует AMO токены
./scripts/release.sh --publish # с git tag + GitHub release
# Без подписи Firefox
./scripts/release.sh --skip-firefox-sign
```
Артефакты релиза (после `./scripts/release.sh`):
```
release/linux/verstak # GUI binary
release/linux/verstak-server # Server binary
release/linux/verstak.deb # DEB-пакет GUI
release/linux/verstak-server.deb # DEB-пакет сервера
release/linux/verstak.rpm # RPM-пакет GUI
release/linux/verstak-server.rpm # RPM-пакет сервера
release/firefox/verstak-firefox-VERSION.xpi # Signed XPI
release/firefox/updates.json # Firefox update manifest
```
## Структура проекта ## Структура проекта
``` ```

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.1.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,8 +19,8 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-C2sdkP-s.js"></script> <script type="module" crossorigin src="/assets/main-CMk8guXZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-Cjkp2F09.css"> <link rel="stylesheet" crossorigin href="/assets/main-BecQca8b.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -9,7 +9,11 @@
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "verstak-bridge@verstak.app", "id": "verstak-bridge@verstak.app",
"strict_min_version": "109.0" "strict_min_version": "115.0",
"update_url": "https://mirv.top/verstak/firefox/updates.json",
"data_collection_permissions": {
"required": ["none"]
}
} }
}, },

4125
extension-firefox/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{
"name": "verstak-bridge-firefox",
"version": "1.0.0",
"private": true,
"description": "Verstak Bridge Firefox extension",
"scripts": {
"lint:firefox": "web-ext lint --source-dir .",
"build:firefox": "web-ext build --source-dir . --artifacts-dir ../web-ext-artifacts --overwrite-dest",
"sign:firefox": "../scripts/sign-firefox-xpi.sh",
"release:firefox": "../scripts/release-firefox-xpi.sh"
},
"devDependencies": {
"web-ext": "^8.0.0"
}
}

View File

@ -2523,6 +2523,14 @@
showSettings = false showSettings = false
} }
async function refreshSystemViews() {
try {
systemViews = await wailsCall('ListSystemViewsWithPlugins') || []
} catch (e) {
// Fallback: keep current views
}
}
function syncResultMessage(result) { function syncResultMessage(result) {
const conflicts = Array.isArray(result?.conflicts) ? result.conflicts : [] const conflicts = Array.isArray(result?.conflicts) ? result.conflicts : []
const applyErrors = Array.isArray(result?.applyErrors) ? result.applyErrors : [] const applyErrors = Array.isArray(result?.applyErrors) ? result.applyErrors : []
@ -3873,7 +3881,7 @@
{/if} {/if}
{#if showSettings} {#if showSettings}
<SettingsWindow onClose={closeSettings} onSyncRefresh={loadSyncStatus} initialSection={settingsInitialSection} /> <SettingsWindow onClose={closeSettings} onSyncRefresh={loadSyncStatus} onPluginToggle={refreshSystemViews} initialSection={settingsInitialSection} />
{/if} {/if}
</main> </main>
</div> </div>

View File

@ -211,7 +211,7 @@
{#if loading} {#if loading}
<p class="loading">Загрузка…</p> <p class="loading">Загрузка…</p>
{:else if error} {:else if error}
<p class="error">{error}</p> <p class="error">{error}</p>
{:else if htmlPanel} {:else if htmlPanel}
<iframe <iframe
bind:this={iframeEl} bind:this={iframeEl}

View File

@ -2,6 +2,8 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { t } from './i18n' import { t } from './i18n'
export let onPluginToggle = null
let plugins = [] let plugins = []
let loading = true let loading = true
let error = '' let error = ''
@ -29,7 +31,9 @@
const newState = !p.active const newState = !p.active
try { try {
await wailsCall('SetPluginEnabled', p.name, newState) await wailsCall('SetPluginEnabled', p.name, newState)
p.active = newState // Force refresh from backend — ensures UI stays in sync
plugins = await wailsCall('ListPlugins') || []
if (onPluginToggle) onPluginToggle()
} catch (e) { } catch (e) {
error = String(e) error = String(e)
} }
@ -42,6 +46,7 @@
p.installed = true p.installed = true
// Refresh list to reflect new state // Refresh list to reflect new state
plugins = await wailsCall('ListPlugins') || [] plugins = await wailsCall('ListPlugins') || []
if (onPluginToggle) onPluginToggle()
} catch (e) { } catch (e) {
error = String(e) error = String(e)
} }
@ -55,6 +60,7 @@
p.installed = false p.installed = false
p.active = false p.active = false
plugins = await wailsCall('ListPlugins') || [] plugins = await wailsCall('ListPlugins') || []
if (onPluginToggle) onPluginToggle()
} catch (e) { } catch (e) {
error = String(e) error = String(e)
} }

View File

@ -26,7 +26,7 @@
<button <button
class="settings-nav-item" class="settings-nav-item"
class:active={activeSection === sec.id} class:active={activeSection === sec.id}
class:disabled={sec.id === 'plugins' || sec.id === 'files' || sec.id === 'activity' || sec.id === 'backup'} class:disabled={sec.id === 'files' || sec.id === 'activity' || sec.id === 'backup'}
on:click={() => select(sec.id)} on:click={() => select(sec.id)}
> >
<span class="settings-nav-icon"> <span class="settings-nav-icon">
@ -101,6 +101,17 @@
background: var(--accent-bg, rgba(99, 102, 241, 0.15)); background: var(--accent-bg, rgba(99, 102, 241, 0.15));
color: var(--accent, #818cf8); color: var(--accent, #818cf8);
font-weight: 600; font-weight: 600;
position: relative;
}
.settings-nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 4px;
bottom: 4px;
width: 3px;
background: var(--accent, #818cf8);
border-radius: 0 2px 2px 0;
} }
.settings-nav-item.disabled { .settings-nav-item.disabled {
opacity: 0.4; opacity: 0.4;

View File

@ -13,6 +13,7 @@
export let onClose = null export let onClose = null
export let onSyncRefresh = null export let onSyncRefresh = null
export let onPluginToggle = null
export let initialSection = 'general' export let initialSection = 'general'
let activeSection = initialSection let activeSection = initialSection
@ -82,7 +83,7 @@
{:else if activeSection === 'templates'} {:else if activeSection === 'templates'}
<SettingsTemplates onRefresh={loadConfig} /> <SettingsTemplates onRefresh={loadConfig} />
{:else if activeSection === 'plugins'} {:else if activeSection === 'plugins'}
<SettingsPlugins /> <SettingsPlugins {onPluginToggle} />
{:else if activeSection === 'files'} {:else if activeSection === 'files'}
<SettingsFiles /> <SettingsFiles />
{:else if activeSection === 'activity'} {:else if activeSection === 'activity'}

View File

@ -110,10 +110,16 @@ func NewManager(vaultRoot string) *Manager {
// Active/Enabled are always false after Discover — call SyncConfig + InitRuntimes to activate. // Active/Enabled are always false after Discover — call SyncConfig + InitRuntimes to activate.
func (m *Manager) Discover() { func (m *Manager) Discover() {
pluginsDir := filepath.Join(m.vaultRoot, ".verstak", "plugins") pluginsDir := filepath.Join(m.vaultRoot, ".verstak", "plugins")
// Ensure plugins directory exists
if err := os.MkdirAll(pluginsDir, 0o750); err != nil {
log.Printf("[plugins] failed to create plugins dir: %v", err)
m.plugins = nil
return
}
entries, err := os.ReadDir(pluginsDir) entries, err := os.ReadDir(pluginsDir)
if err != nil { if err != nil {
m.plugins = nil m.plugins = nil
return // no plugins dir — OK return
} }
newPlugins := make([]Plugin, 0, len(entries)) newPlugins := make([]Plugin, 0, len(entries))

View File

@ -3,7 +3,7 @@ set -e
# ============================================================ # ============================================================
# Verstak build script # Verstak build script
# Usage: ./scripts/build.sh [gui|server|extensions|all] # Usage: ./scripts/build.sh [gui|server|extensions|all|release]
# ============================================================ # ============================================================
BUILD_DIR="build" BUILD_DIR="build"
@ -23,17 +23,17 @@ build_gui() {
# Build Go binary with Wails v2 # Build Go binary with Wails v2
# Tags: webkit2_41 required for WebKitGTK 2.41+, desktop/production for Wails # Tags: webkit2_41 required for WebKitGTK 2.41+, desktop/production for Wails
go build -tags "webkit2_41 desktop production" -ldflags="-s -w" -o "$BUILD_DIR/verstak-gui-linux-amd64" ./cmd/verstak-gui/ go build -tags "webkit2_41 desktop production" -ldflags="-s -w" -o "$BUILD_DIR/verstak" ./cmd/verstak-gui/
echo "==> GUI binary: $BUILD_DIR/verstak-gui-linux-amd64" echo "==> GUI binary: $BUILD_DIR/verstak"
} }
# --- Server binary --- # --- Server binary ---
build_server() { build_server() {
echo "==> Building server binary..." echo "==> Building server binary..."
go build -ldflags="-s -w" -o "$BUILD_DIR/verstak-server-linux-amd64" ./cmd/verstak-server/ go build -ldflags="-s -w" -o "$BUILD_DIR/verstak-server" ./cmd/verstak-server/
echo "==> Server binary: $BUILD_DIR/verstak-server-linux-amd64" echo "==> Server binary: $BUILD_DIR/verstak-server"
} }
# --- Chrome extension --- # --- Chrome extension ---
@ -62,21 +62,19 @@ build_chrome_extension() {
echo "==> Chrome extension: $out_file" echo "==> Chrome extension: $out_file"
} }
# --- Firefox extension --- # --- Firefox extension (unsigned) ---
build_firefox_extension() { build_firefox_extension() {
echo "==> Building Firefox extension..." echo "==> Building Firefox extension..."
local ext_dir="extension-firefox" local ext_dir="extension-firefox"
local out_file="$BUILD_DIR/verstak-bridge-firefox.xpi" local out_file="$BUILD_DIR/verstak-bridge-firefox-unsigned.zip"
if [ ! -d "$ext_dir" ]; then if [ ! -d "$ext_dir" ]; then
echo "ERROR: $ext_dir/ directory not found" echo "ERROR: $ext_dir/ directory not found"
return 1 return 1
fi fi
# Firefox xpi is a zip with .xpi extension
# Must include manifest.json at root + icons
cd "$ext_dir" cd "$ext_dir"
zip -r "../$out_file" \ zip -r "../$out_file" \
manifest.json \ manifest.json \
@ -86,7 +84,7 @@ build_firefox_extension() {
-x "*/.DS_Store" "*/Thumbs.db" "*/__MACOSX/*" "*/.git/*" -x "*/.DS_Store" "*/Thumbs.db" "*/__MACOSX/*" "*/.git/*"
cd .. cd ..
echo "==> Firefox extension: $out_file" echo "==> Firefox extension (unsigned): $out_file"
} }
# --- All extensions --- # --- All extensions ---
@ -112,6 +110,38 @@ build_all() {
ls -lh "$BUILD_DIR/" ls -lh "$BUILD_DIR/"
} }
# --- Release: copy to release/linux/ ---
prepare_release() {
local RELEASE_DIR="release/linux"
mkdir -p "$RELEASE_DIR"
if [ -f "$BUILD_DIR/verstak" ]; then
cp "$BUILD_DIR/verstak" "$RELEASE_DIR/verstak"
chmod 755 "$RELEASE_DIR/verstak"
echo " $RELEASE_DIR/verstak"
else
echo " WARNING: $BUILD_DIR/verstak not found, skipping"
fi
if [ -f "$BUILD_DIR/verstak-server" ]; then
cp "$BUILD_DIR/verstak-server" "$RELEASE_DIR/verstak-server"
chmod 755 "$RELEASE_DIR/verstak-server"
echo " $RELEASE_DIR/verstak-server"
else
echo " WARNING: $BUILD_DIR/verstak-server not found, skipping"
fi
}
build_release() {
echo "==> Building all + preparing release/linux/..."
build_all
prepare_release
echo ""
echo "==> Release binaries in release/linux/"
ls -lh "release/linux/"
}
# --- Main --- # --- Main ---
case "${1:-all}" in case "${1:-all}" in
@ -120,9 +150,10 @@ case "${1:-all}" in
extensions) build_extensions ;; extensions) build_extensions ;;
chrome) mkdir -p "$BUILD_DIR"; build_chrome_extension ;; chrome) mkdir -p "$BUILD_DIR"; build_chrome_extension ;;
firefox) mkdir -p "$BUILD_DIR"; build_firefox_extension ;; firefox) mkdir -p "$BUILD_DIR"; build_firefox_extension ;;
release) build_release ;;
all) build_all ;; all) build_all ;;
*) *)
echo "Usage: $0 [gui|server|extensions|chrome|firefox|all]" echo "Usage: $0 [gui|server|extensions|chrome|firefox|release|all]"
exit 1 exit 1
;; ;;
esac esac

85
scripts/release-firefox-xpi.sh Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env bash
# release-firefox-xpi.sh
# Builds a signed Firefox XPI, copies it to release/firefox/ with a clean name,
# and generates updates.json for self-distributed updates.
# Run from repo root.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [[ -f ".env" ]]; then
set -a
source ".env"
set +a
fi
SOURCE_DIR="${WEB_EXT_SOURCE_DIR:-extension-firefox}"
ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}"
RELEASE_DIR="release/firefox"
UPDATE_BASE_URL="${VERSTAK_FIREFOX_UPDATE_BASE_URL:-https://mirv.top/verstak/firefox}"
# Step 1: sign the XPI
./scripts/sign-firefox-xpi.sh
# Step 2: read version and addon ID from manifest
if [[ ! -f "$SOURCE_DIR/manifest.json" ]]; then
echo "ERROR: $SOURCE_DIR/manifest.json not found" >&2
exit 1
fi
VERSION="$(node -e "console.log(require('./${SOURCE_DIR}/manifest.json').version)")"
if [[ -z "$VERSION" ]]; then
echo "ERROR: could not read version from $SOURCE_DIR/manifest.json" >&2
exit 1
fi
ADDON_ID="$(node -e "console.log(require('./${SOURCE_DIR}/manifest.json').browser_specific_settings.gecko.id)")"
if [[ -z "$ADDON_ID" ]]; then
echo "ERROR: could not read browser_specific_settings.gecko.id from $SOURCE_DIR/manifest.json" >&2
exit 1
fi
echo "Extension version: $VERSION"
echo "Addon ID: $ADDON_ID"
# Step 3: locate the signed XPI
SIGNED_XPI="$(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' | sort | tail -n 1)"
if [[ -z "$SIGNED_XPI" ]]; then
echo "ERROR: no signed XPI found in $ARTIFACTS_DIR" >&2
exit 1
fi
# Step 4: copy to release directory
mkdir -p "$RELEASE_DIR"
RELEASE_XPI="verstak-firefox-${VERSION}.xpi"
cp "$SIGNED_XPI" "$RELEASE_DIR/$RELEASE_XPI"
echo "Copied signed XPI to $RELEASE_DIR/$RELEASE_XPI"
# Step 5: generate updates.json
cat > "$RELEASE_DIR/updates.json" <<EOF
{
"addons": {
"${ADDON_ID}": {
"updates": [
{
"version": "${VERSION}",
"update_link": "${UPDATE_BASE_URL}/${RELEASE_XPI}"
}
]
}
}
}
EOF
echo "Generated $RELEASE_DIR/updates.json"
# Step 6: summary
echo ""
echo "=== Firefox release artifacts ==="
echo "$RELEASE_DIR/$RELEASE_XPI"
echo "$RELEASE_DIR/updates.json"
echo ""
echo "Upload to server:"
echo " ${UPDATE_BASE_URL}/${RELEASE_XPI}"
echo " ${UPDATE_BASE_URL}/updates.json"

294
scripts/release.sh Executable file
View File

@ -0,0 +1,294 @@
#!/usr/bin/env bash
# release.sh — Verstak release script
# Builds all binaries, packages, signs Firefox XPI, creates git tag.
#
# Usage:
# ./scripts/release.sh # Full release (requires AMO tokens for Firefox signing)
# ./scripts/release.sh --skip-firefox-sign # Skip Firefox XPI signing
# ./scripts/release.sh --publish # Push git tag + create GitHub release
# ./scripts/release.sh --help # Show usage
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
# --- Config ---
SKIP_FIREFOX_SIGN=false
PUBLISH=false
for arg in "$@"; do
case "$arg" in
--skip-firefox-sign) SKIP_FIREFOX_SIGN=true ;;
--publish) PUBLISH=true ;;
--help)
echo "Usage: $0 [--skip-firefox-sign] [--publish]"
echo ""
echo " --skip-firefox-sign Skip Firefox extension signing (fails by default if AMO tokens missing)"
echo " --publish Push git tag and create GitHub release"
exit 0
;;
esac
done
VERSION="$(cat VERSION 2>/dev/null || echo "0.1.0-dev")"
RELEASE_DIR="release"
LINUX_DIR="$RELEASE_DIR/linux"
BUILD_DIR="build"
echo "============================================"
echo " Verstak Release v$VERSION"
echo "============================================"
echo ""
# --- Pre-flight checks ---
echo "==> Pre-flight checks..."
if ! git diff --quiet --exit-code 2>/dev/null; then
echo "ERROR: working tree is dirty. Commit or stash changes first."
git diff --stat
exit 1
fi
if ! git diff --cached --quiet --exit-code 2>/dev/null; then
echo "ERROR: staged but uncommitted changes. Commit first."
exit 1
fi
echo " Working tree clean ✓"
if ! go version &>/dev/null; then
echo "ERROR: go not found"
exit 1
fi
echo " Go: $(go version)"
# --- Run tests ---
echo ""
echo "==> Running tests..."
go vet ./...
go test ./... -count=1
echo " Tests: all passed ✓"
# --- Build ---
echo ""
echo "==> Building all binaries..."
mkdir -p "$BUILD_DIR" "$LINUX_DIR"
rm -f "$LINUX_DIR"/*
./scripts/build.sh release
echo " Build complete ✓"
# --- VERSION file ---
echo "$VERSION" > "$LINUX_DIR/VERSION"
# --- DEB package: GUI ---
echo ""
echo "==> Building DEB packages..."
build_deb() {
local binary="$1"
local pkg_name="$2"
local description="$3"
local systemd_unit="${4:-}"
local deb_dir="$BUILD_DIR/deb-tmp/$pkg_name"
mkdir -p "$deb_dir/DEBIAN"
mkdir -p "$deb_dir/usr/local/bin"
mkdir -p "$deb_dir/usr/share/doc/$pkg_name"
cp "$BUILD_DIR/$binary" "$deb_dir/usr/local/bin/$pkg_name"
chmod 755 "$deb_dir/usr/local/bin/$pkg_name"
if [[ -n "$systemd_unit" && -f "$systemd_unit" ]]; then
mkdir -p "$deb_dir/lib/systemd/system"
cp "$systemd_unit" "$deb_dir/lib/systemd/system/"
fi
# Simple control file
cat > "$deb_dir/DEBIAN/control" <<CTRL
Package: $pkg_name
Version: $VERSION
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Verstak <verstak@mirv.top>
Description: $description
CTRL
# Copy README if exists
[[ -f "README.md" ]] && cp "README.md" "$deb_dir/usr/share/doc/$pkg_name/"
dpkg-deb --root-owner-group --build "$deb_dir" "$LINUX_DIR/${pkg_name}.deb" >/dev/null
rm -rf "$BUILD_DIR/deb-tmp"
echo " $LINUX_DIR/${pkg_name}.deb"
}
build_deb "verstak" "verstak" "Verstak — local-first working vault GUI"
build_deb "verstak-server" "verstak-server" "Verstak Sync Server" "cmd/verstak-server/verstak-server.service"
# --- RPM package: GUI ---
echo ""
echo "==> Building RPM packages..."
build_rpm() {
local binary="$1"
local pkg_name="$2"
local description="$3"
local systemd_unit="${4:-}"
local rpm_dir="$BUILD_DIR/rpm-tmp"
mkdir -p "$rpm_dir/RPMS/x86_64"
mkdir -p "$rpm_dir/BUILD"
mkdir -p "$rpm_dir/SOURCES"
# Build root for rpmbuild
local buildroot="$rpm_dir/buildroot"
mkdir -p "$buildroot/usr/local/bin"
mkdir -p "$buildroot/usr/share/doc/$pkg_name"
cp "$BUILD_DIR/$binary" "$buildroot/usr/local/bin/$pkg_name"
chmod 755 "$buildroot/usr/local/bin/$pkg_name"
if [[ -n "$systemd_unit" && -f "$systemd_unit" ]]; then
mkdir -p "$buildroot/lib/systemd/system"
cp "$systemd_unit" "$buildroot/lib/systemd/system/"
fi
[[ -f "README.md" ]] && cp "README.md" "$buildroot/usr/share/doc/$pkg_name/"
# File list
local file_list
file_list="%attr(755, root, root) /usr/local/bin/$pkg_name"
file_list+="%attr(644, root, root) /usr/share/doc/$pkg_name/*"
if [[ -n "$systemd_unit" && -f "$systemd_unit" ]]; then
# systemd file will be included via %files section
file_list+="%attr(644, root, root) /lib/systemd/system/$(basename "$systemd_unit")"
fi
local spec_file="$rpm_dir/SPECS/${pkg_name}.spec"
mkdir -p "$(dirname "$spec_file")"
cat > "$spec_file" <<SPEC
Summary: $description
Name: $pkg_name
Version: $VERSION
Release: 1
License: MIT
Group: Utilities
BuildArch: x86_64
AutoReqProv: no
Prefix: /usr
%description
$description
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/usr/local/bin
cp $BUILD_DIR/$binary %{buildroot}/usr/local/bin/$pkg_name
chmod 755 %{buildroot}/usr/local/bin/$pkg_name
SPEC
if [[ -n "$systemd_unit" && -f "$systemd_unit" ]]; then
cat >> "$spec_file" <<SPEC
mkdir -p %{buildroot}/lib/systemd/system
cp $systemd_unit %{buildroot}/lib/systemd/system/
SPEC
fi
cat >> "$spec_file" <<SPEC
mkdir -p %{buildroot}/usr/share/doc/$pkg_name
cp -r README.md %{buildroot}/usr/share/doc/$pkg_name/ 2>/dev/null || true
%files
/usr/local/bin/$pkg_name
SPEC
if [[ -n "$systemd_unit" && -f "$systemd_unit" ]]; then
echo "/lib/systemd/system/$(basename "$systemd_unit")" >> "$spec_file"
fi
cat >> "$spec_file" <<SPEC
%doc /usr/share/doc/$pkg_name/*
SPEC
rpmbuild --define "_topdir $rpm_dir" \
--define "_builddir $rpm_dir/BUILD" \
--define "_sourcedir $rpm_dir/SOURCES" \
--define "_rpmdir $rpm_dir/RPMS" \
--define "_specdir $rpm_dir/SPECS" \
--define "_buildrootdir $rpm_dir/buildroot" \
-bb "$spec_file" >/dev/null
cp "$rpm_dir/RPMS/x86_64/${pkg_name}-${VERSION}-1.x86_64.rpm" \
"$LINUX_DIR/${pkg_name}.rpm"
rm -rf "$rpm_dir"
echo " $LINUX_DIR/${pkg_name}.rpm"
}
build_rpm "verstak" "verstak" "Verstak — local-first working vault GUI"
build_rpm "verstak-server" "verstak-server" "Verstak Sync Server" "cmd/verstak-server/verstak-server.service"
# --- Firefox signed XPI ---
echo ""
if [[ "$SKIP_FIREFOX_SIGN" == "true" ]]; then
echo "==> Firefox extension signing skipped (--skip-firefox-sign)"
elif [[ -f ".env" ]] && grep -q "WEB_EXT_API_KEY" ".env" 2>/dev/null; then
echo "==> Signing Firefox extension..."
./scripts/release-firefox-xpi.sh
else
echo "ERROR: AMO tokens not found (WEB_EXT_API_KEY / WEB_EXT_API_SECRET in .env)." >&2
echo " Firefox signing is required for a full release." >&2
echo " To skip: run with --skip-firefox-sign" >&2
echo " To sign: copy .env.example -> .env and fill in AMO credentials from" >&2
echo " https://addons.mozilla.org/developers/" >&2
exit 1
fi
# --- Checksums ---
echo ""
echo "==> Generating checksums..."
cd "$LINUX_DIR"
sha256sum verstak verstak-server *.deb *.rpm 2>/dev/null > checksums.txt || true
cd "$ROOT_DIR"
echo " $LINUX_DIR/checksums.txt"
# --- Summary ---
echo ""
echo "============================================"
echo " Release v$VERSION artifacts"
echo "============================================"
echo ""
echo "Linux binaries:"
ls -lh "$LINUX_DIR/" 2>/dev/null
echo ""
if [[ -d "$RELEASE_DIR/firefox" ]]; then
echo "Firefox extension:"
ls -lh "$RELEASE_DIR/firefox/" 2>/dev/null
echo ""
fi
echo "============================================"
# --- Git tag + release ---
if [[ "$PUBLISH" == "true" ]]; then
echo ""
echo "==> Creating git tag v$VERSION..."
git tag -s "v$VERSION" -m "Verstak v$VERSION"
git push origin "v$VERSION"
echo "==> Creating GitHub release..."
gh release create "v$VERSION" \
--title "Verstak v$VERSION" \
--notes "See CHANGELOG.md for details" \
"$LINUX_DIR"/* \
$([[ -d "$RELEASE_DIR/firefox" ]] && echo "$RELEASE_DIR/firefox/"* || true)
echo " GitHub release created: https://github.com/$(git remote get-url origin | sed 's|.*github.com[:/]||;s|\\.git$||')/releases/tag/v$VERSION"
else
echo ""
echo "Dry-run: use --publish to create git tag + GitHub release."
echo "Run: git tag -s v$VERSION -m \"Verstak v$VERSION\""
fi
echo ""
echo "Done."

73
scripts/sign-firefox-xpi.sh Executable file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# sign-firefox-xpi.sh
# Signs the Firefox extension with AMO (Mozilla Add-ons).
# Run from repo root. Reads .env for AMO credentials.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [[ -f ".env" ]]; then
set -a
source ".env"
set +a
fi
SOURCE_DIR="${WEB_EXT_SOURCE_DIR:-extension-firefox}"
ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}"
CHANNEL="${WEB_EXT_CHANNEL:-unlisted}"
if [[ -z "${WEB_EXT_API_KEY:-}" ]]; then
echo "ERROR: WEB_EXT_API_KEY is not set. Put AMO JWT issuer into .env" >&2
exit 1
fi
if [[ -z "${WEB_EXT_API_SECRET:-}" ]]; then
echo "ERROR: WEB_EXT_API_SECRET is not set. Put AMO JWT secret into .env" >&2
exit 1
fi
if [[ "$CHANNEL" != "unlisted" ]]; then
echo "ERROR: only WEB_EXT_CHANNEL=unlisted is allowed for self-distributed Verstak Firefox builds" >&2
exit 1
fi
if [[ ! -f "$SOURCE_DIR/manifest.json" ]]; then
echo "ERROR: manifest.json not found in $SOURCE_DIR" >&2
exit 1
fi
mkdir -p "$ARTIFACTS_DIR"
echo "Linting Firefox extension..."
npx web-ext lint \
--source-dir "$SOURCE_DIR" \
--self-hosted
echo "Signing Firefox extension as unlisted/self-distributed XPI..."
SIGN_ARGS=(
sign
--source-dir "$SOURCE_DIR"
--artifacts-dir "$ARTIFACTS_DIR"
--channel "$CHANNEL"
--self-hosted
--api-key "$WEB_EXT_API_KEY"
--api-secret "$WEB_EXT_API_SECRET"
)
if [[ -n "${WEB_EXT_API_PROXY:-}" ]]; then
SIGN_ARGS+=(--api-proxy "$WEB_EXT_API_PROXY")
fi
npx web-ext "${SIGN_ARGS[@]}"
SIGNED_XPI="$(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' | sort | tail -n 1 || true)"
if [[ -z "$SIGNED_XPI" ]]; then
echo "ERROR: signed XPI was not created in $ARTIFACTS_DIR" >&2
exit 1
fi
echo "Signed XPI created:"
echo "$SIGNED_XPI"