Files
Thingport/.github/workflows/extension-store-release.yml
Tautvydas Derzinskas d33802d8c0 fix: prevent AMO rate limit errors in extension release
Serialize publish-firefox and publish-latest jobs with a 90-second delay
to prevent hitting AMO's rate limit when running in parallel. Ensure
publish-latest still runs even if publish-firefox fails, while
maintaining the job dependency.

Also simplify extension certification notes.
2026-10-20 08:56:24 +02:00

277 lines
12 KiB
YAML

name: Extension store release
# Versioned releases of Thingport Grab via semantic-release (extension/.releaserc.json): every
# Friday at 13:00 Polish time, or on demand. Only commits touching extension/ count; with a
# releasable one it bumps the version, writes the changelog, tags, creates a GitHub release with
# the store zips, publishes to the Chrome Web Store, Edge Add-ons and addons.mozilla.org, and moves the
# extension-latest release to the same build. Per-merge checks are in extension-release.yml.
on:
workflow_dispatch: {}
schedule:
# GitHub cron is UTC-only; fire at both CET and CEST equivalents and let the Warsaw-time step
# pick the right one.
- cron: "0 11 * * 5"
- cron: "0 12 * * 5"
concurrency:
group: extension-store-release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # the release commit, the tag and the GitHub release
outputs:
version: ${{ steps.release.outputs.version }}
tag: ${{ steps.release.outputs.tag }}
defaults:
run:
working-directory: extension
steps:
- name: Friday 13:00 in Warsaw?
id: gate
if: github.event_name == 'schedule'
run: |
offset=$(TZ=Europe/Warsaw date +%z)
case "${{ github.event.schedule }}|$offset" in
"0 11 * * 5|+0200" | "0 12 * * 5|+0100") echo "run=true" >> "$GITHUB_OUTPUT" ;;
*)
echo "This trigger isn't 13:00 in Warsaw (UTC offset $offset) -- the other one is. Skipping."
echo "run=false" >> "$GITHUB_OUTPUT"
;;
esac
- uses: actions/checkout@v4
if: steps.gate.outputs.run != 'false'
with:
fetch-depth: 0 # semantic-release needs the full history and tags
- uses: actions/setup-node@v4
if: steps.gate.outputs.run != 'false'
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: extension/package-lock.json
- if: steps.gate.outputs.run != 'false'
run: npm ci
- name: semantic-release
id: release
if: steps.gate.outputs.run != 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: github-actions[bot]
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: github-actions[bot]
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
run: npx semantic-release
# Not semantic-release's GitHub plugin: it marks releases "latest", a slot Bridge's links rely on.
- name: GitHub release
if: steps.release.outputs.version != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.release.outputs.tag }}" dist/zips/*.zip \
--title "Thingport Grab ${{ steps.release.outputs.version }}" \
--notes-file dist/release-notes.md \
--latest=false
- uses: actions/upload-artifact@v4
if: steps.release.outputs.version != ''
with:
name: thingport-grab-store-zips
path: extension/dist/zips/*.zip
if-no-files-found: error
# Its own job so it can be re-run alone. Signs in as the publisher's service account; see
# extension/CONTRIBUTING.md.
publish-chrome:
needs: release
if: needs.release.outputs.version != ''
runs-on: ubuntu-latest
defaults:
run:
working-directory: extension
env:
CHROME_EXTENSION_ID: ${{ vars.CHROME_EXTENSION_ID }}
CWS_PUBLISHER_ID: ${{ secrets.CWS_PUBLISHER_ID }}
CWS_SERVICE_ACCOUNT_KEY: ${{ secrets.CWS_SERVICE_ACCOUNT_KEY }}
steps:
- name: Credentials set up?
id: creds
run: |
if [ -z "$CWS_SERVICE_ACCOUNT_KEY" ] || [ -z "$CWS_PUBLISHER_ID" ]; then
echo "::notice::CWS_SERVICE_ACCOUNT_KEY / CWS_PUBLISHER_ID aren't set -- skipping the Chrome Web Store (see extension/CONTRIBUTING.md)."
echo "ok=false" >> "$GITHUB_OUTPUT"
fi
working-directory: .
- uses: actions/checkout@v4
if: steps.creds.outputs.ok != 'false'
- uses: actions/setup-node@v4
if: steps.creds.outputs.ok != 'false'
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: extension/package-lock.json
- if: steps.creds.outputs.ok != 'false'
run: npm ci
- uses: actions/download-artifact@v4
if: steps.creds.outputs.ok != 'false'
with:
name: thingport-grab-store-zips
path: extension/dist/zips
- name: Upload and submit for review
if: steps.creds.outputs.ok != 'false'
run: npx tsx scripts/publish-chrome.ts dist/zips/thingport-grab-chrome.zip
# Its own job so it can be re-run alone: Edge takes one submission at a time and fails with
# InProgressSubmission while the previous one is in certification. Credentials: see
# extension/CONTRIBUTING.md.
publish-edge:
needs: release
if: needs.release.outputs.version != ''
runs-on: ubuntu-latest
defaults:
run:
working-directory: extension
env:
EDGE_PRODUCT_ID: ${{ vars.EDGE_PRODUCT_ID }}
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: extension/package-lock.json
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: thingport-grab-store-zips
path: extension/dist/zips
# Edge wants complete certification notes with every submission; see the notes file.
- name: Upload and submit for certification
env:
RELEASE_VERSION: ${{ needs.release.outputs.version }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
EDGE_REVIEW_TEST_SERVER: ${{ secrets.EDGE_REVIEW_TEST_SERVER }}
run: npx tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip --notes-file scripts/edge-certification-notes.txt
# Its own job so it can be re-run alone. Same AMO credentials as publish-latest's unlisted signing;
# see extension/CONTRIBUTING.md.
publish-firefox:
needs: release
if: needs.release.outputs.version != ''
runs-on: ubuntu-latest
defaults:
run:
working-directory: extension
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: extension/package-lock.json
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: thingport-grab-store-zips
path: extension/dist/zips
- name: Unpack the build the release made
run: unzip -q dist/zips/thingport-grab-firefox.zip -d dist/firefox
- name: Notes for Mozilla's reviewers
env:
VERSION: ${{ needs.release.outputs.version }}
TAG: ${{ needs.release.outputs.tag }}
run: |
jq -n --arg notes "Automated update to version $VERSION. Changes: https://github.com/TautvydasDerzinskas/Thingport/releases/tag/$TAG -- the bundles aren't minified; to rebuild them from the attached source (Node 24+): npm ci && npm run build:firefox, output in dist/firefox. Documentation: https://thingport.net/docs/grab/" \
'{version: {approval_notes: $notes}}' > dist/amo-metadata.json
# Mozilla may hold a listed version for manual review, so don't wait for the signed file.
- name: Submit to addons.mozilla.org
env:
WEB_EXT_API_KEY: ${{ secrets.AMO_JWT_ISSUER }}
WEB_EXT_API_SECRET: ${{ secrets.AMO_JWT_SECRET }}
run: |
npx web-ext sign \
--source-dir dist/firefox \
--artifacts-dir dist/signed \
--upload-source-code dist/zips/thingport-grab-sources.zip \
--amo-metadata dist/amo-metadata.json \
--channel listed \
--approval-timeout 0
# The builds for installing by hand, on the fixed "extension-latest" tag rather than GitHub's single
# repo-wide "latest" release, which Bridge's download links rely on.
# Runs after publish-firefox, even if it failed: both sign with the same AMO key, and in parallel they hit
# AMO's rate limit.
publish-latest:
needs: [release, publish-firefox]
if: ${{ !cancelled() && needs.release.outputs.version != '' }}
runs-on: ubuntu-latest
permissions:
contents: write
defaults:
run:
working-directory: extension
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.tag }}
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: extension/package-lock.json
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: thingport-grab-store-zips
path: extension/dist/zips
# AMO rejects a version it has seen on any channel, and publish-firefox submits this one as listed, so the
# self-distributed copy gets the run number as an extra segment (e.g. 1.1.0.456).
- name: Unpack the Firefox build with a version of its own
env:
RUN_NUMBER: ${{ github.run_number }}
run: |
unzip -q dist/zips/thingport-grab-firefox.zip -d dist/firefox
jq --arg run "$RUN_NUMBER" '.version = "\(.version).\($run)"' dist/firefox/manifest.json > manifest.json.tmp
mv manifest.json.tmp dist/firefox/manifest.json
# AMO's throttle window is about a minute; let publish-firefox's requests age out first.
- name: Let AMO's rate limit recover
run: sleep 90
- name: Sign (unlisted -- not published to addons.mozilla.org's public listing)
env:
WEB_EXT_API_KEY: ${{ secrets.AMO_JWT_ISSUER }}
WEB_EXT_API_SECRET: ${{ secrets.AMO_JWT_SECRET }}
run: |
npx web-ext sign \
--source-dir dist/firefox \
--artifacts-dir dist/signed \
--upload-source-code dist/zips/thingport-grab-sources.zip \
--channel unlisted
mv "$(ls dist/signed/*.xpi | head -n1)" dist/signed/thingport-grab-firefox.xpi
- name: Move extension-latest to this release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
TAG: ${{ needs.release.outputs.tag }}
run: |
cat > "$RUNNER_TEMP/notes.md" <<EOF
Thingport Grab $VERSION ([release notes](https://github.com/${{ github.repository }}/releases/tag/$TAG)),
for installing by hand. The store versions keep themselves up to date; see \`extension/README.md\`:
- **Chrome / other Chromium browsers**: the
[Chrome Web Store](https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee).
\`thingport-grab-chrome.zip\` loads by hand (\`chrome://extensions\` → **Developer mode** → **Load unpacked**).
- **Firefox**: [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/).
\`thingport-grab-firefox.xpi\` is Mozilla-signed; open it directly, or drag it onto a Firefox window.
- **Edge**: [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol).
\`thingport-grab-edge.zip\` loads by hand (\`edge://extensions\` → **Developer mode** → **Load unpacked**).
EOF
gh release delete extension-latest --yes --cleanup-tag || true
gh release create extension-latest \
dist/zips/thingport-grab-chrome.zip dist/zips/thingport-grab-edge.zip dist/signed/thingport-grab-firefox.xpi \
--target "$(git rev-parse HEAD)" \
--title "Thingport Grab (latest)" \
--notes-file "$RUNNER_TEMP/notes.md" \
--latest=false