Release workflow
This commit is contained in:
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -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()
|
||||
@@ -26,7 +26,8 @@
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user