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
3 changes: 2 additions & 1 deletion .github/workflows/alert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ on:
- release-winget
- rpm-repo
- Smoke
- Smoke toolchain update
- TS
- Video
types:
Expand All @@ -65,7 +66,7 @@ jobs:
github.event.workflow_run.event != 'pull_request_target' &&
github.event.workflow_run.event != 'merge_group' &&
(github.event.workflow_run.event == 'schedule' ||
!contains(fromJSON('["Interop", "Nightly", "Smoke", "TS", "Video"]'), github.event.workflow_run.name))
!contains(fromJSON('["Interop", "Nightly", "Smoke", "Smoke toolchain update", "TS", "Video"]'), github.event.workflow_run.name))
runs-on: ubuntu-latest

steps:
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ name: Dependabot
# read-only token and no access to secrets, so a `pull_request` run could
# neither read APP_PRIVATE_KEY nor enable auto-merge. That trigger is only safe
# because this job never checks out or executes the pull request's code; it
# calls one `gh` command against the pull request number.
# queries the pull request files and enables auto-merge through `gh`.
#
# The moq-bot token rather than GITHUB_TOKEN: a merge enabled by GITHUB_TOKEN
# does not trigger further workflow runs, which would silently skip
Expand All @@ -25,6 +25,8 @@ on:
jobs:
auto-merge:
name: Auto-merge
permissions:
pull-requests: read
# head.repo guards the `pull_request_target` trigger: Dependabot pushes its
# branches to this repository, so a pull request from a fork claiming to be
# Dependabot is not one.
Expand All @@ -34,6 +36,18 @@ jobs:
runs-on: ubuntu-latest

steps:
# Smoke's flake and npm Playwright pin move together through its updater.
# A lock-only Dependabot PR is discovery, not a change safe to auto-enqueue.
- name: Require a coupled smoke update
env:
GH_TOKEN: ${{ github.token }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: >-
gh pr view "$PR_URL" --json files --jq
'if any(.files[]; .path == "test/smoke/flake.lock") then
error("Use the Smoke toolchain update workflow for the coupled lock and Playwright bump")
else "Update can auto-merge" end'

- name: Generate moq-bot token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
id: token
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/smoke-toolchain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Smoke toolchain update

on:
workflow_dispatch:
schedule:
- cron: "0 9 * * 1"

permissions:
contents: read

concurrency:
group: smoke-toolchain-update
cancel-in-progress: false

jobs:
update:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
persist-credentials: false
- uses: ./.github/actions/nix
- uses: ./.github/actions/just
- name: Update the coupled pins
run: just --justfile test/smoke/justfile update-toolchain
- name: Check the updated browser toolchain
run: just --justfile test/smoke/justfile check-toolchain
# Use the existing app identity to publish the validated draft proposal.
# Mint it after evaluating nixpkgs so the update itself holds no write token.
- name: Generate moq-bot token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
id: token
with:
client-id: ${{ secrets.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: moq-dev
repositories: moq
permission-contents: write
permission-pull-requests: write
- name: Propose the update
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.token.outputs.token }}
branch: smoke/toolchain-update
base: main
draft: always-true
add-paths: |
test/smoke/flake.lock
test/smoke/clients/js/package.json
commit-message: "chore(smoke): refresh the toolchain and matching Playwright pin"
title: "chore(smoke): refresh the toolchain and matching Playwright pin"
body: |
## Problem

The published-package smoke clients need a current toolchain whose Chromium matches npm Playwright.

## Approach

Refresh smoke's flake lock and derive its exact npm Playwright pin from PLAYWRIGHT_VERSION in the same change. The existing browser smoke lane consumes both pins and validates freshness.

## Impact

- Public API and wire: none. Only the smoke test toolchain changes.

## Alternatives

A lock-only update leaves the npm pin behind and fails freshness.

## Follow-ups

The updater validated freshness and launched the updated browser. Dispatch Smoke on this branch to run the full published-package matrix before marking ready. This workflow does not enqueue or merge updates.
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ lands before the cut.
- [Binding check cost](/quest/m1/binding-check-cost.md) - `moq.cpp` compiles once per C++ standard and Gradle stays warm, measured before and after
- [Audio devices on ARM](/quest/m1/audio-devices-arm.md) - `just rs audio-devices` passes on the hosted ARM runner
- [CI housekeeping](/quest/m1/ci-housekeeping.md) - queue nextest at 24 threads, stale Pages artifacts cleared, the actionlint ignore dropped
- [Smoke flake bumps](/quest/m1/smoke-dependabot.md) - `test/smoke/flake.lock` bumps with its Playwright pin, automated or documented

- [Playwright browsers from Nix](/quest/m1/playwright-nix.md) - every Playwright check runs on Nix's Chromium locally and in CI, with npm pinned to match

Expand Down
27 changes: 0 additions & 27 deletions quest/m1/smoke-dependabot.md

This file was deleted.

4 changes: 3 additions & 1 deletion test/smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ just check # shfmt, shellcheck, actionlint, freshness

Packages resolve to latest on every run: `@moq/*` on the `latest` tag, PyPI `moq-rs`, `moq.dev/moq`, `moqdev/*` images, and GitHub release tarballs. `cloudflare.sh` and `moxygen.sh` build from unpinned Git HEAD. `test/smoke/` commits no package lock files. `flake.lock` pins the dev toolchain.

npm `playwright` is pinned to the flake's `PLAYWRIGHT_VERSION`. `just freshness` fails when that pin drifts or a lock file is committed. Bump the toolchain with `nix flake update` and the pin together.
npm `playwright` is pinned to the flake's `PLAYWRIGHT_VERSION`. `just freshness` fails when that pin drifts or a lock file is committed. The weekly [Smoke toolchain update](../../.github/workflows/smoke-toolchain.yml) workflow opens or refreshes one draft PR containing both updates; failures go through the existing alerts workflow. Dispatch that workflow when a smoke-flake Dependabot PR is due, and use the coupled PR instead of merging a lock-only update. Dependabot auto-merge refuses smoke-flake updates so a lock-only proposal cannot land ahead of its matching pin.

For a local update, run `just update-toolchain` here (Nix and Python are required), then `just freshness` inside the refreshed smoke shell. The command updates `flake.lock`, evaluates its `PLAYWRIGHT_VERSION` without downloading the toolchains, and writes the matching npm pin. Commit both files together. `just check-toolchain` validates freshness and launches the updated browser in the smoke shell. The scheduled updater runs it before proposing a PR; dispatch Smoke on that branch for the full published-package matrix.

A broken package fails only its own cells.
2 changes: 1 addition & 1 deletion test/smoke/clients/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"devDependencies": {
"esbuild": "^0.28.1",
"playwright": "1.59.1",
"playwright": "1.64.1",
"vite": "^8.1.5"
}
}
12 changes: 6 additions & 6 deletions test/smoke/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/smoke/freshness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ elif [[ -n "${PLAYWRIGHT_VERSION:-}" ]]; then
if [[ "$pin" == "$PLAYWRIGHT_VERSION" ]]; then
note ok "playwright $pin == toolchain $PLAYWRIGHT_VERSION"
else
note FAIL "playwright pinned to $pin but the toolchain ships $PLAYWRIGHT_VERSION; bump $PKG"
note FAIL "playwright pinned to $pin but the toolchain ships $PLAYWRIGHT_VERSION; run just update-toolchain, then re-enter the smoke shell"
fail=1
fi
else
Expand Down
11 changes: 11 additions & 0 deletions test/smoke/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ negative *args:
# Assert we pin nothing stale: no committed lock files, and any version we are
# forced to pin (npm playwright) matches what the toolchain provides.
freshness:
python3 update.test.py
./freshness.sh

# moq's root `just check` lints these scripts too. shfmt discovers every shell
Expand Down Expand Up @@ -122,3 +123,13 @@ playwright-deps:
cloudflare-check:
cargo update --manifest-path clients/cloudflare/Cargo.toml
cargo test --manifest-path clients/cloudflare/Cargo.toml

# Refresh the toolchain lock and the exact npm Playwright pin in the same change.
update-toolchain:
python3 update.py

# Verify a proposed toolchain with its own version and a real browser launch.
check-toolchain:
nix develop --command just freshness
cd clients/js && nix develop --command bun install
cd clients/js && nix develop --command bun -e 'import {chromium} from "playwright"; const browser = await chromium.launch({channel: "chromium", headless: true}); await browser.close();'
33 changes: 33 additions & 0 deletions test/smoke/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Update smoke's Nix toolchain and its matching npm Playwright pin together."""

import json
from pathlib import Path
import re
import subprocess


def main():
smoke = Path(__file__).resolve().parent
subprocess.run(["nix", "flake", "update"], cwd=smoke, check=True)
system = subprocess.check_output(
["nix", "eval", "--impure", "--raw", "--expr", "builtins.currentSystem"], text=True
).strip()
# Evaluate the hook instead of entering the shell: updating a version must not
# download every client toolchain or run the refreshed shellHook.
hook = subprocess.check_output(
["nix", "eval", "--raw", f".#devShells.{system}.default.shellHook"], cwd=smoke, text=True
)
versions = re.findall(r'^export PLAYWRIGHT_VERSION="([0-9]+\.[0-9]+\.[0-9]+)"$', hook, re.MULTILINE)
if len(versions) != 1:
raise ValueError("smoke's shellHook must export one exact stable PLAYWRIGHT_VERSION")
version = versions[0]
manifest = smoke / "clients/js/package.json"
package = json.loads(manifest.read_text())
package["devDependencies"]["playwright"] = version
manifest.write_text(json.dumps(package, indent="\t", ensure_ascii=False) + "\n")
print(f"smoke toolchain updated with playwright {version}")


if __name__ == "__main__":
main()
72 changes: 72 additions & 0 deletions test/smoke/update.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Exercise coupled updates without downloading nixpkgs or a browser."""

import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest


class UpdateTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
shutil.copy(Path(__file__).with_name("update.py"), self.root)
(self.root / "clients/js").mkdir(parents=True)
self.manifest = self.root / "clients/js/package.json"
self.package = {"dependencies": {"@moq/watch": "latest"}, "devDependencies": {"playwright": "1.0.0"}}
self.manifest.write_text(json.dumps(self.package))
(self.root / "flake.lock").write_text("old lock")
fake = self.root / "nix"
fake.write_text("""#!/usr/bin/env python3
import os
from pathlib import Path
import sys
if sys.argv[1:] == ["flake", "update"]:
if os.environ.get("FAIL_UPDATE"):
sys.exit(1)
Path("flake.lock").write_text("new lock")
elif sys.argv[1:] == ["eval", "--impure", "--raw", "--expr", "builtins.currentSystem"]:
print("aarch64-linux")
elif sys.argv[1:] == ["eval", "--raw", ".#devShells.aarch64-linux.default.shellHook"]:
print(os.environ["HOOK"])
else:
sys.exit(2)
""")
fake.chmod(0o755)
self.env = {**os.environ, "PATH": f"{self.root}:{os.environ['PATH']}"}

def run_update(self, hook, **env):
return subprocess.run(
["python3", str(self.root / "update.py")],
env={**self.env, "HOOK": hook, **env}, text=True, capture_output=True
)

def test_updates_both_files_and_preserves_latest_moq(self):
result = self.run_update('export PLAYWRIGHT_VERSION="1.65.2"')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual((self.root / "flake.lock").read_text(), "new lock")
self.package["devDependencies"]["playwright"] = "1.65.2"
self.assertEqual(json.loads(self.manifest.read_text()), self.package)

def test_refuses_missing_ambiguous_or_unstable_version(self):
for hook in ["", 'export PLAYWRIGHT_VERSION="1.65.2-beta"',
'export PLAYWRIGHT_VERSION="1.65.2"\nexport PLAYWRIGHT_VERSION="1.65.3"']:
with self.subTest(hook=hook):
result = self.run_update(hook)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(json.loads(self.manifest.read_text()), self.package)

def test_failed_lock_update_does_not_move_pin(self):
result = self.run_update('export PLAYWRIGHT_VERSION="1.65.2"', FAIL_UPDATE="1")
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.root / "flake.lock").read_text(), "old lock")
self.assertEqual(json.loads(self.manifest.read_text()), self.package)


if __name__ == "__main__":
unittest.main()
Loading