From c06cbc0abe0958121fcf4b44d13f8c11546058e9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 25 May 2026 21:30:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(ci):=20inline=20extension=20sign=20into=20?= =?UTF-8?q?build.yml=20+=20Forgejo=20Release=20Assets=20as=20XPI=20cache?= =?UTF-8?q?=20(v26.05.25.5)=20=E2=80=94=20bump=20ext=20to=201.0.2=20?= =?UTF-8?q?=E2=80=94=20Co-Authored-By:=20Claude=20Opus=204.7=20(1M=20conte?= =?UTF-8?q?xt)=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .forgejo/workflows/build.yml | 136 ++++++++++++++++++++++++++++++- .forgejo/workflows/extension.yml | 85 ++----------------- extension/manifest.json | 2 +- extension/package.json | 2 +- 4 files changed, 145 insertions(+), 80 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 3743cd0..837b093 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -6,18 +6,150 @@ on: # Requires repo secret RELEASE_TOKEN — a Forgejo PAT with scopes: # - write:package, read:package (for docker push to git.fabledsword.com) -# - write:release (for future release-cutting workflows) +# - write:release (for ext- release asset cache) # - write:issue (for future issue-management automation) # The injected GITHUB_TOKEN cannot be used — it lacks write:package. jobs: - build-web: + # Sign-or-fetch-from-cache: signs the extension via AMO if no ext- + # Forgejo release exists yet, otherwise downloads the cached signed XPI. + # Result is uploaded as an Actions artifact for build-web to consume. + # + # Why this lives in build.yml (not a separate workflow): the merge-commit's + # docker image tagged `:latest` MUST carry the XPI. A separate sign workflow + # racing build.yml leaves `:latest` without the XPI for ~5min (until the + # commit-back triggers another build). Inline ordering eliminates the race. + # Cache strategy: Forgejo Release Assets — picked 2026-05-25 over Generic + # Packages (cleaner API surface) and commit-back-to-side-branch (no extra + # branch to manage). AMO blocks re-signing the same version (returns 409), + # so signing is intentionally one-shot per version bump. + sign-extension: + if: github.ref == 'refs/heads/main' runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 steps: - uses: actions/checkout@v4 + - name: Resolve extension version + id: extver + run: | + VERSION=$(grep -E '"version"' extension/package.json | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Resolved extension version: $VERSION" + + - name: Check Forgejo release-asset cache + id: cache + env: + TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + set -eu + VERSION=${{ steps.extver.outputs.version }} + STATUS=$(curl -s -o release.json -w "%{http_code}" \ + -H "Authorization: token $TOKEN" \ + "https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator/releases/tags/ext-$VERSION" || echo 000) + echo "Tag lookup HTTP status: $STATUS" + if [ "$STATUS" = "200" ]; then + ASSET_ID=$(jq -r '.assets[]? | select(.name | test("\\.xpi$")) | .id' release.json | head -1) + if [ -n "$ASSET_ID" ] && [ "$ASSET_ID" != "null" ]; then + echo "cached=true" >> "$GITHUB_OUTPUT" + echo "asset_id=$ASSET_ID" >> "$GITHUB_OUTPUT" + echo "Cached XPI exists at ext-$VERSION (asset id $ASSET_ID); skipping AMO sign" + else + echo "cached=false" >> "$GITHUB_OUTPUT" + echo "Release ext-$VERSION exists but has no .xpi asset; will re-sign + re-upload" + fi + else + echo "cached=false" >> "$GITHUB_OUTPUT" + echo "No release named ext-$VERSION; will sign via AMO and upload" + fi + + - name: Download cached signed XPI (cache hit) + if: steps.cache.outputs.cached == 'true' + env: + TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + 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) + if: steps.cache.outputs.cached != 'true' + run: | + cd extension && npm install --no-save --no-audit --no-fund && npm run sign + env: + WEB_EXT_API_KEY: ${{ secrets.MOZILLA_AMO_JWT_KEY }} + WEB_EXT_API_SECRET: ${{ secrets.MOZILLA_AMO_JWT_SECRET }} + + - name: Upload signed XPI to ext- release (cache miss) + if: steps.cache.outputs.cached != 'true' + env: + TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + set -eux + VERSION=${{ steps.extver.outputs.version }} + # AMO renames signed XPIs with its internal addon-id-safe-string; + # canonicalize to fabledcurator-.xpi so the FC server's + # whitelist (backend/app/frontend.py expects 'fabledcurator-*.xpi') + # keeps working. + SIGNED=$(ls extension/web-ext-artifacts/*.xpi | head -1) + XPI="extension/web-ext-artifacts/fabledcurator-$VERSION.xpi" + cp "$SIGNED" "$XPI" + # Find-or-create the ext- release. + STATUS=$(curl -s -o release.json -w "%{http_code}" \ + -H "Authorization: token $TOKEN" \ + "https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator/releases/tags/ext-$VERSION" || echo 000) + if [ "$STATUS" != "200" ]; then + curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"ext-$VERSION\",\"name\":\"Extension $VERSION (signed XPI cache)\",\"body\":\"Internal cache for the signed XPI consumed by build.yml's build-web job. Not a user-facing FC release.\",\"target_commitish\":\"main\"}" \ + -o release.json \ + "https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator/releases" + fi + RELEASE_ID=$(jq -r '.id' release.json) + test -n "$RELEASE_ID" && test "$RELEASE_ID" != "null" + curl -s -X POST -H "Authorization: token $TOKEN" \ + -F "attachment=@$XPI" \ + "https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator/releases/$RELEASE_ID/assets?name=fabledcurator-$VERSION.xpi" + echo "Uploaded fabledcurator-$VERSION.xpi to ext-$VERSION release" + + - name: Upload XPI as Actions artifact (handoff to build-web) + uses: actions/upload-artifact@v4 + with: + name: signed-extension + path: extension/web-ext-artifacts/fabledcurator-*.xpi + retention-days: 1 + + build-web: + needs: [sign-extension] + # sign-extension is main-only; on dev it's skipped, build-web still runs. + if: always() && (needs.sign-extension.result == 'success' || needs.sign-extension.result == 'skipped') + runs-on: python-ci + container: + image: git.fabledsword.com/bvandeusen/ci-python:3.14 + steps: + - uses: actions/checkout@v4 + + - name: Download signed extension (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' + run: | + set -eux + 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) + mkdir -p frontend/public/extension + cp "$XPI" "frontend/public/extension/fabledcurator-$VERSION.xpi" + cp "$XPI" "frontend/public/extension/fabledcurator-latest.xpi" + ls -la frontend/public/extension/ + - name: Determine tag id: tag run: | diff --git a/.forgejo/workflows/extension.yml b/.forgejo/workflows/extension.yml index ef007c3..ea12cf2 100644 --- a/.forgejo/workflows/extension.yml +++ b/.forgejo/workflows/extension.yml @@ -1,15 +1,19 @@ name: extension +# Lint-only workflow. The sign-and-publish dance moved into build.yml's +# `sign-extension` job (2026-05-25) — `:latest` now always bundles the XPI +# because sign-extension runs as a build-web dependency in the SAME workflow, +# eliminating the prior race between build.yml and a separate extension.yml. +# Signed XPIs are cached in Forgejo Release Assets named `ext-`. on: push: branches: [dev, main] - # Self-retriggering: include the workflow file itself so edits to - # this workflow (filename-glob fixes, env tweaks, etc.) run the - # sign-and-publish dance on next push without needing a manual - # Forgejo dispatch. Side-effect commits (`ext: publish signed XPI`) - # only touch frontend/public/extension/ so they DON'T re-trigger. paths: - 'extension/**' - '.forgejo/workflows/extension.yml' + pull_request: + branches: [main] + paths: + - 'extension/**' workflow_dispatch: jobs: @@ -23,74 +27,3 @@ jobs: run: cd extension && npm install --no-save --no-audit --no-fund - name: Lint run: cd extension && npm run lint - - sign-and-publish: - needs: lint - if: github.ref == 'refs/heads/main' - runs-on: python-ci - container: - image: node:22-bookworm-slim - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.RELEASE_TOKEN }} - - name: Install web-ext + git - run: | - apt-get update && apt-get install -y --no-install-recommends git ca-certificates - cd extension && npm install --no-save --no-audit --no-fund - - name: Sign XPI - run: cd extension && npm run sign - env: - WEB_EXT_API_KEY: ${{ secrets.MOZILLA_AMO_JWT_KEY }} - WEB_EXT_API_SECRET: ${{ secrets.MOZILLA_AMO_JWT_SECRET }} - - name: Commit signed XPI to frontend/public/extension/ - run: | - set -ex - # AMO renames signed XPIs using its internal addon-id-safe-string - # (e.g. 997017ca3e104e30a75a-1.0.1.xpi), NOT our gecko ID. Match - # any *.xpi in the artifacts dir — fresh CI runner means there's - # exactly one — and rename it on copy so the FC server's - # whitelist (backend/app/frontend.py expects 'fabledcurator-*.xpi') - # keeps working. - # Diagnostic instrumentation 2026-05-25: a prior run (id 1731) - # reported success without producing an `ext: publish signed XPI` - # commit on main. Tracing the cwd, artifacts dir, version - # extraction, staging state, and push response so the next run's - # log explains the gap. - echo "=== cwd ===" - pwd - echo "=== ref / branch ===" - echo "ref: ${GITHUB_REF:-unset} sha: ${GITHUB_SHA:-unset}" - git log --oneline -3 || true - echo "=== artifacts dir ===" - ls -la extension/web-ext-artifacts/ 2>&1 || echo "(dir missing)" - XPI=$(ls extension/web-ext-artifacts/*.xpi 2>/dev/null | head -1) - echo "XPI=$XPI" - if [ -z "$XPI" ]; then - echo "No XPI produced by web-ext sign — exiting" - exit 1 - fi - VERSION=$(grep -E '"version"' extension/package.json | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') - echo "VERSION=$VERSION" - DEST="frontend/public/extension/fabledcurator-${VERSION}.xpi" - echo "DEST=$DEST" - mkdir -p frontend/public/extension - # Wipe any prior versions so the directory doesn't grow each release. - rm -f frontend/public/extension/fabledcurator-*.xpi - cp "$XPI" "$DEST" - cp "$XPI" "frontend/public/extension/fabledcurator-latest.xpi" - echo "=== frontend/public/extension/ after copy ===" - ls -la frontend/public/extension/ - git config user.name "FC extension CI" - git config user.email "noreply@fabledsword.com" - git add frontend/public/extension/ - echo "=== git status after add ===" - git status --short - echo "=== diff --cached --stat ===" - git diff --cached --stat || true - if git diff --cached --quiet; then - echo "No changes to commit (frontend/public/extension/ matches HEAD)" - else - git commit -m "ext: publish signed XPI fabledcurator-${VERSION}.xpi" - git push -v origin HEAD:main - fi diff --git a/extension/manifest.json b/extension/manifest.json index bf740eb..a9d1220 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "FabledCurator", - "version": "1.0.1", + "version": "1.0.2", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "browser_specific_settings": { diff --git a/extension/package.json b/extension/package.json index 181bb0f..463151d 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,6 +1,6 @@ { "name": "fabledcurator-extension", - "version": "1.0.1", + "version": "1.0.2", "private": true, "description": "Firefox extension for FabledCurator", "scripts": {