Compare commits

...

5 Commits

3 changed files with 106 additions and 28 deletions
+42 -28
View File
@@ -68,17 +68,11 @@ jobs:
echo "No release named ext-$VERSION; will sign via AMO and upload" echo "No release named ext-$VERSION; will sign via AMO and upload"
fi fi
- name: Download cached signed XPI (cache hit) # No "download cached XPI in sign-extension" step: build-web
if: steps.cache.outputs.cached == 'true' # fetches directly from the Forgejo ext-<version> release asset
env: # (removed 2026-05-26 alongside the actions/upload-artifact
TOKEN: ${{ secrets.RELEASE_TOKEN }} # removal — sign-extension's job is just to ensure the cache
run: | # exists on Forgejo; the build-web side reads it independently).
set -eu
mkdir -p extension/web-ext-artifacts
curl -sL -H "Authorization: token $TOKEN" \
-o "extension/web-ext-artifacts/fabledcurator-${{ steps.extver.outputs.version }}.xpi" \
"https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator/releases/assets/${{ steps.cache.outputs.asset_id }}"
ls -la extension/web-ext-artifacts/
- name: Sign via AMO (cache miss) - name: Sign via AMO (cache miss)
if: steps.cache.outputs.cached != 'true' if: steps.cache.outputs.cached != 'true'
@@ -148,12 +142,11 @@ jobs:
trap - EXIT trap - EXIT
echo "Uploaded fabledcurator-$VERSION.xpi to ext-$VERSION release" echo "Uploaded fabledcurator-$VERSION.xpi to ext-$VERSION release"
- name: Upload XPI as Actions artifact (handoff to build-web) # No actions/upload-artifact step: Forgejo Actions (and our
uses: actions/upload-artifact@v4 # act_runner) doesn't support upload-artifact@v4+ (GHES limitation
with: # surfaced 2026-05-26). Instead build-web reads the signed XPI
name: signed-extension # straight from the ext-<version> Forgejo release we just uploaded
path: extension/web-ext-artifacts/fabledcurator-*.xpi # to. Same source of truth; no double-store.
retention-days: 1
build-web: build-web:
needs: [sign-extension] needs: [sign-extension]
@@ -165,22 +158,43 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Download signed extension (main only) - name: Download signed XPI from Forgejo release asset (main only)
if: github.ref == 'refs/heads/main'
uses: actions/download-artifact@v4
with:
name: signed-extension
path: extension/web-ext-artifacts/
- name: Place signed XPI in build context (main only)
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: | run: |
set -eux set -eux
VERSION=$(grep -E '"version"' extension/package.json | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') VERSION=$(grep -E '"version"' extension/package.json | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
XPI=$(ls extension/web-ext-artifacts/*.xpi | head -1) # Look up the ext-<version> release; extract the .xpi asset's
# browser_download_url (Forgejo's /releases/assets/<id> endpoint
# returns ASSET METADATA, not the binary blob — operator-flagged
# 2026-05-26: my prior code curl'd the metadata endpoint without
# -f and wrote the resulting 404-page-not-found text into
# fabledcurator-*.xpi, which Firefox then rejected as "corrupt").
# browser_download_url is the canonical binary endpoint and is
# also publicly accessible (no token needed) but we pass the
# token anyway for symmetry with private-repo support.
curl -sf -H "Authorization: token $TOKEN" \
"https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator/releases/tags/ext-$VERSION" \
-o release.json
DOWNLOAD_URL=$(python3 -c "import json; r=json.load(open('release.json')); xpis=[a for a in r.get('assets', []) if a.get('name','').endswith('.xpi')]; print(xpis[0]['browser_download_url'])")
test -n "$DOWNLOAD_URL"
echo "Downloading XPI from: $DOWNLOAD_URL"
mkdir -p frontend/public/extension mkdir -p frontend/public/extension
cp "$XPI" "frontend/public/extension/fabledcurator-$VERSION.xpi" DEST="frontend/public/extension/fabledcurator-$VERSION.xpi"
cp "$XPI" "frontend/public/extension/fabledcurator-latest.xpi" # -f = fail on HTTP error (prevents silent corruption like the
# 2026-05-26 incident); -L = follow redirects.
curl -sfL -H "Authorization: token $TOKEN" -o "$DEST" "$DOWNLOAD_URL"
# Sanity check: the binary should start with the ZIP magic (PK\x03\x04).
# If it's anything else, the next docker build will ship a corrupt XPI.
MAGIC=$(head -c 2 "$DEST" | od -An -c | tr -d ' \n')
if [ "$MAGIC" != "PK" ]; then
echo "ERROR: downloaded XPI does not start with ZIP magic 'PK' (got '$MAGIC')"
echo "File contents preview:"
head -c 200 "$DEST"
exit 1
fi
cp "$DEST" "frontend/public/extension/fabledcurator-latest.xpi"
ls -la frontend/public/extension/ ls -la frontend/public/extension/
- name: Determine tag - name: Determine tag
+19
View File
@@ -4,6 +4,7 @@ No per-platform branching: a small common key set with fallbacks; the
full JSON is kept in raw so anything unmapped is recoverable later. full JSON is kept in raw so anything unmapped is recoverable later.
""" """
import re
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
@@ -21,10 +22,28 @@ class SidecarData:
raw: dict raw: dict
# gallery-dl prefixes media filenames with `NN_` for in-post ordering
# (`01_HOLLOW-ICHIGO.png`, `02_HOLOW ICHIGO.zip`) but writes the sidecar
# under the attachment's stem WITHOUT that ordering prefix
# (`HOLLOW-ICHIGO.json`). Strip the prefix when looking for sidecars.
# Confirmed against real Patreon downloads 2026-05-26 — without this
# strip, every gallery-dl post-level sidecar was invisible to FC since
# FC-3 shipped (24 deep-refresh calls produced 0 Posts in operator's DB).
_NUMBERING_PREFIX = re.compile(r"^\d+_(.+)$")
def find_sidecar(media: Path) -> Path | None: def find_sidecar(media: Path) -> Path | None:
# Attachment-level sidecars (image.jpg.json, image.json).
for cand in (media.with_suffix(".json"), Path(str(media) + ".json")): for cand in (media.with_suffix(".json"), Path(str(media) + ".json")):
if cand.is_file(): if cand.is_file():
return cand return cand
# gallery-dl post-numbered convention: strip the `NN_` prefix from
# the stem and look for that.json in the same directory.
m = _NUMBERING_PREFIX.match(media.stem)
if m:
cand = media.parent / f"{m.group(1)}.json"
if cand.is_file():
return cand
return None return None
+45
View File
@@ -23,6 +23,51 @@ def test_find_sidecar_none(tmp_path):
assert find_sidecar(media) is None assert find_sidecar(media) is None
def test_find_sidecar_gallerydl_numbered_prefix(tmp_path):
"""gallery-dl prefixes media filenames with NN_ for in-post ordering
(e.g. `01_HOLLOW-ICHIGO.png`) but writes the post-level sidecar under
the attachment stem WITHOUT the prefix (`HOLLOW-ICHIGO.json`).
Confirmed against real Patreon downloads 2026-05-26 — operator's deep
scan produced 24 refresh calls but 0 Posts because the unprefixed
sidecar was invisible to find_sidecar."""
media = tmp_path / "01_HOLLOW-ICHIGO.png"
media.write_bytes(b"x")
sc = tmp_path / "HOLLOW-ICHIGO.json"
sc.write_text("{}")
assert find_sidecar(media) == sc
def test_find_sidecar_multidigit_prefix(tmp_path):
"""Numbering prefix can be wider than 2 digits (`001_...`); the strip
handles any \\d+_ form."""
media = tmp_path / "001_mirko-sketch.png"
media.write_bytes(b"x")
sc = tmp_path / "mirko-sketch.json"
sc.write_text("{}")
assert find_sidecar(media) == sc
def test_find_sidecar_prefers_attachment_level_over_post_level(tmp_path):
"""If BOTH a per-attachment sidecar and a post-level sidecar exist,
the attachment-level one wins (it's more specific)."""
media = tmp_path / "01_image.png"
media.write_bytes(b"x")
per_attachment = tmp_path / "01_image.json"
per_attachment.write_text('{"specific": true}')
post_level = tmp_path / "image.json"
post_level.write_text('{"specific": false}')
assert find_sidecar(media) == per_attachment
def test_find_sidecar_no_underscore_not_treated_as_prefix(tmp_path):
"""`01.png` (just digits, no underscore-separated stem) shouldn't
match. The regex requires NN_<something>."""
media = tmp_path / "01.png"
media.write_bytes(b"x")
(tmp_path / ".json").write_text("{}") # would be matched only if buggy
assert find_sidecar(media) is None
def test_parse_empty_dict_all_none(): def test_parse_empty_dict_all_none():
sd = parse_sidecar({}) sd = parse_sidecar({})
assert isinstance(sd, SidecarData) assert isinstance(sd, SidecarData)