Skip to content

Submit releases to both stores from CI - #267

Open
vpetersson-bot wants to merge 5 commits into
masterfrom
feat/automate-store-releases
Open

vpetersson-bot wants to merge 5 commits into
masterfrom
feat/automate-store-releases

Conversation

@vpetersson-bot

@vpetersson-bot vpetersson-bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Description

Releasing meant two manual uploads: pull the zips off the build job, then hand
them to the Chrome Web Store dashboard and the AMO developer hub. This adds
publish.yaml, which does both.

When it runs. On release: released — the moment someone promotes the
pre-release that build.yaml already opens for the tag. That promotion is the
human decision; nothing fires off a bare tag push, because a store submission
cannot be withdrawn. Both jobs sit in a store-release environment, so adding
required reviewers there gives a second gate. There is also a
workflow_dispatch entry point taking a tag, a store, and a dry_run flag,
for when one store fails and the other succeeded.

What it submits. The release's own artifacts, not a rebuild. Each job checks
the artifact's build provenance and that the version inside manifest.json
matches the tag before anything leaves the runner. Both stores then review by
hand over hours to days, so neither job waits — for Firefox,
--approval-timeout 0 returns as soon as AMO accepts the upload.

Chrome authentication stores nothing. The obvious route is an OAuth refresh
token in a secret, and that is what most published recipes do. It has two
problems: it is a standing grant derived from one person clicking through a
consent screen, and Google expires it after six months unused — longer than the
gap between some of our releases, so the first symptom would be a failed
release. API v2 accepts service accounts, so the job exchanges GitHub's OIDC
token for an access token good for an hour. What is stored is a provider
resource name and a service account email; neither grants anything alone, and
the authority behind them is an IAM binding revocable on Google's side.

chrome-webstore-upload-cli can't take an externally minted token — it always
derives one from a client ID and refresh token — so bin/publish_chrome.sh
calls the three v2 endpoints itself. It is a short script, and it matches the
upload headers, uploadState values and fetchStatus polling of the
maintained chrome-webstore-upload v6 client.

Firefox is the other way round: AMO has no federated equivalent, so the issuer
and secret are stored. They don't expire.

Source code for AMO. Mozilla requires sources whenever the shipped code is
bundled or minified, which ours is — popup.bundle.js comes out of webpack in
production mode. The Firefox job attaches an archive of the tagged commit
(bin/package_source.sh) and SOURCE_BUILD_INSTRUCTIONS.md tells a reviewer
how to get from it back to dist/, with the pinned Bun version and both the
Docker and bare paths. Without that the submission is liable to be rejected.

Version pinning note. Chrome Web Store API v1.1 is switched off after 15
October 2026 and most publishing actions on the Marketplace still speak it, so
anything that replaces bin/publish_chrome.sh needs to be checked for v2.

Bug fixed along the way

build.yaml's attestation step was conditioned on
github.event.pull_request.head.repo.full_name == github.repository. On a tag
push there is no pull_request, so the condition is false and the step is
skipped — release artifacts have never carried provenance, and

gh attestation verify screenly-chrome-extension-v1.6.1.zip --repo Screenly/Browser-Extension

returns a 404 today, despite CONTRIBUTING.md telling people to run it. The
intent was clearly to skip forks, which cannot mint attestations, so the
condition now also covers tags. The new verification step depends on this; it
will fail for tags released before this merges, which is the correct answer for
artifacts that genuinely have no provenance.

Two things for a human to decide

Neither is fixed here.

  • web-ext lint warns that strict_min_version: 102 predates Firefox 109,
    which is where host_permissions landed. On 102–108 the extension installs
    with its host permissions ignored. Raising the floor is a compatibility call.
  • It also warns that browser_specific_settings.gecko.data_collection_permissions
    is missing. Mozilla requires it for new extensions and has said it will extend
    that to new versions of existing ones. Declaring what the extension collects is
    a statement about the product, not a CI detail.

Before the next release

The Google side — service account, its workload identity binding to this
repository, the Chrome Web Store API — and the store-release environment with
its reviewers and allowed refs are Terraform, in the infrastructure repo. Two
things it can't do: the Chrome Web Store has no API for registering a service
account, so someone with publisher access pastes the email into Account in
the dashboard; and CHROME_PUBLISHER_ID, AMO_JWT_ISSUER and AMO_JWT_SECRET
are set on the environment by hand rather than written into Terraform state.

Then run it once with dry_run: Chrome leaves an unsubmitted draft, Firefox
only lints.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have tested my changes on Google Chrome.
  • I have tested my changes on Mozilla Firefox.
  • I added a documentation for the changes I have made (when necessary).

Additional Information

No extension code changes, so the browser checkboxes are untestable here. What I
did verify locally:

  • actionlint (with shellcheck) clean on publish.yaml; shellcheck clean on
    both new scripts. The two pre-existing SC2046 warnings in build.yaml and
    test.yaml are untouched.
  • bin/publish_chrome.sh against a stubbed API, eight cases: success;
    IN_PROGRESS then SUCCEEDED via fetchStatus; FAILED; an HTTP error with
    the store's message surfaced; SUBMIT=false stopping before the publish call;
    the poll timing out; a missing package; and a missing variable. Each exits with
    the right status and makes exactly the expected calls.
  • Built dist/ with bunx webpack --config webpack.prod.js, zipped it the way
    build.yaml does, and ran the workflow's version check against it —
    manifest.json is at the zip root, both the unzip -p and the unpacked form
    resolve correctly.
  • web-ext lint --source-dir dist: 0 errors, 13 warnings, exit 0, so the lint
    gate passes on a real build.
  • gh release download --pattern ... --output against v1.6.1, and the
    attestation 404 above.
  • The credential guard and tag parsing, against present, missing and malformed
    inputs.
  • bun run test: 37 specs, 0 failures.

What cannot be tested from here is the live contract with each store: the
request shapes are matched against the reference clients, but the first real run
is the test.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 22, 2026 09:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Publishing jobs execute floating npm package versions with store credentials, allowing changed dependencies to expose them.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Automates Chrome Web Store and Firefox Add-ons submissions from released artifacts, with provenance/version checks and AMO source packaging.

Changes:

  • Adds the publishing workflow with manual and release triggers.
  • Adds source archive generation and reproduction instructions.
  • Enables attestations for tag builds and updates release documentation.
File Description
.github/​workflows/​publish.yaml Submits verified artifacts to both stores.
.github/​workflows/​build.yaml Attests tag build artifacts.
bin/​package_source.sh Creates AMO source archives.
SOURCE_BUILD_INSTRUCTIONS.md Documents source reproduction.
CONTRIBUTING.md Documents release and credential setup.
.gitignore Ignores generated source archives.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/publish.yaml Outdated
Copilot AI review requested due to automatic review settings September 22, 2026 10:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Firefox dry runs currently require AMO credentials, and the documented Chrome source rebuild path selects the Firefox manifest.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Skip AMO credential checks during dry runs

.github/​workflows/​publish.yaml:174

When dry_run is true, Firefox is documented to only lint, but this credential check still runs and fails if the AMO secrets are absent before linting starts. Gate the check on !inputs.dry_run so the dry run can perform its intended credential-free validation; release events still evaluate this as true.

Low severity Select the manifest template based on PLATFORM

SOURCE_BUILD_INSTRUCTIONS.md:46

The bare-metal example always reads manifest-firefox.json, so setting PLATFORM=chrome—which this document says is supported—still builds the Firefox manifest. Select the template from PLATFORM here so the documented Chrome reproduction is correct.

Low severity Pin web-ext to the workflow's installed version

SOURCE_BUILD_INSTRUCTIONS.md:62

web-ext is not a project dependency, so this unversioned bunx command performs an additional registry fetch and can use a different tool version from the workflow's pinned web-ext@10.7.0; it also conflicts with the claim that no network is needed beyond dependency installation. Pin and install the same version for these instructions, or revise the network/reproducibility documentation.

vpetersson-bot and others added 3 commits September 22, 2026 12:06
Publishing was two manual uploads: download the zips from the build job,
then hand them to the Chrome Web Store dashboard and the AMO developer hub.

publish.yaml now does it. It runs when a release is promoted out of
pre-release, downloads that release's own artifacts rather than rebuilding
them, verifies their provenance and their version against the tag, and
submits: Chrome via chrome-webstore-upload-cli, Firefox via web-ext sign
against the listed channel. Both stores review by hand afterwards, so
neither job waits for the result.

AMO requires source alongside a bundled build, so the Firefox job attaches
an archive of the tagged commit and SOURCE_BUILD_INSTRUCTIONS.md tells a
reviewer how to reproduce dist/ from it.

Attestation was skipped on tag builds, because its condition only held for
pull requests from this repository. Release artifacts therefore carried no
provenance at all, contrary to what CONTRIBUTING.md claimed, and the new
verification step depends on it. The condition now covers tags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
npx resolved chrome-webstore-upload-cli@4 and web-ext@10 on every run, so a
bad release inside either major would have run with the store credentials in
its environment. Both are now exact versions, installed in a separate step
that holds no secrets and into $RUNNER_TEMP rather than the checkout, so the
credentialed steps only execute code that is already on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
A refresh token is minted by a human consenting in a browser, so CI can only
use one by keeping a copy. That copy is a standing grant derived from one
person's consent, and Google expires it after six months unused — longer than
some gaps between our releases, so the likeliest first symptom is a failed
release.

API v2 accepts service accounts, so the workflow now exchanges GitHub's OIDC
token for a Google access token valid for an hour. What the repository stores
is a provider resource name and a service account email, neither of which
grants anything by itself; the authority is an IAM binding on Google's side,
revocable without touching this repository.

chrome-webstore-upload-cli cannot take an externally minted token — it always
derives one from a client ID and refresh token — so bin/publish_chrome.sh calls
the three v2 endpoints directly, matching the upload headers, the uploadState
values and the fetchStatus polling of the reference client.

Firefox is unchanged: AMO has no federated equivalent, and its issuer and
secret do not expire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vpetersson-bot
vpetersson-bot force-pushed the feat/automate-store-releases branch from c2a1533 to 53e97e8 Compare September 22, 2026 12:09
Copilot AI review requested due to automatic review settings September 22, 2026 12:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Chrome retries can use the wrong script revision, asynchronous upload failures hide their diagnostics, and the documented Google setup omits the Security Token Service API.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)

Comment thread .github/workflows/publish.yaml
Comment thread CONTRIBUTING.md Outdated
The Firefox job checked out the released tag, so a manual retry paired the
current workflow with whatever bin/package_source.sh looked like at that tag —
and for any tag older than this branch, with no script at all. It now checks
out the default ref like the Chrome job and passes the tag to the script,
which already takes REF, so the archive is still cut from the released commit
while the script itself matches the workflow driving it.

Also enable the Security Token Service API in the setup steps: the OIDC
exchange runs on it, so omitting it fails at authentication rather than at
setup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI review requested due to automatic review settings September 22, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The documented JSON-key authentication fallback is unusable because the workflow still requires a workload identity provider.

Review effort: Lite
Findings: None

Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Chrome preflight rejects JSON-key authentication mode

CONTRIBUTING.md:155

This fallback cannot work as documented: the Chrome preflight in .github/workflows/publish.yaml still requires GCP_WORKLOAD_IDENTITY_PROVIDER, while this mode supplies GCP_SERVICE_ACCOUNT_KEY instead, so the job exits before google-github-actions/auth runs. Update the credential guard and the credential table along with this instruction so JSON-key mode requires the key and omits the WIF provider.

Low severity Undocumented network dependency for web-ext and Docker image

SOURCE_BUILD_INSTRUCTIONS.md:21

web-ext is not listed in package.json, so bunx web-ext lint below downloads another package from the registry (and the Docker path may also pull oven/bun:1.4.2). That makes the statement that no network is needed beyond the dependency install inaccurate; either document/pin this additional install or revise the network prerequisite.

Copilot AI review requested due to automatic review settings September 22, 2026 12:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The source-build instructions currently produce the Firefox manifest for Chrome and contain an inaccurate network prerequisite.

Review effort: Lite
Findings: None

Previously missed (2)

In code that hasn't changed since last review

Low severity Declare or document the web-ext dependency for offline verification

SOURCE_BUILD_INSTRUCTIONS.md:21

web-ext is not present in package.json or bun.lock, so bunx web-ext below will fetch another package during verification. That contradicts the claim that no network access is needed beyond dependency installation and prevents an offline reviewer from running all listed checks; either install/pin web-ext as part of setup or qualify this prerequisite.

Low severity Use the platform-specific manifest template for non-Docker builds

SOURCE_BUILD_INSTRUCTIONS.md:46

The non-Docker instructions always generate manifest.json from manifest-firefox.json, even though this section says the same steps support both platforms. Following these steps for the Chrome release adds Firefox's browser_specific_settings.gecko block, so the rebuilt package cannot match the Chrome artifact; select the template from the requested platform here.

The workload identity provider resource name and the service account email
are not secrets — neither authorises anything without the IAM binding behind
them — and keeping them secret hid which identity a failed run had tried. They
are now environment variables, provisioned alongside the environment itself,
so the value in GitHub follows the value in the Google project instead of
being copied across by hand.

CONTRIBUTING.md records which values are provisioned and which are set by
hand, and keeps the one step the Chrome Web Store has no API for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI review requested due to automatic review settings September 22, 2026 12:35
@vpetersson-bot
vpetersson-bot force-pushed the feat/automate-store-releases branch from 1dc15b6 to 20c1700 Compare September 22, 2026 12:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The documented no-Docker rebuild selects the Firefox manifest for every platform, and asynchronous Chrome upload failures discard the store’s detailed status response.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)

Comment on lines +45 to +46
jq --arg version "<version>" '.version = $version' \
src/manifest-firefox.json > src/manifest.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants