Cloudsteading
EdgeEver mobile notes list with upstream demonstration notes

EdgeEver

Self-hosted notes, web clipping and synchronization on Workers

EdgeEver is a self-hosted Evernote/Notion alternative built on Cloudflare (D1, R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: AGPL-3.0

License TL;DR

You can use and change it, even commercially. If people use your modified version over a network, offer them its corresponding source under the AGPL. Sharing copies has source-sharing duties too. Sharing source code is different from sharing users’ content.

Explain AGPL v3 in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@tianma-if

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

Small personal note libraries can fit Workers, D1 and standard R2 free allowances. R2 activation requires a billing-enabled account even when usage stays free. Storage, writes and Worker CPU must remain within plan limits; optional external AI usage is separately billed.

Hosting requirements
  • Provision your own D1 database and R2 bucket, run the documented schema/deployment steps and keep the deployment updated.
  • The 10ms Free Worker CPU ceiling and per-day D1 limits have not been benchmarked for this app. Larger resources or AI usage can produce additional charges.
  • Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: 9918570. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • evernote ↗

    > 💡 **Serverless & 100% Free Forever** > EdgeEver can run within Cloudflare's free quotas with no server purchase or VPS maintenance. Users who

  • notion ↗

    > 💡 **Serverless & 100% Free Forever** > EdgeEver can run within Cloudflare's free quotas with no server purchase or VPS maintenance. Users who

  • workers ↗

    name = "edgeever" main = ".wrangler/edgeever-worker/index.js" compatibility_date = "2026-06-26" workers_dev = true no_bundle = true find_additional_modules = true base_dir = ".wrangler/edgeever-worker" [[rules]] type = "ESModule" globs = ["modules/*.js"] fallthrough = true [build] command = "bun scripts/build-cloudflare-worker.mjs" watch_dir = ["apps/api/src", "packag

  • d1 ↗

    binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = ["/api/*", "/mcp", "/__scheduled"] [[d1_databases]] binding = "DB" database_name = "edgeever" database_id = "00000000-0000-0000-0000-000000000000" migrations_dir = "migrations" [[r2_buckets]] binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resources-preview" [observability] enabled = true

  • r2 ↗

    nding = "DB" database_name = "edgeever" database_id = "00000000-0000-0000-0000-000000000000" migrations_dir = "migrations" [[r2_buckets]] binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resources-preview" [observability] enabled = true

  • free-tier-eligible ↗

    name = "edgeever" main = ".wrangler/edgeever-worker/index.js" compatibility_date = "2026-06-26" workers_dev = true no_bundle = true find_additional_modules = true base_dir = ".wrangler/edgeever-worker" [[rules]] type = "ESModule" globs = ["modules/*.js"] fallthrough = true [build] command = "bun scripts/build-cloudflare-worker.mjs" watch_dir = ["apps/api/src", "packages/shared/src"] [assets] directory = "apps/web/dist" binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = ["/api/*", "/mcp", "/__scheduled"] [[d1_databases]] binding = "DB" database_name = "edgeever" database_id = "00000000-0000-0000-0000-000000000000" migrations_dir = "migrations" [[r2_buckets]] binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resour

  • free-tier-eligible ↗

    | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |

  • free-tier-eligible ↗

    | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |

  • free-tier-eligible ↗

    | Storage | 10 GB-month / month |

  • free-tier-eligible ↗

    | Class A Operations | 1 million requests / month |

  • free-tier-eligible ↗

    | Class B Operations | 10 million requests / month |

  • free-tier-eligible ↗

    | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows |

  • AGPL-3.0 ↗

    GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you

  • architecture ↗

    name = "edgeever" main = ".wrangler/edgeever-worker/index.js" compatibility_date = "2026-06-26" workers_dev = true no_bundle = true find_additional_modules = true base_dir = ".wrangler/edgeever-worker" [[rules]] type = "ESModule" globs = ["modules/*.js"] fallthrough = true [build] command = "bun scripts/build-cloudflare-worker.mjs" watch_dir = ["apps/api/src", "packages/shared/src"] [assets] directory = "apps/web/dist" binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = ["/api/*", "/mcp", "/__scheduled"] [[d1_databases]] binding = "DB" database_name = "edgeever" database_id = "00000000-0000-0000-0000-000000000000" migrations_dir = "migrations" [[r2_buckets]] binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resour

  • architecture ↗

    name = "edgeever" main = ".wrangler/edgeever-worker/index.js" compatibility_date = "2026-06-26" workers_dev = true no_bundle = true find_additional_modules = true base_dir = ".wrangler/edgeever-worker" [[rules]] type = "ESModule" globs = ["modules/*.js"] fallthrough = true [build] command = "bun scripts/build-cloudflare-worker.mjs" watch_dir = ["apps/api/src", "packag

  • architecture ↗

    binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = ["/api/*", "/mcp", "/__scheduled"] [[d1_databases]] binding = "DB" database_name = "edgeever" database_id = "00000000-0000-0000-0000-000000000000" migrations_dir = "migrations" [[r2_buckets]] binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resources-preview" [observability] enabled = true

  • architecture ↗

    nding = "DB" database_name = "edgeever" database_id = "00000000-0000-0000-0000-000000000000" migrations_dir = "migrations" [[r2_buckets]] binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resources-preview" [observability] enabled = true

Upstream screenshot · tianma-if/edgeever repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

external SaaS target
varies
→ D1 + R2 + Workers
external SaaS target
varies
→ D1 + R2 + Workers

How it works

The shape of EdgeEver on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
edgeever
wrangler.toml
↓
App
edgeever
entry
Cloudflare Workers
Entrypoint: .wrangler/edgeever-worker/index.js
↓

Configuration and workflow sources

Reviewed commit 9918570868c8. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 1 files
wrangler.toml ↗

Cloudflare Workers · compatibility 2026-06-26

edgeever · default

Entrypoint: .wrangler/edgeever-worker/index.js

Build: bun scripts/build-cloudflare-worker.mjs

Static assets: apps/web/dist · single-page-application · Worker first: ["/api/*","/mcp","/__scheduled"]

  • DB → D1
  • RESOURCES → R2
  • ASSETS → Static assets

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

No direct runtime declarations resolved from this snapshot. Generated framework bundles or dynamic entrypoints need manual tracing.

Build and deployment pipeline · 18 GitHub Actions workflows

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

Audit Android Play Signature · .github/workflows/android-play-signature-audit.yml ↗

Triggers: workflow_dispatch

Require Play-signed Draft APK · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Validate matching Draft Releaserelease="$(gh release view "$RELEASE_TAG" \ --repo "$GITHUB_REPOSITORY" \ --json isDraft,isPrerelease,tagName,targetCommitish)" test "$(jq -r '.isDraft' <<<"$release")" = "true" test "$(jq -r '.isPrerelease' <<<"$release")" = "false" test "$(jq -r '.tagName' <<<"$release")" = "$RELEASE_TAG" release_target="$(jq -r '.targetCommitish' <<<"$release")" test -n "$release_target" echo "target_commitish=$release_target" >> "$GITHUB_OUTPUT"
  2. Check out the immutable Draft targetactions/checkout@v5
  3. Verify the checked-out Draft targettest "$(git rev-parse HEAD)" = "$(git rev-parse "${RELEASE_TARGET}^{commit}")"
  4. Download the Draft Android APKmkdir -p "$RUNNER_TEMP/android-release" gh release download "$RELEASE_TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern 'edgeever-android-v*-arm64-v8a.apk' \ --dir "$RUNNER_TEMP/android-release" test "$(find "$RUNNER_TEMP/android-release" -type f -name 'edgeever-android-v*-arm64-v8a.apk' | wc -l | tr -d ' ')" = "1" apk_path="$(find "$RUNNER_TEMP/android-release" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)" echo "apk_path=$apk_path" >> "$GITHUB_ENV"
  5. Require the Play app-signing certificatetest -n "$ANDROID_PLAY_APP_SIGNER_SHA256" node scripts/verify-android-apk-signature.mjs "$apk_path"
Deploy Demo · .github/workflows/deploy-demo.yml ↗

Triggers: release, workflow_dispatch

Build and deploy Demo Worker · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Checkout Demo sourceactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Install dependenciesbun install --frozen-lockfile
  4. Resolve Release metadataif [ -z "${RELEASE_PUBLISHED_AT}" ]; then RELEASE_PUBLISHED_AT="$(gh api repos/tianma-if/edgeever/releases/latest --jq '.published_at')" fi echo "EDGE_EVER_RELEASED_AT=${RELEASE_PUBLISHED_AT}" >> "${GITHUB_ENV}"
  5. Deploy Demo Workerbun run deploy
  6. Verify Demo URLcurl --fail --silent --show-error --location --max-time 20 "https://${DEMO_DEPLOY_DOMAIN}/"Condition: env.DEMO_DEPLOY_DOMAIN != ''
Deploy personal EdgeEver · .github/workflows/deploy-personal.yml ↗

Triggers: release, workflow_dispatch

Build and deploy personal Worker · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Resolve formal Releaseset -euo pipefail release_tag="${EVENT_RELEASE_TAG}" release_published_at="${EVENT_RELEASE_PUBLISHED_AT}" if [[ -z "${release_tag}" ]]; then release_tag="$(gh api repos/tianma-if/edgeever/releases/latest --jq '.tag_name')" release_published_at="$(gh api repos/tianma-if/edgeever/releases/latest --jq '.published_at')" fi if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Expected a formal vX.Y.Z Release, got ${release_tag}" >&2 exit 1 fi { echo "tag=${release_tag}" echo "version…
  2. Checkout personal instance sourceactions/checkout@v5
  3. Set up Bunoven-sh/setup-bun@v2
  4. Install dependenciesbun install --frozen-lockfile
  5. Deploy personal Workerbun run deploy
  6. Verify deployed Release versionnode --input-type=module <<'NODE' const rawBaseUrl = process.env.EDGE_EVER_DEPLOYMENT_URL?.trim() ?? ""; const expectedVersion = process.env.EXPECTED_RELEASE_VERSION?.trim() ?? ""; if (!rawBaseUrl || !expectedVersion) { throw new Error("Deployment URL and expected Release version are required."); } const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(rawBaseUrl) ? rawBaseUrl : `https://${rawBaseUrl}`; const releaseUrl = new URL("/api/release", `${baseUrl.replace(/\/$/, "")}/`); let actualVersion = "…
Deploy EdgeEver Site · .github/workflows/deploy-site.yml ↗

Triggers: push, workflow_dispatch

Deploy Cloudflare Pages · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Checkoutactions/checkout@v5
  2. Setup Bunoven-sh/setup-bun@v2
  3. Install dependenciesbun install --frozen-lockfile
  4. Build sitebun run build:site
  5. Deploy sitecloudflare/wrangler-action@v3Wrangler command: pages deploy apps/site/dist --project-name=edgeever-official
Test deployment tools · .github/workflows/deploy-tools.yml ↗

Triggers: pull_request, push

Wrangler runner (${{ matrix.os }}) · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Checkoutactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Install dependenciesbun install --frozen-lockfile
  4. Launch project-local Wranglerbun scripts/run-wrangler.mjs --version
  5. Test deployment toolingbun test tests/wrangler-runner.test.ts tests/deployment-verify.test.ts tests/deployment-entrypoints.test.ts
Desktop build · .github/workflows/desktop-build.yml ↗

Triggers: workflow_dispatch, release

Plan desktop release asset · no job dependencies declared

Condition: ${{ github.repository == 'tianma-if/edgeever' && ( github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ) }}

  1. Check out sourceactions/checkout@v5
  2. Validate Draft Release targetexpected_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")" test "$RELEASE_TAG" = "v${expected_version}" test "$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')" = "true" target_commitish="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json targetCommitish --jq '.targetCommitish')" test "$(git rev-parse "${target_commitish}^{commit}")" = "$(git rev-parse "${GITHUB_SHA}^{commit}")"Condition: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
  3. Compare with previous formal releasemapfile -t release_tags < <( gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ --jq '.[] | select(.draft == false and .prerelease == false) | .tag_name' ) previous_tag="" for release_tag in "${release_tags[@]}"; do if [[ "$release_tag" != "$CURRENT_TAG" ]]; then previous_tag="$release_tag" break fi done test -n "$previous_tag" git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}" echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT" plan_output="$(node scripts/plan-na…

Reuse desktop release assets · after release-plan

Condition: ${{ github.repository == 'tianma-if/edgeever' && github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'false' }}

  1. Copy latest compatible desktop assets without renamingmkdir -p "$RUNNER_TEMP/native-assets" gh release download "$PREVIOUS_TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern 'EdgeEver-*-mac-arm64.dmg' \ --pattern 'EdgeEver-*-mac-arm64.dmg.blockmap' \ --pattern 'EdgeEver-*-mac-arm64.zip' \ --pattern 'EdgeEver-*-mac-arm64.zip.blockmap' \ --pattern 'EdgeEver-*-mac-x64.dmg' \ --pattern 'EdgeEver-*-mac-x64.dmg.blockmap' \ --pattern 'EdgeEver-*-mac-x64.zip' \ --pattern 'EdgeEver-*-mac-x64.zip.blockmap' \ --pattern 'latest-mac.yml' \ --pattern 'EdgeEver-*-wi…

macOS ${{ matrix.arch }} · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && ( (github.event_name == 'workflow_dispatch' && inputs.release_tag == '') || ( github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'true' ) ) }}

  1. Check out sourceactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Set up Rustdtolnay/rust-toolchain@stable
  4. Cache Bun dependencies for shared validationactions/cache@v5Condition: matrix.shared_validation
  5. Cache Rust dependencies and sidecar buildsactions/cache@v5
  6. Cache Electron downloadsactions/cache@v5
  7. Import macOS signing identityif [[ -z "$MAC_CERTIFICATE_DER_BASE64" || -z "$MAC_PRIVATE_KEY_BASE64" ]]; then echo "::error::EDGEEVER_MAC_CERTIFICATE_DER_BASE64 and EDGEEVER_MAC_PRIVATE_KEY_BASE64 are required" exit 1 fi keychain_path="$RUNNER_TEMP/edgeever-build.keychain-db" keychain_password="$(openssl rand -hex 32)" certificate_path="$RUNNER_TEMP/edgeever-mac-signing.cer" private_key_path="$RUNNER_TEMP/edgeever-mac-signing.pem" traditional_key_path="$RUNNER_TEMP/edgeever-mac-signing-rsa.pem" printf '%s' "$MAC_CERTIFICATE…
  8. Install dependenciesbun install --frozen-lockfile
  9. Build Web rendererbun run build:web
  10. Verify Web performance budgetbun run verify:web-performanceCondition: matrix.shared_validation
  11. Run project type checksbun run typecheck bun run typecheck:mobileCondition: matrix.shared_validation
  12. Build debug sidecar for integration testscargo build --manifest-path crates/desktop-sidecar/Cargo.tomlCondition: matrix.shared_validation
  13. Run desktop regression testsbun run test:desktopCondition: matrix.shared_validation
  14. Verify renderer origin storage migrationbun run verify:renderer-origin-migrationCondition: matrix.shared_validation
  15. Verify packaged renderer startupbun run verify:desktop-rendererCondition: matrix.shared_validation
  16. Validate Rust sidecarcargo fmt --manifest-path crates/desktop-sidecar/Cargo.toml -- --check cargo clippy --manifest-path crates/desktop-sidecar/Cargo.toml --all-targets -- -D warnings cargo test --manifest-path crates/desktop-sidecar/Cargo.tomlCondition: matrix.shared_validation
  17. Build architecture-specific Rust sidecarcargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
  18. Prepare Apple notarization API keyif [[ -z "$APPLE_API_KEY_BASE64" || -z "$APPLE_API_KEY_ID" || -z "$APPLE_API_ISSUER" ]]; then echo "::error::Apple notarization API key secrets are required" exit 1 fi apple_api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" printf '%s' "$APPLE_API_KEY_BASE64" | base64 -D > "$apple_api_key_path" chmod 600 "$apple_api_key_path" echo "APPLE_API_KEY=$apple_api_key_path" >> "$GITHUB_ENV" echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV" echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> …
  19. Package desktop installerbun scripts/run-desktop-builder.mjs --publish never
  20. Verify packaged macOS cross-version startupprevious_directory="$RUNNER_TEMP/edgeever-previous-${{ matrix.arch }}" mkdir -p "$previous_directory/archive" "$previous_directory/app" gh release download "$PREVIOUS_TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern 'EdgeEver-*-mac-${{ matrix.arch }}.zip' \ --dir "$previous_directory/archive" previous_archive="$(find "$previous_directory/archive" -maxdepth 1 -type f -name '*.zip' -print -quit)" test -n "$previous_archive" ditto -x -k "$previous_archive" "$previous_directory/app" previous_executab…Condition: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
  21. Verify packaged macOS first launchunpacked_directory="mac-${{ matrix.arch }}" if [[ "${{ matrix.arch }}" == "x64" ]]; then unpacked_directory="mac" fi bun run verify:packaged-desktop-startup -- "release/desktop/$unpacked_directory/EdgeEver.app/Contents/MacOS/EdgeEver"
  22. Verify packaged macOS private protocol file flowsunpacked_directory="mac-${{ matrix.arch }}" if [[ "${{ matrix.arch }}" == "x64" ]]; then unpacked_directory="mac" fi bun run verify:desktop-protocol-e2e -- "release/desktop/$unpacked_directory/EdgeEver.app/Contents/MacOS/EdgeEver"
  23. Verify desktop installerbun scripts/verify-desktop-package.mjs
  24. Stage architecture-specific desktop assetsactions/upload-artifact@v6

Windows x64 unsigned Preview · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && ( (github.event_name == 'workflow_dispatch' && inputs.release_tag == '') || ( github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'true' ) ) }}

  1. Check out sourceactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Set up Rustdtolnay/rust-toolchain@stable
  4. Cache Bun dependenciesactions/cache@v5
  5. Cache Rust dependencies and sidecar buildsactions/cache@v5
  6. Cache Electron downloadsactions/cache@v5
  7. Install dependenciesfor ($attempt = 1; $attempt -le 3; $attempt++) { bun install --frozen-lockfile $installExitCode = $LASTEXITCODE if ($installExitCode -eq 0) { exit 0 } if ($attempt -eq 3) { exit $installExitCode } Write-Warning "Dependency installation failed on attempt $attempt; retrying." Start-Sleep -Seconds (15 * $attempt) }
  8. Build Web rendererbun run build:web
  9. Build x64 Rust sidecarcargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
  10. Package unsigned Windows installerbun scripts/run-desktop-builder.mjs --publish never
  11. Verify packaged Windows cross-version startup$previousDirectory = Join-Path $env:RUNNER_TEMP "edgeever-previous-windows-x64" New-Item -ItemType Directory -Force -Path $previousDirectory | Out-Null gh release download $env:PREVIOUS_TAG ` --repo $env:GITHUB_REPOSITORY ` --pattern 'EdgeEver-*-windows-x64.exe' ` --dir $previousDirectory if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $previousInstaller = Get-ChildItem -LiteralPath $previousDirectory -Filter '*.exe' | Select-Object -First 1 if (-not $previousInstaller) { throw "Previous Window…Condition: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
  12. Verify Windows package contents and architecturebun scripts/verify-desktop-package.mjs
  13. Run packaged Windows sidecar integration tests$env:EDGE_EVER_SIDECAR_PATH = (Resolve-Path "release/desktop/win-unpacked/resources/sidecar/edgeever-sidecar.exe").Path $env:EDGE_EVER_MIGRATIONS_PATH = (Resolve-Path "release/desktop/win-unpacked/resources/migrations").Path bun scripts/test-desktop-sidecar.mjs
  14. Verify packaged Windows first launchbun run verify:packaged-desktop-startup -- release/desktop/win-unpacked/EdgeEver.exe
  15. Verify packaged Windows private protocol file flowsbun run verify:desktop-protocol-e2e -- release/desktop/win-unpacked/EdgeEver.exe
  16. Verify that Preview executables are unsigned$version = (Get-Content apps/desktop/package.json | ConvertFrom-Json).version $paths = @( "release/desktop/EdgeEver-$version-windows-x64.exe", "release/desktop/win-unpacked/EdgeEver.exe", "release/desktop/win-unpacked/resources/sidecar/edgeever-sidecar.exe" ) foreach ($path in $paths) { $signature = Get-AuthenticodeSignature -LiteralPath $path if ($signature.Status -ne "NotSigned") { throw "Unsigned Preview asset unexpectedly has an Authenticode signature: $path ($($signature.Status))" } }
  17. Create Windows update metadata$version = (Get-Content apps/desktop/package.json | ConvertFrom-Json).version node scripts/create-windows-update-metadata.mjs release/desktop $version
  18. Stage Windows Preview assetsactions/upload-artifact@v6

Linux x64 AppImage Preview · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && ( (github.event_name == 'workflow_dispatch' && inputs.release_tag == '') || ( github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'true' ) ) }}

  1. Check out sourceactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Set up Rustdtolnay/rust-toolchain@stable
  4. Install AppImage runtime dependenciessudo apt-get update sudo apt-get install --yes libfuse2
  5. Cache Bun dependenciesactions/cache@v5
  6. Cache Rust dependencies and sidecar buildsactions/cache@v5
  7. Cache Electron downloadsactions/cache@v5
  8. Install dependenciesfor attempt in 1 2 3; do if bun install --frozen-lockfile; then exit 0 fi if [[ "$attempt" = "3" ]]; then exit 1 fi sleep "$((15 * attempt))" done
  9. Build Web rendererbun run build:web
  10. Build x64 Rust sidecarcargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
  11. Build Linux automatic update predecessorif [[ -n "$PREVIOUS_TAG" ]]; then git fetch --depth=1 origin "refs/tags/${PREVIOUS_TAG}:refs/tags/${PREVIOUS_TAG}" if git show "${PREVIOUS_TAG}:apps/desktop/src/main/index.mjs" | grep -q 'linuxUpdateTestMode'; then previous_directory="$RUNNER_TEMP/edgeever-previous-linux-x64" mkdir -p "$previous_directory" gh release download "$PREVIOUS_TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern 'EdgeEver-*-linux-x64.AppImage' \ --dir "$previous_directory" previous_app_image="$(find "$previous_directory" -m…
  12. Package Linux AppImagebun scripts/run-desktop-builder.mjs --publish never
  13. Verify Linux package contents, architecture, and glibc baselinebun scripts/verify-desktop-package.mjs
  14. Run packaged Linux sidecar integration testsbun scripts/test-desktop-sidecar.mjs
  15. Verify packaged Linux first launchexecutable="" for candidate in release/desktop/linux-unpacked/EdgeEver release/desktop/linux-unpacked/edgeever; do if [[ -x "$candidate" ]]; then executable="$candidate" break fi done test -n "$executable" xvfb-run -a bun run verify:packaged-desktop-startup -- "$executable"
  16. Verify packaged Linux private protocol file flowsexecutable="" for candidate in release/desktop/linux-unpacked/EdgeEver release/desktop/linux-unpacked/edgeever; do if [[ -x "$candidate" ]]; then executable="$candidate" break fi done test -n "$executable" xvfb-run -a bun run verify:desktop-protocol-e2e -- "$executable"
  17. Create Linux checksumversion="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")" cd release/desktop sha256sum "EdgeEver-${version}-linux-x64.AppImage" > SHA256SUMS-linux.txt sha256sum --check SHA256SUMS-linux.txt
  18. Verify Linux automatic update metadataversion="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")" node scripts/verify-linux-update-release.mjs release/desktop "$version"
  19. Verify real Linux AppImage automatic updateversion="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")" xvfb-run -a bun scripts/verify-linux-appimage-update.mjs \ "$RUNNER_TEMP/EdgeEver-linux-update-source.AppImage" \ "release/desktop/EdgeEver-${version}-linux-x64.AppImage" \ "$version"
  20. Stage Linux Preview assetsactions/upload-artifact@v6

Report desktop build timings · after desktop, windows, linux

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && (needs.desktop.result != 'skipped' || needs.windows.result != 'skipped' || needs.linux.result != 'skipped') }}

  1. Check out timing reporteractions/checkout@v5
  2. Collect native build timingsgh api \ "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?filter=latest&per_page=100" \ > "$RUNNER_TEMP/actions-jobs.json" node scripts/report-native-build-timings.mjs \ --input "$RUNNER_TEMP/actions-jobs.json" \ --platform desktop \ --json "$RUNNER_TEMP/native-build-timings.json" \ --markdown "$RUNNER_TEMP/native-build-timings.md" cat "$RUNNER_TEMP/native-build-timings.md" >> "$GITHUB_STEP_SUMMARY"
  3. Upload machine-readable timing reportactions/upload-artifact@v6

Finalize desktop release assets · after release-plan, desktop, windows, linux

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && needs.desktop.result == 'success' && needs.windows.result == 'success' && needs.linux.result == 'success' && ( (github.event_name == 'workflow_dispatch' && inputs.release_tag == '') || ( github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'true' ) ) }}

  1. Check out sourceactions/checkout@v5
  2. Download platform-specific assetsactions/download-artifact@v7
  3. Create and audit combined update metadataversion="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")" node scripts/create-mac-update-metadata.mjs release/desktop "$version" node scripts/verify-linux-update-release.mjs release/desktop "$version" asset_ready="$( find release/desktop -maxdepth 1 -type f -exec basename {} \; | node scripts/check-native-release-assets.mjs desktop true "v${version}" "$version" allow-missing-windows-signature )" test "$asset_ready" = "true"
  4. Upload verified desktop assets to Releasegh release upload "$RELEASE_TAG" release/desktop/* \ --repo "$GITHUB_REPOSITORY" \ --clobberCondition: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
  5. Upload combined desktop installers and metadataactions/upload-artifact@v6

Audit signed Windows update · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'true' && ( github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ) }}

  1. Check out sourceactions/checkout@v5
  2. Download Windows release assetsmkdir -p release/desktop node scripts/download-release-assets.mjs \ --repo "$GITHUB_REPOSITORY" \ --tag "$RELEASE_TAG" \ --dir release/desktop \ --pattern 'EdgeEver-*-windows-x64.exe' \ --pattern 'latest.yml' \ --pattern 'latest-windows.json' \ --pattern 'latest-windows.json.sig' \ --pattern 'SHA256SUMS-windows.txt'
  3. Verify signature and installer digestsnode scripts/verify-windows-update-release.mjs release/desktop

Audit Linux Preview asset · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'true' && ( github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ) }}

  1. Check out sourceactions/checkout@v5
  2. Download Linux release assetsmkdir -p release/desktop node scripts/download-release-assets.mjs \ --repo "$GITHUB_REPOSITORY" \ --tag "$RELEASE_TAG" \ --dir release/desktop \ --pattern 'EdgeEver-*-linux-x64.AppImage' \ --pattern 'latest-linux.yml' \ --pattern 'SHA256SUMS-linux.txt'
  3. Verify Linux automatic update releasenode scripts/verify-linux-update-release.mjs release/desktop
Build and publish Docker image · .github/workflows/docker-image.yml ↗

Triggers: pull_request, push, workflow_dispatch, release

Build and verify container · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever' && github.event_name != 'release'

  1. Checkoutactions/checkout@v5
  2. Build local imagedocker build --build-arg EDGE_EVER_BUILD_ID="${GITHUB_SHA}" --tag edgeever:ci .
  3. Start containerdocker run --detach --name edgeever-ci \ --env EDGE_EVER_AUTH_PASSWORD=container-ci-password \ --publish 127.0.0.1:8787:8787 \ edgeever:ci
  4. Verify shared API and persistent storagefor attempt in $(seq 1 60); do if health="$(curl --fail --silent --show-error http://127.0.0.1:8787/api/health)"; then break fi sleep 1 done echo "${health}" | jq -e '.ok == true and .runtime == "self-hosted-bun" and .authMode == "required"' curl --fail --silent --show-error http://127.0.0.1:8787/ | grep -F '<!doctype html>' docker exec edgeever-ci test -f /data/edgeever.sqlite docker exec edgeever-ci test -d /data/resources docker exec edgeever-ci test -s /app/scripts/self-hosted-server.js doc…
  5. Stop container gracefullydocker stop --timeout 30 edgeever-ci docker logs edgeever-ci | grep -F '[self-hosted] shutdown complete'
  6. Show container logs on failuredocker logs edgeever-ci || trueCondition: failure()

Publish official multi-platform image · after container-test

Condition: github.repository == 'tianma-if/edgeever' && github.event_name != 'pull_request' && github.event_name != 'release'

  1. Checkoutactions/checkout@v5
  2. Resolve image tagsif [[ -n "${RELEASE_TAG}" ]]; then if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "release_tag must match vX.Y.Z" >&2 exit 1 fi release_json="$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json isDraft,isPrerelease,targetCommitish)" if [[ "$(jq -r '.isDraft' <<<"${release_json}")" != "true" ]] || [[ "$(jq -r '.isPrerelease' <<<"${release_json}")" != "false" ]]; then echo "${RELEASE_TAG} must identify a non-prerelease Draft release" >&2 exit 1 fi version=…
  3. Set up QEMUdocker/setup-qemu-action@v3
  4. Set up Docker Buildxdocker/setup-buildx-action@v3
  5. Sign in to GitHub Container Registrydocker/login-action@v3
  6. Build and publish imagedocker/build-push-action@v6
  7. Verify anonymous GHCR image accessdocker logout ghcr.io docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" docker buildx imagetools inspect --raw "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" | jq -e '([.manifests[].platform.architecture] | index("amd64") != null) and ([.manifests[].platform.architecture] | index("arm64") != null)' docker pull "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" container_name=edgeever-published-audit trap 'docker rm --force "${container_…

Audit published Docker image · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever' && github.event_name == 'release'

  1. Verify prepared public image tagsrelease_inspect="$(docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:${RELEASE_TAG}")" latest_inspect="$(docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:latest")" release_digest="$(sed -n 's/^Digest:[[:space:]]*//p' <<<"${release_inspect}" | head -n 1)" latest_digest="$(sed -n 's/^Digest:[[:space:]]*//p' <<<"${latest_inspect}" | head -n 1)" test -n "${release_digest}" test "${release_digest}" = "${latest_digest}" docker buildx imagetools inspect --raw "${GHCR_IMAGE_NAME}:${RELEASE_TAG}"…
Build Tencent TCR image through CNB · .github/workflows/docker-tcr-mirror.yml ↗

Triggers: release, workflow_dispatch

Trigger asynchronous Tencent-side image build · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Resolve verified source and CNB destinationif [[ "${EVENT_NAME}" == "release" ]]; then source_ref="${RELEASE_TAG}" destination_ref="${RELEASE_TAG}" else source_ref="${INPUT_SOURCE_REF}" destination_ref="${INPUT_DESTINATION_REF}" fi if [[ "${destination_ref}" != "main" ]] && [[ ! "${destination_ref}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "destination_ref must be main or vX.Y.Z" >&2 exit 1 fi { echo "source_ref=${source_ref}" echo "destination_ref=${destination_ref}" } >> "${GITHUB_OUTPUT}"
  2. Check out the GHCR-verified sourceactions/checkout@v5
  3. Verify formal source metadatatest "$(jq -r '.version' package.json)" = "${RELEASE_TAG#v}" test "$(git rev-parse HEAD)" = "$(git rev-list -n 1 "${RELEASE_TAG}")"Condition: steps.source.outputs.destination_ref != 'main'
  4. Push the same Git commit to CNBtest -n "${CNB_PUSH_TOKEN}" auth="$(printf 'cnb:%s' "${CNB_PUSH_TOKEN}" | base64 -w 0)" if [[ "${DESTINATION_REF}" == "main" ]]; then destination="refs/heads/main" else destination="refs/tags/${DESTINATION_REF}" fi git -c http.extraHeader="Authorization: Basic ${auth}" push \ https://cnb.cool/tianma-if/edgeever \ "HEAD:${destination}"
Build browser extensions · .github/workflows/extension-build.yml ↗

Triggers: pull_request, push, workflow_dispatch

Build Chromium and Firefox · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Checkoutactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Install dependenciesbun install --frozen-lockfile
  4. Test extension manifestsbun run test:extension
  5. Build Chromium extensionbun run build:extension
  6. Build Firefox extensionbun run build:extension:firefox
  7. Lint Firefox extensionbun run lint:extension:firefox
Build EdgeEver iOS · .github/workflows/ios-build.yml ↗

Triggers: push, pull_request, workflow_dispatch

Xcode build + unit tests · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Checkoutactions/checkout@v5
  2. Select Xcodesudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer || \ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer xcodebuild -version
  3. Install XcodeGenbrew install xcodegen
  4. Generate projectxcodegen generate
  5. Resolve packagesxcodebuild -project EdgeEver.xcodeproj -scheme EdgeEver \ -resolvePackageDependencies \ -destination 'generic/platform=iOS Simulator'
  6. Buildset -o pipefail xcodebuild build \ -project EdgeEver.xcodeproj \ -scheme EdgeEver \ -destination 'generic/platform=iOS Simulator' \ CODE_SIGNING_ALLOWED=NO \ | xcpretty || true xcodebuild build \ -project EdgeEver.xcodeproj \ -scheme EdgeEver \ -destination 'generic/platform=iOS Simulator' \ CODE_SIGNING_ALLOWED=NO
  7. TestDEST="$( xcodebuild -project EdgeEver.xcodeproj -scheme EdgeEver -showdestinations 2>/dev/null \ | sed -n 's/.*name:\(iPhone[^,}]*\).*/\1/p' \ | head -1 )" if [[ -z "$DEST" ]]; then DEST="iPhone 16" fi echo "Using simulator: $DEST" xcodebuild test \ -project EdgeEver.xcodeproj \ -scheme EdgeEver \ -destination "platform=iOS Simulator,name=$DEST" \ CODE_SIGNING_ALLOWED=NO \ -only-testing:EdgeEverTests
Control iOS Xcode Cloud · .github/workflows/ios-xcode-cloud.yml ↗

Triggers: workflow_dispatch

xcode-cloud · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Check out Xcode Cloud toolingactions/checkout@v5
  2. Install App Store Connect client dependenciespython3 -m pip install --quiet PyJWT cryptography
  3. Inspect, start, or monitor Xcode Cloudcase "$XCODE_CLOUD_MODE" in inspect) python3 scripts/xcode-cloud-control.py inspect ;; start) args=(start --wait) if [[ -n "$XCODE_CLOUD_WORKFLOW_ID" ]]; then args+=(--workflow-id "$XCODE_CLOUD_WORKFLOW_ID") fi python3 scripts/xcode-cloud-control.py "${args[@]}" ;; describe) if [[ -z "$XCODE_CLOUD_BUILD_RUN_ID" ]]; then echo "::error::build_run_id is required in describe mode" exit 1 fi python3 scripts/xcode-cloud-control.py describe \ --build-run-id "$XCODE_CLOUD_BUILD_RUN_ID" ;; monitor) if […
Build EdgeEver Mobile · .github/workflows/mobile-build.yml ↗

Triggers: workflow_dispatch, release, push

Plan Android release asset · no job dependencies declared

Condition: ${{ github.repository == 'tianma-if/edgeever' && ( github.event_name == 'release' || github.event_name == 'workflow_dispatch' ) }}

  1. Checkoutactions/checkout@v5
  2. Validate Draft Release targetexpected_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")" test "$RELEASE_TAG" = "v${expected_version}" test "$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')" = "true" target_commitish="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json targetCommitish --jq '.targetCommitish')" test "$(git rev-parse "${target_commitish}^{commit}")" = "$(git rev-parse "${GITHUB_SHA}^{commit}")"Condition: github.event_name == 'workflow_dispatch'
  3. Set up Bun for release validationoven-sh/setup-bun@v2
  4. Install release validation dependenciesbun install --frozen-lockfile
  5. Run full project regression testsbun run test
  6. Compare with previous formal releasemapfile -t release_tags < <( gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ --jq '.[] | select(.draft == false and .prerelease == false) | .tag_name' ) previous_tag="" for release_tag in "${release_tags[@]}"; do if [[ "$release_tag" != "$CURRENT_TAG" ]]; then previous_tag="$release_tag" break fi done test -n "$previous_tag" git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}" echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT" plan_output="$(node scripts/plan-na…
  7. Verify existing APK uses the pinned signermkdir -p "$RUNNER_TEMP/native-assets" node scripts/download-release-assets.mjs \ --repo "$GITHUB_REPOSITORY" \ --tag "$CURRENT_TAG" \ --dir "$RUNNER_TEMP/native-assets" \ --pattern 'edgeever-android-v*-arm64-v8a.apk' apk_path="$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)" test -n "$apk_path" test -n "$ANDROID_PLAY_APP_SIGNER_SHA256" node scripts/verify-android-apk-signature.mjs "$apk_path"Condition: steps.plan.outputs.asset_ready == 'true'

Restore invalid Android Release to Draft · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && github.event_name == 'release' && needs.release-plan.result == 'failure' }}

  1. Restore Draft stategh release edit "$CURRENT_TAG" --repo "$GITHUB_REPOSITORY" --draft=true

Reuse Android release APK · after release-plan

Condition: ${{ github.repository == 'tianma-if/edgeever' && github.event_name == 'workflow_dispatch' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'false' }}

  1. Checkoutactions/checkout@v5
  2. Copy latest compatible APK without renamingmkdir -p "$RUNNER_TEMP/native-assets" gh release download "$PREVIOUS_TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern 'edgeever-android-v*-arm64-v8a.apk' \ --dir "$RUNNER_TEMP/native-assets" test "$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' | wc -l | tr -d ' ')" = "1" apk_path="$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)" test -n "$ANDROID_PLAY_APP_SIGNER_SHA256" node scripts/verify-android-apk-signa…

Build Android packages · after release-plan

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && ( (github.event_name == 'push' && github.ref == 'refs/heads/main') || ( github.event_name == 'workflow_dispatch' && needs.release-plan.result == 'success' && needs.release-plan.outputs.asset_ready == 'false' && needs.release-plan.outputs.rebuild == 'true' ) ) }}

  1. Checkoutactions/checkout@v5
  2. Install dependenciesbun install --frozen-lockfile
  3. Check Expo dependenciesif ! bun --cwd apps/mobile expo install --check; then echo "::warning::Expo reported compatible patch-version updates; continuing with the frozen lockfile for this build." fi
  4. Typecheckbun run typecheck:mobile
  5. Reset Metro cache for this checkoutbun --cwd apps/mobile expo export --platform android --clear --max-workers 1 --output-dir "$RUNNER_TEMP/edgeever-metro-warmup"
  6. Restore Android signing keystoretest -n "$ANDROID_KEYSTORE_BASE64" printf '%s' "$ANDROID_KEYSTORE_BASE64" | openssl base64 -d -A > "$RUNNER_TEMP/edgeever-upload.p12" chmod 600 "$RUNNER_TEMP/edgeever-upload.p12"
  7. Build fast main-branch APKbun run build:android:fastCondition: github.event_name == 'push'
  8. Build signed release APK for GitHub Releasebun run build:android:apkCondition: github.event_name == 'workflow_dispatch'
  9. Verify release APK signaturenode scripts/verify-android-apk-signature.mjs apps/mobile/android/app/build/outputs/apk/release/app-release.apkCondition: github.event_name == 'workflow_dispatch'
  10. Upload APKactions/upload-artifact@v7Condition: github.event_name == 'push'
  11. Upload release APK to GitHub Releaserelease_asset="$RUNNER_TEMP/edgeever-android-${RELEASE_TAG}-arm64-v8a.apk" cp apps/mobile/android/app/build/outputs/apk/release/app-release.apk "$release_asset" gh release upload "$RELEASE_TAG" "$release_asset" --repo "$GITHUB_REPOSITORY" --clobberCondition: github.event_name == 'workflow_dispatch'
  12. Remove temporary upload keystorerm -f "$RUNNER_TEMP/edgeever-upload.p12"Condition: always()

Report Android build timings · after android

Condition: ${{ always() && github.repository == 'tianma-if/edgeever' && needs.android.result != 'skipped' }}

  1. Checkout timing reporteractions/checkout@v5
  2. Collect native build timingsgh api \ "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?filter=latest&per_page=100" \ > "$RUNNER_TEMP/actions-jobs.json" node scripts/report-native-build-timings.mjs \ --input "$RUNNER_TEMP/actions-jobs.json" \ --platform android \ --json "$RUNNER_TEMP/native-build-timings.json" \ --markdown "$RUNNER_TEMP/native-build-timings.md" cat "$RUNNER_TEMP/native-build-timings.md" >> "$GITHUB_STEP_SUMMARY"
  3. Upload machine-readable timing reportactions/upload-artifact@v7
Audit Play Generated APKs · .github/workflows/play-generated-apk-audit.yml ↗

Triggers: workflow_dispatch

audit · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. actions/checkout@v5actions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Install dependenciesbun install --frozen-lockfile
  4. Inspect generated APK typesnode scripts/download-play-universal-apk.mjs "${{ inputs.version_code }}" --inspect
Report Release build timings · .github/workflows/release-timings.yml ↗

Triggers: workflow_dispatch

Collect all Release endpoint timings · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Check out timing reporteractions/checkout@v5
  2. Resolve Release and post-publication workflow runsrelease_json="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease,publishedAt,targetCommitish,url)" test "$(jq -r '.isDraft' <<<"$release_json")" = "false" test "$(jq -r '.isPrerelease' <<<"$release_json")" = "false" test "$(jq -r '.targetCommitish' <<<"$release_json")" = "$RELEASE_SHA" find_release_run() { local workflow="$1" local deadline=$((SECONDS + 120)) while ((SECONDS < deadline)); do run_id="$( gh run list \ --repo "$GITHUB_REPOSITORY" \ --workflow …
  3. Wait for Cloudflare deployment and TCR source syncwait_for_run() { local run_id="$1" while true; do run_json="$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json status,conclusion,url)" status="$(jq -r '.status' <<<"$run_json")" if [[ "$status" = "completed" ]]; then jq -r '.conclusion' <<<"$run_json" return 0 fi sleep 10 done } echo "demo_conclusion=$(wait_for_run "$DEMO_RUN_ID")" >> "$GITHUB_OUTPUT" echo "tcr_source_conclusion=$(wait_for_run "$TCR_RUN_ID")" >> "$GITHUB_OUTPUT"
  4. Observe public TCR Release readinessstatus="failure" ready_at="" if [[ "$SOURCE_CONCLUSION" = "success" ]]; then for attempt in $(seq 1 180); do if raw_manifest="$(docker buildx imagetools inspect --raw "${TCR_IMAGE}:${RELEASE_TAG}" 2>/dev/null)" && jq -e '([.manifests[].platform.architecture] | index("amd64") != null) and ([.manifests[].platform.architecture] | index("arm64") != null)' <<<"$raw_manifest" >/dev/null; then metadata="$(docker buildx imagetools inspect "${TCR_IMAGE}:${RELEASE_TAG}" --format '{{json .Image}}' 2>/dev/…
  5. Download exact Actions timing datacollect_run() { local name="$1" local run_id="$2" gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}" > "$RUNNER_TEMP/${name}-run.json" gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/${name}-jobs.json" jq -s '{run: .[0], jobs: .[1].jobs}' \ "$RUNNER_TEMP/${name}-run.json" \ "$RUNNER_TEMP/${name}-jobs.json" \ > "$RUNNER_TEMP/${name}.json" } collect_run desktop "$DESKTOP_RUN_ID" collect_run mobile "$MOBILE_RUN_ID" collect_run dock…
  6. Generate unified Release timing reportstore_args=() if [[ -f "$RUNNER_TEMP/store.json" ]]; then store_args=(--store "$RUNNER_TEMP/store.json") fi node scripts/report-release-timings.mjs \ --tag "${{ inputs.release_tag }}" \ --sha "${{ inputs.release_sha }}" \ --published-at "${{ steps.release.outputs.published_at }}" \ --release-url "$RELEASE_URL" \ --desktop "$RUNNER_TEMP/desktop.json" \ --desktop-mode "${{ inputs.desktop_mode }}" \ --mobile "$RUNNER_TEMP/mobile.json" \ --mobile-mode "${{ inputs.mobile_mode }}" \ --docker "$RUNNER…
  7. Return timing report to the related Issuemarker="<!-- edgeever-release-build-timings:${RELEASE_TAG} -->" comment_file="$RUNNER_TEMP/release-build-timings-comment.md" printf '%s\n\n' "$marker" > "$comment_file" cat "$RUNNER_TEMP/release-build-timings.md" >> "$comment_file" comment_id="$( gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/comments?per_page=100" \ --paginate \ --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1 )" if [[ -n "$comment_id" ]]; then jq -n --rawfile body "$comment_file" '{body: $body…
  8. Upload machine-readable Release timing reportactions/upload-artifact@v7
Deliver Mobile Stores · .github/workflows/store-delivery.yml ↗

Triggers: workflow_dispatch

Validate store delivery source · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Validate Release and resolve its formal predecessorrelease="$( gh release view "$RELEASE_TAG" \ --repo "$GITHUB_REPOSITORY" \ --json isDraft,isPrerelease,tagName,targetCommitish )" release_is_draft="$(jq -r '.isDraft' <<<"$release")" test "$release_is_draft" = "true" -o "$release_is_draft" = "false" test "$(jq -r '.isPrerelease' <<<"$release")" = "false" test "$(jq -r '.tagName' <<<"$release")" = "$RELEASE_TAG" release_target="$(jq -r '.targetCommitish' <<<"$release")" test -n "$release_target" test "$release_target" != "null" echo "release_tar…
  2. Check out the immutable Release targetactions/checkout@v5
  3. Validate the checked-out Release targettest "$(git rev-parse HEAD)" = "$(git rev-parse "${RELEASE_TARGET}^{commit}")" git fetch origin "refs/tags/${PREVIOUS_TAG}:refs/tags/${PREVIOUS_TAG}" if git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}" >/dev/null; then test "$(git rev-parse "${RELEASE_TAG}^{commit}")" = "$(git rev-parse "${RELEASE_TARGET}^{commit}")" else git tag "$RELEASE_TAG" "$RELEASE_TARGET" fi
  4. Validate mobile version and audited change rangeset -o pipefail node scripts/validate-store-delivery.mjs \ "$RELEASE_TAG" \ "$PREVIOUS_TAG" \ "$PLATFORM" \ "$ANDROID_TRACK" | tee -a "$GITHUB_OUTPUT"

Deliver Google Play · after plan

Condition: ${{ github.repository == 'tianma-if/edgeever' && (inputs.platform == 'android' || inputs.platform == 'both') }}

  1. Check out the immutable Release targetactions/checkout@v5
  2. Set up Bunoven-sh/setup-bun@v2
  3. Set up EAS CLIexpo/expo-github-action@v8Condition: ${{ !inputs.recover_play_apk }}
  4. Install dependenciesfor attempt in 1 2 3; do if bun install --frozen-lockfile \ --cache-dir "$RUNNER_TEMP/edgeever-bun-cache-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${attempt}"; then exit 0 fi echo "Dependency install failed on attempt ${attempt}/3" >&2 done exit 1Condition: ${{ !inputs.recover_play_apk }}
  5. Install Play APK recovery dependencyif ! bun install --frozen-lockfile \ --cache-dir "$RUNNER_TEMP/edgeever-bun-cache-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"; then node --input-type=module -e "await import('google-auth-library')" fiCondition: ${{ inputs.recover_play_apk }}
  6. Restore Android upload keystoretest -n "$ANDROID_KEYSTORE_BASE64" printf '%s' "$ANDROID_KEYSTORE_BASE64" | openssl base64 -d -A > "$RUNNER_TEMP/edgeever-upload.p12" chmod 600 "$RUNNER_TEMP/edgeever-upload.p12"Condition: ${{ !inputs.recover_play_apk }}
  7. Build and verify signed Play bundlebun run build:android:playCondition: ${{ !inputs.recover_play_apk }}
  8. Preserve Play bundle and deobfuscation mappingactions/upload-artifact@v7Condition: ${{ !inputs.recover_play_apk }}
  9. Upload bundle to Google Playtest -n "$EXPO_TOKEN" eas submit \ --platform android \ --profile "store-${ANDROID_TRACK}" \ --path android/app/build/outputs/bundle/release/app-release.aab \ --non-interactive \ --waitCondition: ${{ !inputs.recover_play_apk }}
  10. Download Play-signed universal APKapk_path="$RUNNER_TEMP/edgeever-android-${RELEASE_TAG}-arm64-v8a.apk" node scripts/download-play-universal-apk.mjs "$VERSION_CODE" "$apk_path" echo "apk_path=$apk_path" >> "$GITHUB_ENV"
  11. Verify Play app signaturenode scripts/verify-android-apk-signature.mjs "$apk_path"
  12. Verify Play APK architectureactual_archs="$(unzip -Z1 "$apk_path" | sed -n 's#^lib/\([^/]*\)/.*#\1#p' | sort -u | paste -sd, -)" if [[ "$actual_archs" != "arm64-v8a" ]]; then echo "::error::Expected an arm64-v8a Play APK, received: ${actual_archs:-no native libraries}." exit 1 fi
  13. Replace GitHub Release APK with the Play-signed buildgh release upload "$RELEASE_TAG" "$apk_path" --repo "$GITHUB_REPOSITORY" --clobber
  14. Remove temporary upload keystorerm -f "$RUNNER_TEMP/edgeever-upload.p12"Condition: always()

Deliver App Store Connect · after plan

Condition: ${{ github.repository == 'tianma-if/edgeever' && (inputs.platform == 'ios' || inputs.platform == 'both') }}

  1. Check out the immutable Release targetactions/checkout@v5
  2. Check out current store-delivery toolingactions/checkout@v5
  3. Resolve the App Store buildif [[ -n "$REQUESTED_BUILD_NUMBER" ]]; then echo "build_number=$REQUESTED_BUILD_NUMBER" >> "$GITHUB_OUTPUT" echo "Using existing App Store Connect build $REQUESTED_BUILD_NUMBER" exit 0 fi test -n "$ASC_API_ISSUER" test -n "$ASC_API_KEY_ID" test -n "$ASC_API_KEY_BASE64" python3 -m pip install --quiet PyJWT cryptography log_file="$RUNNER_TEMP/xcode-cloud-control.log" # Manual Xcode Cloud workflows reject tags that are not in the start # condition. Start the Archive workflow on its configured defa…
  4. Load App Store release notesnode --input-type=module <<'NODE' import { appendFileSync, readFileSync } from "node:fs"; const summary = JSON.parse(readFileSync("release-summary.json", "utf8")); const join = (items) => (Array.isArray(items) ? items : []) .map((item) => String(item).trim()) .filter(Boolean) .join("\n"); const write = (key, value) => { appendFileSync(process.env.GITHUB_OUTPUT, `${key}<<EOF\n${value}\nEOF\n`); }; write("notes_en", join(summary.changes?.["en-US"])); write("notes_zh", join(summary.changes?.["zh-C…
  5. Set up Ruby and fastlaneruby/setup-ruby@v1
  6. Submit the uploaded build to App Reviewtest -n "$APP_STORE_CONNECT_API_ISSUER_ID" test -n "$APP_STORE_CONNECT_API_KEY_ID" test -n "$APP_STORE_CONNECT_API_KEY_P8_BASE64" test -n "$APP_STORE_BUILD_NUMBER" log_file="$RUNNER_TEMP/edgeever-app-store-review.log" for attempt in {1..20}; do set +e bundle exec fastlane ios submit_review 2>&1 | tee "$log_file" status="${PIPESTATUS[0]}" set -e if [[ "$status" -eq 0 ]]; then exit 0 fi if ! grep -Fq "Build number: ${APP_STORE_BUILD_NUMBER} does not exist" "$log_file" || [[ "$attempt" -eq 20 ]]; …
Sync Docker installer mirror · .github/workflows/sync-docker-installer.yml ↗

Triggers: push, workflow_dispatch

Publish Tencent COS mirror · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Checkoutactions/checkout@v5
  2. Install Tencent COSCMDpython3 -m venv "${RUNNER_TEMP}/coscmd" "${RUNNER_TEMP}/coscmd/bin/pip" install --disable-pip-version-check 'coscmd==1.9.0.6' echo "${RUNNER_TEMP}/coscmd/bin" >> "${GITHUB_PATH}"
  3. Configure COSCMDtest -n "${COS_BUCKET}" test -n "${COS_REGION}" test -n "${COS_SECRET_ID}" test -n "${COS_SECRET_KEY}" coscmd config \ -a "${COS_SECRET_ID}" \ -s "${COS_SECRET_KEY}" \ -b "${COS_BUCKET}" \ -r "${COS_REGION}"
  4. Upload installer filescoscmd upload -f apps/site/public/install.sh install.sh coscmd upload -f apps/site/public/compose.yaml compose.yaml
  5. Verify public mirrorbase_url="https://${COS_BUCKET}.cos.${COS_REGION}.myqcloud.com" curl --fail --silent --show-error "${base_url}/install.sh" | cmp apps/site/public/install.sh - curl --fail --silent --show-error "${base_url}/compose.yaml" | cmp apps/site/public/compose.yaml -
Update deployed EdgeEver · .github/workflows/sync-edgeever-upstream.yml ↗

Triggers: schedule, workflow_dispatch

Sync Fork and trigger deployment · no job dependencies declared

Condition: github.repository != 'tianma-if/edgeever'

  1. Checkout deployed repositoryactions/checkout@v5
  2. Require a GitHub Forkset -euo pipefail is_fork="$(gh api "repos/${GITHUB_REPOSITORY}" --jq '.fork')" if [ "${is_fork}" != "true" ]; then echo "This deployment repository must be a GitHub Fork of ${UPSTREAM_REPOSITORY}." >&2 echo "Create the repository with the EdgeEver Fork flow before enabling upstream updates." >&2 exit 1 fi
  3. Resolve upstream versionset -euo pipefail git remote add upstream "https://github.com/${UPSTREAM_REPOSITORY}.git" git fetch upstream main --tags case "${UPDATE_CHANNEL}" in stable) target_ref="$(gh api "repos/${UPSTREAM_REPOSITORY}/releases/latest" --jq '.tag_name')" ;; edge) target_ref="upstream/main" ;; *) echo "Unsupported update channel: ${UPDATE_CHANNEL}" >&2 exit 1 ;; esac target_commit="$(git rev-parse "${target_ref}^{commit}")" head_commit="$(git rev-parse HEAD)" target_version="$(git show "${target_commit}:pa…
  4. Align to upstreamset -euo pipefail git config user.name "edgeever-updater[bot]" git config user.email "edgeever-updater[bot]@users.noreply.github.com" helper_path="${RUNNER_TEMP}/prepare-upstream-sync.mjs" if git cat-file -e "${TARGET_COMMIT}:scripts/prepare-upstream-sync.mjs" 2>/dev/null; then git show "${TARGET_COMMIT}:scripts/prepare-upstream-sync.mjs" > "${helper_path}" else cp scripts/prepare-upstream-sync.mjs "${helper_path}" fi EDGE_SYNC_ALIGN_MODE="${ALIGN_MODE}" \ EDGE_SYNC_BASE_COMMIT="HEAD" \ EDGE_SY…Condition: steps.upstream.outputs.update_required == 'true'
  5. Set up Bunoven-sh/setup-bun@v2Condition: steps.upstream.outputs.align_mode == 'merge'
  6. Verify customized mergeset -euo pipefail bun install --frozen-lockfile bun run db:migrate:local bun run test bun run typecheck bun run buildCondition: steps.upstream.outputs.align_mode == 'merge'
  7. Publish updateset -euo pipefail git config user.name "edgeever-updater[bot]" git config user.email "edgeever-updater[bot]@users.noreply.github.com" if [ "${REPUBLISH_ONLY}" = "true" ] && [ "${UPDATE_REQUIRED}" != "true" ]; then git commit --allow-empty -m "chore: retrigger EdgeEver deployment for ${TARGET_REF}" git push origin HEAD:main echo "mode=empty_commit" >> "${GITHUB_OUTPUT}" echo "Pushed empty commit to retrigger Cloudflare Workers Builds." exit 0 fi git commit \ -m "chore: update EdgeEver from ${TAR…Condition: steps.upstream.outputs.update_required == 'true' || steps.upstream.outputs.republish_only == 'true'
  8. Request Cloudflare deploymentset -euo pipefail if [ -z "${DEPLOY_HOOK_URL:-}" ]; then echo "No EDGE_EVER_CLOUDFLARE_DEPLOY_HOOK_URL secret — relying on Cloudflare Git integration for main pushes." echo "Optional: add a Workers/Pages Deploy Hook secret if pushes do not start builds." >> "${GITHUB_STEP_SUMMARY}" echo "method=git_push" >> "${GITHUB_OUTPUT}" exit 0 fi curl -fsS -X POST "${DEPLOY_HOOK_URL}" >/dev/null echo "Triggered Cloudflare Deploy Hook." echo "method=git_push_and_deploy_hook" >> "${GITHUB_OUTPUT}" echo "" >…Condition: steps.publish.outcome == 'success'
  9. Report result / 输出结果set -euo pipefail final_commit="$(git rev-parse HEAD)" git_result="Not requested / 未请求" deploy_result="Not requested / 未请求" live_result="Not changed by this run / 本次运行未改变线上部署" live_status="unchanged" notice_level="notice" notice_title="Scheduled check / 定时检查" notice_body="Fork code is already v${TARGET_VERSION}. No Git push or Cloudflare deployment was requested." if [ "${UPDATE_REQUIRED}" = "true" ] || [ "${REPUBLISH_ONLY}" = "true" ]; then live_result="Not verified by this workflow / 本工作流未验证"…Condition: always() && steps.upstream.outcome == 'success'
  10. Report an update failureset -euo pipefail title="EdgeEver automatic update requires attention" existing="$(gh issue list --state open --search "${title} in:title" --json number --jq '.[0].number // empty')" if [ -z "${existing}" ]; then gh issue create \ --title "${title}" \ --body "The daily update from \`${UPSTREAM_REPOSITORY}\` (${TARGET_REF}) could not be aligned or verified. The current production deployment was left unchanged. Open the failed **Update deployed EdgeEver** workflow run for details (see the job sum…Condition: failure()
Windows test signing · .github/workflows/windows-test-signing.yml ↗

Triggers: workflow_dispatch

Build and test-sign Windows x64 installer · no job dependencies declared

Condition: github.repository == 'tianma-if/edgeever'

  1. Check out sourceactions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
  2. Set up Bunoven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
  3. Set up Rustdtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
  4. Install dependenciesfor ($attempt = 1; $attempt -le 3; $attempt++) { bun install --frozen-lockfile $installExitCode = $LASTEXITCODE if ($installExitCode -eq 0) { exit 0 } if ($attempt -eq 3) { exit $installExitCode } Write-Warning "Dependency installation failed on attempt $attempt; retrying." Start-Sleep -Seconds (15 * $attempt) }
  5. Run project type checksbun run typecheck bun run typecheck:mobile
  6. Build Web rendererbun run build:web
  7. Build Rust sidecarcargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
  8. Package unsigned Windows installerbun scripts/run-desktop-builder.mjs --publish never
  9. Verify Windows package contents and architecturebun scripts/verify-desktop-package.mjs
  10. Run packaged Windows sidecar integration tests$env:EDGE_EVER_SIDECAR_PATH = (Resolve-Path "release/desktop/win-unpacked/resources/sidecar/edgeever-sidecar.exe").Path $env:EDGE_EVER_MIGRATIONS_PATH = (Resolve-Path "release/desktop/win-unpacked/resources/migrations").Path bun scripts/test-desktop-sidecar.mjs
  11. Verify packaged Windows first launchbun run verify:packaged-desktop-startup -- release/desktop/win-unpacked/EdgeEver.exe
  12. Verify unsigned Windows installer$version = (Get-Content apps/desktop/package.json | ConvertFrom-Json).version $installer = "release/desktop/EdgeEver-$version-windows-x64.exe" if (-not (Test-Path -LiteralPath $installer)) { throw "Expected Windows installer was not created: $installer" } $signature = Get-AuthenticodeSignature -LiteralPath $installer if ($signature.Status -ne "NotSigned") { throw "Installer must be unsigned before SignPath submission; status: $($signature.Status)" } "path=$installer" >> $env:GITHUB_OUTPUT
  13. Upload unsigned installer for SignPathactions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
  14. Submit SignPath test-signing requestSignPath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd
  15. Verify SignPath test signature$signedInstallers = @( Get-ChildItem -LiteralPath "${{ runner.temp }}\edgeever-signpath-signed" -Filter *.exe -File -Recurse ) if ($signedInstallers.Count -ne 1) { throw "Expected exactly one signed installer, found $($signedInstallers.Count)" } $signedInstaller = $signedInstallers[0] $signature = Get-AuthenticodeSignature -LiteralPath $signedInstaller.FullName if ($null -eq $signature.SignerCertificate -or $signature.Status -eq "NotSigned") { throw "SignPath did not return an Authenticode-sign…
  16. Upload test-signed installeractions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
package.json ↗
  • build:android:fast: bash scripts/build-android-local.sh fast
  • build:android:fast:local: bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fast
  • build:android:apk: bash scripts/build-android-local.sh apk
  • build:android:apk:local: bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:apk
  • build:android:play: bash scripts/build-android-local.sh play
  • build:android:play:local: bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:play
  • build: bun run build:web
  • build:self-hosted: bun build scripts/self-hosted-server.mjs --target=bun --format=esm --outdir=dist/self-hosted --sourcemap=none
  • build:plugin-api: bun run --cwd packages/plugin-api build
  • build:extension: vite build --config apps/extension/vite.config.ts
  • build:extension:firefox: bun run --cwd apps/extension build:firefox
  • build:web: vite build --config apps/web/vite.config.ts
  • build:desktop:sidecar: cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
  • build:desktop: EDGE_EVER_DESKTOP_BUILD=1 bun run prepare:desktop:icons && bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist
  • release: bun scripts/release.mjs
  • publish:stores: bun scripts/store-delivery.mjs
  • build:worker: bun scripts/build-cloudflare-worker.mjs
  • build:cloudflare: bun run typecheck && bun run build && bun run build:worker
  • build:site: bun --cwd apps/site build
  • deploy:doctor: bun scripts/cloudflare-deploy.mjs doctor
  • deploy:setup: bun scripts/cloudflare-deploy.mjs setup
  • deploy: bun run build:cloudflare && EDGE_EVER_USE_EXISTING_AUTH_SECRET=true bun run deploy:ci
  • deploy:manual: export EDGE_EVER_DEPLOYMENT_TRIGGER=manual EDGE_EVER_DEPLOYMENT_METHOD=local_cli && bun run deploy:doctor && bun run build:cloudflare && bun run deploy:ci
  • deploy:ci: bun run db:migrate:remote && bun run deploy:worker && bun run deploy:verify
  • deploy:worker: bun scripts/run-wrangler.mjs deploy
  • deploy:cloudflare-builds: EDGE_EVER_USE_EXISTING_AUTH_SECRET=true bun run deploy:ci
  • deploy:verify: bun scripts/verify-deployment.mjs
  • deploy:builds:setup: bun scripts/cloudflare-workers-builds.mjs setup
  • deploy:site: bun run build:site && wrangler pages deploy apps/site/dist --project-name=edgeever-official
apps/extension/package.json ↗
  • build: vite build --config vite.config.ts
  • build:firefox: EDGE_EVER_EXTENSION_TARGET=firefox vite build --config vite.config.ts
apps/site/package.json ↗
  • build: astro build
packages/plugin-api/package.json ↗
  • build: bun build ./src/index.ts --outdir ./dist --target browser --format esm && tsc -p tsconfig.build.json

Full upstream document by @tianma-if · README.md · snapshot 9918570

EdgeEver logo EdgeEver

An open-source, AI-native knowledge base & portable Evernote alternative

GitHub Stars GitHub Forks Docker Pulls Product Hunt Featured|HelloGitHub Sponsor on Afdian

简体中文 | 繁體中文 | English | 日本語

💬 Telegram Group  |  🌐 Live Demo  |  📱 Client Downloads

EdgeEver is a modern, open-source notes and knowledge base workspace. It revives the beloved Evernote-style three-pane layout while offering an open data architecture and seamless AI Agent integration for complete ownership and smart productivity.

💡 Serverless & 100% Free Forever EdgeEver can run within Cloudflare's free quotas with no server purchase or VPS maintenance. Users who prefer a VPS, NAS, or home server can deploy the same application with Docker.

⭐ If EdgeEver is useful to you, consider giving it a Star. Your support helps more people discover the project.

Why EdgeEver

Many long-time Evernote users simply want a reliable, open, and fast personal knowledge base. However, existing mainstream solutions all present tradeoffs:

  • Evernote: It has grown increasingly bloated with commercial ads and unnecessary features, degrading performance. Data export is cumbersome, free tiers are heavily restricted, and AI/MCP features require costly subscriptions.
  • Obsidian: Open files, closed-source core. Official Sync is paid and third-party sync is tedious; relying entirely on flat local file scanning causes noticeable cold-start and search lag once notes reach thousands or heavy plugins are loaded; storing images and attachments alongside notes quickly bloats vaults, making mobile sync sluggish and leaving orphaned files behind; and it is overly heavy for lightweight, capture-anywhere use.
  • Memos & Stream Notes: Clean and simple, but their social-timeline layouts differ fundamentally from the structured productivity of a classic three-pane workflow.
  • SiYuan & Block-based PKMs: Powerful with self-hosting support, but their granular "block-level" architecture imposes noticeable cognitive overhead for quick daily capture and continuous prose writing. Furthermore, they lack a true zero-cost serverless deployment tier, and multi-device sync relies on paid official subscriptions or paying extra to unlock S3/WebDAV sync features with your own storage.

EdgeEver fills this gap: The entire stack is open source, including sync and self-hosting. It keeps the three-pane layout you know, stays silky-smooth and lightweight even with 10,000+ notes, and ships native AI agents with zero-cost deployment.

💡 Recommended Workflow: Capture inspiration seamlessly across all devices and organize deeply in the classic three-pane view. Powered by native MCP and ACP, external agents can retrieve and organize your notes seamlessly, while the desktop app lets you collaborate deeply with local AI agents on your machine. Publish anywhere with one-click formatting—100% self-hosted at zero cost, building an open and truly owned second brain.

Online Demo

The public demo resets every day at 3:00 AM (China Standard Time) and restores sample notes. Do not store private content there.

Client Downloads

Download EdgeEver for macOS   Download EdgeEver for Windows   Download the EdgeEver Linux x86_64 AppImage Preview   Download EdgeEver for Android from Google Play   Download EdgeEver for iOS from the App Store

The iOS app requires an Apple ID from outside mainland China.

Features

  • Deploy Your Way: Run on Cloudflare's free serverless platform or with Docker on a VPS, NAS, or home server. Based on Cloudflare's free storage allowances, a personal deployment can hold roughly 150,000 short notes and 50,000 images; Docker storage scales on demand to easily support millions of notes and a vast image library.
  • Open Data, No Vendor Lock-in: Built on standard SQLite with complete REST API, MCP, and CLI access. Your knowledge is stored transparently and accessible anytime without being locked to a single app.
  • Lossless ZIP Backup & Portability: Export your complete library as a clean archive containing Markdown, Front Matter, nested folders, relative attachment links, and version histories for instant restoration anywhere.
  • Native AI Agent Synergy: Built-in Model Context Protocol (MCP) support allows external AI Agents to directly read, organize, and summarize notes; the desktop app also connects directly to local AI Agents running on your machine (such as Codex, Antigravity, Claude Code, and WorkBuddy) via Agent Client Protocol (ACP) for collaborative writing.
  • Bring Your Own AI Models: Connect OpenAI, Anthropic, or Gemini-compatible services and third-party API relays to power the built-in Agent and companion sidebar, bringing smart note summarization, key point extraction, proofreading, translation, and text continuation to full notes or selected text.
  • Rich Plugin API: Extend EdgeEver with the Plugin API.
  • Unlimited Multi-Device Sync: No commercial device caps or paywalls. Enjoy seamless synchronization across PC, tablet, and mobile via web, PWA, or browser.
  • Classic Three-Pane Layout & Focus Mode: Clean navigation featuring notebook trees, note lists, and an expansive editor, with a desktop focus mode to eliminate distractions.
  • Light, Lasting Desktop Performance: Switching notes does not keep old images and documents in memory, and the desktop app stays responsive after sitting in the background.
  • Unlimited Nested Notebooks: Organize your knowledge with arbitrary folder depth.
  • One-Click Rich Copy for Newsletters & Blogs: Designed for creators to convert notes into beautifully formatted rich text with inline CSS, ready to paste directly into Substack, Medium, WordPress, or newsletter editors without extra tools.
  • Seamless Dual-View Editor: Switch effortlessly between intuitive rich text editing and Markdown source code on desktop.
  • Convenient Single-Note Export: Export the current note directly as Markdown, HTML, or PDF for standalone storage, sharing, or publishing.
  • Native Mermaid Diagram Rendering: Render clear flowcharts, sequence diagrams, and mind maps directly in notes, preserving clean, editable source code across Markdown and rich text views.
  • Visual Diagram Notes: Ditch external drawing tools and sketch mind maps, flowcharts, and architecture diagrams directly in notes. Backed by a structured IR, the built-in assistant and external AI agents can generate and refine diagrams from a single prompt, complete with smart auto-layout, cross-device sync, and vector export. See the visual diagram notes design.
  • Revision History: Inspect and restore previous iterations of your notes with built-in version tracking.
  • Public Note Sharing: Share a note publicly and stop sharing it at any time. Optionally protect the link with an auto-generated access password.
  • WeChat Article Clipping on Mobile: Share a WeChat Official Account article to EdgeEver on your phone to extract its content and save it as an editable note.
  • Smart Local Image Compression: Client-side WebP compression reduces file sizes by 50%-90% before uploading, saving storage and speeding up page loads without extra server costs.
  • Universal File Attachments: Attach and preview PDFs, Office documents, zip files, audio, and video directly within notes. Chunked uploads and streaming safely support files up to 1 GiB.
  • Batch Operations & Flexible Sorting: Easily merge or relocate multiple notes, with drag-and-drop notebook reordering.
  • Offline Drafts & Queueing: Draft and edit uninterrupted while offline; changes automatically sync once reconnected.
  • Brute-Force Login Protection: Server-side account- and IP-based failed-login throttling with automatic cooldowns helps protect private notes against brute-force and password-spraying attacks.
  • Multi-Tenant Account Isolation: Host multiple user accounts on a single instance with strictly partitioned spaces and clean admin account management.
  • Everywhere You Need It: Available on the Web, Android, macOS, Windows, Linux, and iOS; the Web Clipper supports Chrome, Edge, and Firefox.

Deployment

Cloudflare is the recommended zero-server deployment. Docker is available for users who prefer a VPS, NAS, or home server.

For Cloudflare, choose either of the following online deployment options:

Copy the prompt below directly into an AI Agent (such as Codex, Claude, Cursor, WorkBuddy, Antigravity, OpenClaw, Hermes Agent, etc.). During execution, if access to GitHub or Cloudflare is required, review the requested permissions and follow the prompts to authorize access.

Deploy EdgeEver entirely through GitHub and Cloudflare:
1. Fork https://github.com/tianma-if/edgeever.
2. Create D1 `edgeever` and R2 `edgeever-resources` in Cloudflare.
3. In Workers & Pages, create a Worker named `edgeever` from the Fork's `main` branch.
   Use the repository root, keep Cloudflare's default Workers Builds deploy command,
   and ensure its API token can read and edit D1. Select Save and Deploy.
4. After the Worker is created, add the user's chosen password as the runtime Secret
   `EDGE_EVER_AUTH_PASSWORD` (preferably at least 32 characters).
   The username defaults to `admin`.
   If the user specifies another, set `EDGE_EVER_AUTH_USERNAME` as a Workers Builds
   variable before the next build.
5. Run the build again, verify `/api/health` and `/api/openapi.json`, then log in
   with that administrator username and password.
6. Enable and manually run the GitHub Actions workflow named `Update deployed EdgeEver`
   once so the Fork can automatically receive future stable releases and fixes.

Detailed requirements: AI Agent Cloudflare Deployment.

Option B: Manual Online Deployment

Complete setup in 6 web steps:

  1. Fork the Repository: Click Fork at the top right of GitHub to fork EdgeEver into your personal account.
  2. Create Cloudflare Resources: Create D1 edgeever and R2 edgeever-resources.
  3. Import & Configure the Project: Create a Worker named edgeever from the Fork's main branch in Cloudflare Workers & Pages. Use the repository root and keep Cloudflare's default Workers Builds deploy command, which runs in Cloudflare. Ensure its API token can read and edit D1. The deploy command creates the bindings; do not edit Fork files.
  4. Choose the Administrator Password: Choose an administrator password, preferably at least 32 characters. Once the Worker is created, save it as the runtime Secret EDGE_EVER_AUTH_PASSWORD.
  5. Build & Verify: Save and Deploy creates the Worker and starts a build. If it fails because the administrator Secret is missing, add the runtime Secret from step 4 and retry. The username defaults to admin; to use another, set the EDGE_EVER_AUTH_USERNAME Workers Builds variable before retrying. Once deployed, confirm /api/health returns 200, then log in with the configured username and password.
  6. Enable Automatic Updates: Open the Fork's Actions tab, click I understand my workflows, go ahead and enable them, then manually run Update deployed EdgeEver once so the Fork can automatically receive future stable releases and fixes.

📖 For full step-by-step instructions and configuration details, see the Online Deployment Guide.

💡 Custom Domain & Access: After deployment, you can directly use the default *.workers.dev domain assigned by Cloudflare, or attach your own custom domain in the Worker settings under Settings → Domains & Routes.

💡 Cloudflare R2 Activation: Although Cloudflare R2 offers a generous free storage allowance that note-taking workloads remain completely within, you must first activate an R2 subscription and add a payment method. Cloudflare officially supports UnionPay, Visa, Mastercard, and other cards, as well as PayPal, Apple Pay, Google Pay, and other payment methods.

Option C: Docker on a VPS or NAS

Use the GitHub-hosted installer and the official GHCR image:

curl -fsSL https://edgeever.org/install.sh | bash

The command pulls the latest image, generates an administrator password, and starts EdgeEver with Docker Compose.

See the Docker deployment guide for manual deployment and configuration.

After installation, updates run automatically each day by default. To update manually, run ~/edgeever/update.sh on the deployment server.


Multi-Account Login

Once deployed, a single instance supports multi-account login.

The instance administrator can create, disable, or reset member accounts in Profile -> User accounts. Each member gets a fully isolated personal workspace, including notebooks, notes, attachments, Trash, import/export, and MCP tokens.

Browser Web Clipper

Install EdgeEver Web Clipper for Google Chrome   Install EdgeEver Web Clipper for Microsoft Edge   Install EdgeEver Web Clipper for Firefox

  • Smart Article Extraction: Automatically extracts article content and converts it into clean Markdown, preserving the source URL and clipping timestamp.
  • Selection & Context Menu Clipping: Save selected text or right-clicked images directly as notes without capturing the entire page.
  • X (Twitter) Post Clipping: Right-click any post to automatically expand full text and archive the author, timestamp, and attached images together.
  • Private Self-Hosted Direct Connection: Sends clipped content directly to your personal EdgeEver instance without third-party relays.

Community and Feedback

  • Bugs, feature requests, and deployment issues: GitHub Issues
  • Code contributions: read the Contribution Guide. If your Fork is also used to deploy EdgeEver, keep its main branch deployment-only. Create a separate branch from the official upstream/main for synchronization, development, and pull requests; do not develop on or Sync fork the deployment main.

Telegram Community

Welcome to the EdgeEver community. Join us to discuss the EdgeEver experience, real-world AI Agent applications, cost-effective or free AI resources, and automation workflows.

👉 Join the EdgeEver Telegram group

Plugins and Themes

Web and desktop apps support functional plugins and custom themes, installable from the official marketplace, GitHub, or a Manifest URL, with seamless sync across your workspace. Developers can extend capabilities using @edgeever/plugin-api; see the plugin development guide and marketplace submission policy.

Tech Stack

  • Bun workspace monorepo with Web, API, official site, and shared type package.
  • Frontend: Vite, React, React Router, TanStack Query, Tailwind CSS, shadcn/ui, and Radix UI.
  • Editor: TipTap / ProseMirror with Markdown support; PWA uses vite-plugin-pwa, Workbox, and Dexie.
  • Android app: Expo + React Native in apps/mobile, with SQLite local storage and incremental sync.
  • iOS app: Native SwiftUI in apps/ios (iOS 17+), with a packaged TipTap EditorBundle, GRDB local mirror/outbox, and Android-aligned shell chrome.
  • Native desktop app: Electron + Rust sidecar combines a consistent cross-platform experience with high-performance local data services; SQLite enables offline editing, incremental sync when back online, and local backups.
  • Web clipper: Manifest V3, Mozilla Readability, and Turndown for Chrome, Microsoft Edge, and Firefox.
  • Backend: one Hono/Zod business application with REST API and Remote MCP; Cloudflare uses Workers/D1/R2, while Docker uses Bun/SQLite/local files or S3.
  • Official site: Astro static site in apps/site, deployable to Cloudflare Pages.

Quick Start

bun install
bun run dev

Local development signs in automatically; fresh databases use owner / edgeever-local-dev. Log out to test the login screen.

Project Structure

apps/web          Vite + React frontend, PWA, offline drafts, and sync queue
apps/extension    Chrome/Edge/Firefox Manifest V3 web clipper
apps/api          Cloudflare Worker + Hono API, MCP endpoint
apps/mobile       Expo + React Native Android app
apps/ios          Native SwiftUI iOS app (TipTap EditorBundle, GRDB)
apps/desktop      Electron desktop shell, preload bridge, and native packaging
apps/site         Astro official website, deployable independently
packages/client   Shared API client for web and mobile apps
packages/shared   Shared types, Zod schemas, TipTap / Markdown conversion
crates/desktop-sidecar
                   Rust sidecar for local SQLite, offline data, backups, and resources
scripts           Wrangler wrapper, password hash, CLI, MCP stdio bridge, Evernote ENEX import
migrations        Shared append-only D1/SQLite database migrations
docs              Architecture, migration, and deployment docs
.github/workflows CI for web, mobile, iOS, desktop packaging, deployment, and releases
wrangler.toml     Cloudflare Workers, Assets, D1, R2 configuration

Content Formats

EdgeEver stores note content in three forms:

content_json      TipTap/ProseMirror document, the editor source of truth
content_markdown  API, Agent, import, and export format
content_text      Search, summary, and indexing text

Open Profile -> Import and export to export or import an EdgeEver ZIP. Its notes/ directory is directly readable and portable as Markdown, while its structured data supports complete recovery between EdgeEver instances. Import preserves unrelated target data and overwrites records with matching EdgeEver IDs.

MCP

Create an API token in Profile -> MCP settings and give it to your AI Agent. The Agent can then securely manage your knowledge base within your account permissions. It supports both text notes and diagram notes (including mind maps, flowcharts, and architecture diagrams) with full CRUD capabilities. The Agent can create a structured table note from a field plan, edit its fields, and read, add, update, or delete its records. The Agent can also manage note templates and AI instructions.

💡 Inspiration: Make AI your true knowledge orchestrator and creative co-pilot—instantly turn concepts into interactive mind maps and architecture diagrams, while supplying private context to your AI Agents. Paired with EdgeEver’s powerful rich-text editing and elegant typography, AI-assisted content becomes beautifully structured, polished, and publication-ready knowledge assets.

Image Compression

Image compression happens in the Web client before upload and is controlled by the Compress note images setting. When enabled, PNG, JPEG, WebP, and AVIF files are converted to WebP when beneficial, with the longest edge limited to 2560px. If compression does not reduce size, the original file is kept.

EdgeEver avoids Worker-side image processing to reduce compute and image-processing quota usage. REST API and MCP upload paths store the file content provided by the client without additional server-side compression.

Advanced Object Storage

The instance owner can configure S3-compatible object storage under Settings → Advanced → OSS object storage. Changing storage does not migrate or affect existing attachments.

Migration

If you want to migrate notes from other platforms to EdgeEver, please refer to the following simple migration guides:

Docker Deployment

Docker runs the same frontend, API routes, services, authentication, MCP implementation, and migrations as Cloudflare. The container uses SQLite with local files or S3-compatible attachment storage and supports amd64 and arm64. See Deploy EdgeEver with Docker and Self-hosting and Docker architecture.

Sync Timing

Web, PWA, and desktop upload memo edits after 30 seconds of inactivity and check for remote changes every 5 minutes while visible; focus and manual refresh remain immediate. Adjust DEFERRED_MEMO_SYNC_DELAY_MS and BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS in apps/web/src/lib/workspace-refresh.ts.

Acknowledgements

  • EdgeEver's note-taking product design was also informed by the publicly available product experiences of mature note-taking tools such as Evernote. The related features were independently designed and implemented by EdgeEver.
  • The product design of mind-map and visual-diagram notes was informed by the publicly available product experiences of XMind and ProcessOn. These features were independently designed and implemented by EdgeEver.
  • Editor theme typography, heading hierarchy, and chapter structure draw from the public work of obsidian-minimal, Outline, and 墨格. Names, assets, and implementations are original to EdgeEver.

Trademark and Brand Use

The EdgeEver name, logo, and other brand identifiers distinguish the official project. Forks and modified versions may state that they are based on EdgeEver, but must not imply official status or mislead users. The open-source license does not grant trademark rights; other uses require prior written permission from the project maintainers.

Disclaimer

EdgeEver is an independent open-source note-taking application developed and maintained by individuals and the community. It is not affiliated with, authorized, sponsored, or endorsed by Evernote Corporation or its affiliates.

EdgeEver is self-hosted software. Except for official demo instances, project maintainers do not host, control, or review user content. Content stored or displayed by an instance is the responsibility of its users or operators and does not represent the maintainers' views.

Frequently asked about EdgeEver

What is EdgeEver?+

EdgeEver is a self-hosted Evernote/Notion alternative built on the Cloudflare developer platform. Self-hosted notes, web clipping and synchronization on Workers

What does EdgeEver replace?+

EdgeEver is listed as an alternative to Evernote, Notion. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does EdgeEver use?+

EdgeEver is built on D1, R2, Workers.

How much does EdgeEver cost to run?+

Small personal note libraries can fit Workers, D1 and standard R2 free allowances. R2 activation requires a billing-enabled account even when usage stays free. Storage, writes and Worker CPU must remain within plan limits; optional external AI usage is separately billed. Provision your own D1 database and R2 bucket, run the documented schema/deployment steps and keep the deployment updated. The 10ms Free Worker CPU ceiling and per-day D1 limits have not been benchmarked for this app. Larger resources or AI usage can produce additional charges. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Check current Cloudflare pricing before deploying.

Is EdgeEver open source?+

The upstream repository declares the AGPL-3.0 license. Read its terms at https://raw.githubusercontent.com/tianma-if/edgeever/9918570868c835d163151c92dc82288b62890e96/LICENSE. Source code and contributor credit are available at https://github.com/tianma-if/edgeever.

Discussion · 0

sign in to comment →
No comments yet — be the first.