The R2 credentials exist only as GitHub secrets. Anything built on a developer machine reaches the public bucket the same way:
.github/workflows/publish-assets.yml).The workflow checks the files first and uploads nothing if a check fails.
Rules that apply to every run:
gh workflow run only finds
a workflow_dispatch workflow that exists on main. So merge the branch that
adds or changes publish-assets.yml into main first (see step 0 below).contents: read, and that token cannot see draft releases, so
gh release download fails. Use --prerelease instead if the release
should not look final.The upload logic is in scripts/r2-publish-release-assets.sh. With
DRY_RUN=1 it runs every check and prints the planned uploads. It needs no
credentials and makes no network calls (it skips the existence check of
immutable objects and the rollback check against the published
firefox-updates.json). The three R2 variables must still be set, because the
URLs written into the manifests come from them:
export DRY_RUN=1 R2_BUCKET=dry-run R2_ENDPOINT_URL=https://dry-run.invalid \
R2_PUBLIC_BASE_URL=https://pub-5cf47e052a1b41d0ad8c4ddf5fc03c1e.r2.dev
REV=2125428-2008035-f45589d9b800
scripts/r2-publish-release-assets.sh dictionaries "dict-$REV" "builds/dictionaries/$REV"
# a directory holding exactly one signed .xpi:
scripts/r2-publish-release-assets.sh firefox-xpi v0.4.0 ~/xpi-dir releases/firefox-updates.json
scripts/r2-publish-release-assets.sh cors
firefox-updates.json entry for
the new version) into main.v0.4.0 on main. A tag made before the merge carries the old
firefox-updates.json, and the Firefox publish refuses it.dict_base.db. Read sources.dict.path in the published build.json and
pass that database with --dict unless a newer one exists:
npm run build:dictionaries -- --dict <database>. The output is
builds/dictionaries/<revision>/ (see dictionary-pair.md).Create a release whose tag is dict-<revision> with the three files:
REV=2125428-2008035-f45589d9b800
D=builds/dictionaries/$REV
gh release create "dict-$REV" --prerelease --title "Dictionaries $REV" \
--notes "Dictionary pair $REV" \
"$D/wty-en-en.zip" "$D/flibooster-en-ru.zip" "$D/build.json"
gh workflow run publish-assets.yml -f kind=dictionaries -f tag="dict-$REV".The workflow checks that:
index.json hold the same revision as the tag;index.json title matches its file name;build.json holds the same revision and the sha256 of both zips.It uploads dictionaries/<revision>/{wty-en-en.zip, flibooster-en-ru.zip}
with a one-year immutable cache, and build.json beside them with a 5-minute
cache: build.json is not part of the revision and may be replaced.
main, so an unmerged check does not apply.DRY_RUN skips this check, because it needs the network. The check compares
only row counts: a pair with equal or more rows but older text passes.url in index.json is refused.dictionaries/latest.json cannot be read, including a 404. To publish into an empty bucket, change the script.x-amz-meta-sha256 metadata set on upload, not
from the ETag: files over 8 MB go up as multipart, and a multipart ETag is not
an MD5 of the object. An object without that metadata is downloaded and
hashed.Only after that does it write dictionaries/latest.json (5-minute cache):
{
"schema": 1,
"revision": "<rev>",
"published_at": "<iso utc>",
"dictionaries": [
{
"title": "wty-en-en",
"file": "wty-en-en.zip",
"url": "…",
"size": 0,
"sha256": "…"
},
{
"title": "flibooster-en-ru",
"file": "flibooster-en-ru.zip",
"url": "…",
"size": 0,
"sha256": "…"
}
]
}
latest.json always follows the last run. Running the workflow for an older
dict-* tag rolls the dictionaries back.
build.json is published as the builder wrote it; it is public anyway as a
release asset.
Since v0.4.2 Firefox is listed on AMO. AMO hosts the add-on and updates it,
so the manifest has no update_url: AMO rejects a listed version that has one
(MANIFEST_UPDATE_URL). Each version needs a new AMO submission.
R2 now serves only one purpose: it moves copies installed before v0.4.2 onto
AMO. Those copies still read firefox-updates.json, and a version without
update_url then updates from AMO. Firefox shows the newly
declared data. As with new permissions, the update is expected to wait for the
user to accept; this has not been tested on a real install.
Firefox 115–139 cannot install v0.4.2 and never moves.
releases/firefox-updates.json for the new version: the
4-segment version (0.4.2.0),
update_link = <R2 public base>/releases/v0.4.2/yomitan-firefox.xpi and
the same strict_min_version as the manifest. Keep the older entries.
Commit it before tagging: the workflow reads the file from the tag.npm run build -- --target firefox --version 0.4.2.0, then
npx addons-linter builds/yomitan-firefox.zip. The linter runs in listed mode
by default and must report 0 errors.v0.4.2. release.yml builds all variants, uploads the
unsigned zips to R2 and creates the GitHub Release v0.4.2.git archive --format=zip --prefix=flib-club-0.4.2/ -o flib-club-0.4.2-source.zip v0.4.2.yomitan-firefox.zip and the source archive. Texts for the form are in
AMO_SUBMISSION_NOTES.md. First check that the
published privacy policy shows this version: Pages rebuilds it after the merge
to main..xpi from the version’s page on AMO.release.yml has finished. It deletes and recreates the release
when a tag is re-pushed, which also drops uploaded assets. Then attach the
XPI: gh release upload v0.4.2 ~/Downloads/flib-club-0.4.2.xpi -R xxxvita/yomitan_anki_conf.gh workflow run publish-assets.yml -R xxxvita/yomitan_anki_conf --ref main -f kind=firefox-xpi -f tag=v0.4.2.Merge to main before tagging, as in step 0. Publish versions from oldest to
newest. The rollback check compares the tag’s
firefox-updates.json with the published one. So once v0.4.2 is published, an
older tag can no longer be published, because its file lacks 0.4.2.0.
An entry whose XPI was never published (0.4.0.0, if AMO did not sign it in
time) is harmless for Firefox 140 and later, which take the newest compatible
version. Firefox 115–139 would pick that entry, get a 404 and stay on the
installed version without any message.
The workflow checks that:
*.xpi;META-INF/mozilla.rsa or META-INF/cose.sig);manifest.json version is the tag padded to 4 segments;firefox-updates.json, with exactly the expected update_link;firefox-updates.json keeps every version in the published manifest
(no rollback from an old tag).It then uploads, in this order:
releases/v<ver>/yomitan-firefox.xpi (immutable, with x-amz-meta-sha256;
like the dictionary zips, it is never overwritten with different bytes);releases/latest/yomitan-firefox.xpi (5-minute cache; the website links to
it), but only if this is the newest version in the manifest;releases/firefox-updates.json (5-minute cache), last.firefox-updates.jsonOnly the XPI publish does: this workflow, or the local script below.
release.yml no longer uploads it. A tag push only has the unsigned zip, so
publishing the manifest then would send Firefox installs to an update_link
that does not exist until the signed XPI is uploaded.
With local R2 credentials, scripts/firefox-publish-signed.sh <ver> <xpi>
still works. It calls r2-publish-release-assets.sh firefox-xpi, so it runs
the same checks and uploads the same three objects. The one difference: it
reads firefox-updates.json from the working tree rather than from the tag.
On Firefox, extension pages fetch latest.json and the zips cross-origin.
Run the workflow with kind=cors before releasing the extension version that
downloads dictionaries; otherwise that version’s downloads fail in Firefox.
The tag is ignored. The run sets the bucket policy to: GET and HEAD from any
origin, no credentials.
If the R2 token cannot change bucket settings, the run logs an AccessDenied warning, also shown in the run’s summary, and still succeeds. Then set the policy by hand: Cloudflare dashboard → R2 → the bucket → Settings → CORS Policy:
[
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": [
"Content-Length",
"Content-Type",
"ETag",
"Last-Modified"
],
"MaxAgeSeconds": 86400
}
]