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
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.
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.
Note editing, organization and web clipping; no complete workspace-database, collaboration or Evernote/Notion feature parity claim.
See supporting source ↗Note editing, organization and web clipping; no complete workspace-database, collaboration or Evernote/Notion feature parity claim.
See supporting source ↗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 ↗Configuration and workflow sources
Reviewed commit 9918570868c8. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 1 files
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→ D1RESOURCES→ R2ASSETS→ 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.
Triggers: workflow_dispatch
Require Play-signed Draft APK · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Validate matching Draft Release
release="$(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" - Check out the immutable Draft target
actions/checkout@v5 - Verify the checked-out Draft target
test "$(git rev-parse HEAD)" = "$(git rev-parse "${RELEASE_TARGET}^{commit}")" - Download the Draft Android APK
mkdir -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" - Require the Play app-signing certificate
test -n "$ANDROID_PLAY_APP_SIGNER_SHA256" node scripts/verify-android-apk-signature.mjs "$apk_path"
Triggers: release, workflow_dispatch
Build and deploy Demo Worker · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Checkout Demo source
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Install dependencies
bun install --frozen-lockfile - Resolve Release metadata
if [ -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}" - Deploy Demo Worker
bun run deploy - Verify Demo URL
curl --fail --silent --show-error --location --max-time 20 "https://${DEMO_DEPLOY_DOMAIN}/"Condition: env.DEMO_DEPLOY_DOMAIN != ''
Triggers: release, workflow_dispatch
Build and deploy personal Worker · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Resolve formal Release
set -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… - Checkout personal instance source
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Install dependencies
bun install --frozen-lockfile - Deploy personal Worker
bun run deploy - Verify deployed Release version
node --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 = "…
Triggers: push, workflow_dispatch
Deploy Cloudflare Pages · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Checkout
actions/checkout@v5 - Setup Bun
oven-sh/setup-bun@v2 - Install dependencies
bun install --frozen-lockfile - Build site
bun run build:site - Deploy site
cloudflare/wrangler-action@v3Wrangler command: pages deploy apps/site/dist --project-name=edgeever-official
Triggers: pull_request, push
Wrangler runner (${{ matrix.os }}) · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Checkout
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Install dependencies
bun install --frozen-lockfile - Launch project-local Wrangler
bun scripts/run-wrangler.mjs --version - Test deployment tooling
bun test tests/wrangler-runner.test.ts tests/deployment-verify.test.ts tests/deployment-entrypoints.test.ts
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 != '') ) }}
- Check out source
actions/checkout@v5 - Validate Draft Release target
expected_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 != '' - Compare with previous formal release
mapfile -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' }}
- Copy latest compatible desktop assets without renaming
mkdir -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' ) ) }}
- Check out source
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Set up Rust
dtolnay/rust-toolchain@stable - Cache Bun dependencies for shared validation
actions/cache@v5Condition: matrix.shared_validation - Cache Rust dependencies and sidecar builds
actions/cache@v5 - Cache Electron downloads
actions/cache@v5 - Import macOS signing identity
if [[ -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… - Install dependencies
bun install --frozen-lockfile - Build Web renderer
bun run build:web - Verify Web performance budget
bun run verify:web-performanceCondition: matrix.shared_validation - Run project type checks
bun run typecheck bun run typecheck:mobileCondition: matrix.shared_validation - Build debug sidecar for integration tests
cargo build --manifest-path crates/desktop-sidecar/Cargo.tomlCondition: matrix.shared_validation - Run desktop regression tests
bun run test:desktopCondition: matrix.shared_validation - Verify renderer origin storage migration
bun run verify:renderer-origin-migrationCondition: matrix.shared_validation - Verify packaged renderer startup
bun run verify:desktop-rendererCondition: matrix.shared_validation - Validate Rust sidecar
cargo 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 - Build architecture-specific Rust sidecar
cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release - Prepare Apple notarization API key
if [[ -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" >> … - Package desktop installer
bun scripts/run-desktop-builder.mjs --publish never - Verify packaged macOS cross-version startup
previous_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 != '' - Verify packaged macOS first launch
unpacked_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" - Verify packaged macOS private protocol file flows
unpacked_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" - Verify desktop installer
bun scripts/verify-desktop-package.mjs - Stage architecture-specific desktop assets
actions/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' ) ) }}
- Check out source
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Set up Rust
dtolnay/rust-toolchain@stable - Cache Bun dependencies
actions/cache@v5 - Cache Rust dependencies and sidecar builds
actions/cache@v5 - Cache Electron downloads
actions/cache@v5 - Install dependencies
for ($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) } - Build Web renderer
bun run build:web - Build x64 Rust sidecar
cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release - Package unsigned Windows installer
bun scripts/run-desktop-builder.mjs --publish never - 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 != '' - Verify Windows package contents and architecture
bun scripts/verify-desktop-package.mjs - 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 - Verify packaged Windows first launch
bun run verify:packaged-desktop-startup -- release/desktop/win-unpacked/EdgeEver.exe - Verify packaged Windows private protocol file flows
bun run verify:desktop-protocol-e2e -- release/desktop/win-unpacked/EdgeEver.exe - 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))" } } - Create Windows update metadata
$version = (Get-Content apps/desktop/package.json | ConvertFrom-Json).version node scripts/create-windows-update-metadata.mjs release/desktop $version - Stage Windows Preview assets
actions/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' ) ) }}
- Check out source
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Set up Rust
dtolnay/rust-toolchain@stable - Install AppImage runtime dependencies
sudo apt-get update sudo apt-get install --yes libfuse2 - Cache Bun dependencies
actions/cache@v5 - Cache Rust dependencies and sidecar builds
actions/cache@v5 - Cache Electron downloads
actions/cache@v5 - Install dependencies
for 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 - Build Web renderer
bun run build:web - Build x64 Rust sidecar
cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release - Build Linux automatic update predecessor
if [[ -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… - Package Linux AppImage
bun scripts/run-desktop-builder.mjs --publish never - Verify Linux package contents, architecture, and glibc baseline
bun scripts/verify-desktop-package.mjs - Run packaged Linux sidecar integration tests
bun scripts/test-desktop-sidecar.mjs - Verify packaged Linux first launch
executable="" 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" - Verify packaged Linux private protocol file flows
executable="" 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" - Create Linux checksum
version="$(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 - Verify Linux automatic update metadata
version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")" node scripts/verify-linux-update-release.mjs release/desktop "$version" - Verify real Linux AppImage automatic update
version="$(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" - Stage Linux Preview assets
actions/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') }}
- Check out timing reporter
actions/checkout@v5 - Collect native build timings
gh 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" - Upload machine-readable timing report
actions/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' ) ) }}
- Check out source
actions/checkout@v5 - Download platform-specific assets
actions/download-artifact@v7 - Create and audit combined update metadata
version="$(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" - Upload verified desktop assets to Release
gh release upload "$RELEASE_TAG" release/desktop/* \ --repo "$GITHUB_REPOSITORY" \ --clobberCondition: github.event_name == 'workflow_dispatch' && inputs.release_tag != '' - Upload combined desktop installers and metadata
actions/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 != '') ) }}
- Check out source
actions/checkout@v5 - Download Windows release assets
mkdir -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' - Verify signature and installer digests
node 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 != '') ) }}
- Check out source
actions/checkout@v5 - Download Linux release assets
mkdir -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' - Verify Linux automatic update release
node scripts/verify-linux-update-release.mjs release/desktop
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'
- Checkout
actions/checkout@v5 - Build local image
docker build --build-arg EDGE_EVER_BUILD_ID="${GITHUB_SHA}" --tag edgeever:ci . - Start container
docker run --detach --name edgeever-ci \ --env EDGE_EVER_AUTH_PASSWORD=container-ci-password \ --publish 127.0.0.1:8787:8787 \ edgeever:ci - Verify shared API and persistent storage
for 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… - Stop container gracefully
docker stop --timeout 30 edgeever-ci docker logs edgeever-ci | grep -F '[self-hosted] shutdown complete' - Show container logs on failure
docker 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'
- Checkout
actions/checkout@v5 - Resolve image tags
if [[ -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=… - Set up QEMU
docker/setup-qemu-action@v3 - Set up Docker Buildx
docker/setup-buildx-action@v3 - Sign in to GitHub Container Registry
docker/login-action@v3 - Build and publish image
docker/build-push-action@v6 - Verify anonymous GHCR image access
docker 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'
- Verify prepared public image tags
release_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}"…
Triggers: release, workflow_dispatch
Trigger asynchronous Tencent-side image build · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Resolve verified source and CNB destination
if [[ "${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}" - Check out the GHCR-verified source
actions/checkout@v5 - Verify formal source metadata
test "$(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' - Push the same Git commit to CNB
test -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}"
Triggers: pull_request, push, workflow_dispatch
Build Chromium and Firefox · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Checkout
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Install dependencies
bun install --frozen-lockfile - Test extension manifests
bun run test:extension - Build Chromium extension
bun run build:extension - Build Firefox extension
bun run build:extension:firefox - Lint Firefox extension
bun run lint:extension:firefox
Triggers: push, pull_request, workflow_dispatch
Xcode build + unit tests · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Checkout
actions/checkout@v5 - Select Xcode
sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer || \ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer xcodebuild -version - Install XcodeGen
brew install xcodegen - Generate project
xcodegen generate - Resolve packages
xcodebuild -project EdgeEver.xcodeproj -scheme EdgeEver \ -resolvePackageDependencies \ -destination 'generic/platform=iOS Simulator' - Build
set -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 - Test
DEST="$( 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
Triggers: workflow_dispatch
xcode-cloud · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Check out Xcode Cloud tooling
actions/checkout@v5 - Install App Store Connect client dependencies
python3 -m pip install --quiet PyJWT cryptography - Inspect, start, or monitor Xcode Cloud
case "$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 […
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' ) }}
- Checkout
actions/checkout@v5 - Validate Draft Release target
expected_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' - Set up Bun for release validation
oven-sh/setup-bun@v2 - Install release validation dependencies
bun install --frozen-lockfile - Run full project regression tests
bun run test - Compare with previous formal release
mapfile -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… - Verify existing APK uses the pinned signer
mkdir -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' }}
- Restore Draft state
gh 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' }}
- Checkout
actions/checkout@v5 - Copy latest compatible APK without renaming
mkdir -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' ) ) }}
- Checkout
actions/checkout@v5 - Install dependencies
bun install --frozen-lockfile - Check Expo dependencies
if ! 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 - Typecheck
bun run typecheck:mobile - Reset Metro cache for this checkout
bun --cwd apps/mobile expo export --platform android --clear --max-workers 1 --output-dir "$RUNNER_TEMP/edgeever-metro-warmup" - Restore Android signing keystore
test -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" - Build fast main-branch APK
bun run build:android:fastCondition: github.event_name == 'push' - Build signed release APK for GitHub Release
bun run build:android:apkCondition: github.event_name == 'workflow_dispatch' - Verify release APK signature
node scripts/verify-android-apk-signature.mjs apps/mobile/android/app/build/outputs/apk/release/app-release.apkCondition: github.event_name == 'workflow_dispatch' - Upload APK
actions/upload-artifact@v7Condition: github.event_name == 'push' - Upload release APK to GitHub Release
release_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' - Remove temporary upload keystore
rm -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' }}
- Checkout timing reporter
actions/checkout@v5 - Collect native build timings
gh 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" - Upload machine-readable timing report
actions/upload-artifact@v7
Triggers: workflow_dispatch
audit · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- actions/checkout@v5
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Install dependencies
bun install --frozen-lockfile - Inspect generated APK types
node scripts/download-play-universal-apk.mjs "${{ inputs.version_code }}" --inspect
Triggers: workflow_dispatch
Collect all Release endpoint timings · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Check out timing reporter
actions/checkout@v5 - Resolve Release and post-publication workflow runs
release_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 … - Wait for Cloudflare deployment and TCR source sync
wait_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" - Observe public TCR Release readiness
status="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/… - Download exact Actions timing data
collect_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… - Generate unified Release timing report
store_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… - Return timing report to the related Issue
marker="<!-- 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… - Upload machine-readable Release timing report
actions/upload-artifact@v7
Triggers: workflow_dispatch
Validate store delivery source · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Validate Release and resolve its formal predecessor
release="$( 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… - Check out the immutable Release target
actions/checkout@v5 - Validate the checked-out Release target
test "$(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 - Validate mobile version and audited change range
set -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') }}
- Check out the immutable Release target
actions/checkout@v5 - Set up Bun
oven-sh/setup-bun@v2 - Set up EAS CLI
expo/expo-github-action@v8Condition: ${{ !inputs.recover_play_apk }} - Install dependencies
for 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 }} - Install Play APK recovery dependency
if ! 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 }} - Restore Android upload keystore
test -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 }} - Build and verify signed Play bundle
bun run build:android:playCondition: ${{ !inputs.recover_play_apk }} - Preserve Play bundle and deobfuscation mapping
actions/upload-artifact@v7Condition: ${{ !inputs.recover_play_apk }} - Upload bundle to Google Play
test -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 }} - Download Play-signed universal APK
apk_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" - Verify Play app signature
node scripts/verify-android-apk-signature.mjs "$apk_path" - Verify Play APK architecture
actual_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 - Replace GitHub Release APK with the Play-signed build
gh release upload "$RELEASE_TAG" "$apk_path" --repo "$GITHUB_REPOSITORY" --clobber - Remove temporary upload keystore
rm -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') }}
- Check out the immutable Release target
actions/checkout@v5 - Check out current store-delivery tooling
actions/checkout@v5 - Resolve the App Store build
if [[ -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… - Load App Store release notes
node --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… - Set up Ruby and fastlane
ruby/setup-ruby@v1 - Submit the uploaded build to App Review
test -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 ]]; …
Triggers: push, workflow_dispatch
Publish Tencent COS mirror · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Checkout
actions/checkout@v5 - Install Tencent COSCMD
python3 -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}" - Configure COSCMD
test -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}" - Upload installer files
coscmd upload -f apps/site/public/install.sh install.sh coscmd upload -f apps/site/public/compose.yaml compose.yaml - Verify public mirror
base_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 -
Triggers: schedule, workflow_dispatch
Sync Fork and trigger deployment · no job dependencies declared
Condition: github.repository != 'tianma-if/edgeever'
- Checkout deployed repository
actions/checkout@v5 - Require a GitHub Fork
set -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 - Resolve upstream version
set -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… - Align to upstream
set -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' - Set up Bun
oven-sh/setup-bun@v2Condition: steps.upstream.outputs.align_mode == 'merge' - Verify customized merge
set -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' - Publish update
set -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' - Request Cloudflare deployment
set -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' - 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' - Report an update failure
set -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()
Triggers: workflow_dispatch
Build and test-sign Windows x64 installer · no job dependencies declared
Condition: github.repository == 'tianma-if/edgeever'
- Check out source
actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - Set up Bun
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - Set up Rust
dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c - Install dependencies
for ($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) } - Run project type checks
bun run typecheck bun run typecheck:mobile - Build Web renderer
bun run build:web - Build Rust sidecar
cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release - Package unsigned Windows installer
bun scripts/run-desktop-builder.mjs --publish never - Verify Windows package contents and architecture
bun scripts/verify-desktop-package.mjs - 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 - Verify packaged Windows first launch
bun run verify:packaged-desktop-startup -- release/desktop/win-unpacked/EdgeEver.exe - 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 - Upload unsigned installer for SignPath
actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - Submit SignPath test-signing request
SignPath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd - 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… - Upload test-signed installer
actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
build:android:fast: bash scripts/build-android-local.sh fastbuild:android:fast:local: bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fastbuild:android:apk: bash scripts/build-android-local.sh apkbuild:android:apk:local: bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:apkbuild:android:play: bash scripts/build-android-local.sh playbuild:android:play:local: bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:playbuild: bun run build:webbuild:self-hosted: bun build scripts/self-hosted-server.mjs --target=bun --format=esm --outdir=dist/self-hosted --sourcemap=nonebuild:plugin-api: bun run --cwd packages/plugin-api buildbuild:extension: vite build --config apps/extension/vite.config.tsbuild:extension:firefox: bun run --cwd apps/extension build:firefoxbuild:web: vite build --config apps/web/vite.config.tsbuild:desktop:sidecar: cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --releasebuild: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 distrelease: bun scripts/release.mjspublish:stores: bun scripts/store-delivery.mjsbuild:worker: bun scripts/build-cloudflare-worker.mjsbuild:cloudflare: bun run typecheck && bun run build && bun run build:workerbuild:site: bun --cwd apps/site builddeploy:doctor: bun scripts/cloudflare-deploy.mjs doctordeploy:setup: bun scripts/cloudflare-deploy.mjs setupdeploy: bun run build:cloudflare && EDGE_EVER_USE_EXISTING_AUTH_SECRET=true bun run deploy:cideploy:manual: export EDGE_EVER_DEPLOYMENT_TRIGGER=manual EDGE_EVER_DEPLOYMENT_METHOD=local_cli && bun run deploy:doctor && bun run build:cloudflare && bun run deploy:cideploy:ci: bun run db:migrate:remote && bun run deploy:worker && bun run deploy:verifydeploy:worker: bun scripts/run-wrangler.mjs deploydeploy:cloudflare-builds: EDGE_EVER_USE_EXISTING_AUTH_SECRET=true bun run deploy:cideploy:verify: bun scripts/verify-deployment.mjsdeploy:builds:setup: bun scripts/cloudflare-workers-builds.mjs setupdeploy:site: bun run build:site && wrangler pages deploy apps/site/dist --project-name=edgeever-official
build: vite build --config vite.config.tsbuild:firefox: EDGE_EVER_EXTENSION_TARGET=firefox vite build --config vite.config.ts
build: vite build
build: astro build
build: bun build ./src/index.ts --outdir ./dist --target browser --format esm && tsc -p tsconfig.build.json
Repository README
View original on GitHub ↗Full upstream document by @tianma-if · README.md · snapshot 9918570
EdgeEver
An open-source, AI-native knowledge base & portable Evernote alternative
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
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:
Option A: Deploy with an AI Agent (Recommended)
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:
- Fork the Repository: Click Fork at the top right of GitHub to fork EdgeEver into your personal account.
- Create Cloudflare Resources: Create D1
edgeeverand R2edgeever-resources. - Import & Configure the Project: Create a Worker named
edgeeverfrom the Fork'smainbranch 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. - 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. - 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 theEDGE_EVER_AUTH_USERNAMEWorkers Builds variable before retrying. Once deployed, confirm/api/healthreturns200, then log in with the configured username and password. - 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.devdomain 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
- 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
mainbranch deployment-only. Create a separate branch from the officialupstream/mainfor synchronization, development, and pull requests; do not develop on or Sync fork the deploymentmain.
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:
- Evernote Migration: Please refer to docs/evernote-migration-guide.md
- flomo Migration: Please refer to docs/flomo-migration-guide.md
- Memos Migration: Please refer to docs/memos-migration-guide.md
- Notion Migration: Please refer to docs/notion-migration-guide.md
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 →