Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 59 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,34 @@ jobs:
echo "version=${VERSION%%-*}"
echo "version_name=$VERSION"
echo "zip=testomat-io-$VERSION.zip"
echo "store_zip=testomat-io-$VERSION-webstore.zip"
[[ "$VERSION" == *-* ]] && echo "prerelease=true" || echo "prerelease=false"
} >> "$GITHUB_OUTPUT"

- name: Check the manifest
run: |
python3 - <<'PY'
import json, pathlib, sys
import base64, hashlib, json, pathlib, sys

root = pathlib.Path('extension')
try:
m = json.loads((root / 'manifest.json').read_text())
except json.JSONDecodeError as e:
sys.exit(f"::error::manifest.json is not valid JSON: {e}")

# The key is the Web Store item's public key, so a GitHub copy answers to the store's ID.
STORE_ID = 'amcnjlghmkkjfaajanfeghpgbjakdcka'
try:
digest = hashlib.sha256(base64.b64decode(m['key'], validate=True)).hexdigest()[:32]
except (KeyError, TypeError, ValueError):
sys.exit("::error::manifest.json has no readable key, so every unpacked copy would get its own ID")
got_id = ''.join(chr(97 + int(c, 16)) for c in digest)
if got_id != STORE_ID:
sys.exit(f"::error::manifest.json's key gives the ID {got_id}, not the Web Store item's {STORE_ID}")
# update_url marks a store install, and a copy carrying it never reads a host's handoff.json.
if 'update_url' in m:
sys.exit("::error::manifest.json carries update_url, which only the Web Store may add")

refs = list((m.get('icons') or {}).values())
refs += list(((m.get('action') or {}).get('default_icon') or {}).values())
for path in ((m.get('side_panel') or {}).get('default_path'),
Expand Down Expand Up @@ -92,21 +106,61 @@ jobs:
- name: Pack
run: |
set -euo pipefail
(cd extension && zip -qr "../${{ steps.v.outputs.zip }}" . -x '*.DS_Store')
# A host's handoff.json holds its credentials: it never ships, in either zip.
(cd extension && zip -qr "../${{ steps.v.outputs.zip }}" . -x '*.DS_Store' -x 'handoff.json')
# The Web Store refuses a manifest with a key: it assigns the item's ID itself.
rm -rf webstore && cp -R extension webstore && rm -f webstore/handoff.json
python3 - <<'PY'
import json, pathlib

p = pathlib.Path('webstore/manifest.json')
m = json.loads(p.read_text())
del m['key']
p.write_text(json.dumps(m, indent=2, ensure_ascii=False) + '\n')
PY
(cd webstore && zip -qr "../${{ steps.v.outputs.store_zip }}" . -x '*.DS_Store')
unzip -l "${{ steps.v.outputs.zip }}" | tail -1
unzip -l "${{ steps.v.outputs.store_zip }}" | tail -1

- name: Check the two zips
env:
ZIP: ${{ steps.v.outputs.zip }}
STORE_ZIP: ${{ steps.v.outputs.store_zip }}
run: |
python3 - <<'PY'
import json, os, sys, zipfile

github, store = zipfile.ZipFile(os.environ['ZIP']), zipfile.ZipFile(os.environ['STORE_ZIP'])
names = sorted(github.namelist())
if names != sorted(store.namelist()):
diff = sorted(set(names) ^ set(store.namelist()))
sys.exit(f"::error::the two zips hold different files: {', '.join(diff)}")
if 'handoff.json' in names:
sys.exit("::error::a host's handoff.json is in the zips")
mg, ms = json.loads(github.read('manifest.json')), json.loads(store.read('manifest.json'))
if 'key' in ms or 'update_url' in ms:
sys.exit("::error::the Web Store manifest carries key or update_url")
if {k: v for k, v in mg.items() if k != 'key'} != ms:
sys.exit("::error::the two manifests differ by more than the key")
differ = [n for n in names if n != 'manifest.json' and github.read(n) != store.read(n)]
if differ:
sys.exit(f"::error::these files differ between the zips: {', '.join(differ)}")
print(f"{len(names)} entries in each zip; the Web Store one lacks only the manifest key")
PY

- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TAG='${{ steps.v.outputs.tag }}'
ZIPS=('${{ steps.v.outputs.zip }}' '${{ steps.v.outputs.store_zip }}')
# Publishing from the Releases UI makes tag and release together, so the
# existence check can lose that race — a failed create means it appeared.
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" '${{ steps.v.outputs.zip }}' --clobber
elif ! gh release create "$TAG" '${{ steps.v.outputs.zip }}' \
gh release upload "$TAG" "${ZIPS[@]}" --clobber
elif ! gh release create "$TAG" "${ZIPS[@]}" \
--title "$TAG" --generate-notes \
${{ steps.v.outputs.prerelease == 'true' && '--prerelease' || '' }}; then
gh release upload "$TAG" '${{ steps.v.outputs.zip }}' --clobber
gh release upload "$TAG" "${ZIPS[@]}" --clobber
fi
3 changes: 2 additions & 1 deletion docs/guide/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ your disk, once, and Chrome keeps it. Chrome 123 or newer.
1. Get the folder — either way works:
- download the newest `testomat-io-<version>.zip` from
[Releases](https://github.com/testomatio/browser-extension/releases) —
the single file under **Assets** — and unpack it, **or**
the file under **Assets** without `-webstore` in its name (that one is
the Chrome Web Store upload) — and unpack it, **or**

![The zip under Assets on the Releases page](img/install-release-zip.png)

Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@
"permissions": ["storage", "sidePanel", "debugger", "scripting", "webRequest",
"tabCapture", "offscreen", "contextMenus"],
"host_permissions": ["<all_urls>"],
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqlLbtaIjeOwOUDDkGbTb05aCcYnS5CMqNqCW1WBQrB61jzlqzG3wY9y1MAoXCS77kZKSgkCW/VF8WSx23UA5EWzNKph4ZrxdOxvUPMD5ScUKoCNWZkPTvP163wm17w+mCeT6UItKze09WeVzuotbj/7PgNBWGKgzYk1VLMDu+0ZHfEH/yjP1E5hYkxGkB+5f8rLc0bB/MM4iG+8gWTkFlHXlFQp3xEkRui05kO03Z8M5+VMrBeWL6VHIZioOAecTj/+xYLNFb73Y+fl0TrkdwtPFXng7viad8l9HtBovv2KQz5ckKp1yY/mJZdr17bmhZhlOuVSyUewnOqfzkgJtQIDAQAB"
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoNO5FmLZNVum8CahrRGHtqGXQUI/D8AU9CpCKuAgRV+hRDYu+IYNeF62isIeLL3g6iHoeQtEAjbujj6g4he90DV+0UlUw4zanyXmSynvCHZMCyKDnqu7wg6LujuuYLfJY7utUvMKtWC64dUx5E5Z8nxhz3tU+snfmYaClyie6ai+gHfXelDM1Pass06KE/xgKiwZeIVTJPsOrkzXff2PAMEzVE9PYTRKqcG4Bm38pJb64ptKi0355F8zfSsQxvtldBuOEAZQAbACfUXZWcxZqWrQVGA5PMasYRPlQxqC4PTcwXep1TISYH1BKqA3tP2I7Nb2QvXpKxHypHMvOPHtlwIDAQAB"
}
Loading