Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
156 changes: 156 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Standalone, so the published package lints on its own: every rule is
* written out here rather than extended from a shared config.
*
* @type {import('eslint').Linter.Config}
*/
module.exports = {
root: true,
ignorePatterns: ['node_modules', 'dist', 'build', 'desktop/release'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', '@eslint-community/eslint-comments'],
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
project: [
'./tsconfig.json',
'./desktop/tsconfig.json',
'./scripts/tsconfig.json',
],
tsconfigRootDir: __dirname,
ecmaFeatures: { jsx: true },
},
env: {
node: true,
es6: true,
},
overrides: [
{
// The app's client runs in the browser: Node's globals and modules
// aren't there (the Node types the tests need would hide it otherwise).
files: ['src/app/client/**/*.{ts,tsx}'],
env: { browser: true },
rules: {
'no-restricted-globals': [
'error',
...[
'process',
'Buffer',
'__dirname',
'__filename',
'require',
'global',
].map((name) => ({
name,
message: 'The client runs in the browser.',
})),
],
// The typescript-eslint version, for `allowTypeImports`: the client
// may share the server's types, never its code.
'no-restricted-imports': 'off',
'@typescript-eslint/no-restricted-imports': [
'error',
{
paths: [
'fs',
'path',
'os',
'crypto',
'child_process',
'http',
'https',
'net',
'stream',
'url',
'util',
'events',
].map((name) => ({
name,
message: 'The client runs in the browser.',
})),
patterns: [
{ group: ['node:*'], message: 'The client runs in the browser.' },
{
group: ['@repo/*'],
message: 'Lambda is standalone: no @repo/* imports.',
},
{
group: [
'../server',
'../server/*',
'../devProxy',
'../../mothership',
'../../mothership/*',
'../../runner',
'../../runner/*',
'../../events',
'../../events/*',
'../../commands/*',
'../../pod-store',
'../../pod-store/*',
],
allowTypeImports: true,
message:
'Server code runs in Node, not the browser: `import type` only.',
},
],
},
],
},
},
],
rules: {
'@eslint-community/eslint-comments/require-description': 'warn',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/consistent-type-assertions': [
'warn',
{ assertionStyle: 'never' },
],
'arrow-body-style': ['warn', 'as-needed'],
curly: ['warn', 'all'],
'no-else-return': ['warn', { allowElseIf: false }],
'prefer-destructuring': ['warn', { array: true, object: true }],
'no-restricted-syntax': [
'warn',
{
selector: 'VariableDeclaration[kind="let"]',
message:
'Use const instead of let. Prefer functional patterns (ternary operators, early returns, or extracted functions) over let for better code clarity and immutability.',
},
{
selector: "UnaryExpression[operator='!'][argument.operator='!']",
message: 'Use Boolean(value) instead of !!value for type coercion.',
},
{
selector:
"CallExpression[callee.property.name='push']:not([callee.object.name='router']):not([callee.object.name='history']):not([callee.object.property.name='router']):not([callee.object.property.name='history'])",
message:
'Array.push() mutates arrays. Use spread operator [...arr, item] or concat() instead.',
},
],
'no-unneeded-ternary': ['warn', { defaultAssignment: false }],
'padding-line-between-statements': [
'warn',
{ blankLine: 'always', prev: '*', next: 'return' },
{ blankLine: 'always', prev: '*', next: 'throw' },
{ blankLine: 'always', prev: '*', next: 'break' },
{ blankLine: 'always', prev: '*', next: 'continue' },
{ blankLine: 'always', prev: '*', next: 'if' },
{ blankLine: 'always', prev: '*', next: 'for' },
{ blankLine: 'always', prev: '*', next: 'while' },
{ blankLine: 'always', prev: '*', next: 'try' },
{ blankLine: 'always', prev: '*', next: 'switch' },
],
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@repo/*'],
message: 'Lambda is standalone: no @repo/* imports.',
},
],
},
],
},
};
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
env:
# Only the Mac app's build needs Electron itself.
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci --no-audit --no-fund
- run: npm run build
- run: npm run lint
- run: npm run typecheck
# Some tests start a real Chromium, and some a real tmux server.
- run: npx playwright install --with-deps chromium
- run: sudo apt-get update && sudo apt-get install -y tmux
- run: npm test
181 changes: 181 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
name: Release

# On every push to main: publish to npm and attach the Mac app to a GitHub
# Release, but only when package.json's version isn't on npm yet. The `build`
# job installs, tests and builds with no secrets beyond the packaging step's
# Apple ones, and hands on only its artifacts. Both `build` and `publish` run
# in the `release` environment, which waits for its reviewer's Approve: the
# Apple secrets are `release` environment secrets, and GitHub gives those only
# to jobs that name it. `publish` runs nothing from the repository: npm
# trusted publishing (OIDC) is tied to this file and that environment, and
# there is no npm token anywhere.
#
# So a release asks for two Approves, one after the other: `build`'s (for the
# Apple secrets), then `publish`'s (for the id-token). That is deliberate. One
# job would hand an id-token that can publish to npm to the install, test and
# build steps, which run other people's code; approving only the first leaves
# the app built and nothing published, and the run can be approved later.
#
# Actions are pinned to commits (the comment names the tag).

on:
push:
branches: [main]

permissions:
contents: read

jobs:
version:
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.check.outputs.publish }}
version: ${{ steps.check.outputs.version }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: Is this version on npm yet?
id: check
run: npx --yes jiti@2.6.1 scripts/shouldPublish.ts

build:
needs: version
if: needs.version.outputs.publish == 'true'
runs-on: macos-15
environment: release
permissions:
contents: read
outputs:
name: ${{ steps.pack.outputs.name }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci --no-audit --no-fund
- run: npm run build
# Some tests start a real Chromium, and some a real tmux server.
- run: npx playwright install --with-deps chromium
- run: brew install tmux
- run: npm test

# What npm publishes is this tarball, exactly as tested.
- name: Pack the npm package
id: pack
run: |
TARBALL="$(npm pack --ignore-scripts | tail -n 1)"
echo "tarball=${TARBALL}" >> "${GITHUB_OUTPUT}"
echo "name=$(node -p "require('./package.json').name")" >> "${GITHUB_OUTPUT}"

# The Mac app builds before anything is published: once npm has a
# version it can't be published again, so a failed build after it
# would leave that version with no Release.
- run: npm run build:desktop
- run: bash desktop/tools/fetch-tools.sh
- run: npm ci --prefix desktop/kokoro --omit=dev --no-audit --no-fund
# Native modules rebuilt for Electron here, with no secrets: the
# packaging step then runs no install scripts and downloads nothing.
- run: PATH="${PWD}/node_modules/.bin:${PATH}" electron-builder install-app-deps --platform darwin --arch arm64

- name: Package the Mac app (signed and notarised only with the Apple secrets)
env:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_P12 }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_P8 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
run: |
SIGNING=()
if [ -n "${CSC_LINK}" ] && [ -n "${APPLE_API_KEY_BASE64}" ]; then
export APPLE_API_KEY="${RUNNER_TEMP}/apple-api-key.p8"
trap 'rm -f "${APPLE_API_KEY}"' EXIT
echo "${APPLE_API_KEY_BASE64}" | base64 --decode > "${APPLE_API_KEY}"
SIGNING=(-c.mac.notarize=true)
else
# package-mac.sh ad-hoc signs the bundle when CSC_LINK is unset.
echo "No Apple secrets: building without a Developer ID (ad-hoc signed)."
unset CSC_LINK CSC_KEY_PASSWORD
fi
PATH="${PWD}/node_modules/.bin:${PATH}" bash desktop/tools/package-mac.sh \
-c.npmRebuild=false \
-c.publish.provider=github \
-c.publish.owner="${OWNER}" \
-c.publish.repo="${REPO}" \
${SIGNING[@]+"${SIGNING[@]}"}

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: npm-package
path: ${{ steps.pack.outputs.tarball }}
if-no-files-found: error
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mac-app
path: |
desktop/release/*.dmg
desktop/release/*.zip
desktop/release/*.blockmap
desktop/release/latest-mac.yml
if-no-files-found: error

publish:
needs: [version, build]
if: needs.version.outputs.publish == 'true'
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
id-token: write
env:
VERSION: ${{ needs.version.outputs.version }}
NAME: ${{ needs.build.outputs.name }}
TARBALL: ${{ needs.build.outputs.tarball }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
SERVER_URL: ${{ github.server_url }}
steps:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
registry-url: https://registry.npmjs.org
# Trusted publishing needs npm 11.5.1 or later.
- run: npm install -g npm@^11.5.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: npm-package
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: mac-app
path: mac-app

# A draft first: nobody (and no updater) sees it until npm has the
# version. Each step skips what a previous attempt already did, so a
# failed run can be rerun.
- name: Draft Release v${{ env.VERSION }} with the app and latest-mac.yml
run: |
if gh release view "v${VERSION}" > /dev/null 2>&1; then
gh release upload "v${VERSION}" mac-app/* --clobber
exit 0
fi
gh release create "v${VERSION}" --draft \
--target "${SHA}" \
--title "Lambda ${VERSION}" \
--notes "Install the Mac app: ${SERVER_URL}/${GH_REPO}#install-the-mac-app" \
mac-app/*

- name: Publish to npm
run: |
if [ -n "$(npm view "${NAME}@${VERSION}" version 2> /dev/null)" ]; then
echo "${NAME}@${VERSION} is on npm already."
exit 0
fi
npm publish "${TARBALL}" --provenance --access public --ignore-scripts

- name: Publish Release v${{ env.VERSION }}
run: gh release edit "v${VERSION}" --draft=false
14 changes: 6 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
.DS_Store
node_modules/
dist/
.next/
test-results/
playwright-report/
.env*
*.local.json
.shutdown_confirmed
.ralph-stop-log
.eslintcache
# The Mac app's build output and what build:mac fetches.
desktop/release/
desktop/tools/out/
desktop/tools/.work/
desktop/kokoro/node_modules/
Loading
Loading