feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

5
.github/FUNDING.yml vendored 普通文件
查看文件

@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: joplin
github: laurent22
custom: https://joplinapp.org/donate/

9
.github/ISSUE_TEMPLATE.md vendored 普通文件
查看文件

@@ -0,0 +1,9 @@
👉 Please follow one of these issue templates:
- https://github.com/laurent22/joplin/issues/new/choose
⚠️
The GitHub issue tracker is for **bugs** and **security issues** ONLY. For feature requests and support, please use the forum:
https://discourse.joplinapp.org/
⚠️
Note: to keep the backlog clean and actionable, issues may be immediately closed if they do not follow one of the above issue templates.

62
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored 普通文件
查看文件

@@ -0,0 +1,62 @@
name: Bug Report
description: Report a reproducible bug or regression in Joplin.
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: os
attributes:
label: "Operating system"
multiple: false
options:
- "Windows"
- "macOS"
- "Linux"
- "Android"
- "iOS"
validations:
required: true
- type: input
id: version
attributes:
label: "Joplin version"
placeholder: "For example 1.0.5"
description: Please note that we will close the issue if the exact version is not provided, as we cannot help without this.
validations:
required: true
- type: textarea
id: desktop-about-content
attributes:
label: "Desktop version info"
description: "If this issue is about the **desktop app**, please open the \"About\" dialog under the \"Help\" or \"Joplin\" menu and copy its content here."
placeholder: "Joplin 2.13.5 (dev, darwin)\n\nClient ID: ..."
- type: textarea
id: current
attributes:
label: Current behaviour
description: What did Joplin do? Include screenshots and video recordings for UI problems if needed. If you are reporting a clipper bug, please include an example URL that shows the issue.
placeholder: |
1. This
2. Then that
3. Then this
4. Etc.
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What did you expect Joplin to do?
- type: textarea
id: logs
attributes:
label: Logs
description: "If relevant, please provide a log file as described here: https://joplinapp.org/help/apps/debugging"

8
.github/ISSUE_TEMPLATE/config.yml vendored 普通文件
查看文件

@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Feature Requests
url: https://discourse.joplinapp.org/c/features/
about: Discuss ideas for new features or changes
- name: Support
url: https://discourse.joplinapp.org/c/support/
about: Please ask for help here

47
.github/PULL_REQUEST_TEMPLATE vendored 普通文件
查看文件

@@ -0,0 +1,47 @@
<!--
Before contributing, please read the contribution guidelines: https://github.com/laurent22/joplin/blob/dev/readme/dev/index.md
If this is a Google Summer of Code pull request, please read the [GSoC pull request guidelines](https://github.com/joplin/gsoc/blob/master/pull_request_guidelines.md).
---
**Pull request title**: Please prefix the title with the area you are targeting, then add the issue you are addressing.
The format is:
<Prefix>: <Fixes|Resolves> #<issue>: <description>
Use "Resolves #123" for new features or improvements, and "Fixes #123" for bug fixes.
Examples of good titles:
- Desktop: Resolves #123: Added new setting to change font
- Mobile, Desktop: Fixes #456: Fixed config screen error
- Mobile, Desktop, Cli: Resolves #777: Improved note search performance
Valid prefixes:
- `Desktop` — the Windows/macOS/Linux app (Electron app)
- `Mobile` — the mobile app (both Android and iOS)
- `Android` — only the Android app
- `iOS` — only the iOS app
- `Web` — only the web app
- `Windows` — Windows-specific changes
- `Linux` — Linux-specific changes
- `macOS` — macOS-specific changes
- `Cli` — the command line app
- `Server` — the Joplin Server
- `Clipper` — the web clipper browser extension
- `Transcribe` — the Transcribe server
- `Plugins` — the plugin API or built-in plugins
- `Api` — the data API
- `Cloud` — Joplin Cloud
- `Tools` — internal scripts and build tools
- `CI` — continuous integration and GitHub workflows
- `Doc` — documentation, README, website content
- `Chore` — maintenance work that does not fit any of the above (dependency bumps, refactoring, cleanup)
If the change targets several areas, separate them with commas — for example "Mobile, Desktop, Cli: Resolves #777: Improved note search performance".
-->

25
.github/lock.yml vendored 普通文件
查看文件

@@ -0,0 +1,25 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 7
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: false
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: false
# Limit to only `issues` or `pulls`
only: issues
# Optionally, specify configuration settings just for `issues` or `pulls`
# issues:
# exemptLabels:
# - help-wanted
# lockLabel: outdated
# pulls:
# daysUntilLock: 30
# Repository to extend settings from
# _extends: repo

67
.github/scripts/build_windows.ps1 vendored 普通文件
查看文件

@@ -0,0 +1,67 @@
# Force the postinstall build to run sequentially and retry `yarn install` a
# few times. Parallel postinstall builds randomly crash on Windows with
# STATUS_STACK_BUFFER_OVERRUN (0xC0000409), and node-pre-gyp prebuilt sqlite3
# downloads from GitHub Releases occasionally fail. Setting the env vars here
# (rather than only on the workflow step) ensures they reach every child
# process spawned by `yarn install` -> postinstall -> gulp.
function Install-WindowsDeps {
$env:BUILD_SEQUENCIAL = '1'
$env:IS_CONTINUOUS_INTEGRATION = '1'
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
# Deliberately not setting npm_config_build_from_source to force the
# sqlite3 source build: sharp treats that variable as a boolean and
# would then also build libvips from source, which fails. sqlite3
# installs with --fallback-to-build, so it compiles by itself when the
# win32-arm64 prebuilt is missing.
# Deliberately not installing the VSSetup PowerShell module here: with it
# present, node-gyp probes Visual Studio via `Get-VSSetupInstance |
# ConvertTo-Json`, whose output exceeds the default 1MB child process
# buffer on the VS 2026 image. That fails with
# ERR_CHILD_PROCESS_STDIO_MAXBUFFER and node-gyp then reports the
# install as version "undefined". Without the module it falls through to
# its own Find-VisualStudio.cs helper, which prints far less.
}
$attempts = 3
for ($i = 1; $i -le $attempts; $i++) {
yarn install
if ($LASTEXITCODE -eq 0) { return }
if ($i -eq $attempts) { exit $LASTEXITCODE }
Write-Host "yarn install failed (attempt $i/$attempts) - retrying..."
Start-Sleep -Seconds 10
}
}
# Install dependencies and run `yarn dist` for the Windows desktop app.
# Extra args are forwarded to `yarn dist` (e.g. --publish=never).
#
# Each runner builds only its own arch: cross compiling arm64 from x64 silently
# ships x64 .node files, as @electron/rebuild doesn't resolve native modules
# hoisted to the workspace root.
# https://github.com/electron-userland/electron-builder/issues/10187
function Build-WindowsApp {
param([string[]]$DistArgs = @())
Install-WindowsDeps
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
# NSIS gets an `-arm64` suffix on its own, but the portable name is fixed
# and the update metadata is `latest.yml` for every Windows arch. Both
# would clobber the x64 runner's artifacts.
# https://github.com/electron-userland/electron-builder/issues/6372
$archArgs = @(
'--arm64'
'-c.portable.artifactName=${productName}Portable-arm64.${ext}'
'-c.publish.provider=github'
'-c.publish.channel=latest-win-arm64'
)
} else {
$archArgs = @('--x64', '--ia32')
}
cd packages/app-desktop
yarn dist @archArgs @DistArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}

163
.github/scripts/check_pr_title.js vendored 普通文件
查看文件

@@ -0,0 +1,163 @@
// Validates the PR title and acts on the result.
//
// - Renovate is filtered out at the workflow level (job `if:`).
// - Translation-only PRs (every changed file is a .po) are skipped.
// - Users in `softCheckUsers` get a relaxed check (issue number optional)
// and only ever receive a comment, never a close.
// - Everyone else must match the strict format. Invalid titles get a
// comment and the PR is closed. We also apply a marker label so that
// we can later tell our closures apart from any other closure.
// - If the title becomes valid and the marker label is present, the PR
// is reopened and the label is removed. Closures by humans (or by
// another workflow) lack the label and are never overturned.
//
// Invoked from .github/workflows/check-pr-title.yml via actions/github-script.
// Required inputs come from `env`: PR_AUTHOR, PR_NUMBER. The title is
// fetched from the API rather than passed via env to avoid YAML expansion
// silently stripping leading whitespace from `${{ ... }}`.
module.exports = async ({ github, context, core }) => {
const softCheckUsers = ['laurent22', 'personalizedrefrigerator', 'mrjo118', 'tessus', 'CalebJohn', 'Rygaa'];
const autoClosedLabel = 'auto-closed: invalid-title';
// Product-area prefixes are kept in sync with the list recognised by
// packages/tools/git-changelog.ts so that valid titles are picked up by
// the changelog generator. Non-product prefixes (Tools, Chore, CI, Doc,
// Plugin Repo) are administrative categories for changes that do not
// ship to users and so do not appear in the changelog.
const prefix = '(Desktop|Mobile|Android|iOS|Web|Windows|Linux|macOS|Cli|Clipper|Server|Transcribe|Plugins|Api|Cloud|Tools|Chore|CI|Doc)';
const prefixList = `${prefix}(,\\s*${prefix})*`;
const strictRegex = new RegExp(`^${prefixList}: (Fixes|Resolves) #[0-9]+: .+`);
const softRegex = new RegExp(`^${prefixList}: ((Fixes|Resolves) #[0-9]+: )?.+`);
const author = process.env.PR_AUTHOR;
const prNumber = Number(process.env.PR_NUMBER);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const title = pr.title;
core.info(`Title (length=${title.length}): ${JSON.stringify(title)}`);
const isSoft = softCheckUsers.includes(author);
// listFiles returns up to 30 files per page; a pure translation PR is
// small, so checking the first page is enough.
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const isTranslationOnly = files.length > 0 && files.every(f => f.filename.endsWith('.po'));
if (isTranslationOnly) {
core.info('Translation-only PR — skipping title check.');
return;
}
// Doc-only PRs do not require an issue number.
const isDocOnly = /^Doc(,\s*Doc)*:/.test(title);
const regex = isSoft || isDocOnly ? softRegex : strictRegex;
if (regex.test(title)) {
core.info('Title is valid.');
// If we previously closed this PR for an invalid title and the
// title is now valid, reopen it. We only reopen if our marker
// label is present, so closures by humans (or other workflows)
// are never overturned. A maintainer can also remove the label
// by hand to lock a PR closed regardless of future fixes.
const wasAutoClosed = pr.state === 'closed' && pr.labels.some(l => l.name === autoClosedLabel);
if (wasAutoClosed) {
try {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'open',
});
} catch (error) {
// GitHub refuses to reopen a PR when another open PR
// already exists from the same head→base branch pair.
// In that case the contributor has already opened a
// replacement, so leave this PR closed.
if (error.status === 422) {
core.info('Cannot reopen — another PR is already open from the same branch.');
return;
}
throw error;
}
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: autoClosedLabel,
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `@${author} thanks for fixing the title — this PR has been reopened.`,
});
core.info('PR reopened after title was fixed.');
}
return;
}
// Hidden marker so we can detect a previous nag and avoid reposting on
// every `synchronize` event (push to the PR branch).
const commentMarker = '<!-- joplin-bot:invalid-pr-title -->';
const helpMessage = [
commentMarker,
`@${author} the pull request title does not match the required format.`,
'',
'Please prefix the title with the area you are targeting, then add the issue you are addressing. If the change targets several areas, separate them with commas. For example:',
'',
'- `Desktop: Resolves #123: Added new setting to change font`',
'- `Mobile, Desktop: Fixes #456: Fixed config screen error`',
'- `Mobile, Desktop, Cli: Resolves #777: Improved note search performance`',
'',
'See the [pull request template](https://github.com/laurent22/joplin/blob/dev/.github/PULL_REQUEST_TEMPLATE) for the list of valid prefixes and the full specification.',
'',
isSoft
? '_This PR has been left open — please update the title when you have a moment._'
: '_This PR has been closed automatically. Once you update the title to match the format above, the PR will be reopened automatically._',
].join('\n');
const existingComments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const alreadyCommented = existingComments.some(c => c.body && c.body.includes(commentMarker));
if (!alreadyCommented) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: helpMessage,
});
} else {
core.info('Invalid-title comment already posted — not commenting again.');
}
if (!isSoft) {
// Label first so the marker is set before the close event lands.
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: [autoClosedLabel],
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.setFailed('Pull request title does not match the required format.');
}
};

查看文件

@@ -0,0 +1,43 @@
#!/bin/bash
VERSION=$(echo "$GIT_TAG_NAME" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
echo "VERSION=$VERSION"
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
# Check if it's a server release, otherwise exit
if [[ $GIT_TAG_NAME != $SERVER_TAG_PREFIX-* ]]; then
exit 0
fi
docker manifest inspect $SERVER_REPOSITORY:arm64-$VERSION > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Image $SERVER_REPOSITORY:arm64-$VERSION does not exist on the remote registry."
exit 0
fi
docker manifest inspect $SERVER_REPOSITORY:amd64-$VERSION > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Image $SERVER_REPOSITORY:amd64-$VERSION does not exist on the remote registry."
exit 0
fi
docker manifest create $SERVER_REPOSITORY:$VERSION \
$SERVER_REPOSITORY:arm64-$VERSION \
$SERVER_REPOSITORY:amd64-$VERSION
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:arm64-$VERSION --arch arm64
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
docker manifest push $SERVER_REPOSITORY:$VERSION
docker manifest create $SERVER_REPOSITORY:latest \
$SERVER_REPOSITORY:arm64-$VERSION \
$SERVER_REPOSITORY:amd64-$VERSION
docker manifest annotate $SERVER_REPOSITORY:latest $SERVER_REPOSITORY:arm64-$VERSION --arch arm64
docker manifest annotate $SERVER_REPOSITORY:latest $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
docker manifest push $SERVER_REPOSITORY:latest

73
.github/scripts/repeat_changed_tests.sh vendored 可执行文件
查看文件

@@ -0,0 +1,73 @@
#!/bin/bash
# Run the test files changed by a pull request several times, to catch flaky
# tests before they are merged.
#
# Usage: repeat_changed_tests.sh <base_ref> [repeat_count]
set -u
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ROOT_DIR="$SCRIPT_DIR/../.."
BASE_REF="${1:-}"
REPEAT_COUNT="${2:-5}"
cd "$ROOT_DIR"
# The CI checkout is shallow, so the base branch is not present locally. Always
# fetch it rather than using a local branch of the same name, which may be
# outdated.
if git fetch --no-tags --depth=1 origin "$BASE_REF" 2> /dev/null; then
BASE_REF=FETCH_HEAD
elif ! git rev-parse --verify --quiet "$BASE_REF" > /dev/null; then
echo "Could not resolve base ref \"$BASE_REF\""
exit 1
fi
# `integration-tests` holds the Playwright specs (see app-desktop/playwright.config.ts),
# which Jest cannot run. Other `.spec.*` files are Jest tests, so they are kept.
testFiles=$(git diff --name-only --diff-filter=d "$BASE_REF" HEAD | grep -E '^packages/[^/]+/.*\.(test|spec)\.(ts|tsx|js|jsx)$' | grep -v -E '^packages/[^/]+/integration-tests/' || true)
if [ -z "$testFiles" ]; then
echo "No test files were changed - skipping flaky test detection"
exit 0
fi
echo "Changed test files:"
echo "$testFiles" | sed 's/^/ /'
# Each package has its own Jest config and must be run from its own directory.
packages=$(echo "$testFiles" | sed -E 's|^packages/([^/]+)/.*|\1|' | sort -u)
for package in $packages; do
packageDir="$ROOT_DIR/packages/$package"
# Some packages have test files but no test runner configured.
if ! node -e "const s = require('$packageDir/package.json').scripts; process.exit(s && s['test-ci'] ? 0 : 1)" 2> /dev/null; then
echo "Package $package has no test-ci script - skipping"
continue
fi
# See packages/app-mobile/tools/runTestsConditionally.js
if [ "$package" == "app-mobile" ] && [ "$RUNNER_OS" == "macOS" ]; then
echo "Skipping app-mobile tests on macOS"
continue
fi
# Pass the file names without their extension: some packages run the
# TypeScript sources while others run the compiled files, and Jest matches
# these patterns against the full path either way.
packageTestNames=$(echo "$testFiles" | grep -E "^packages/$package/" | xargs -n 1 basename | sed -E 's#\.[a-z]+$##')
cd "$packageDir"
for i in $(seq 1 "$REPEAT_COUNT"); do
echo "Running $package tests - attempt $i/$REPEAT_COUNT..."
# shellcheck disable=SC2086
if ! yarn jest $packageTestNames --forceExit; then
echo "Tests failed on attempt $i/$REPEAT_COUNT in $package - they are flaky if earlier attempts passed"
exit 1
fi
done
cd "$ROOT_DIR"
done

37
.github/scripts/retry.sh vendored 可执行文件
查看文件

@@ -0,0 +1,37 @@
#!/bin/bash
# Retry a command up to a few times. Used to absorb random CI flakes (network
# failures, slow mirrors, etc).
#
# Usage: retry [attempts] [sleep_seconds] -- command [args...]
# Both attempts and sleep_seconds are optional and default to 3 and 10.
retry() {
local attempts=3
local sleep_seconds=10
if [[ "$1" =~ ^[0-9]+$ ]]; then
attempts="$1"
shift
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
sleep_seconds="$1"
shift
fi
if [ "$1" = '--' ]; then
shift
fi
local i=1
while true; do
"$@"
local result=$?
if [ $result -eq 0 ]; then
return 0
fi
if [ $i -ge $attempts ]; then
return $result
fi
echo "\`$*\` failed (attempt $i/$attempts) - retrying in ${sleep_seconds}s..."
i=$((i + 1))
sleep "$sleep_seconds"
done
}

383
.github/scripts/run_ci.sh vendored 可执行文件
查看文件

@@ -0,0 +1,383 @@
#!/bin/bash
# =============================================================================
# Setup environment variables
# =============================================================================
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ROOT_DIR="$SCRIPT_DIR/../.."
TRANSCRIBE_TAG_PREFIX=transcribe
TRANSCRIBE_REPOSITORY=joplin/transcribe
IS_PULL_REQUEST=0
IS_DESKTOP_RELEASE=0
IS_SERVER_RELEASE=0
IS_TRANSCRIBE_RELEASE=0
IS_LINUX=0
IS_MACOS=0
# If pull requests are coming from a branch of the main repository,
# IS_PULL_REQUEST will be zero.
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
IS_PULL_REQUEST=1
fi
if [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
IS_SERVER_RELEASE=1
fi
if [[ $GIT_TAG_NAME = $TRANSCRIBE_TAG_PREFIX-* ]]; then
IS_TRANSCRIBE_RELEASE=1
fi
if [[ $GIT_TAG_NAME = v* ]]; then
IS_DESKTOP_RELEASE=1
fi
if [ "$RUNNER_OS" == "Linux" ]; then
IS_LINUX=1
IS_MACOS=0
else
IS_LINUX=0
IS_MACOS=1
fi
DOCKER_IMAGE_PLATFORM="linux/amd64"
# Tests can randomly fail in some cases, so only run them when not publishing
# a release
RUN_TESTS=0
if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ] && [ "$IS_TRANSCRIBE_RELEASE" = 0 ]; then
RUN_TESTS=1
fi
if [ "$RUNNER_ARCH" == "ARM64" ]; then
if [ "$IS_SERVER_RELEASE" == "0" ] && [ "$IS_TRANSCRIBE_RELEASE" == "0" ]; then
# We exit now because nothing works properly with the ARM64 architecture.
# We only proceed if building the server image.
echo "Running on ARM64 and not trying to build server image - early exit"
exit 0
fi
fi
if [ "$RUNNER_ARCH" == "ARM64" ]; then
# Canvas is only needed for tests and it doesn't build in ARM64 so remove it
RUN_TESTS=0
cd "$ROOT_DIR/packages/lib"
yarn remove canvas
cd "$ROOT_DIR"
DOCKER_IMAGE_PLATFORM="linux/arm64"
# Delete certain directories because `yarn install` will fail on ARM64.
rm -rf app-desktop
rm -rf app-mobile
fi
# =============================================================================
# Print environment
# =============================================================================
echo "RUNNER_OS=$RUNNER_OS"
echo "RUNNER_ARCH=$RUNNER_ARCH"
echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW"
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
echo "GITHUB_REF=$GITHUB_REF"
echo "RUNNER_OS=$RUNNER_OS"
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
echo "TRANSCRIBE_TAG_PREFIX=$TRANSCRIBE_TAG_PREFIX"
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
echo "IS_DESKTOP_RELEASE=$IS_DESKTOP_RELEASE"
echo "IS_SERVER_RELEASE=$IS_SERVER_RELEASE"
echo "IS_TRANSCRIBE_RELEASE=$IS_TRANSCRIBE_RELEASE"
echo "RUN_TESTS=$RUN_TESTS"
echo "IS_LINUX=$IS_LINUX"
echo "IS_MACOS=$IS_MACOS"
echo "Node $( node -v )"
echo "Npm $( npm -v )"
echo "Yarn $( yarn -v )"
echo "Rust $( rustc --version )"
# =============================================================================
# Install packages
# =============================================================================
cd "$ROOT_DIR"
source "$(dirname "$0")/retry.sh"
retry yarn install
testResult=$?
if [ $testResult -ne 0 ]; then
echo "Yarn installation failed. Search for 'exit code 1' in the log for more information."
exit $testResult
fi
# =============================================================================
# Run test units
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
echo "Step: Running tests..."
# On Linux, we run the Joplin Server tests using PostgreSQL
if [ "$IS_LINUX" == "1" ]; then
echo "Running Joplin Server tests using PostgreSQL..."
# Docker Hub auth/pulls occasionally time out on CI, so retry a few
# times before giving up.
cmdResult=1
for attempt in 1 2 3; do
sudo docker compose --parallel 1 --file docker-compose.db-dev.yml up -d
cmdResult=$?
if [ $cmdResult -eq 0 ]; then
break
fi
echo "docker compose up failed (attempt $attempt). Retrying in 15s..."
sudo docker compose --file docker-compose.db-dev.yml down --remove-orphans || true
sleep 15
done
if [ $cmdResult -ne 0 ]; then
exit $cmdResult
fi
export JOPLIN_TESTS_SERVER_DB=pg
else
echo "Running Joplin Server tests using SQLite..."
fi
# Need this because we're getting this error:
#
# @joplin/lib: FATAL ERROR: Ineffective mark-compacts near heap limit
# Allocation failed - JavaScript heap out of memory
#
# https://stackoverflow.com/questions/38558989
export NODE_OPTIONS="--max-old-space-size=32768"
yarn test-ci
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
# Run the test files changed by the pull request several times, to catch
# flaky tests before they are merged.
if [ "$IS_PULL_REQUEST" == "1" ]; then
echo "Step: Checking changed tests for flakiness..."
"$SCRIPT_DIR/repeat_changed_tests.sh" "$GITHUB_BASE_REF"
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
fi
# =============================================================================
# Run linter for pull requests only. We also don't want this to make the desktop
# release randomly fail.
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
echo "Step: Running linter..."
yarn linter-ci ./
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
yarn packageJsonLint
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
# =============================================================================
# Validate translations - this is needed as some users manually edit .po files
# (and often make mistakes) instead of using a proper tool like poedit. Doing it
# for Linux only is sufficient.
# =============================================================================
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Validating translations..."
node packages/tools/validate-translation.js
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
# =============================================================================
# Check that we didn't lose any string due to gettext not being able to parse
# newly modified or added scripts. This is convenient to quickly view on GitHub
# what commit may have broken translation building.
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Checking for lost translation strings..."
xgettext --version
node packages/tools/build-translation.js --missing-strings-check-only
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
fi
# =============================================================================
# Check .gitignore and .ignore.eslint files - they should be updated when
# new TypeScript files are added by running `yarn updateIgnored`.
# See coding_style.md
# =============================================================================
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Checking for files that should have been ignored..."
# .gitignore and .ignore.eslint can be modified during yarn install. Reset them
# so that checkIgnoredFiles works.
git restore .gitignore .ignore.eslint
node packages/tools/checkIgnoredFiles.js
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
# =============================================================================
# Check that the website still builds
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Check that the website still builds..."
mkdir -p ../joplin-website/docs
CROWDIN_PERSONAL_TOKEN="$CROWDIN_PERSONAL_TOKEN" yarn crowdinDownload
SKIP_SPONSOR_PROCESSING=1 yarn buildWebsite
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
fi
# =============================================================================
# Spellchecking
# =============================================================================
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Spellchecking..."
yarn spellcheck --all
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
# =============================================================================
# Find out if we should run the build or not. Electron-builder gets stuck when
# building PRs so we disable it in this case. The Linux build should provide
# enough info if the app builds or not.
# https://github.com/electron-userland/electron-builder/issues/4263
# =============================================================================
if [ "$IS_PULL_REQUEST" == "1" ]; then
if [ "$IS_MACOS" == "1" ]; then
echo "Step: Not building Electron app"
exit 0
fi
fi
# =============================================================================
# Build the Electron app or Docker image depending on the current tag.
#
# If the current tag is a desktop release tag (starts with "v", such as
# "v1.4.7"), we build and publish to GitHub. Otherwise we only build but don't
# publish to GitHub. It helps finding out any issue in pull requests and dev
# branch.
# =============================================================================
cd "$ROOT_DIR/packages/app-desktop"
# On macOS, `yarn dist` can fail randomly with `hdiutil detach ... Resource
# busy` when packaging the DMG, because another process (Spotlight mds,
# diskimages-helper, etc.) still holds the mounted image. Retry the whole
# build a couple of times to absorb the flake.
run_yarn_dist() {
local attempts=3
local i=1
while true; do
USE_HARD_LINKS=false yarn dist "$@"
local result=$?
if [ $result -eq 0 ]; then
return 0
fi
if [ "$IS_MACOS" != "1" ] || [ $i -ge $attempts ]; then
return $result
fi
echo "yarn dist failed (attempt $i/$attempts) - retrying..."
i=$((i + 1))
sleep 10
done
}
if [ "$IS_DESKTOP_RELEASE" == "1" ]; then
echo "Step: Building and publishing desktop application..."
# cd "$ROOT_DIR/packages/tools"
# node bundleDefaultPlugins.js
cd "$ROOT_DIR/packages/app-desktop"
if [ "$IS_MACOS" == "1" ]; then
# This is to fix this error:
#
# Exit code: ENOENT. spawn /usr/bin/python ENOENT
#
# Ref: https://github.com/electron-userland/electron-builder/issues/6767#issuecomment-1096589528
#
# It can be removed once we upgrade to electron-builder@23, however we
# cannot currently do this due to this error:
# https://github.com/laurent22/joplin/issues/8149
#
# electron-builder@24, however, still expects the python binary to be named
# "python" and seems to no longer respect the PYTHON_PATH environment variable.
# We work around this by aliasing python.
alias python=$(which python3)
fi
run_yarn_dist
elif [[ $IS_LINUX = 1 ]] && [ "$IS_SERVER_RELEASE" == "1" ]; then
echo "Step: Building Joplin Server Docker Image..."
cd "$ROOT_DIR"
yarn buildServerDocker --docker-file Dockerfile.server --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
elif [[ $IS_LINUX = 1 ]] && [ "$IS_TRANSCRIBE_RELEASE" == "1" ]; then
echo "Step: Building Joplin Transcribe Docker Image..."
cd "$ROOT_DIR"
yarn buildServerDocker --docker-file Dockerfile.transcribe --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $TRANSCRIBE_REPOSITORY
else
echo "Step: Building but *not* publishing desktop application..."
if [ "$IS_MACOS" == "1" ]; then
# See above why we need to specify Python
alias python=$(which python3)
# We also want to disable signing the app in this case, because
# it randomly fails and we don't even need it
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
export CSC_IDENTITY_AUTO_DISCOVERY=false
npm pkg set 'build.mac.identity'=null --json
fi
run_yarn_dist --publish=never
fi

25
.github/workflows/automerge.yml vendored 普通文件
查看文件

@@ -0,0 +1,25 @@
name: automerge
on:
schedule:
- cron: '*/10 * * * *'
jobs:
# This job will make the action fail if any of the checks hasn't passed
# https://github.com/marketplace/actions/allcheckspassed
# allchecks:
# runs-on: ubuntu-latest
# steps:
# - uses: wechuli/allcheckspassed@v1
automerge:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- id: automerge
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: "squash"
LOG: "DEBUG"

84
.github/workflows/build-android.yml vendored 普通文件
查看文件

@@ -0,0 +1,84 @@
# The goal of this action is to compile the Android debug build. That should
# tell us automatically if something got broken when a dependency was changed.
name: react-native-android-build-apk
on: [push, pull_request]
jobs:
AssembleRelease:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
steps:
- name: Install Linux dependencies
run: |
sudo apt-get update || true
sudo apt-get install -y libsecret-1-dev
# Free disk space before anything else so the Gradle/NDK transforms
# (which can write 20GB+ during react-native-quick-crypto's OpenSSL
# build) don't exhaust the runner's ~14GB free space. Keep the Android
# SDK and Java toolchains; remove the large preinstalled packages this
# workflow doesn't use.
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/share/swift || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/local/share/powershell || true
docker system prune -af || true
df -h /
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '20'
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'yarn'
- uses: ./.github/workflows/shared/install-yarn
- name: Install
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
env:
SKIP_ONENOTE_CONVERTER_BUILD: 1
- name: Assemble Android Release
run: |
cd packages/app-mobile/android
sed -i -- 's/signingConfig signingConfigs.release/signingConfig signingConfigs.debug/' app/build.gradle
./gradlew assembleRelease
- name: Verify alignment
run: |
cd packages/app-mobile/android/app
APK_FILE="./build/outputs/apk/release/app-release.apk"
if test ! -f "$APK_FILE" ; then
echo "APK file not found."
exit 1
else
echo "APK file found at: $APK_FILE"
fi
BUILD_TOOLS_PATH="$ANDROID_HOME/build-tools/"
if test ! -d "$BUILD_TOOLS_PATH" ; then
echo "Build tools not found at $BUILD_TOOLS_PATH ($ANDROID_HOME, $BUILD_TOOLS_VERSION)"
exit 1
fi
# The build-tools/ directory contains different subdirectories
# for each build tools version. As a result, there may be multiple
# zipalign tools. Select the most recent (biggest two-digit version number):
ZIPALIGN_PATH="$(find $BUILD_TOOLS_PATH -name "zipalign" -print | sort | tail -n1)"
if test ! -x "$ZIPALIGN_PATH" ; then
echo "zipalign not found (searching in $BUILD_TOOLS_PATH, candidate: $ZIPALIGN_PATH)"
exit 1
fi
"$ZIPALIGN_PATH" -c -P 16 -v 4 "$APK_FILE"

94
.github/workflows/build-macos-m1.yml vendored 普通文件
查看文件

@@ -0,0 +1,94 @@
name: Build macOS M1
on: [push, pull_request]
jobs:
Main:
# We always process desktop release tags, because they also publish the release
if: github.repository == 'laurent22/joplin'
runs-on: macos-15
steps:
- uses: actions/checkout@v6
- uses: olegtarasov/get-tag@v2.1.4
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'yarn'
- uses: ./.github/workflows/shared/install-yarn
- name: Install macOs dependencies
if: runner.os == 'macOS'
run: |
# Required for building the canvas package
brew install pango
# See github-action-main.yml for explanation
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Set Publish Flag
run: |
if [[ $GIT_TAG_NAME = v* ]]; then
echo "PUBLISH_ENABLED=true" >> $GITHUB_ENV
else
echo "PUBLISH_ENABLED=false" >> $GITHUB_ENV
fi
- name: Build macOS M1 app
env:
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
PUBLISH_ENABLED: ${{ env.PUBLISH_ENABLED }}
run: |
export npm_config_arch=arm64
export npm_config_target_arch=arm64
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
cd packages/app-desktop
npm pkg set 'build.mac.artifactName'='${productName}-${version}-${arch}.${ext}'
npm pkg delete 'build.mac.target'
npm pkg set 'build.mac.target[0].target'='dmg'
npm pkg set 'build.mac.target[0].arch[0]'='arm64'
npm pkg set 'build.mac.target[1].target'='zip'
npm pkg set 'build.mac.target[1].arch[0]'='arm64'
# Only enable pkg build in the main repository CI. As of 01/15/2026, pkg
# build fails when running on external pull requests.
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
npm pkg set 'build.mac.target[2].target'='pkg'
npm pkg set 'build.mac.target[2].arch[0]'='arm64'
fi
build_dist() {
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
echo "Building and publishing desktop application..."
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
else
echo "Building but *not* publishing desktop application..."
# We also want to disable signing the app in this case, because
# it doesn't work and we don't need it.
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
export CSC_IDENTITY_AUTO_DISCOVERY=false
npm pkg set 'build.mac.identity'=null --json
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64 --publish=never
fi
}
build_dist || build_dist

31
.github/workflows/check-pr-title.yml vendored 普通文件
查看文件

@@ -0,0 +1,31 @@
name: Check pull request title
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
jobs:
main:
# Skip the check entirely for these automation accounts.
if: github.event.pull_request.user.login != 'renovate[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
# Sparse checkout so we only pull the script, not the full repo.
# `pull_request_target` checks out the base branch by default,
# which is what we want — we never execute PR-supplied code.
- uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Check title
uses: actions/github-script@v9
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
script: |
const check = require('./.github/scripts/check_pr_title.js');
await check({ github, context, core });

37
.github/workflows/cla.yml vendored 普通文件
查看文件

@@ -0,0 +1,37 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
jobs:
CLAAssistant:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
path-to-signatures: 'readme/cla/signatures.json'
path-to-document: 'https://github.com/laurent22/joplin/blob/dev/readme/cla.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'cla_signatures'
allowlist: joplinbot,renovate[bot]
# the followings are the optional inputs - If the optional inputs are not given, then default values will be taken
#remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository)
#remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository)
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
lock-pullrequest-aftermerge: false
#use-dco-flag: true - If you are using DCO instead of CLA

24
.github/workflows/close-stale-issues.yml vendored 普通文件
查看文件

@@ -0,0 +1,24 @@
name: 'Close stale issues'
on:
schedule:
- cron: '0 16 * * *'
permissions:
issues: write
jobs:
ProcessStaleIssues:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
# Use this to do a dry run from a pull request
# debug-only: true
stale-issue-message: "Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? If you require support or are requesting an enhancement or feature then please create a topic on the [Joplin forum](https://discourse.joplinapp.org/). This issue may be closed if no further activity occurs. You may comment on the issue and I will leave it open. Thank you for your contributions."
days-before-stale: 30
days-before-close: 7
operations-per-run: 1000
exempt-issue-labels: 'good first issue,upstream,backlog,high,medium,spec,cannot reproduce,enhancement'
stale-issue-label: 'stale'
close-issue-message: 'Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, feel free to create a new issue with up-to-date information.'
# Don't process pull requests at all
days-before-pr-stale: -1

14
.github/workflows/comment-on-failure.yml vendored 普通文件
查看文件

@@ -0,0 +1,14 @@
name: comment-on-failure
on:
workflow_run:
workflows:
- Joplin Continuous Integration
- react-native-android-build-apk
- Build macOS M1
types: [ completed ]
jobs:
comment-failure:
runs-on: ubuntu-latest
steps:
- uses: quipper/comment-failure-action@v0.1.1

查看文件

@@ -0,0 +1,67 @@
name: Delete CodeRabbit PR Comments
on:
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
jobs:
delete-coderabbit-comments:
if: >
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(github.event.comment.user.login == 'coderabbitai' || github.event.comment.user.login == 'coderabbitai[bot]'))
||
(github.event_name == 'pull_request_review' &&
(github.event.review.user.login == 'coderabbitai' || github.event.review.user.login == 'coderabbitai[bot]'))
runs-on: ubuntu-latest
steps:
# Fork PRs trigger `pull_request_review` without exposing repo secrets,
# so the App token can't be minted. Skip the job cleanly in that case
# instead of letting it fail loudly.
- name: Check secrets availability
id: check-secrets
env:
APP_ID: ${{ secrets.CODERABBIT_CLEANUP_APP_ID }}
run: |
if [ -z "$APP_ID" ]; then
echo "App secrets not available (likely a fork PR event); skipping."
echo "available=false" >> "$GITHUB_OUTPUT"
else
echo "available=true" >> "$GITHUB_OUTPUT"
fi
- name: Generate GitHub App token
if: steps.check-secrets.outputs.available == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.CODERABBIT_CLEANUP_APP_ID }}
private-key: ${{ secrets.CODERABBIT_CLEANUP_APP_PRIVATE_KEY }}
- name: Clean up CodeRabbit comments
if: steps.check-secrets.outputs.available == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
run: |
# Clear the body of all CodeRabbit reviews on this PR
# (We can't delete reviews, but clearing the body removes the spam
# while keeping inline code comments visible)
REVIEW_IDS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
--jq '[.[] | select(.user.login == "coderabbitai[bot]" and .body != "") | .id] | .[]')
for REVIEW_ID in $REVIEW_IDS; do
gh api -X PUT "repos/${REPO}/pulls/${PR_NUMBER}/reviews/${REVIEW_ID}" \
-f body="."
done
# Delete all CodeRabbit issue comments on this PR
COMMENT_URLS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
--jq '[.[] | select(.user.login == "coderabbitai[bot]") | .url] | .[]')
for URL in $COMMENT_URLS; do
gh api -X DELETE "$URL"
done

227
.github/workflows/github-actions-main.yml vendored 普通文件
查看文件

@@ -0,0 +1,227 @@
name: Joplin Continuous Integration
on: [push, pull_request]
jobs:
Main:
# We always process server or desktop release tags, because they also publish the release
if: github.repository == 'laurent22/joplin'
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
#
# windows-11-vs2026-arm is pinned because windows-11-arm migrates to
# Visual Studio 2026 in late September 2026, and that job compiles
# sqlite3 from source.
# https://github.com/actions/runner-images/issues/14602
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm, windows-11-vs2026-arm]
steps:
- uses: actions/checkout@v6
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
- name: Install Docker Engine
if: runner.os == 'Linux'
run: |
sudo apt-get install -y apt-transport-https
sudo apt-get install -y ca-certificates
sudo apt-get install -y curl
sudo apt-get install -y gnupg
sudo apt-get install -y lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- name: Free disk space
if: runner.os == 'Linux'
run: |
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
docker system prune -af || true
docker builder prune -af || true
sudo rm -rf /var/lib/docker/tmp/* || true
# Login to Docker only if we're on a server release tag. If we run this on
# a pull request it will fail because the PR doesn't have access to
# secrets
- uses: docker/login-action@v4
if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/transcribe-v'))
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# - name: Test Windows app signing
# if: runner.os == 'Windows'
# env:
# GH_TOKEN: ${{ secrets.GH_TOKEN }}
# IS_CONTINUOUS_INTEGRATION: 1
# BUILD_SEQUENCIAL: 1
# SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
# SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
# SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
# SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
# SIGN_APPLICATION: 1
# # To ensure that the operations stop on failure, all commands
# # should be on one line with "&&" in between.
# run: |
# yarn install && cd packages/app-desktop && yarn dist
- name: Run tests, build and publish Linux and macOS apps
if: runner.os == 'Linux' || runner.os == 'macOs'
env:
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
SERVER_REPOSITORY: joplin/server
SERVER_TAG_PREFIX: server
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
run: |
"${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh"
- name: Build and publish Windows app
if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
SIGN_APPLICATION: 1
# `yarn install` can fail randomly on CI when node-pre-gyp downloads
# prebuilt sqlite3 binaries from GitHub Releases - retry a few times.
run: |
. "${env:GITHUB_WORKSPACE}/.github/scripts/build_windows.ps1"
Build-WindowsApp
# Build and package the Windows app, without publishing it, just to
# verify that the build process hasn't been broken.
- name: Build Windows app (no publishing)
if: runner.os == 'Windows' && !startsWith(github.ref, 'refs/tags/v')
env:
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
SERVER_REPOSITORY: joplin/server
SERVER_TAG_PREFIX: server
run: |
. "${env:GITHUB_WORKSPACE}/.github/scripts/build_windows.ps1"
Build-WindowsApp -DistArgs "--publish=never"
- name: Publish Docker manifest
if: runner.os == 'Linux'
env:
SERVER_REPOSITORY: joplin/server
SERVER_TAG_PREFIX: server
run: |
chmod 700 "${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh"
"${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh"
ServerDockerImage:
if: github.repository == 'laurent22/joplin'
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [ubuntu-22.04, ubuntu-22.04-arm]
steps:
- name: Install Docker Engine
run: |
sudo apt-get install -y apt-transport-https
sudo apt-get install -y ca-certificates
sudo apt-get install -y curl
sudo apt-get install -y gnupg
sudo apt-get install -y lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
- name: Free disk space
if: runner.os == 'Linux'
run: |
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
docker system prune -af || true
docker builder prune -af || true
sudo rm -rf /var/lib/docker/tmp/* || true
- uses: ./.github/workflows/shared/install-yarn
- name: Build Docker Image
env:
BUILD_SEQUENCIAL: 1
run: |
if [ "$RUNNER_ARCH" == "ARM64" ]; then
DOCKER_IMAGE_PLATFORM="linux/arm64"
fi
echo "RUNNER_OS=$RUNNER_OS"
echo "RUNNER_ARCH=$RUNNER_ARCH"
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
yarn buildServerDocker --docker-file Dockerfile.server --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
# Basic test to ensure that the created build is valid. It should exit with
# code 0 if it works.
docker run joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/index.js migrate list
- name: Check HTTP request
run: |
# Need to pass environment variables:
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/index.js --env dev &
# Wait for server to start
sleep 120
# Check if status code is correct
# if the actual_status DOES NOT include the expected_status
# it exits the process with code 1
expected_status="HTTP/1.1 200 OK"
actual_status=$(curl -I -X GET http://localhost:22300/api/ping | head -n 1)
if [[ ! "$actual_status" =~ "$expected_status" ]]; then
echo 'Failed while checking the status code after request to /api/ping'
echo 'expected: ' $expected_status
echo 'actual: ' $actual_status
exit 1;
fi
# Check if the body response is correct
# if the actual_body is different of expected_body exit with code 1
expected_body='{"status":"ok","message":"Joplin Server is running"}'
actual_body=$(curl http://localhost:22300/api/ping)
if [[ "$actual_body" != "$expected_body" ]]; then
echo 'Failed while checking the body response after request to /api/ping'
exit 1;
fi
- name: Check sync
run: |
yarn syncFuzzer start --use-running-server --steps 5

查看文件

@@ -0,0 +1,22 @@
name: 'Install Yarn'
description: 'Enable Corepack and install the pinned Yarn version, with retries'
runs:
using: 'composite'
steps:
- name: Install Yarn
shell: bash
# https://yarnpkg.com/getting-started/install
#
# `corepack enable` only sets up the shims - Corepack downloads the
# pinned Yarn version from repo.yarnpkg.com on first use. That fetch has
# a short timeout and no built-in retry, so a transient network blip on
# CI fails the whole job. Prepare it eagerly with a retry loop.
env:
COREPACK_NETWORK_TIMEOUT: '60000'
run: |
corepack enable
for i in 1 2 3; do
corepack prepare --activate && break
echo "corepack prepare failed (attempt $i), retrying in 15s..."
sleep 15
done

查看文件

@@ -0,0 +1,72 @@
name: 'Setup build environment'
description: 'Install Joplin build dependencies'
runs:
using: 'composite'
steps:
# Trying to fix random networking issues on Windows
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
# - name: Disable TCP/UDP offload on Windows
# if: runner.os == 'Windows'
# shell: pwsh
# run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
- name: Disable TCP/UDP offload on Linux
if: runner.os == 'Linux'
shell: bash
run: sudo ethtool -K eth0 tx off rx off
- name: Disable TCP/UDP offload on macOS
if: runner.os == 'macOS'
shell: bash
run: |
sudo sysctl -w net.link.generic.system.hwcksum_tx=0
sudo sysctl -w net.link.generic.system.hwcksum_rx=0
# libsecret-1-dev is required for keytar - https://github.com/atom/node-keytar
# xvfb provides a virtual display on Linux, used for Playwright tests.
# The apt-get update is allowed to fail (some sources may be temporarily
# unreachable, especially on arm64) but the install is retried to absorb
# transient mirror failures.
- name: Install Linux dependencies
if: runner.os == 'Linux'
shell: bash
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
sudo apt-get update || true
retry 5 15 sudo apt-get install -y \
gettext \
libsecret-1-dev \
translate-toolkit \
rsync \
xvfb
- name: Install macOs dependencies
if: runner.os == 'macOS'
shell: bash
run: |
# Required for building the canvas package
brew install pango
- uses: olegtarasov/get-tag@v2.1.4
- uses: dtolnay/rust-toolchain@stable
if: ${{ runner.os != 'Windows' }}
- uses: actions/setup-node@v6
with:
node-version: '24.15'
# Disable the cache on ARM runners. For now, we don't run "yarn install" on these
# environments and this breaks actions/setup-node.
# See https://github.com/laurent22/joplin/commit/47d0d3eb9e89153a609fb5441344da10904c6308#commitcomment-159577783.
# cache: ${{ (!contains(runner.os, 'arm') && 'yarn') || '' }}
- uses: ./.github/workflows/shared/install-yarn
# macos-latest ships with Python 3.12 by default, but this removes a
# utility that's used by electron-builder (distutils) so we need to pin
# Python to an earlier version.
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
# Ref: https://github.com/nodejs/node-gyp/issues/2869
- uses: actions/setup-python@v6
with:
python-version: '3.14'

65
.github/workflows/ui-tests.yml vendored 普通文件
查看文件

@@ -0,0 +1,65 @@
name: Joplin UI tests
on: [push, pull_request]
permissions:
contents: read
jobs:
Main:
# Don't run on forks
if: github.repository == 'laurent22/joplin'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, windows-2025]
steps:
- uses: actions/checkout@v6
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
- name: Build (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
SKIP_ONENOTE_CONVERTER_BUILD: 1
run: |
. "${env:GITHUB_WORKSPACE}/.github/scripts/build_windows.ps1"
Install-WindowsDeps
- name: Build
if: runner.os != 'Windows'
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
env:
# The onenote-converter package uses Rust, which isn't installed on all CI
# runners. Since the onenote-converter doesn't have UI tests, it can be excluded
# from build:
SKIP_ONENOTE_CONVERTER_BUILD: 1
- name: Run desktop UI tests
shell: bash
run: |
cd ${GITHUB_WORKSPACE}/packages/app-desktop/
bash ./integration-tests/run-ci.sh
# See https://playwright.dev/docs/ci-intro#setting-up-github-actions
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.os }}
path: packages/app-desktop/playwright-report/
retention-days: 7
Mobile:
if: github.repository == 'laurent22/joplin'
runs-on: macos-26
steps:
- uses: actions/checkout@v6
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
- name: Build
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
env:
SKIP_ONENOTE_CONVERTER_BUILD: 1
- name: Run mobile UI tests
shell: bash
run: |
cd ${GITHUB_WORKSPACE}/packages/app-mobile/
yarn test-ui-ios