Builder's Manual · Night Arcade Cabinet
Speedy Bird Service Manual
The technical docs now sit inside the same arcade world as the game: a service panel for the speed curve, ReactLynx architecture, sprite engine, native hosts, and release pipeline.
Speedy Bird Lynx
Flappy Bird clone built with ReactLynx and TypeScript. The checked-in project runs on Android and Web from a single codebase and includes iOS host source files for Xcode project setup. Lynx uses a native C++ rendering engine and dual-threaded architecture instead of a WebView.
Wiki Pages
| Page | Description |
|---|---|
| About Lynx | What Lynx is, how it differs from React Native and WebViews, and key concepts used in this project |
| Architecture | Project structure, component hierarchy, rendering approach, and dual-threaded model |
| Assets and Sprites | Sprite organization, asset loading, tile-based pipe rendering, and audio |
| CI/CD Pipeline | GitHub Actions workflows for building, signing, deploying, and releasing |
| Game Engine | Physics, collision detection, state machine, scoring, and the game loop |
| Getting Started | Setup, dev server, production builds, and platform-specific instructions |
| Native Host Apps | Android and iOS native shells that embed the Lynx runtime |
Key Features
- Tap/click to flap in the ReactLynx app; the GitHub Pages canvas demo also supports Space
- Speed increases 1% per pipe cleared
- Medal system: Bronze (10+), Silver (25+), Gold (50+), Platinum (100+)
- Element-based rendering with CSS transforms (no canvas)
- Tile-based pipe construction and parallax scrolling
- AABB collision detection with circular hitbox approximation
- Automated CI/CD for type-checking, bundle builds, CodeQL, Pages deployment, and release artifacts
About Lynx
Lynx is an open-source cross-platform native UI framework created by ByteDance (the company behind TikTok). Open-sourced in early 2025, it allows developers to write apps in TypeScript/TSX using React-like APIs and render them as truly native UIs — not WebViews.
Why Lynx for This Project
This project exists to learn Lynx by building something real. A Flappy Bird clone is a good fit because it exercises:
- Element-based rendering — no canvas, all positioning via
<view>+ CSS transforms - Frequent state updates — 60 FPS game loop pushing React state
- Touch input — tap events for gameplay
- Asset loading — images, sprites, audio
- Cross-platform builds — same code on web, Android, iOS
- CI/CD — automated build and release pipeline
How Lynx Differs from Other Frameworks
| Feature | Lynx | React Native | WebView (Cordova) |
|---|---|---|---|
| Rendering | Native C++ engine | Native bridge | Browser engine |
| UI elements | <view>, <image>, <text> etc. |
<View>, <Image>, <Text> |
HTML elements |
| Styling | CSS (200+ properties) | StyleSheet (CSS subset) | Full CSS |
| Threading | Dual (background + main) | Bridge-based | Single |
| JS engine | QuickJS (PrimJS) | Hermes/JSC | V8/JSC |
| React compat | React 17+ (ReactLynx) | React Native | React DOM |
| Build tool | Rspack (@lynx-js/rspeedy) |
Metro | Webpack/Vite |
Key Lynx Concepts Used in Speedy Bird
Built-in Elements
Lynx only has ~10 built-in elements. This game uses:
<view>— every container, positioned absolutely with transforms<image>— bird sprites, pipe tiles, background, ground, medals, digits<text>— score numbers on the game-over panel
There is no <canvas>, <svg>, <audio>, or <video>.
CSS Differences
- Default
box-sizing: border-boxandposition: relative - No margin collapsing, no inline elements
displayvalues:linear(default),flex,grid,noneoverflowonly supportsvisibleandhidden- Units:
px,%,vh— noem,rem,vw
Dual-Threaded Architecture
React reconciliation (diffing, state updates) runs on a background thread. The main thread handles native rendering and touch events. This means:
- The game loop and React state live on the background thread
- Touch events (
bindtap) are serialized from main to background thread - Native element updates are applied on the main thread after reconciliation
Native Modules
Lynx does not have built-in audio. The audio.ts module detects the environment:
- Web: uses
HTMLAudioElement(works immediately) - Native: calls a native module via
requireModule('AudioModule')(requires per-platform implementation)
Resources
Architecture
Project Structure
speedy-bird-lynx/
├── src/ # Lynx/ReactLynx application
│ ├── index.tsx # Entry point
│ ├── App.tsx # Root component
│ ├── types.ts # TypeScript types (GameState, PipeData)
│ ├── constants.ts # All game constants
│ ├── hooks/
│ │ └── useGameEngine.ts # Core game loop and physics
│ ├── components/
│ │ ├── Bird.tsx # Animated bird sprite
│ │ ├── Pipe.tsx # Tile-based pipe rendering
│ │ ├── Background.tsx # Parallax scrolling background
│ │ ├── Ground.tsx # Scrolling ground layer
│ │ ├── ScoreDisplay.tsx # Sprite-based digit rendering
│ │ ├── GetReadyScreen.tsx # Start screen overlay
│ │ └── GameOverScreen.tsx # Game over panel with medals
│ └── audio/
│ └── audio.ts # Audio abstraction (web + native)
│
├── android/ # Native Android host app
├── ios/ # Native iOS host app (source files)
├── assets/ # Sprites, audio, medals, digits
├── docs/ # Astro GitHub Pages site + playable canvas demo
├── web-host/ # Advanced/dev-only standalone <lynx-view> host
├── .github/workflows/ # CI/CD pipelines
├── lynx.config.ts # Lynx build configuration
├── rsbuild.web-host.config.ts # Web host build configuration
└── tsconfig.json # TypeScript configuration
Component Hierarchy
App
├── Background (z-index: 0, parallax scroll)
├── Pipe[] (z-index: 1, tile-based, scroll left)
├── Bird (z-index: 2, animated sprite + rotation)
├── Ground (z-index: 3, scroll matches pipe speed)
├── ScoreDisplay (z-index: 4, visible during play)
├── GetReadyScreen (z-index: 5, visible on ready)
└── GameOverScreen (z-index: 5, visible on game over)
Rendering Approach
Lynx has no canvas element. All visuals are composed from built-in elements:
<view>— containers and positioning via CSS transforms<image>— sprites loaded as individual PNGs<text>— score display on the game over panel
Every game entity is absolutely positioned at top: 0, left: 0 and moved using CSS transform: translate(Xpx, Ypx). This avoids layout recalculations — the engine only updates transform strings.
Dual-Threaded Model
Lynx runs on two threads:
| Thread | Responsibility |
|---|---|
| Background | React reconciliation, game logic, state management |
| Main | Native rendering, layout, touch event delivery |
The game loop (setInterval at 17ms) runs on the background thread. It updates a React state object (RenderState) which triggers reconciliation. Lynx’s main thread then applies the resulting native element updates.
State Management
The useGameEngine hook manages all game state:
engine.current— mutable ref holding physics state (position, velocity, pipe list). Updated every tick without triggering renders.renderState— React state snapshot pushed to components viasetRenderState(). Only updated at the end of each tick.
This separation means physics calculations do not allocate React objects — only the final render snapshot does.
Web Rendering Surfaces
| Surface | Source | Purpose |
|---|---|---|
| ReactLynx web preview | bun run dev and http://localhost:3000/__web_preview?casename=main.web.bundle |
Development preview of the compiled main.web.bundle |
| GitHub Pages canvas demo | docs/src/pages/index.astro |
Public playable browser demo; it ports the game state machine and physics to an inline <canvas> script for zero-dependency Pages playback |
| Standalone web host | web-host/ with rsbuild.web-host.config.ts |
Advanced/dev-only host that renders main.web.bundle inside <lynx-view>; web-host/index.html currently points at the configured bundle URL |
The canvas demo intentionally uses a 400x600 viewport to fit the landing-page phone frame. Core physics values such as flap force, gravity, pipe gap, speed ramp, and medal thresholds mirror src/constants.ts; the viewport height is adapted from the ReactLynx game’s 400x750 canvas height.
Game Engine
The game engine lives in src/hooks/useGameEngine.ts. It handles the game loop, physics, collision detection, scoring, and state transitions.
State Machine
The game has three states:
STATE_READY (0) ──tap──> STATE_PLAY (1) ──collision──> STATE_OVER (2)
^ │
└──────────────────────tap─────────────────────────────┘
| State | Bird | Pipes | Input |
|---|---|---|---|
| Ready | Hovers at Y=280, wing animation (20-frame interval) | None on screen | Tap starts game |
| Play | Falls with gravity, flaps on tap, fast animation (4-frame interval) | Spawn, scroll left, score on pass | Tap = flap |
| Over | Continues falling until ground contact; frame/rotation follow the same falling logic, then clamp on ground | Frozen | Tap resets to Ready |
Game Loop
The loop runs via setInterval(tick, 17) targeting ~60 FPS. Each tick:
- Update bird — apply gravity, update velocity, clamp position, compute rotation, advance animation frame
- Update pipes — spawn new pipes on interval, move all pipes left, despawn off-screen pipes (incrementing score), check collisions
- Update scenery — scroll background and ground at their respective speeds
- Push render state — call
setRenderState()with the current snapshot for React to render
Physics
All values are in pixels per frame (at 60 FPS):
| Parameter | Value | Effect |
|---|---|---|
| Gravity | 0.28 px/frame² | Downward acceleration |
| Flap velocity | -7.25 px/frame | Upward impulse on tap |
| Pipe base speed | 2.7 px/frame | Horizontal scroll speed |
| Speed scaling | +1% per pipe | speed = 2.7 * (1 + score * 0.01) |
| Background scroll | 0.2 px/frame | Slower parallax layer |
| Ground scroll | 2.7 px/frame | Matches pipe speed |
Bird Rotation
Rotation is determined by vertical velocity:
| Velocity | Rotation | Visual |
|---|---|---|
<= BIRD_FLAP (7.25) |
-15 deg | Nose up |
Between BIRD_FLAP and BIRD_FLAP + 2 (7.25–9.25) |
0 deg | Level |
>= BIRD_FLAP + 2 (9.25) |
70 deg | Nose dive |
During nose dive, the animation frame is set to frame 1 (wings level). A flap starts with velocity -BIRD_FLAP, so the bird stays nose-up until gravity increases velocity past the positive threshold.
Bird Animation
The bird has a 4-frame cycle: bird-0, bird-1, bird-2, bird-1 (ping-pong).
- Ready state: frame advances every 20 ticks (slow flutter)
- Play state: frame advances every 4 ticks (fast flapping)
- Over state: after pipe collision, the same falling/rotation branch continues while pipes stay frozen; ground collision clamps the bird to frame 2 with nose-dive rotation
Collision Detection
Collision uses AABB (Axis-Aligned Bounding Box) with a circular bird hitbox approximated as a square:
Bird bounding box:
left: BIRD_X - BIRD_RADIUS (80 - 12 = 68)
right: BIRD_X + BIRD_RADIUS (80 + 12 = 92)
top: birdY - BIRD_RADIUS
bottom: birdY + BIRD_RADIUS
Per pipe pair, two checks:
Top pipe: x to x+55, y to y+300
Bottom pipe: x to x+55, y+300+150 to y+600+150
Ground collision triggers when birdY + BIRD_H/2 >= CANVAS_HEIGHT - GROUND_H.
Pipe Spawning
- Interval: every 77 frames (~1.3s), adjusted by speed:
Math.max(20, Math.round(77 / speedMultiplier)) - Y position: random between -200 and -80 (controls gap vertical placement)
- Gap size: 150px fixed
- Despawn: when
pipe.x < -55(fully off-screen left), score increments
Scoring and Medals
Score increments by 1 each time a pipe scrolls off-screen. Best score persists within the session.
| Score | Medal |
|---|---|
| 10+ | Bronze |
| 25+ | Silver |
| 50+ | Gold |
| 100+ | Platinum |
Audio
Five sound effects, managed by src/audio/audio.ts:
| Event | Sound | File |
|---|---|---|
| Tap/flap | Wing flap | sfx_wing.wav |
| Pipe passed | Score point | sfx_point.wav |
| Pipe collision | Hit | sfx_hit.wav |
| Ground collision | Fall | sfx_die.wav |
| Reset game | Swoosh | sfx_swooshing.wav |
On web, audio uses HTMLAudioElement. On native platforms, it falls back to a native module stub (AudioModule).
Assets and Sprites
All game assets are pre-sliced individual PNG files. Lynx has no canvas or sprite sheet slicing — each visual element is a separate <image> element.
Directory Structure
assets/
├── sprites/
│ ├── bird-0.png # Wings down
│ ├── bird-1.png # Wings level
│ ├── bird-2.png # Wings up
│ ├── background.png # Sky + city background tile (276x228)
│ ├── ground.png # Ground tile (224x129)
│ ├── get-ready.png # "Get Ready" overlay (174x160)
│ ├── game-over.png # Game over panel with score boxes (226x158)
│ ├── pipes/
│ │ ├── pipe-top.png # Top pipe body tile
│ │ ├── pipe-top-mouth.png # Top pipe mouth (lip)
│ │ ├── pipe-bottom.png # Bottom pipe body tile
│ │ └── pipe-bottom-mouth.png # Bottom pipe mouth (lip)
│ ├── digits/
│ │ └── digit-0.png … digit-9.png # Score display (18x27 each)
│ └── medals/
│ ├── medal-bronze.png
│ ├── medal-silver.png
│ ├── medal-gold.png
│ └── medal-platinum.png
│
└── audio/
├── sfx_wing.wav # Flap
├── sfx_point.wav # Score
├── sfx_hit.wav # Pipe collision
├── sfx_die.wav # Ground collision
└── sfx_swooshing.wav # Game reset
How Assets Are Loaded
In the Lynx app, assets are loaded via top-level import statements at the module level:
import bird0 from '../../assets/sprites/bird-0.png';
import bird1 from '../../assets/sprites/bird-1.png';
import bird2 from '../../assets/sprites/bird-2.png';
const BIRD_SPRITES = [bird0, bird1, bird2, bird1]; // ping-pong cycle
The Rspeedy bundler resolves these to URLs (dev server) or embeds them in the bundle (production).
Pipe Rendering
Pipes use a tile-based approach instead of stretching a single image. Each pipe is composed of:
- Body tiles — repeated vertically to fill the pipe height (extends well past the screen edge)
- Mouth tile — placed at the opening where the bird flies through
This avoids visual stretching artifacts and matches the original game’s pixel-art style. Each tile is 55px wide and ~53px tall.
Background and Ground Tiling
Both the background and ground use 5 copies laid out horizontally in a flex row. The container is translated left via CSS transform to create seamless scrolling. When the offset exceeds one tile width, it wraps back.
- Background: 5 tiles at 276px each = 1380px total, scrolls at
0.2 * speedMultiplierpx/frame - Ground: 5 tiles at 224px each = 1120px total, scrolls at
2.7 * speedMultiplierpx/frame
Score Display
The in-game score uses sprite-based digit rendering (ScoreDisplay.tsx). Each digit is a separate <image> element (18x27px) laid out horizontally with 2px gaps, centered on screen.
The game-over panel score uses <text> elements positioned absolutely over the panel sprite.
Credits
- Sprites from The Spriters Resource
- Sound effects from The Sounds Resource
- Original game by Dong Nguyen
Getting Started
Prerequisites
- Bun for the root ReactLynx/Rspeedy app (
bun install,bun run dev,bun run build) - Node.js >=22.12 for the Astro 7 documentation site in
docs/(npm run dev/build/preview) - Java 17 and Android SDK (for Android builds)
- Xcode 15+ and CocoaPods (for iOS builds after creating the Xcode project from the included source scaffold)
Quick Start
git clone https://github.com/jonathanperis/speedy-bird-lynx.git
cd speedy-bird-lynx
bun install
bun run dev
This starts the Rspeedy dev server with hot module replacement. The game is accessible at:
- Web preview:
http://localhost:3000/__web_preview?casename=main.web.bundle - Lynx bundle:
http://localhost:3000/main.lynx.bundle
To view on a mobile device, open the Lynx bundle URL in Lynx Explorer or Lynx Go (replace localhost with your machine’s IP).
Building for Production
bun run build
This outputs:
dist/main.lynx.bundle— native bundle for Android/iOSdist/main.web.bundle— web bundle
Android
bun run build
cp dist/main.lynx.bundle android/app/src/main/assets/
cd android && ./gradlew assembleDebug
The APK is at android/app/build/outputs/apk/debug/app-debug.apk. Install via adb install or transfer to your device.
For release builds with signing, see CI/CD Pipeline.
iOS
Requires Xcode and an Xcode project (
.xcodeproj). The repository includes the Swift/CocoaPods source scaffold, but the Xcode project/workspace must be created locally before building. See Native Host Apps for setup instructions.
bun run build
cp dist/main.lynx.bundle ios/SpeedyBird/Resources/
cd ios && pod install
open SpeedyBird.xcworkspace
Build and run from Xcode on a simulator or device.
Web / GitHub Pages
The public web surface is the Astro site in docs/. It renders the landing page, embeds the playable canvas demo, and generates wiki pages from docs/wiki/*.md. Astro 7 requires Node.js >=22.12, so use the npm scripts for the docs dev server/build even though dependencies are installed from bun.lock.
cd docs
bun install
npm run dev
For a production build:
npm run build
npm run preview
The static output is written to docs/out/ and is deployed to GitHub Pages by the shared Pages workflow.
Web Surfaces
| Surface | How to use it | Notes |
|---|---|---|
| ReactLynx web preview | bun run dev, then open http://localhost:3000/__web_preview?casename=main.web.bundle |
Uses the compiled main.web.bundle from Rspeedy for development |
| GitHub Pages canvas demo | cd docs && npm run dev, then open the local Astro URL |
Browser-only playable demo in docs/src/pages/index.astro; physics mirror the ReactLynx game, while the 400x600 viewport is adapted to the landing-page phone frame |
| Standalone web host | Advanced/dev-only web-host/ + rsbuild.web-host.config.ts path |
Renders main.web.bundle inside <lynx-view> and currently expects the bundle URL configured in web-host/index.html |
Project Commands
| Command | Description |
|---|---|
bun run dev |
Start Rspeedy dev server with HMR |
bun run build |
Production build (Lynx + Web bundles) |
cd docs && npm run dev |
Start Astro docs/dev site with Node >=22.12 |
cd docs && npm run build |
Build Astro GitHub Pages output to docs/out/ with Node >=22.12 |
cd docs && npm run preview |
Preview the production docs build with Node >=22.12 |
cd android && ./gradlew assembleDebug |
Build debug Android APK |
cd android && ./gradlew assembleRelease |
Build release Android APK |
Native Host Apps
Lynx bundles do not run standalone — they need a thin native shell that embeds the Lynx runtime and loads the bundle. This project includes a ready-to-build Android host app and iOS source files that must be added to a locally created Xcode project before building.
Android
The Android host app is a minimal Kotlin application in android/.
Key Files
| File | Purpose |
|---|---|
SpeedyBirdApplication.kt |
Initializes Lynx engine, Fresco (image loading), and registers services |
MainActivity.kt |
Creates a LynxView and loads main.lynx.bundle from assets |
AssetTemplateProvider.kt |
Implements AbsTemplateProvider to read bundles from APK assets |
AndroidManifest.xml |
Fullscreen, portrait-only, internet permission |
build.gradle.kts |
Lynx SDK 3.7.0 dependencies, signing config from env vars |
proguard-rules.pro |
Keep rules for Lynx SDK classes during R8 minification |
Dependencies
| Artifact | Purpose |
|---|---|
org.lynxsdk.lynx:lynx |
Core rendering engine |
org.lynxsdk.lynx:lynx-jssdk |
JavaScript bridge |
org.lynxsdk.lynx:primjs |
QuickJS JavaScript engine |
org.lynxsdk.lynx:lynx-trace |
Performance tracing |
org.lynxsdk.lynx:lynx-service-image |
Image loading (wraps Fresco) |
org.lynxsdk.lynx:lynx-service-log |
Logging |
org.lynxsdk.lynx:lynx-service-http |
Network requests |
org.lynxsdk.lynx:xelement |
Extended UI elements |
org.lynxsdk.lynx:xelement-input |
Extended input elements used by the Android Lynx runtime |
com.facebook.fresco:* |
Image loading and animated image support required by lynx-service-image |
com.squareup.okhttp3:okhttp |
HTTP client support for Lynx services |
com.google.code.gson:gson |
JSON (required by Lynx internals) |
How It Works
SpeedyBirdApplication.onCreate()initializes Fresco, registers Lynx services, and callsLynxEnv.inst().init()MainActivity.onCreate()builds aLynxViewviaLynxViewBuilder, attaches theAssetTemplateProvider, and callsrenderTemplateUrl("main.lynx.bundle", "")- The
AssetTemplateProviderreads the bundle bytes fromassets/main.lynx.bundleand passes them to the Lynx engine
Signing
The build.gradle.kts reads signing configuration from environment variables:
KEYSTORE_FILE— path to keystore fileKEYSTORE_PASSWORD— keystore passwordKEY_ALIAS— key aliasKEY_PASSWORD— key password
These are populated by CI from GitHub Secrets. For local release builds, export the same environment variables in your shell before running Gradle.
Android Artifacts
| Build path | Command/workflow | Output | Signing |
|---|---|---|---|
| Local debug | bun run build, copy dist/main.lynx.bundle into android/app/src/main/assets/, then cd android && ./gradlew assembleDebug |
android/app/build/outputs/apk/debug/app-debug.apk |
Debug-signed by Android tooling |
| Local release | Same bundle copy, then cd android && ./gradlew assembleRelease |
android/app/build/outputs/apk/release/ |
Signed only when KEYSTORE_FILE, KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD are exported |
| CI release | build-android.yml on main, v* tags, or manual dispatch |
APK artifact and GitHub Release attachment | Signed only when KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD GitHub Secrets are configured |
Native Audio Status
The game audio abstraction in src/audio/audio.ts uses HTMLAudioElement on web. Native builds currently have stubs: if an Android or iOS AudioModule is not implemented and registered with the Lynx bridge, the game logs a warning and continues without sound. To enable native sound effects, implement the platform-specific module, expose the same play/preload contract used by the web implementation, and register it during app startup.
iOS
The iOS host app source files are in ios/. An Xcode project must be created manually before the app can be built or archived.
Included Files
| File | Purpose |
|---|---|
Podfile |
Lynx 3.7.0, PrimJS, SDWebImage, XElement |
AppDelegate.swift |
Initializes LynxEnv |
SceneDelegate.swift |
Creates window with ViewController |
ViewController.swift |
Fullscreen LynxView, portrait-only, hidden status bar |
BundleTemplateProvider.swift |
Loads main.lynx.bundle from the app bundle |
SpeedyBird-Bridging-Header.h |
Objective-C bridge for Lynx SDK headers |
Info.plist |
App metadata, scene configuration |
Setup Steps
- Open Xcode > File > New > Project > App
- Product Name:
SpeedyBird - Bundle Identifier:
com.jonathanperis.speedybird - Language: Swift
- Product Name:
- Delete the auto-generated Swift files
- Add all files from
ios/SpeedyBird/to the project - Build Settings > Swift Compiler > Objective-C Bridging Header > set to
SpeedyBird/SpeedyBird-Bridging-Header.h - Build Settings > User Script Sandboxing > set to NO
- Terminal:
cd ios && pod install - Open
SpeedyBird.xcworkspace(not.xcodeproj) - Build Phases > Copy Bundle Resources > add
main.lynx.bundle
Apple Developer Program
| Feature | Free | Paid ($99/year) |
|---|---|---|
| Simulator builds | Yes | Yes |
| Sideload to own device | 7-day expiry | Unlimited |
| TestFlight | No | Yes |
| App Store distribution | No | Yes |
| CI signing (certificates) | No | Yes |
Without an Apple Developer Program configuration, CI/local workflows can only produce unsigned simulator or archive outputs after the Xcode project exists. TestFlight/App Store distribution requires signing certificates, provisioning profiles, and workflow secrets.
CI/CD Pipeline
All automation runs on GitHub Actions. Workflows are in .github/workflows/.
Workflow Overview
| Workflow | File | Trigger | Description |
|---|---|---|---|
| Build Check | ci.yml |
Manual, push to main/lynx-migration, PR to main |
Type-check and build Lynx bundles; no unit-test framework is configured yet |
| CodeQL | codeql.yml |
Push/PR to main, weekly |
Security and quality analysis |
| Deploy Web | deploy.yml |
Push to main, manual |
Build and deploy the Astro docs/ site to GitHub Pages via the shared Pages workflow |
| Build Android | build-android.yml |
Push to main, v* tags, manual |
Build release APK, sign when secrets are configured, create GitHub Release |
| Build iOS | build-ios.yml |
v* tags, manual |
Build iOS archive (unsigned) |
| Release | release.yml |
v* tags, manual |
Full release pipeline with all artifacts |
Build Check
Runs on manual dispatch, pushes to main/lynx-migration, and pull requests targeting main. Validates the codebase compiles and builds:
bun install --frozen-lockfile— install dependenciesbunx tsc --noEmit— TypeScript type-checkingbun run build— build Lynx and web bundles- Upload bundles as artifact (14-day retention)
Current quality gates are TypeScript type-checking, production bundle builds, CodeQL, and Pages deployment. Unit tests, browser smoke tests, and lint/format checks are not configured yet.
Deploy Web
Deploys the Astro site in docs/ to GitHub Pages on pushes to main or manual dispatch. The repository delegates deployment to the shared reusable workflow jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@main; that shared workflow installs dependencies, uses Node.js 22 for Astro 7, builds the docs site, and publishes the static output.
Build Android
The main production workflow. On every push to main:
- Version computation — from git tag (
v1.2.3→1.2.3) or short SHA (0.0.0-a1b2c3d) - Build Lynx bundle —
bun run build - Copy bundle — into
android/app/src/main/assets/ - Decode keystore when configured — from
KEYSTORE_BASE64secret - Gradle build —
./gradlew assembleRelease, signed only when the signing env vars are present - Git tag — creates
build/<version>tag onmainpushes - GitHub Release — creates a release with APK attached
Android Signing Secrets
| Secret | Purpose |
|---|---|
KEYSTORE_BASE64 |
Base64-encoded release keystore |
KEYSTORE_PASSWORD |
Keystore password |
KEY_ALIAS |
Key alias name |
KEY_PASSWORD |
Key password |
Android Artifact Matrix
| Artifact | Trigger/path | Signing/status |
|---|---|---|
| Local debug APK | cd android && ./gradlew assembleDebug after copying dist/main.lynx.bundle into assets |
Debug-signed by Android tooling |
| CI main-build APK | build-android.yml on push to main or manual dispatch |
Release build; signed only when keystore secrets are configured |
| Tagged release APK | build-android.yml or release.yml on v* tags |
Attached to the GitHub Release; signed when KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD are present |
Versioning
| Trigger | Version Name | Version Code | Release Type |
|---|---|---|---|
Push to main |
0.0.0-<sha> |
Epoch-based | Automated build release (build/<version> tag) |
Tag v1.2.3 |
1.2.3 |
Epoch-based | Full release |
Build iOS
Scaffolded but requires manual setup:
- Create Xcode project (see Native Host Apps)
- Enroll in Apple Developer Program ($99/year)
- Configure signing secrets
Currently builds an unsigned archive only after an Xcode project exists. The checked-in ios/ directory contains Swift/CocoaPods source files, not a generated .xcodeproj; local developers must create the Xcode project and add the source files before building. TestFlight/App Store distribution requires Apple Developer Program signing assets and workflow secrets. See the workflow file header for detailed setup instructions.
Release
Triggered by version tags (v*) or manual dispatch. Builds Lynx bundles and conditionally builds Android/iOS if their native projects exist. Creates a GitHub Release with all available artifacts and auto-generated release notes.