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
50 changes: 41 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- 'feature/**'
pull_request:
branches:
- main
- develop
- 'feature/**'
workflow_call:
Expand All @@ -20,19 +21,21 @@ on:
jobs:
compute-matrix:
name: Compute matrix
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set.outputs.matrix }}
name_suffix: ${{ steps.set.outputs.name_suffix }}
steps:
- id: set
env:
FULL_MATRIX: ${{ inputs.full || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
run: |
if [[ "${{ inputs.full }}" == "true" ]]; then
echo 'matrix={"os":["ubuntu-latest","windows-latest"],"node":[18,20,22]}' >> $GITHUB_OUTPUT
echo 'name_suffix=(full matrix)' >> $GITHUB_OUTPUT
if [[ "$FULL_MATRIX" == "true" ]]; then
echo 'matrix={"os":["ubuntu-24.04","windows-latest"],"node":[18,20,22]}' >> "$GITHUB_OUTPUT"
echo 'name_suffix=(full matrix)' >> "$GITHUB_OUTPUT"
else
echo 'matrix={"os":["ubuntu-latest"],"node":[20]}' >> $GITHUB_OUTPUT
echo 'name_suffix=' >> $GITHUB_OUTPUT
echo 'matrix={"os":["ubuntu-24.04"],"node":[20]}' >> "$GITHUB_OUTPUT"
echo 'name_suffix=' >> "$GITHUB_OUTPUT"
fi

build-and-test:
Expand All @@ -46,7 +49,7 @@ jobs:
matrix: ${{ fromJSON(needs.compute-matrix.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Configure git EOL (Windows)
if: runner.os == 'Windows'
Expand All @@ -55,7 +58,7 @@ jobs:
git config core.eol lf

- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm
Expand All @@ -80,7 +83,36 @@ jobs:

- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-${{ matrix.os }}-node${{ matrix.node }}
path: coverage

renovate-config:
name: Validate Renovate configuration
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Use Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24

- name: Validate configuration
run: npx --yes --package renovate@44.155.8 -- renovate-config-validator --strict renovate.json

ci-success:
name: CI passed
if: ${{ always() }}
needs: [compute-matrix, build-and-test, renovate-config]
runs-on: ubuntu-24.04
permissions: {}
steps:
- name: Require all checks to pass
env:
CHECKS_PASSED: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped') }}
run: test "$CHECKS_PASSED" = true
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ jobs:

release:
name: Release & Publish
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: ci
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
fetch-tags: true

- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
cache: 'npm'
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/renovate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Update Dependencies

on:
schedule:
- cron: '15 2 * * 0'
timezone: America/Los_Angeles
workflow_dispatch:

permissions:
contents: read

concurrency:
group: renovate-dependencies
cancel-in-progress: false

jobs:
renovate:
name: Create or update dependency PR
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- name: Check GitHub App configuration
env:
APP_CLIENT_ID: ${{ vars.RENOVATE_APP_CLIENT_ID }}
APP_PRIVATE_KEY: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}
run: |
if [[ -z "$APP_CLIENT_ID" || -z "$APP_PRIVATE_KEY" ]]; then
echo '::error::Set RENOVATE_APP_CLIENT_ID (Actions variable) and RENOVATE_APP_PRIVATE_KEY (Actions secret). See CONTRIBUTING.md.'
exit 1
fi

- name: Create repository-scoped GitHub App token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
# Omitting owner/repositories scopes the shared app's token to this repository.
client-id: ${{ vars.RENOVATE_APP_CLIENT_ID }}
private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}

- name: Resolve bot identity
id: bot
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
BOT_LOGIN="${APP_SLUG}[bot]"
BOT_ID=$(gh api "/users/${BOT_LOGIN}" --jq .id)
printf 'username=%s\n' "$BOT_LOGIN" >> "$GITHUB_OUTPUT"
printf 'git-author=%s <%s+%s@users.noreply.github.com>\n' "$BOT_LOGIN" "$BOT_ID" "$BOT_LOGIN" >> "$GITHUB_OUTPUT"

- name: Run Renovate
uses: renovatebot/github-action@f0f41e5f2e706165f6814b1d0f0d0e319de7d1d9 # v46.3.8
with:
token: ${{ steps.app-token.outputs.token }}
renovate-version: 44.155.8
env:
RENOVATE_PLATFORM: github
RENOVATE_REPOSITORIES: ${{ github.repository }}
RENOVATE_AUTODISCOVER: 'false'
RENOVATE_ONBOARDING: 'false'
RENOVATE_REQUIRE_CONFIG: required
RENOVATE_USERNAME: ${{ steps.bot.outputs.username }}
RENOVATE_GIT_AUTHOR: ${{ steps.bot.outputs.git-author }}
81 changes: 78 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Please also read and follow our [Code of Conduct](./CODE_OF_CONDUCT.md).
- Commit messages (Conventional Commits)
- What affects versioning (SemVer policy)
- Release process (release-it + GitHub Actions)
- Automated dependency updates
- Code style and quality
- Local development
- Running tests
Expand All @@ -40,6 +41,7 @@ We follow a simplified GitFlow:
Typical flows:
- New work: branch from `develop` into `feature/<name>`, open PR into `develop`.
- Release: open a pull request from the chosen base branch (typically `develop`) into `main`. After merge, CI publishes a new version and npm package. Then merge `main` back into `develop` to sync.
- Dependency updates: Renovate creates `deps/update-dependencies` from `main` and opens a PR back into `main`.

Note: We do not use `release/*` or `hotfix/*` branches.

Expand Down Expand Up @@ -97,8 +99,8 @@ Notes:
We automate releases with [release-it](https://github.com/release-it/release-it) and GitHub Actions.

Branches/Triggers:
- Pull requests into `main`: CI runs checks and may run a release dry-run (no tag, no publish) to validate versioning and the
generated notes.
- Pull requests into `main`: CI runs the full OS/Node matrix, including published consumer declarations, and validates
the Renovate configuration. The `CI passed` check requires every job to succeed.
- `main` (on merge): CI runs a Release Publish that will:
- determine the next version from commits (SemVer + Conventional Commits),
- update `CHANGELOG.md`,
Expand All @@ -115,6 +117,79 @@ Important:
- After a successful release, merge `main` back into `develop` to keep versions and files in sync.
- We do not use `release/*` or `hotfix/*` branches.

## Automated dependency updates

The **Update Dependencies** workflow is scheduled every Sunday at 02:15 in `America/Los_Angeles`.
GitHub handles daylight saving changes in that timezone and can delay scheduled runs. To run it at any other time,
open **Actions → Update Dependencies → Run workflow**, select `main`, and start the workflow.
The weekly schedule does not restrict manual runs.

Renovate reads `renovate.json` from the default branch (`main`) and manages one branch, `deps/update-dependencies`,
with a PR into `main`. It updates only `@addon-core/browser` in `dependencies` and `@types/chrome` in `devDependencies`,
together with `package-lock.json`. Stable updates of any size share that PR, including when only one package changes.
The Chrome peer dependency remains `*`; unrelated direct dependencies and standalone lockfile maintenance are disabled.
Transitive lockfile changes required by the selected updates are still included.

The bot uses `fix(deps)` commits, so an approved dependency-only update produces a **patch** release of Storage,
including Chrome-types-only updates and updates to a new upstream major version. Keep the `fix(deps): ...` title when
squashing; do not change it to `chore`, `feat`, or add a breaking marker. Other commits included in a release continue
to follow the normal release policy. Review upstream compatibility before merging.

PR creation or updates automatically trigger CI through the GitHub App's token. **Merging is always manual.** Failed
checks leave the PR open; they do not update `main` or publish a release. Fix the incompatibility, rerun a transiently
failed check, or let a later weekly/manual run update the PR to a newer dependency version. If you add your own commit
to the bot branch, Renovate stops updating it automatically to preserve your changes. If no update or rebase is needed,
a manual run does not create an empty PR or rerun unchanged CI; use Actions **Re-run jobs** for that.

After a successful merge, the existing Release workflow runs its checks, publishes Storage, and synchronizes `main`
back into `develop`.

### One-time GitHub setup

1. Create one shared GitHub App in the `addon-stack` organization, for example `addon-stack-dependency-updates`.
It does not need webhooks or user authorization. Install it using **Only select repositories**, starting with
`storage`; add other package repositories to the same installation as their workflows are introduced.
2. Give the app these repository permissions: **Contents**, **Pull requests**, **Issues**, **Checks**, and
**Commit statuses**: read and write; **Administration**: read; **Metadata**: read (automatic).
For an organization app, grant **Members**: read. Workflow-file writes, merge-queue access, and Dependabot-alert
access are not needed for this configuration. Do not give the dependency bot a branch-protection bypass.
3. Under the organization's **Settings → Secrets and variables → Actions**, create:
- Organization variable `RENOVATE_APP_CLIENT_ID`: the app's **Client ID**.
- Organization secret `RENOVATE_APP_PRIVATE_KEY`: the full PEM private key generated for the app.
Restrict access to the selected package repositories for both. The workflow reads organization-level values through
the same `vars` and `secrets` expressions; repository-level values also work if organization-level sharing is
unavailable. It creates a short-lived installation token scoped to the current repository. No PAT or npm publishing
token is needed by Renovate. Release publishing keeps its existing authentication.
4. Merge the workflow, `renovate.json`, and CI changes into `main`. Scheduled runs and the manual workflow entry require
the workflow file on the default branch. Do not also enable the hosted Mend Renovate App for this repository.
5. Run **Update Dependencies** manually and inspect the first PR. Require **CI passed** in the `main` branch
rules, require the branch to be up to date, and keep maintainer review/manual merging. Select the PR workflow's check,
not the nested check from a post-merge release. A failed, cancelled, or skipped prerequisite makes this check fail.
6. Account for the existing release workflow when configuring branch rules: `release-it` pushes its version/changelog
commit directly to `main`, and the sync job writes to `develop`. A rule requiring PRs for every write must have a
narrowly scoped exception for the release automation. The dependency app and its credentials are not used for
release publishing. Do not activate a rule that blocks these release writes without configuring a release writer.

### Reusing the app in other packages

Each package can keep its own workflow, schedule, dependency rules, CI, and release process. The shared app supplies
the bot identity and authentication. To onboard another package, add the repository to the app installation and to
the access lists for the organization variable and secret, then add its workflow and `renovate.json`.

The current token action intentionally omits `owner` and `repositories`, which limits each generated token to the
repository running the workflow. `RENOVATE_REPOSITORIES` also targets that repository only. Concurrency groups are
repository-scoped, so matching workflow names in different packages do not block one another.

The private key itself can mint tokens for all repositories accessible to the app. Share it only among repositories
with the same trusted maintainers; use separate apps for different trust boundaries or permission requirements.

Renovate and the GitHub App token action are pinned in `.github/workflows/renovate.yml`. When upgrading Renovate itself,
also update the matching validator version in `.github/workflows/ci.yml`.

References: [Renovate GitHub Action](https://github.com/renovatebot/github-action),
[GitHub App authentication](https://docs.renovatebot.com/modules/platform/github/), and
[GitHub workflow schedules](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule).

## Code style and quality
We use [ESLint](https://eslint.org/) with TypeScript support and
[ESLint Stylistic](https://eslint.style/). The single configuration is `eslint.config.js`.
Expand Down Expand Up @@ -249,7 +324,7 @@ this checkout's Git state. Formatting checks also verify that a second pass make

## Pull requests
Checklist for contributors:
- [ ] Branch from `develop`.
- [ ] Branch from `develop`, except for the automated dependency PR described above.
- [ ] Follow code style and run locally:
- `npm run lint && npm run typecheck && npm run test:all && npm run build`
- [ ] Write or update tests when applicable.
Expand Down
54 changes: 54 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranchPatterns": [
"main"
],
"branchPrefix": "deps/",
"enabledManagers": [
"npm"
],
"includePaths": [
"package.json",
"package-lock.json"
],
"automerge": false,
"dependencyDashboard": false,
"ignoreUnstable": true,
"prCreation": "immediate",
"rebaseWhen": "behind-base-branch",
"semanticCommits": "enabled",
"semanticCommitType": "fix",
"semanticCommitScope": "deps",
"separateMajorMinor": false,
"separateMinorPatch": false,
"lockFileMaintenance": {
"enabled": false
},
"vulnerabilityAlerts": {
"enabled": false
},
"packageRules": [
{
"description": "Keep dependency updates limited to the explicitly enabled packages.",
"matchPackageNames": [
"*"
],
"enabled": false
},
{
"matchPackageNames": [
"@addon-core/browser",
"@types/chrome"
],
"matchDepTypes": [
"dependencies",
"devDependencies"
],
"enabled": true,
"rangeStrategy": "bump",
"groupName": "dependencies",
"groupSlug": "update-dependencies",
"groupSingleUpdates": true
}
]
}
5 changes: 2 additions & 3 deletions tests/consumer-types/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ try {
type: "module",
dependencies: {
"@addon-core/storage": `file:${archive}`,
"@types/chrome": readJson(join(root, "node_modules/@addon-core/browser/package.json")).dependencies[
"@types/chrome"
],
"@addon-core/browser": installedVersion("@addon-core/browser"),
"@types/chrome": installedVersion("@types/chrome"),
react: react === "19" ? installedVersion("react") : "18.3.1",
"react-dom": react === "19" ? installedVersion("react-dom") : "18.3.1",
"@types/react": react === "19" ? installedVersion("@types/react") : "^18.3.0",
Expand Down