Release workflow

This commit is contained in:
Olaf
2026-08-25 09:22:28 +02:00
parent 5d60bd801c
commit 08b0ee013f
7 changed files with 182 additions and 1 deletions
+90
View File
@@ -0,0 +1,90 @@
name: Release LinkLog
on:
push:
tags:
- 'v*'
workflow_dispatch:
env:
IMAGE_NAME: git.kolkman.org/olaf/link-log
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check out release tag
uses: actions/checkout@v4
- name: Validate versions and signed XPI
id: release
run: |
python3 scripts/release/validate_release.py
version=$(python3 -c "import json; print(json.load(open('frontend/version.json'))['version'])")
echo "version=$version" >> "$GITHUB_OUTPUT"
if [ "${GITHUB_REF_NAME#v}" != "$version" ]; then
echo "tag ${GITHUB_REF_NAME} does not match release version $version" >&2
exit 1
fi
- name: Log in to Gitea container registry
uses: docker/login-action@v3
with:
registry: git.kolkman.org
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and publish Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.release.outputs.version }}
${{ env.IMAGE_NAME }}:latest
labels: |
org.opencontainers.image.version=${{ steps.release.outputs.version }}
org.opencontainers.image.source=https://git.kolkman.org/olaf/Link-Log
- name: Create Gitea release
id: gitea_release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
VERSION: ${{ steps.release.outputs.version }}
run: |
response=$(curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"v$VERSION\",\"name\":\"LinkLog $VERSION\",\"draft\":false,\"prerelease\":false}" \
https://git.kolkman.org/api/v1/repos/olaf/Link-Log/releases)
release_id=$(printf '%s' "$response" | jq -r '.id')
test "$release_id" != null
test "$release_id" != 0
upload_url="https://git.kolkman.org/api/v1/repos/olaf/Link-Log/releases/$release_id/assets"
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
- name: Upload signed XPI and update manifest
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
UPLOAD_URL: ${{ steps.gitea_release.outputs.upload_url }}
VERSION: ${{ steps.release.outputs.version }}
run: |
curl --fail-with-body --silent --show-error \
-X POST -H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/x-xpinstall' \
--data-binary "@XPI/signed/LinkLog-$VERSION.xpi" \
"$UPLOAD_URL?name=LinkLog-$VERSION.xpi"
curl --fail-with-body --silent --show-error \
-X POST -H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary @webextension/updates.json \
"$UPLOAD_URL?name=updates.json"
- name: Publish release links
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
echo "Docker image: $IMAGE_NAME:$VERSION"
echo "Signed XPI: https://git.kolkman.org/olaf/Link-Log/releases/download/v$VERSION/LinkLog-$VERSION.xpi"
echo "Firefox update manifest: https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json"
+10
View File
@@ -104,6 +104,16 @@ make xpi
This creates `XPI/unsigned/LinkLog-0.1.0.xpi` from the `webextension/` package and excludes macOS metadata and minified artifacts. The version is read from `webextension/manifest.json`. The `XPI/signed/` directory is reserved for signed release bundles. This creates `XPI/unsigned/LinkLog-0.1.0.xpi` from the `webextension/` package and excludes macOS metadata and minified artifacts. The version is read from `webextension/manifest.json`. The `XPI/signed/` directory is reserved for signed release bundles.
## Releases
Releases run in Gitea Actions when a `v*` tag is pushed. The Docker release version comes from `frontend/version.json`; the Firefox plugin version comes from `webextension/manifest.json`. CI also requires both to match `LINKLOG_VERSION`'s default in `backend/app/core/config.py`.
The signed XPI is produced manually and must be checked into `XPI/signed/LinkLog-<version>.xpi` before creating the tag. The workflow validates the embedded manifest, publishes the XPI and `webextension/updates.json` as Gitea release assets, and publishes Docker images to `git.kolkman.org/olaf/link-log:<version>` and `:latest`.
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. The release page provides a direct install link at `https://git.kolkman.org/olaf/Link-Log/releases/download/v<version>/LinkLog-<version>.xpi`.
The workflow requires Gitea Actions secrets named `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `GITEA_TOKEN`. The token needs permission to create releases and upload release assets; the registry credentials need permission to push the image.
Appending a username to the root URL, such as `/alice`, opens that user's public feed and profile information. Appending a username to the root URL, such as `/alice`, opens that user's public feed and profile information.
Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user. Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
"version": "0.1.0"
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Validate the version and checked-in artifacts for a LinkLog release."""
import json
import re
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json'
FRONTEND_VERSION_PATH = ROOT / 'frontend' / 'version.json'
SETTINGS_PATH = ROOT / 'backend' / 'app' / 'core' / 'config.py'
UPDATES_PATH = ROOT / 'webextension' / 'updates.json'
SIGNED_DIR = ROOT / 'XPI' / 'signed'
def fail(message: str) -> None:
raise SystemExit(f'release validation failed: {message}')
def main() -> None:
manifest = json.loads(MANIFEST_PATH.read_text())
extension_version = manifest.get('version')
if not isinstance(extension_version, str) or not re.fullmatch(r'\d+\.\d+\.\d+', extension_version):
fail('webextension/manifest.json has no valid three-part version')
frontend_version = json.loads(FRONTEND_VERSION_PATH.read_text()).get('version')
if frontend_version != extension_version:
fail(f'frontend version {frontend_version} does not match extension version {extension_version}')
settings = SETTINGS_PATH.read_text()
match = re.search(r"version: str = os\.getenv\('LINKLOG_VERSION', '([^']+)'\)", settings)
if not match:
fail('backend version default could not be found')
backend_version = match.group(1)
if backend_version != extension_version:
fail(f'backend version {backend_version} does not match extension version {extension_version}')
xpi_path = SIGNED_DIR / f'LinkLog-{extension_version}.xpi'
if not xpi_path.is_file():
fail(f'missing manually signed artifact: {xpi_path.relative_to(ROOT)}')
with zipfile.ZipFile(xpi_path) as archive:
try:
packaged_manifest = json.loads(archive.read('manifest.json'))
except KeyError:
fail('signed XPI does not contain manifest.json')
if packaged_manifest.get('version') != extension_version:
fail('signed XPI manifest version does not match webextension/manifest.json')
if archive.testzip() is not None:
fail('signed XPI contains a corrupt member')
updates = json.loads(UPDATES_PATH.read_text())
addon_id = manifest['browser_specific_settings']['gecko']['id']
update_entries = updates.get('addons', {}).get(addon_id, {}).get('updates', [])
if not any(entry.get('version') == extension_version for entry in update_entries):
fail(f'webextension/updates.json has no update entry for {extension_version}')
print(f'validated LinkLog release {frontend_version}')
print(f'xpi={xpi_path.relative_to(ROOT)}')
if __name__ == '__main__':
main()
+2 -1
View File
@@ -26,7 +26,8 @@
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "linklog@kolkman.org", "id": "linklog@kolkman.org",
"strict_min_version": "109.0" "strict_min_version": "109.0",
"update_url": "https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json"
} }
}, },
"options_ui": { "options_ui": {
+12
View File
@@ -0,0 +1,12 @@
{
"addons": {
"linklog@kolkman.org": {
"updates": [
{
"version": "0.1.0",
"update_link": "https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-0.1.0.xpi"
}
]
}
}
}