Change release - don't publish the XPI but a readme instead
Build LinkLog Development Image / development-image (push) Failing after 1s
Release LinkLog / release (push) Failing after 1s

This commit is contained in:
Olaf
2026-08-27 08:31:56 +02:00
parent c27aad58ae
commit c11b25c20a
5 changed files with 129 additions and 62 deletions
+70 -29
View File
@@ -19,11 +19,10 @@ jobs:
- 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
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))")
if [ "${GITHUB_REF_NAME#v}" != "$backend_version" ]; then
echo "tag ${GITHUB_REF_NAME} does not match backend version $backend_version" >&2
exit 1
fi
@@ -40,25 +39,75 @@ jobs:
context: .
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.release.outputs.version }}
${{ env.IMAGE_NAME }}:${{ steps.release.outputs.backend_version }}
${{ env.IMAGE_NAME }}:latest
labels: |
org.opencontainers.image.version=${{ steps.release.outputs.version }}
org.opencontainers.image.version=${{ steps.release.outputs.backend_version }}
org.opencontainers.image.source=https://git.kolkman.org/olaf/Link-Log
- name: Generate release README
env:
BACKEND_VERSION: ${{ steps.release.outputs.backend_version }}
PLUGIN_VERSION: ${{ steps.release.outputs.plugin_version }}
SIGNED_XPI: ${{ steps.release.outputs.signed_xpi }}
run: |
cat > release-readme.md <<EOF
# LinkLog $BACKEND_VERSION
LinkLog is a Firefox extension and Python web service for saving links with a title, comment, timestamp, and tracking parameters removed. The service stores links in SQLite and can publish them through plugins, including Mastodon.
## Docker Container
The current backend/container version is $BACKEND_VERSION. Pull it from the Gitea container registry:
\`\`\`sh
docker pull $IMAGE_NAME:$BACKEND_VERSION
\`\`\`
The same image is also published as:
\`\`\`sh
docker pull $IMAGE_NAME:latest
\`\`\`
The developer version of the backend is always published as $IMAGE_NAME:latest, which may be ahead of the current release version and may be unstable.
Additional information about the backend can be found in the [README](https://git.kolkman.org/olaf/Link-Log/src/branch/main/backend/README.md).
## Firefox Extension
The current signed Firefox plugin version, compatible with this version of the backend, is $PLUGIN_VERSION. Download it from the raw repository artifact:
https://git.kolkman.org/olaf/Link-Log/raw/branch/main/$SIGNED_XPI
EOF
- name: Create Gitea release
id: gitea_release
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
VERSION: ${{ steps.release.outputs.version }}
VERSION: ${{ steps.release.outputs.backend_version }}
run: |
payload_file=$(mktemp)
python3 - <<'PY' > "$payload_file"
import json
import os
from pathlib import Path
version = os.environ['VERSION']
print(json.dumps({
'tag_name': f'v{version}',
'name': f'LinkLog {version}',
'body': Path('release-readme.md').read_text(),
'draft': False,
'prerelease': False,
}))
PY
response_file=$(mktemp)
response_status=$(curl --silent --show-error -o "$response_file" -w '%{http_code}' \
-X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"v$VERSION\",\"name\":\"LinkLog $VERSION\",\"draft\":false,\"prerelease\":false}" \
--data-binary "@$payload_file" \
https://git.kolkman.org/api/v1/repos/olaf/Link-Log/releases)
rm -f "$payload_file"
if [ "$response_status" = 409 ]; then
response_status=$(curl --silent --show-error -o "$response_file" -w '%{http_code}' \
-H "Authorization: token $RELEASE_TOKEN" \
@@ -76,35 +125,27 @@ jobs:
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
- name: Upload release README
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
UPLOAD_URL: ${{ steps.gitea_release.outputs.upload_url }}
VERSION: ${{ steps.release.outputs.version }}
run: |
response_status=$(curl --silent --show-error -o /tmp/linklog-xpi-upload-response -w '%{http_code}' \
response_status=$(curl --silent --show-error -o /tmp/linklog-readme-upload-response -w '%{http_code}' \
-X POST -H "Authorization: token $RELEASE_TOKEN" \
-H 'Content-Type: application/x-xpinstall' \
--data-binary "@XPI/signed/LinkLog-$VERSION.xpi" \
"$UPLOAD_URL?name=LinkLog-$VERSION.xpi")
-H 'Content-Type: text/markdown' \
--data-binary @release-readme.md \
"$UPLOAD_URL?name=README.md")
if [ "$response_status" -lt 200 ] || [ "$response_status" -ge 300 ]; then
cat /tmp/linklog-xpi-upload-response >&2
exit 1
fi
response_status=$(curl --silent --show-error -o /tmp/linklog-updates-upload-response -w '%{http_code}' \
-X POST -H "Authorization: token $RELEASE_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary @webextension/updates.json \
"$UPLOAD_URL?name=updates.json")
if [ "$response_status" -lt 200 ] || [ "$response_status" -ge 300 ]; then
cat /tmp/linklog-updates-upload-response >&2
cat /tmp/linklog-readme-upload-response >&2
exit 1
fi
- name: Publish release links
env:
VERSION: ${{ steps.release.outputs.version }}
BACKEND_VERSION: ${{ steps.release.outputs.backend_version }}
PLUGIN_VERSION: ${{ steps.release.outputs.plugin_version }}
SIGNED_XPI: ${{ steps.release.outputs.signed_xpi }}
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"
echo "Docker image: $IMAGE_NAME:$BACKEND_VERSION"
echo "Signed XPI: https://git.kolkman.org/olaf/Link-Log/raw/branch/main/$SIGNED_XPI (version $PLUGIN_VERSION)"
echo "Release README: README.md"
+3 -3
View File
@@ -110,11 +110,11 @@ This publishes `${APP_PORT:-8000}` and defaults the application URL to `http://l
## 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`.
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.
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 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 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 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 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.
+18
View File
@@ -1184,3 +1184,21 @@ Continue to document every prompt and chat in the VIBE directory.
### Assistant outcome
Recorded this prompt and its outcome in `VIBE/PROMPTS.md` and `VIBE/CHAT_LOG.md`. Future visible prompts and chat outcomes will continue to be appended there.
### User
The release action should work differently. Rather than publishing the signed XPI and `updates.json`, it should produce a README describing the project, the latest Docker container version, and the latest signed XPI download URL with version number.
### Assistant outcome
Changed the Gitea release workflow to generate `release-readme.md`, use it as the release body, and upload `README.md` as the release asset instead of publishing the signed XPI and `updates.json`. The README describes LinkLog, the Docker image pull location, and the raw signed XPI download URL.
### User
The tagged version will be the version of the backend. However, the version of the plugin is set manually, just use the most recent signed plugin version that lives in the signed repo.
### Assistant outcome
Decoupled backend and plugin release versions. The release tag and Docker image now use the backend `LINKLOG_VERSION`; the workflow discovers the newest semver signed artifact in `XPI/signed/`, validates its embedded manifest, and writes the plugin version and raw XPI URL into the generated release README. Updated release documentation and validated the script, workflow run blocks, and generated README output.
### User
Update the VIBE directory with what you have done.
### Assistant outcome
Appended the release workflow changes and this VIBE update request to `VIBE/PROMPTS.md` and `VIBE/CHAT_LOG.md` in the existing append-only format.
+3
View File
@@ -209,6 +209,9 @@
202. Check the modified pages for the correct versioned signed XPI link.
203. When posting to Mastodon, add an empty line between "From my #LinkLog:" and the title.
204. Fix the release workflow because the runner's curl does not support `--fail-with-body`.
205. The release action should work differently. Rather than publishing the signed XPI and `updates.json`, it should produce a README describing the project, the latest Docker container version, and the latest signed XPI download URL with version number.
206. The tagged version will be the version of the backend. However, the version of the plugin is set manually, just use the most recent signed plugin version that lives in the signed repo.
207. Update the VIBE directory with what you have done.
## Future entries
+35 -30
View File
@@ -9,61 +9,66 @@ 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'
VERSION_RE = re.compile(r'\d+\.\d+\.\d+')
def fail(message: str) -> None:
raise SystemExit(f'release validation failed: {message}')
def version_key(version: str) -> tuple[int, int, int]:
return tuple(int(part) for part in version.split('.'))
def find_latest_signed_xpi() -> tuple[str, Path]:
candidates = []
for xpi_path in SIGNED_DIR.glob('LinkLog-*.xpi'):
match = re.fullmatch(r'LinkLog-(\d+\.\d+\.\d+)\.xpi', xpi_path.name)
if match:
candidates.append((match.group(1), xpi_path))
if not candidates:
fail(f'no signed plugin artifacts found in {SIGNED_DIR.relative_to(ROOT)}')
return max(candidates, key=lambda candidate: version_key(candidate[0]))
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}')
gecko_settings = manifest.get('browser_specific_settings', {}).get('gecko', {})
data_permissions = gecko_settings.get('data_collection_permissions')
if data_permissions != {'required': ['websiteActivity'], 'optional': []}:
fail('Firefox data_collection_permissions must require websiteActivity and have no optional categories')
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}')
if not VERSION_RE.fullmatch(backend_version):
fail(f'backend version {backend_version} is not a valid three-part 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)}')
extension_version, xpi_path = find_latest_signed_xpi()
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')
fail('signed XPI manifest version does not match its filename')
gecko_settings = packaged_manifest.get('browser_specific_settings', {}).get('gecko', {})
data_permissions = gecko_settings.get('data_collection_permissions')
if data_permissions != {'required': ['websiteActivity'], 'optional': []}:
fail('Firefox data_collection_permissions must require websiteActivity and have no optional categories')
if archive.testzip() is not None:
fail('signed XPI contains a corrupt member')
updates = json.loads(UPDATES_PATH.read_text())
addon_id = gecko_settings['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}')
signed_xpi = xpi_path.relative_to(ROOT)
if len(sys.argv) == 3 and sys.argv[1] == '--github-output':
with Path(sys.argv[2]).open('a') as output:
print(f'backend_version={backend_version}', file=output)
print(f'plugin_version={extension_version}', file=output)
print(f'signed_xpi={signed_xpi}', file=output)
elif len(sys.argv) != 1:
fail('usage: validate_release.py [--github-output <path>]')
print(f'validated LinkLog release {frontend_version}')
print(f'xpi={xpi_path.relative_to(ROOT)}')
print(f'validated LinkLog backend release {backend_version}')
print(f'plugin_version={extension_version}')
print(f'signed_xpi={signed_xpi}')
if __name__ == '__main__':