feat: add initial implementation of eSodisce / GURS batch helper extension
Publish Firefox extension / publish (push) Successful in 4m21s

This commit is contained in:
komelt committed 2026-10-05 14:42:50 +02:00
1 parent 902cf5d813
commit f1a16ddec0
10 files changed
+769 -1

No files matched your search

+454
View File
@@ -0,0 +1,454 @@
name: Publish Firefox extension
on:
push:
branches:
- main
# The workflow's own update-manifest-only commit must not start another run.
paths-ignore:
- updates.json
permissions:
contents: write
concurrency:
group: publish-main
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
env:
API_BASE: https://git.vahta.net/api/v1
REPOSITORY: komelt/zemljiska-knjiga-vticnik
RUN_NUMBER: ${{ github.run_number }}
TARGET_COMMIT: ${{ github.sha }}
steps:
- name: Check out source
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
persist-credentials: true
- name: Validate and stage extension
shell: bash
run: |
set -Eeuo pipefail
command -v jq >/dev/null
command -v node >/dev/null
command -v curl >/dev/null
command -v npx >/dev/null
command -v sha256sum >/dev/null
jq -e . manifest.json >/dev/null
SOURCE_VERSION="$(jq -er '.version | strings' manifest.json)"
if [[ ! "$SOURCE_VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "manifest.json version must contain one to three numeric components" >&2
exit 1
fi
if [[ ! "$RUN_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "The Actions run number is missing or is not a positive integer" >&2
exit 1
fi
VERSION="${SOURCE_VERSION}.${RUN_NUMBER}"
if [[ ! "$VERSION" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; then
echo "Computed Firefox version is invalid: $VERSION" >&2
exit 1
fi
STAGING_DIR="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION"
SIGNED_XPI="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION.xpi"
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
RUNTIME_FILES=(manifest.json background.js content-script.js popup.html popup.css popup.js)
cp -- "${RUNTIME_FILES[@]}" "$STAGING_DIR/"
jq --arg version "$VERSION" '.version = $version' \
"$STAGING_DIR/manifest.json" > "$STAGING_DIR/manifest.json.tmp"
mv -- "$STAGING_DIR/manifest.json.tmp" "$STAGING_DIR/manifest.json"
jq -e . "$STAGING_DIR/manifest.json" >/dev/null
for javascript_file in background.js content-script.js popup.js; do
node --check "$STAGING_DIR/$javascript_file"
done
printf 'VERSION=%s\nSTAGING_DIR=%s\nSIGNED_XPI=%s\n' \
"$VERSION" "$STAGING_DIR" "$SIGNED_XPI" >> "$GITHUB_ENV"
- name: Reuse an existing Gitea release asset when available
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -Eeuo pipefail
ASSET_NAME="zemljiska-knjiga-vticnik-$VERSION.xpi"
RELEASE_RESPONSE="$RUNNER_TEMP/gitea-release-$VERSION.json"
RELEASE_URL="$API_BASE/repos/$REPOSITORY/releases/tags/v$VERSION"
if ! RELEASE_STATUS="$(curl --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--write-out '%{http_code}' --output "$RELEASE_RESPONSE" \
"$RELEASE_URL")"; then
echo 'Could not query the Gitea release.' >&2
exit 1
fi
case "$RELEASE_STATUS" in
404)
printf 'RELEASE_ID=\nRELEASE_ASSET_EXISTS=false\n' >> "$GITHUB_ENV"
exit 0
;;
2??)
;;
*)
echo "Gitea release lookup returned HTTP $RELEASE_STATUS" >&2
cat "$RELEASE_RESPONSE" >&2
exit 1
;;
esac
RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")"
matching_assets="$(jq -c --arg name "$ASSET_NAME" \
'[.assets[]? | select(.name == $name)]' "$RELEASE_RESPONSE")"
asset_count="$(jq -er 'length' <<< "$matching_assets")"
if [[ "$asset_count" -gt 1 ]]; then
echo "Gitea release contains multiple assets named $ASSET_NAME" >&2
exit 1
fi
if [[ "$asset_count" -eq 0 ]]; then
printf 'RELEASE_ID=%s\nRELEASE_ASSET_EXISTS=false\n' "$RELEASE_ID" >> "$GITHUB_ENV"
exit 0
fi
asset_size="$(jq -r '.[0].size // empty' <<< "$matching_assets")"
asset_url="$(jq -r '.[0].browser_download_url // empty' <<< "$matching_assets")"
if [[ ! "$asset_url" =~ ^https://[^[:space:]]+$ ]]; then
echo 'The existing release asset has a missing or invalid HTTPS browser download URL.' >&2
exit 1
fi
rm -f -- "$SIGNED_XPI"
curl --fail --silent --show-error --location \
--header "Authorization: token $GITEA_TOKEN" \
--output "$SIGNED_XPI" \
"$asset_url"
[[ -s "$SIGNED_XPI" ]] || {
echo 'The existing release asset downloaded as an empty file.' >&2
exit 1
}
if [[ -n "$asset_size" && "$(wc -c < "$SIGNED_XPI")" != "$asset_size" ]]; then
echo 'The downloaded release asset size does not match Gitea metadata.' >&2
exit 1
fi
XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')"
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the existing release asset.' >&2
exit 1
}
expected_hash="$(jq -r --arg version "$VERSION" \
'[.addons[]?.updates[]? | select(.version == $version) | .update_hash] | unique | if length == 1 then .[0] else "" end' \
updates.json 2>/dev/null || true)"
if [[ -n "$expected_hash" && "$expected_hash" != "sha256:$XPI_SHA256" ]]; then
echo 'The existing release asset does not match the update manifest hash.' >&2
exit 1
fi
printf 'RELEASE_ID=%s\nRELEASE_ASSET_EXISTS=true\nXPI_SHA256=%s\n' \
"$RELEASE_ID" "$XPI_SHA256" >> "$GITHUB_ENV"
- name: Sign unlisted build through AMO
if: env.RELEASE_ASSET_EXISTS != 'true'
shell: bash
env:
AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }}
AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }}
run: |
set -Eeuo pipefail
ARTIFACTS_DIR="$RUNNER_TEMP/web-ext-artifacts-$VERSION"
rm -rf "$ARTIFACTS_DIR" "$SIGNED_XPI"
mkdir -p "$ARTIFACTS_DIR"
npx --yes web-ext@10.7.0 sign \
--channel=unlisted \
--source-dir "$STAGING_DIR" \
--artifacts-dir "$ARTIFACTS_DIR" \
--api-key "$AMO_JWT_ISSUER" \
--api-secret "$AMO_JWT_SECRET"
mapfile -t signed_files < <(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' -print)
if [[ "${#signed_files[@]}" -ne 1 ]]; then
echo "Expected exactly one signed XPI, found ${#signed_files[@]}" >&2
exit 1
fi
cp -- "${signed_files[0]}" "$SIGNED_XPI"
XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')"
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the signed XPI.' >&2
exit 1
}
printf 'XPI_SHA256=%s\n' "$XPI_SHA256" >> "$GITHUB_ENV"
- name: Create public Gitea release and upload XPI
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -Eeuo pipefail
ASSET_NAME="zemljiska-knjiga-vticnik-$VERSION.xpi"
RELEASE_RESPONSE="$RUNNER_TEMP/gitea-release-$VERSION.json"
download_asset() {
local asset_url="$1"
local asset_size="$2"
if [[ ! "$asset_url" =~ ^https://[^[:space:]]+$ ]]; then
echo 'The existing release asset has a missing or invalid HTTPS browser download URL.' >&2
exit 1
fi
rm -f -- "$SIGNED_XPI"
curl --fail --silent --show-error --location \
--header "Authorization: token $GITEA_TOKEN" \
--output "$SIGNED_XPI" \
"$asset_url"
[[ -s "$SIGNED_XPI" ]] || {
echo 'The existing release asset downloaded as an empty file.' >&2
exit 1
}
if [[ -n "$asset_size" && "$(wc -c < "$SIGNED_XPI")" != "$asset_size" ]]; then
echo 'The downloaded release asset size does not match Gitea metadata.' >&2
exit 1
fi
XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')"
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the existing release asset.' >&2
exit 1
}
local expected_hash
expected_hash="$(jq -r --arg version "$VERSION" \
'[.addons[]?.updates[]? | select(.version == $version) | .update_hash] | unique | if length == 1 then .[0] else "" end' \
updates.json 2>/dev/null || true)"
if [[ -n "$expected_hash" && "$expected_hash" != "sha256:$XPI_SHA256" ]]; then
echo 'The existing release asset does not match the update manifest hash.' >&2
exit 1
fi
RELEASE_ASSET_EXISTS=true
}
inspect_assets() {
local asset_count asset_url asset_size
asset_count="$(jq -er --arg name "$ASSET_NAME" \
'[.assets[]? | select(.name == $name)] | length' "$RELEASE_RESPONSE")"
if [[ "$asset_count" -gt 1 ]]; then
echo "Gitea release contains multiple assets named $ASSET_NAME" >&2
exit 1
fi
if [[ "$asset_count" -eq 1 ]]; then
asset_size="$(jq -r --arg name "$ASSET_NAME" \
'.assets[] | select(.name == $name) | .size // empty' "$RELEASE_RESPONSE")"
asset_url="$(jq -r --arg name "$ASSET_NAME" \
'.assets[] | select(.name == $name) | .browser_download_url // empty' "$RELEASE_RESPONSE")"
download_asset "$asset_url" "$asset_size"
fi
}
fetch_release_by_tag() {
local status
if ! status="$(curl --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--write-out '%{http_code}' --output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/tags/v$VERSION")"; then
echo 'Could not query the Gitea release after a concurrent change.' >&2
exit 1
fi
[[ "$status" == 2?? ]] || {
echo "Gitea release lookup returned HTTP $status" >&2
cat "$RELEASE_RESPONSE" >&2
exit 1
}
RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")"
}
if [[ -z "${RELEASE_ID:-}" ]]; then
RELEASE_PAYLOAD="$(jq -n \
--arg tag "v$VERSION" \
--arg name "zemljiska-knjiga-vticnik $VERSION" \
--arg target "$TARGET_COMMIT" \
'{tag_name: $tag, name: $name, target_commitish: $target, draft: false, prerelease: false}')"
if ! CREATE_STATUS="$(curl --silent --show-error \
--request POST \
--header "Authorization: token $GITEA_TOKEN" \
--header 'Content-Type: application/json' \
--data "$RELEASE_PAYLOAD" \
--write-out '%{http_code}' --output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases")"; then
echo 'Could not create the Gitea release.' >&2
exit 1
fi
case "$CREATE_STATUS" in
2??)
RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")"
;;
409|422)
fetch_release_by_tag
;;
*)
echo "Gitea release creation returned HTTP $CREATE_STATUS" >&2
cat "$RELEASE_RESPONSE" >&2
exit 1
;;
esac
fi
if [[ "${RELEASE_ASSET_EXISTS:-false}" != 'true' ]]; then
curl --fail --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID"
inspect_assets
fi
if [[ "${RELEASE_ASSET_EXISTS:-false}" != 'true' ]]; then
UPLOAD_RESPONSE="$RUNNER_TEMP/gitea-upload-$VERSION.json"
if ! UPLOAD_STATUS="$(curl --silent --show-error \
--request POST \
--header "Authorization: token $GITEA_TOKEN" \
--header 'Content-Type: application/x-xpinstall' \
--data-binary "@$SIGNED_XPI" \
--write-out '%{http_code}' --output "$UPLOAD_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID/assets?name=$ASSET_NAME")"; then
echo 'Could not upload the signed XPI to the Gitea release.' >&2
exit 1
fi
case "$UPLOAD_STATUS" in
2??)
;;
409|422)
curl --fail --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID"
inspect_assets
[[ "${RELEASE_ASSET_EXISTS:-false}" == 'true' ]] || {
echo 'Gitea rejected the upload but the expected asset is still absent.' >&2
exit 1
}
;;
*)
echo "Gitea asset upload returned HTTP $UPLOAD_STATUS" >&2
cat "$UPLOAD_RESPONSE" >&2
exit 1
;;
esac
fi
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the signed XPI.' >&2
exit 1
}
UPDATE_LINK="https://git.vahta.net/komelt/zemljiska-knjiga-vticnik/releases/download/v$VERSION/$ASSET_NAME"
UPDATE_MANIFEST="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION-updates.json"
jq -n \
--arg id 'eprostor-gurs-batch@example.invalid' \
--arg version "$VERSION" \
--arg update_link "$UPDATE_LINK" \
--arg update_hash "sha256:$XPI_SHA256" \
'{addons: {($id): {updates: [{version: $version, update_link: $update_link, update_hash: $update_hash}]}}}' \
> "$UPDATE_MANIFEST"
jq -e . "$UPDATE_MANIFEST" >/dev/null
printf 'XPI_SHA256=%s\nUPDATE_MANIFEST=%s\n' "$XPI_SHA256" "$UPDATE_MANIFEST" >> "$GITHUB_ENV"
- name: Commit update manifest
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -Eeuo pipefail
export LC_ALL=C
normalize_component() {
local component="$1"
component="${component#${component%%[!0]*}}"
printf '%s\n' "${component:-0}"
}
compare_firefox_versions() {
local left="$1"
local right="$2"
local left_part right_part left_normalized right_normalized
local max_parts i
local -a left_parts right_parts
IFS='.' read -r -a left_parts <<< "$left"
IFS='.' read -r -a right_parts <<< "$right"
max_parts="${#left_parts[@]}"
[[ "${#right_parts[@]}" -gt "$max_parts" ]] && max_parts="${#right_parts[@]}"
for ((i = 0; i < max_parts; i += 1)); do
left_part="${left_parts[i]:-0}"
right_part="${right_parts[i]:-0}"
left_normalized="$(normalize_component "$left_part")"
right_normalized="$(normalize_component "$right_part")"
if [[ "${#left_normalized}" -lt "${#right_normalized}" ]]; then
printf '%s\n' -1
return
fi
if [[ "${#left_normalized}" -gt "${#right_normalized}" ]]; then
printf '%s\n' 1
return
fi
if [[ "$left_normalized" < "$right_normalized" ]]; then
printf '%s\n' -1
return
fi
if [[ "$left_normalized" > "$right_normalized" ]]; then
printf '%s\n' 1
return
fi
done
printf '%s\n' 0
}
compare_current_manifest_versions() {
local current_version comparison
[[ -f updates.json ]] || return 0
jq -e . updates.json >/dev/null
while IFS= read -r current_version; do
[[ -z "$current_version" ]] && continue
if [[ ! "$current_version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then
echo "Current updates.json contains an invalid Firefox version: $current_version" >&2
exit 1
fi
comparison="$(compare_firefox_versions "$VERSION" "$current_version")"
if [[ "$comparison" != '1' ]]; then
echo "Current update version $current_version is newer than or equal to $VERSION; skipping manifest commit."
return 1
fi
done < <(jq -er '.addons[]?.updates[]?.version // empty' updates.json)
return 0
}
for attempt in 1 2 3; do
git fetch --prune origin main
git reset --hard origin/main
if ! compare_current_manifest_versions; then
exit 0
fi
cp -- "$UPDATE_MANIFEST" updates.json
jq -e . updates.json >/dev/null
git config user.name 'gitea-actions[bot]'
git config user.email 'gitea-actions[bot]@users.noreply.git.vahta.net'
git add -- updates.json
if git diff --cached --quiet; then
echo 'updates.json is already current; nothing to commit.'
exit 0
fi
git commit --only updates.json -m "chore: publish Firefox update $VERSION"
if git push origin HEAD:main; then
exit 0
fi
echo "The main branch advanced during manifest publication; retrying ($attempt/3)." >&2
git reset --hard origin/main
sleep "$((attempt * 2))"
done
echo 'Could not publish updates.json after three bounded retries.' >&2
exit 1