From a4a77d0edee24583929e4cc68db83cbc531f3bdf Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Thu, 8 Oct 2026 09:00:46 +0400 Subject: [PATCH] CI: check what npm receives, keep the tree stable, stop the VPS1 deploy - pr-any: after each step the tree must be unchanged (yarn lint runs eslint --fix, yarn build rewrites packageInfo.ts), and after build scripts/check-built-manifests.py checks the package.json of each package in scripts/published-packages.txt: entry points exist, no ./build/ or ./cjs/cjs/ paths, no workspace: ranges. - push-main bumped the patch version on every push, pushed that commit to main as a bot, and copied the zips as root to 37.60.230.9, a host that has been retired; js.pezkuwichain.io no longer serves them and the extension is distributed through the stores. It now builds, checks the manifests and keeps the Chrome and Firefox packages as artifacts of the commit. The version is set in the release PR. - The auto-approve and auto-merge bots are removed. - ff-diff/, the output of yarn diff, is ignored by git and eslint. --- .github/workflows/auto-approve.yml | 16 ----- .github/workflows/auto-merge.yml | 16 ----- .github/workflows/pr-any.yml | 11 ++++ .github/workflows/push-main.yml | 98 +++++++---------------------- .gitignore | 3 + eslint.config.js | 4 ++ scripts/check-built-manifests.py | 99 ++++++++++++++++++++++++++++++ scripts/published-packages.txt | 5 ++ 8 files changed, 145 insertions(+), 107 deletions(-) delete mode 100644 .github/workflows/auto-approve.yml delete mode 100644 .github/workflows/auto-merge.yml create mode 100755 scripts/check-built-manifests.py create mode 100644 scripts/published-packages.txt diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml deleted file mode 100644 index 566790f..0000000 --- a/.github/workflows/auto-approve.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: bot - -on: - pull_request: - types: [labeled] - -jobs: - approve: - if: "! startsWith(github.event.head_commit.message, '[CI Skip]') && (!github.event.pull_request || github.event.pull_request.head.repo.full_name == github.repository)" - runs-on: ubuntu-latest - steps: - - uses: jacogr/action-approve@795afd1dd096a2071d7ec98740661af4e853b7da - with: - authors: jacogr, TarikGul - labels: -auto - token: ${{ secrets.GH_PAT_BOT }} diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml deleted file mode 100644 index caf5da1..0000000 --- a/.github/workflows/auto-merge.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: bot - -on: - pull_request: - types: [labeled] - -jobs: - merge: - runs-on: ubuntu-latest - steps: - - uses: jacogr/action-merge@d2d64b4545acd93b0a9575177d3d215ae3f92029 - with: - checks: pr (build),pr (lint),pr (test) - labels: -auto - strategy: squash - token: ${{ secrets.GH_PAT_BOT }} diff --git a/.github/workflows/pr-any.yml b/.github/workflows/pr-any.yml index 19444d2..3ae6fa5 100644 --- a/.github/workflows/pr-any.yml +++ b/.github/workflows/pr-any.yml @@ -21,3 +21,14 @@ jobs: corepack enable yarn install --immutable yarn ${{ matrix.step }} + # `yarn lint` runs eslint --fix and `yarn build` rewrites packageInfo.ts and + # the i18n catalogue, so both pass on a tree they changed. The committed + # tree must already be what they produce. + - name: tree unchanged + run: git diff --exit-code --stat + # The package.json each published package's build/ holds is what npm + # receives: entry points must exist, no ./build/ or ./cjs/cjs/ paths, no + # workspace: ranges (extension-base 0.62.19 went out with workspace:*). + - name: check built manifests + if: matrix.step == 'build' + run: python3 scripts/check-built-manifests.py . $(cat scripts/published-packages.txt) diff --git a/.github/workflows/push-main.yml b/.github/workflows/push-main.yml index 0593043..129718d 100644 --- a/.github/workflows/push-main.yml +++ b/.github/workflows/push-main.yml @@ -5,97 +5,45 @@ on: branches: - main +permissions: + contents: read + jobs: - build-and-deploy: - if: "! startsWith(github.event.head_commit.message, '[CI Skip]')" + # Builds the store packages from what was merged. Publishing to the Chrome + # Web Store and Firefox Add-ons is a manual release step: the version in + # package.json and both manifests is set in the release PR, not here. + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v4 with: node-version: 'lts/*' - - name: Bump Version - id: version - run: | - # Get current version from manifest - CURRENT_VERSION=$(grep -oP '"version":\s*"\K[^"]+' packages/extension/manifest_chrome.json | head -1) - echo "Current version: $CURRENT_VERSION" - - # Parse version parts - MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1) - MINOR=$(echo $CURRENT_VERSION | cut -d. -f2) - PATCH=$(echo $CURRENT_VERSION | cut -d. -f3) - - # Increment patch version - NEW_PATCH=$((PATCH + 1)) - NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" - echo "New version: $NEW_VERSION" - - # Update manifest files - sed -i "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" packages/extension/manifest_chrome.json - sed -i "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" packages/extension/manifest_firefox.json - - echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT - - - name: Commit Version Bump - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add packages/extension/manifest_chrome.json packages/extension/manifest_firefox.json - git diff --staged --quiet || git commit -m "[CI Skip] Bump version to ${{ steps.version.outputs.version }}" - git push || echo "Nothing to push" - - - name: Build Chrome + - name: Build run: | corepack enable yarn install --immutable - yarn build:chrome + yarn build - - name: Build Firefox - run: | - yarn build:ff + - name: Check built manifests + run: python3 scripts/check-built-manifests.py . $(cat scripts/published-packages.txt) - - name: Upload Chrome Build + - name: Upload Chrome package uses: actions/upload-artifact@v4 with: - name: chrome-build - path: master-chrome-build.zip - if-no-files-found: ignore + name: chrome-${{ github.sha }} + path: | + master-chrome-build.zip + master-chrome-src.zip + if-no-files-found: error - - name: Upload Firefox Build + - name: Upload Firefox package uses: actions/upload-artifact@v4 with: - name: firefox-build - path: master-ff-build.zip - if-no-files-found: ignore - - - name: Setup SSH - run: | - mkdir -p ~/.ssh - echo "${{ secrets.VPS1_SSH_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H 37.60.230.9 >> ~/.ssh/known_hosts - - - name: Deploy to VPS1 - run: | - # Upload zip files - scp master-chrome-build.zip master-ff-build.zip root@37.60.230.9:/var/www/subdomains/js/download/ - - # Rename and extract on server - ssh root@37.60.230.9 << 'EOF' - cd /var/www/subdomains/js/download - mv master-chrome-build.zip pezkuwi-extension-chrome.zip - mv master-ff-build.zip pezkuwi-extension-firefox.zip - - cd /var/www/subdomains/js - rm -rf extension/* - unzip -o download/pezkuwi-extension-chrome.zip -d extension/ - - echo "Deployed version:" - grep '"version"' extension/manifest.json - EOF + name: firefox-${{ github.sha }} + path: | + master-ff-build.zip + master-ff-src.zip + if-no-files-found: error diff --git a/.gitignore b/.gitignore index b0c530c..8036017 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ ff-diff*.zip temp_ff_validate/ pezkuwi-chrome-*.zip pezkuwi-firefox-*.zip + +# output of `yarn diff` (scripts/diff.sh) +ff-diff/ diff --git a/eslint.config.js b/eslint.config.js index 5cea750..29ba509 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,6 +4,10 @@ import baseConfig from '@pezkuwi/dev/config/eslint'; export default [ + { + // output of `yarn diff` (scripts/diff.sh) + ignores: ['ff-diff/**'] + }, ...baseConfig, { rules: { diff --git a/scripts/check-built-manifests.py b/scripts/check-built-manifests.py new file mode 100755 index 0000000..9b379de --- /dev/null +++ b/scripts/check-built-manifests.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Check the package.json the build wrote into each packages/*/build directory. + +This is the manifest npm receives. It fails on what broke the January 2026 +releases and wasm 7.5.19: an entry point (main, module, types, browser, +react-native, every exports target) naming a file that is not in the build +directory, a path with ./build/ or a doubled ./cjs/cjs/ in it, numeric keys +inside exports (a string spread into an object), and workspace:/portal:/link: +dependency ranges. + + check-built-manifests.py [package ...] + +Prints one line per package and exits 1 if any package fails. +""" +import json +import pathlib +import sys + +ENTRY_FIELDS = ('main', 'module', 'types', 'typings', 'browser', 'react-native') +DEP_FIELDS = ('dependencies', 'peerDependencies', 'optionalDependencies') + + +def targets(node, path): + """Yield (json-path, target string) for every string leaf under exports.""" + if isinstance(node, str): + yield path, node + elif isinstance(node, dict): + for key, value in node.items(): + yield from targets(value, f'{path}/{key}') + elif isinstance(node, list): + for i, value in enumerate(node): + yield from targets(value, f'{path}[{i}]') + + +def numeric_keys(node, path): + if isinstance(node, dict): + for key, value in node.items(): + if key.isdigit(): + yield f'{path}/{key}' + yield from numeric_keys(value, f'{path}/{key}') + + +def check(build): + problems = [] + manifest = build / 'package.json' + if not manifest.is_file(): + return ['no build/package.json'] + pkg = json.loads(manifest.read_text()) + + def check_path(where, target): + if not isinstance(target, str) or not target.startswith('.'): + return + if '/build/' in target or target.startswith('./build') or '/cjs/cjs/' in target: + problems.append(f'{where}: suspicious path {target}') + if '*' in target: + base = build / target.split('*', 1)[0] + if not base.parent.exists(): + problems.append(f'{where}: {target} has no directory') + return + if not (build / target).is_file(): + problems.append(f'{where}: {target} does not exist') + + for field in ENTRY_FIELDS: + value = pkg.get(field) + if isinstance(value, str): + check_path(field, value) + for where, target in targets(pkg.get('exports'), 'exports'): + check_path(where, target) + for where in numeric_keys(pkg.get('exports'), 'exports'): + problems.append(f'{where}: numeric key (a string spread into an object)') + for field in DEP_FIELDS: + for dep, rng in (pkg.get(field) or {}).items(): + if str(rng).startswith(('workspace:', 'portal:', 'link:', 'file:')): + problems.append(f'{field}.{dep}: {rng}') + return problems + + +def main(): + repo = pathlib.Path(sys.argv[1]) + names = sys.argv[2:] + builds = [repo / 'packages' / n / 'build' for n in names] if names else sorted(repo.glob('packages/*/build')) + failed = 0 + for build in builds: + problems = check(build) + name = build.parent.name + if problems: + failed += 1 + print(f'FAIL {name}') + for p in problems[:12]: + print(f' {p}') + if len(problems) > 12: + print(f' ... {len(problems) - 12} more') + else: + print(f'ok {name}') + sys.exit(1 if failed else 0) + + +if __name__ == '__main__': + main() diff --git a/scripts/published-packages.txt b/scripts/published-packages.txt new file mode 100644 index 0000000..34d9be2 --- /dev/null +++ b/scripts/published-packages.txt @@ -0,0 +1,5 @@ +extension-base +extension-chains +extension-compat-metamask +extension-dapp +extension-inject