Versioning consistency and bump both the XPI and backend to version 0.2.0
This commit is contained in:
@@ -20,7 +20,7 @@ jobs:
|
|||||||
id: release
|
id: release
|
||||||
run: |
|
run: |
|
||||||
python3 scripts/release/validate_release.py --github-output "$GITHUB_OUTPUT"
|
python3 scripts/release/validate_release.py --github-output "$GITHUB_OUTPUT"
|
||||||
backend_version=$(python3 -c "import re; text=open('backend/app/core/config.py').read(); print(re.search(r\"version: str = os\\.getenv\\('LINKLOG_VERSION', '([^']+)'\\)\", text).group(1))")
|
backend_version=$(python3 -c "import json; print(json.load(open('frontend/version.json'))['version'])")
|
||||||
if [ "${GITHUB_REF_NAME#v}" != "$backend_version" ]; then
|
if [ "${GITHUB_REF_NAME#v}" != "$backend_version" ]; then
|
||||||
echo "tag ${GITHUB_REF_NAME} does not match backend version $backend_version" >&2
|
echo "tag ${GITHUB_REF_NAME} does not match backend version $backend_version" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
* Added a "Re-fetch Title" button to manually re-scrape the title after editing the URL
|
* Added a "Re-fetch Title" button to manually re-scrape the title after editing the URL
|
||||||
* New Entry duplicate detection and submission strip known tracking parameters (utm_*, gclid, fbclid, etc.) from URLs, mirroring the browser extension
|
* New Entry duplicate detection and submission strip known tracking parameters (utm_*, gclid, fbclid, etc.) from URLs, mirroring the browser extension
|
||||||
* Browser extension popup now matches the New Entry page: it warns on duplicate title/URL, notes when the stored link has a different URL, and gained a "Re-fetch title" button; bumped extension version to 0.2.0
|
* Browser extension popup now matches the New Entry page: it warns on duplicate title/URL, notes when the stored link has a different URL, and gained a "Re-fetch title" button; bumped extension version to 0.2.0
|
||||||
|
* Generate Firefox update metadata from every signed XPI with verified SHA-256 archive hashes
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fixed element ID conflict on the Admin page so Available Themes load correctly
|
* Fixed element ID conflict on the Admin page so Available Themes load correctly
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ XPI_OUTPUT := $(XPI_UNSIGNED_DIR)/$(XPI_FILE)
|
|||||||
EXTENSION_FILES := manifest.json logo.svg icon-16.png icon-32.png icon-48.png icon-96.png options.css options.html options.js popup.css popup.html popup.js l10n.js _locales/en-US/messages.json _locales/es/messages.json _locales/de/messages.json _locales/fr/messages.json _locales/nl/messages.json
|
EXTENSION_FILES := manifest.json logo.svg icon-16.png icon-32.png icon-48.png icon-96.png options.css options.html options.js popup.css popup.html popup.js l10n.js _locales/en-US/messages.json _locales/es/messages.json _locales/de/messages.json _locales/fr/messages.json _locales/nl/messages.json
|
||||||
EXTENSION_SOURCES := $(addprefix webextension/,$(EXTENSION_FILES))
|
EXTENSION_SOURCES := $(addprefix webextension/,$(EXTENSION_FILES))
|
||||||
XPI_VALIDATOR := scripts/release/validate_xpi.py
|
XPI_VALIDATOR := scripts/release/validate_xpi.py
|
||||||
|
UPDATES_GENERATOR := scripts/release/generate_updates.py
|
||||||
|
|
||||||
.PHONY: all logos xpi check-tools clean-generated
|
.PHONY: all logos xpi update-updates check-tools clean-generated
|
||||||
|
|
||||||
all: logos
|
all: logos
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ logos: check-tools $(GENERATED_LOGOS)
|
|||||||
|
|
||||||
xpi: $(XPI_OUTPUT)
|
xpi: $(XPI_OUTPUT)
|
||||||
|
|
||||||
|
update-updates: $(UPDATES_GENERATOR) webextension/manifest.json
|
||||||
|
@python3 $(UPDATES_GENERATOR)
|
||||||
|
|
||||||
$(XPI_OUTPUT): $(EXTENSION_SOURCES) $(GENERATED_LOGOS) $(XPI_VALIDATOR)
|
$(XPI_OUTPUT): $(EXTENSION_SOURCES) $(GENERATED_LOGOS) $(XPI_VALIDATOR)
|
||||||
@test -n "$(EXTENSION_VERSION)" || { echo "Error: extension version is missing from webextension/manifest.json" >&2; exit 1; }
|
@test -n "$(EXTENSION_VERSION)" || { echo "Error: extension version is missing from webextension/manifest.json" >&2; exit 1; }
|
||||||
@mkdir -p $(XPI_UNSIGNED_DIR) $(XPI_SIGNED_DIR)
|
@mkdir -p $(XPI_UNSIGNED_DIR) $(XPI_SIGNED_DIR)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ For local development:
|
|||||||
|
|
||||||
The backend currently uses FastAPI, uvicorn, SQLite, and Pydantic. `httpx2` is included for the Starlette-compatible test client.
|
The backend currently uses FastAPI, uvicorn, SQLite, and Pydantic. `httpx2` is included for the Starlette-compatible test client.
|
||||||
Jinja2 is included for server-rendered HTML templates.
|
Jinja2 is included for server-rendered HTML templates.
|
||||||
The backend version is `0.1.1` and is exposed through the FastAPI/OpenAPI metadata. It can be overridden with `LINKLOG_VERSION`.
|
The backend version is `0.2.0` and is exposed through the FastAPI/OpenAPI metadata. It is read from `frontend/version.json`, the single source of truth shared by the backend and frontend.
|
||||||
LinkLog is licensed under the GNU General Public License, version 3 or any later version. See [LICENSE](LICENSE).
|
LinkLog is licensed under the GNU General Public License, version 3 or any later version. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
## Local Installation
|
## Local Installation
|
||||||
@@ -110,11 +110,11 @@ This publishes `${APP_PORT:-8000}` and defaults the application URL to `http://l
|
|||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
Releases run in Gitea Actions when a `v*` tag is pushed. The Docker release version comes from `LINKLOG_VERSION`'s default in `backend/app/core/config.py`; the tag must match that backend version. The Firefox plugin version is independent and comes from the most recent signed `XPI/signed/LinkLog-<version>.xpi` checked into the repository.
|
Releases run in Gitea Actions when a `v*` tag is pushed. The Docker release version comes from `frontend/version.json`; the tag must match that version. The Firefox plugin version is independent and comes from the most recent signed `XPI/signed/LinkLog-<version>.xpi` checked into the repository.
|
||||||
|
|
||||||
The signed XPI is produced manually and should be checked into `XPI/signed/LinkLog-<version>.xpi`. The workflow validates the latest signed XPI's embedded manifest, publishes Docker images to `git.kolkman.org/olaf/link-log:<backend-version>` and `:latest`, and creates a release README that describes the project, the current backend/container version, and the raw signed XPI download URL with the plugin version.
|
The signed XPI is produced manually and should be checked into `XPI/signed/LinkLog-<version>.xpi`. Run `make update-updates` after adding a signed XPI to regenerate `webextension/updates.json` from every valid signed release artifact. The workflow validates the latest signed XPI's embedded manifest, publishes Docker images to `git.kolkman.org/olaf/link-log:<backend-version>` and `:latest`, and creates a release README that describes the project, the current backend/container version, and the raw signed XPI download URL with the plugin version.
|
||||||
|
|
||||||
The extension's `update_url` points at the stable raw repository URL `https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json`. Update `webextension/updates.json` with each signed XPI version and commit it together with the XPI. Release READMEs point to the raw signed XPI at `https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-<version>.xpi`.
|
The extension's `update_url` points at the stable raw repository URL `https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json`. Commit the regenerated `webextension/updates.json` together with each signed XPI. Release READMEs point to the raw signed XPI at `https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-<version>.xpi`.
|
||||||
|
|
||||||
The workflow requires Gitea Actions secrets named `REGISTRY_USERNAME`, `REGISTRY_TOKEN`, and `RELEASE_TOKEN`. `REGISTRY_TOKEN` is a Gitea access token with permission to push packages; `RELEASE_TOKEN` needs permission to create releases and upload release assets.
|
The workflow requires Gitea Actions secrets named `REGISTRY_USERNAME`, `REGISTRY_TOKEN`, and `RELEASE_TOKEN`. `REGISTRY_TOKEN` is a Gitea access token with permission to push packages; `RELEASE_TOKEN` needs permission to create releases and upload release assets.
|
||||||
|
|
||||||
|
|||||||
@@ -1466,3 +1466,27 @@ Make sure the webplugin has the same functionality as just implemented in the ne
|
|||||||
|
|
||||||
### Assistant outcome
|
### Assistant outcome
|
||||||
Brought `webextension/popup.js` to parity with `new-entry.js`: the duplicate check now distinguishes an exact title/URL match from a title match with a differing stored URL (using the same `/api/links/check` `url_matches`/`stored_url` fields) and shows an additional localized note in the latter case; added a "Re-fetch title" button that calls the backend `/api/scrape` endpoint (guarded by existing backend-permission checks) and auto-fills the title on URL blur when empty; reworked `setStatus` to render real `<br>` line breaks via DOM APIs instead of `textContent`. Added `duplicateLinkDifferentUrl`, `refetchTitle`, and `titleFetchFailed` strings to all five locale files, updated `popup.html` with the new button, and bumped `manifest.json` to version `0.2.0`. Full backend test suite (36 tests) passed.
|
Brought `webextension/popup.js` to parity with `new-entry.js`: the duplicate check now distinguishes an exact title/URL match from a title match with a differing stored URL (using the same `/api/links/check` `url_matches`/`stored_url` fields) and shows an additional localized note in the latter case; added a "Re-fetch title" button that calls the backend `/api/scrape` endpoint (guarded by existing backend-permission checks) and auto-fills the title on URL blur when empty; reworked `setStatus` to render real `<br>` line breaks via DOM APIs instead of `textContent`. Added `duplicateLinkDifferentUrl`, `refetchTitle`, and `titleFetchFailed` strings to all five locale files, updated `popup.html` with the new button, and bumped `manifest.json` to version `0.2.0`. Full backend test suite (36 tests) passed.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Add a feature to the Make file that updates the updates.json file based on available signed releases.
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Added `make update-updates`, which regenerates `webextension/updates.json` from valid versioned signed XPIs. The generator reads each packaged manifest, validates its version, add-on ID, and Firefox minimum version, and ignores non-release artifacts.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Add sha hashes to the updates.json
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Added Firefox-compatible `sha256:<hex>` update hashes generated from each signed XPI's bytes. Release validation now rejects missing or mismatched hashes; the generated metadata and release validator passed for signed versions 0.2.0 and 0.1.0.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Update changelog and VIBE
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Recorded the signed-XPI update metadata automation and SHA-256 integrity hashes in the v0.2.0 changelog and VIBE logs.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Make sure that when the plugin is signed the version in the link it the about page is updated too
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Extended `make update-updates` to set the About page plugin download URL to the newest valid signed XPI. Release validation now also rejects an About page whose link does not match the latest signed artifact; verified with LinkLog 0.2.0.
|
||||||
|
|||||||
@@ -263,6 +263,10 @@
|
|||||||
241. In the check duplicate I have a `<br>` tag, but that shows as formatted text on the page, I want a real break to occur at that position.
|
241. In the check duplicate I have a `<br>` tag, but that shows as formatted text on the page, I want a real break to occur at that position.
|
||||||
242. Update VIBE and Changelog.
|
242. Update VIBE and Changelog.
|
||||||
243. Make sure the webplugin has the same functionality as just implemented in the new-entry page. Increase its version number to 0.2.0.
|
243. Make sure the webplugin has the same functionality as just implemented in the new-entry page. Increase its version number to 0.2.0.
|
||||||
|
244. Add a feature to the Make file that updates the updates.json file based on available signed releases.
|
||||||
|
245. Add sha hashes to the updates.json
|
||||||
|
246. Update changelog and VIBE
|
||||||
|
247. Make sure that when the plugin is signed the version in the link it the about page is updated too
|
||||||
|
|
||||||
## Future entries
|
## Future entries
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,8 @@
|
|||||||
## Copyright © 2026 Olaf Kolkman
|
## Copyright © 2026 Olaf Kolkman
|
||||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ from cryptography.fernet import Fernet
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
DB_PATH = BASE_DIR / 'data' / 'linklog.db'
|
DB_PATH = BASE_DIR / 'data' / 'linklog.db'
|
||||||
|
VERSION_FILE = BASE_DIR.parent / 'frontend' / 'version.json'
|
||||||
|
|
||||||
|
|
||||||
def normalize_public_url(value: str) -> str:
|
def normalize_public_url(value: str) -> str:
|
||||||
@@ -20,11 +22,19 @@ def normalize_public_url(value: str) -> str:
|
|||||||
return f'{scheme}://{value}'
|
return f'{scheme}://{value}'
|
||||||
|
|
||||||
|
|
||||||
|
def load_version() -> str:
|
||||||
|
# frontend/version.json is the single source of truth for the app version, shared by backend and frontend.
|
||||||
|
try:
|
||||||
|
return json.loads(VERSION_FILE.read_text())['version']
|
||||||
|
except (OSError, KeyError, ValueError):
|
||||||
|
return '0.0.0'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Settings:
|
class Settings:
|
||||||
app_env: str = os.getenv('APP_ENV', 'development').lower()
|
app_env: str = os.getenv('APP_ENV', 'development').lower()
|
||||||
app_name: str = os.getenv('LINKLOG_APP_NAME', 'LinkLog')
|
app_name: str = os.getenv('LINKLOG_APP_NAME', 'LinkLog')
|
||||||
version: str = os.getenv('LINKLOG_VERSION', '0.1.1')
|
version: str = field(default_factory=load_version)
|
||||||
database_url: str = os.getenv('LINKLOG_DATABASE_URL', f'sqlite:///{DB_PATH}')
|
database_url: str = os.getenv('LINKLOG_DATABASE_URL', f'sqlite:///{DB_PATH}')
|
||||||
secret_key: str = os.getenv('LINKLOG_SECRET_KEY', 'dev-secret-key-change-me')
|
secret_key: str = os.getenv('LINKLOG_SECRET_KEY', 'dev-secret-key-change-me')
|
||||||
data_encryption_key: str = os.getenv('LINKLOG_DATA_ENCRYPTION_KEY', '')
|
data_encryption_key: str = os.getenv('LINKLOG_DATA_ENCRYPTION_KEY', '')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
import threading
|
import threading
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
@@ -11,6 +12,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from backend.app.main import app
|
from backend.app.main import app
|
||||||
|
from backend.app.core.config import settings
|
||||||
from backend.app.database import get_connection, hash_password
|
from backend.app.database import get_connection, hash_password
|
||||||
from backend.app.services.email_service import get_smtp_settings
|
from backend.app.services.email_service import get_smtp_settings
|
||||||
from backend.app.services.login_throttle import clear_login_failures
|
from backend.app.services.login_throttle import clear_login_failures
|
||||||
@@ -31,7 +33,7 @@ def login_headers(username='alice'):
|
|||||||
|
|
||||||
|
|
||||||
def test_login_returns_token():
|
def test_login_returns_token():
|
||||||
assert app.version == '0.1.1'
|
assert app.version == settings.version
|
||||||
response = client.post('/api/auth/login', json={
|
response = client.post('/api/auth/login', json={
|
||||||
'email': 'alice@example.com',
|
'email': 'alice@example.com',
|
||||||
'password': 'secret123',
|
'password': 'secret123',
|
||||||
@@ -806,7 +808,9 @@ def test_public_and_admin_pages_render_html():
|
|||||||
assert about_page.status_code == 200
|
assert about_page.status_code == 200
|
||||||
assert 'Save the good stuff' in about_page.text
|
assert 'Save the good stuff' in about_page.text
|
||||||
assert '<h2>Plugin</h2>' in about_page.text
|
assert '<h2>Plugin</h2>' in about_page.text
|
||||||
assert 'https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.1.0.xpi' in about_page.text
|
updates = json.loads((Path(__file__).resolve().parents[2] / 'webextension' / 'updates.json').read_text())
|
||||||
|
latest_update = updates['addons']['linklog@kolkman.org']['updates'][0]
|
||||||
|
assert latest_update['update_link'] in about_page.text
|
||||||
assert 'id="auth-about-link" href="/about"' in about_page.text
|
assert 'id="auth-about-link" href="/about"' in about_page.text
|
||||||
assert client.get('/admin').status_code == 200
|
assert client.get('/admin').status_code == 200
|
||||||
admin_page = client.get('/admin').text
|
admin_page = client.get('/admin').text
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: git.kolkman.org/olaf/link-log:development # or :latest or a version-tag
|
image: git.kolkman.org/olaf/link-log:${LINKLOG_VERSION:-latest} # or :development or a :version-tag
|
||||||
container_name: ${APP_CONTAINER_NAME:-linklog-app}
|
container_name: ${APP_CONTAINER_NAME:-linklog-app}
|
||||||
volumes:
|
volumes:
|
||||||
- ./linklog_data:/app/backend/data
|
- ./linklog_data:/app/backend/data
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<section class="link-item">
|
<section class="link-item">
|
||||||
<h2>Plugin</h2>
|
<h2>Plugin</h2>
|
||||||
<p>Install the Firefox plugin to save links directly from your browser. <a
|
<p>Install the Firefox plugin to save links directly from your browser. <a
|
||||||
href="https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.1.0.xpi"
|
href="https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.2.0.xpi"
|
||||||
download>Download and install the Plugin</a>.</p>
|
download>Download and install the Plugin</a>.</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1.1"
|
"version": "0.2.0"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Firefox update metadata from LinkLog signed XPI artifacts."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SIGNED_DIR = ROOT / 'XPI' / 'signed'
|
||||||
|
MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json'
|
||||||
|
UPDATES_PATH = ROOT / 'webextension' / 'updates.json'
|
||||||
|
ABOUT_TEMPLATE_PATH = ROOT / 'frontend' / 'templates' / 'about.html'
|
||||||
|
RAW_BASE_URL = 'https://git.kolkman.org/olaf/Link-Log/raw/branch/main'
|
||||||
|
XPI_NAME_RE = re.compile(r'LinkLog-(\d+\.\d+\.\d+)\.xpi')
|
||||||
|
ABOUT_XPI_URL_RE = re.compile(rf'{re.escape(RAW_BASE_URL)}/XPI/signed/LinkLog-\d+\.\d+\.\d+\.xpi')
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message: str) -> None:
|
||||||
|
raise SystemExit(f'update metadata generation failed: {message}')
|
||||||
|
|
||||||
|
|
||||||
|
def version_key(version: str) -> tuple[int, int, int]:
|
||||||
|
return tuple(int(part) for part in version.split('.'))
|
||||||
|
|
||||||
|
|
||||||
|
def read_packaged_manifest(xpi_path: Path) -> dict:
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(xpi_path) as archive:
|
||||||
|
if archive.testzip() is not None:
|
||||||
|
fail(f'{xpi_path.relative_to(ROOT)} contains a corrupt member')
|
||||||
|
return json.loads(archive.read('manifest.json'))
|
||||||
|
except (OSError, KeyError, json.JSONDecodeError, zipfile.BadZipFile) as error:
|
||||||
|
fail(f'could not read {xpi_path.relative_to(ROOT)}: {error}')
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_digest(xpi_path: Path) -> str:
|
||||||
|
with xpi_path.open('rb') as xpi_file:
|
||||||
|
return hashlib.file_digest(xpi_file, 'sha256').hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def update_about_plugin_link(xpi_path: Path) -> None:
|
||||||
|
about_template = ABOUT_TEMPLATE_PATH.read_text()
|
||||||
|
latest_xpi_url = f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}'
|
||||||
|
updated_template, replacements = ABOUT_XPI_URL_RE.subn(latest_xpi_url, about_template)
|
||||||
|
if replacements != 1:
|
||||||
|
fail(f'expected one signed XPI link in {ABOUT_TEMPLATE_PATH.relative_to(ROOT)}; found {replacements}')
|
||||||
|
ABOUT_TEMPLATE_PATH.write_text(updated_template)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
source_manifest = json.loads(MANIFEST_PATH.read_text())
|
||||||
|
addon_id = source_manifest.get('browser_specific_settings', {}).get('gecko', {}).get('id')
|
||||||
|
if not addon_id:
|
||||||
|
fail('webextension/manifest.json is missing browser_specific_settings.gecko.id')
|
||||||
|
|
||||||
|
releases = []
|
||||||
|
for xpi_path in SIGNED_DIR.glob('LinkLog-*.xpi'):
|
||||||
|
match = XPI_NAME_RE.fullmatch(xpi_path.name)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
version = match.group(1)
|
||||||
|
manifest = read_packaged_manifest(xpi_path)
|
||||||
|
if manifest.get('version') != version:
|
||||||
|
fail(f'{xpi_path.relative_to(ROOT)} manifest version does not match its filename')
|
||||||
|
gecko = manifest.get('browser_specific_settings', {}).get('gecko', {})
|
||||||
|
if gecko.get('id') != addon_id:
|
||||||
|
fail(f'{xpi_path.relative_to(ROOT)} add-on id does not match webextension/manifest.json')
|
||||||
|
strict_min_version = gecko.get('strict_min_version')
|
||||||
|
if not strict_min_version:
|
||||||
|
fail(f'{xpi_path.relative_to(ROOT)} is missing browser_specific_settings.gecko.strict_min_version')
|
||||||
|
releases.append((version, xpi_path, strict_min_version, sha256_digest(xpi_path)))
|
||||||
|
|
||||||
|
if not releases:
|
||||||
|
fail(f'no signed LinkLog release artifacts found in {SIGNED_DIR.relative_to(ROOT)}')
|
||||||
|
|
||||||
|
releases.sort(key=lambda release: version_key(release[0]), reverse=True)
|
||||||
|
updates = [
|
||||||
|
{
|
||||||
|
'version': version,
|
||||||
|
'update_link': f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}',
|
||||||
|
'update_hash': f'sha256:{digest}',
|
||||||
|
'applications': {
|
||||||
|
'gecko': {
|
||||||
|
'strict_min_version': strict_min_version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for version, xpi_path, strict_min_version, digest in releases
|
||||||
|
]
|
||||||
|
UPDATES_PATH.write_text(json.dumps({'addons': {addon_id: {'updates': updates}}}, indent=2) + '\n')
|
||||||
|
update_about_plugin_link(releases[0][1])
|
||||||
|
print(f'updated {UPDATES_PATH.relative_to(ROOT)} with {len(updates)} signed release(s)')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Validate the version and checked-in artifacts for a LinkLog release."""
|
"""Validate the version and checked-in artifacts for a LinkLog release."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -9,9 +10,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
SETTINGS_PATH = ROOT / 'backend' / 'app' / 'core' / 'config.py'
|
VERSION_FILE = ROOT / 'frontend' / 'version.json'
|
||||||
SIGNED_DIR = ROOT / 'XPI' / 'signed'
|
SIGNED_DIR = ROOT / 'XPI' / 'signed'
|
||||||
MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json'
|
MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json'
|
||||||
|
UPDATES_PATH = ROOT / 'webextension' / 'updates.json'
|
||||||
|
ABOUT_TEMPLATE_PATH = ROOT / 'frontend' / 'templates' / 'about.html'
|
||||||
|
RAW_BASE_URL = 'https://git.kolkman.org/olaf/Link-Log/raw/branch/main'
|
||||||
VERSION_RE = re.compile(r'\d+\.\d+\.\d+')
|
VERSION_RE = re.compile(r'\d+\.\d+\.\d+')
|
||||||
|
|
||||||
|
|
||||||
@@ -34,12 +38,62 @@ def find_latest_signed_xpi() -> tuple[str, Path]:
|
|||||||
return max(candidates, key=lambda candidate: version_key(candidate[0]))
|
return max(candidates, key=lambda candidate: version_key(candidate[0]))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_self_update(source_manifest: dict, extension_version: str, xpi_path: Path) -> None:
|
||||||
|
gecko_settings = source_manifest.get('browser_specific_settings', {}).get('gecko', {})
|
||||||
|
addon_id = gecko_settings.get('id')
|
||||||
|
strict_min_version = gecko_settings.get('strict_min_version')
|
||||||
|
if not addon_id:
|
||||||
|
fail('webextension/manifest.json is missing browser_specific_settings.gecko.id')
|
||||||
|
|
||||||
|
updates_data = json.loads(UPDATES_PATH.read_text())
|
||||||
|
addon_entry = updates_data.get('addons', {}).get(addon_id)
|
||||||
|
if not addon_entry:
|
||||||
|
fail(f'{UPDATES_PATH.relative_to(ROOT)} has no entry for add-on id {addon_id!r}')
|
||||||
|
|
||||||
|
entry = next((u for u in addon_entry.get('updates', []) if u.get('version') == extension_version), None)
|
||||||
|
if entry is None:
|
||||||
|
fail(
|
||||||
|
f'{UPDATES_PATH.relative_to(ROOT)} has no update entry for version {extension_version!r}; '
|
||||||
|
'add one alongside the signed XPI so the self-update mechanism can find it'
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_link = f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}'
|
||||||
|
if entry.get('update_link') != expected_link:
|
||||||
|
fail(
|
||||||
|
f'{UPDATES_PATH.relative_to(ROOT)} update_link {entry.get("update_link")!r} does not match '
|
||||||
|
f'the expected raw signed XPI URL {expected_link!r}'
|
||||||
|
)
|
||||||
|
|
||||||
|
with xpi_path.open('rb') as xpi_file:
|
||||||
|
expected_hash = f'sha256:{hashlib.file_digest(xpi_file, "sha256").hexdigest()}'
|
||||||
|
if entry.get('update_hash') != expected_hash:
|
||||||
|
fail(
|
||||||
|
f'{UPDATES_PATH.relative_to(ROOT)} update_hash {entry.get("update_hash")!r} does not match '
|
||||||
|
f'the SHA-256 hash of {xpi_path.relative_to(ROOT)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
entry_min_version = entry.get('applications', {}).get('gecko', {}).get('strict_min_version')
|
||||||
|
if entry_min_version != strict_min_version:
|
||||||
|
fail(
|
||||||
|
f'{UPDATES_PATH.relative_to(ROOT)} applications.gecko.strict_min_version {entry_min_version!r} '
|
||||||
|
f'does not match webextension/manifest.json strict_min_version {strict_min_version!r}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_about_plugin_link(xpi_path: Path) -> None:
|
||||||
|
expected_link = f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}'
|
||||||
|
if expected_link not in ABOUT_TEMPLATE_PATH.read_text():
|
||||||
|
fail(
|
||||||
|
f'{ABOUT_TEMPLATE_PATH.relative_to(ROOT)} does not link to the latest signed XPI '
|
||||||
|
f'{xpi_path.relative_to(ROOT)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
settings = SETTINGS_PATH.read_text()
|
version_data = json.loads(VERSION_FILE.read_text())
|
||||||
match = re.search(r"version: str = os\.getenv\('LINKLOG_VERSION', '([^']+)'\)", settings)
|
backend_version = version_data.get('version')
|
||||||
if not match:
|
if not backend_version:
|
||||||
fail('backend version default could not be found')
|
fail(f'version could not be found in {VERSION_FILE.relative_to(ROOT)}')
|
||||||
backend_version = match.group(1)
|
|
||||||
if not VERSION_RE.fullmatch(backend_version):
|
if not VERSION_RE.fullmatch(backend_version):
|
||||||
fail(f'backend version {backend_version} is not a valid three-part version')
|
fail(f'backend version {backend_version} is not a valid three-part version')
|
||||||
|
|
||||||
@@ -64,6 +118,9 @@ def main() -> None:
|
|||||||
if archive.testzip() is not None:
|
if archive.testzip() is not None:
|
||||||
fail('signed XPI contains a corrupt member')
|
fail('signed XPI contains a corrupt member')
|
||||||
|
|
||||||
|
validate_self_update(source_manifest, extension_version, xpi_path)
|
||||||
|
validate_about_plugin_link(xpi_path)
|
||||||
|
|
||||||
signed_xpi = xpi_path.relative_to(ROOT)
|
signed_xpi = xpi_path.relative_to(ROOT)
|
||||||
if len(sys.argv) == 3 and sys.argv[1] == '--github-output':
|
if len(sys.argv) == 3 and sys.argv[1] == '--github-output':
|
||||||
with Path(sys.argv[2]).open('a') as output:
|
with Path(sys.argv[2]).open('a') as output:
|
||||||
|
|||||||
@@ -2,11 +2,27 @@
|
|||||||
"addons": {
|
"addons": {
|
||||||
"linklog@kolkman.org": {
|
"linklog@kolkman.org": {
|
||||||
"updates": [
|
"updates": [
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"update_link": "https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.2.0.xpi",
|
||||||
|
"update_hash": "sha256:86619ec9aa35345c5c2bcad8fa5701762b8bd237ad9457b131404ff68bc2ce6b",
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"strict_min_version": "142.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"update_link": "https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.1.0.xpi"
|
"update_link": "https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.1.0.xpi",
|
||||||
|
"update_hash": "sha256:d95e23339facfa2a499bb35f9130d41739f622a3c0ac197ac3fd8cb76d6d6110",
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"strict_min_version": "142.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user