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
一些检查失败了
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
这个提交包含在:
67
.github/scripts/build_windows.ps1
vendored
普通文件
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
普通文件
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.');
|
||||
}
|
||||
};
|
||||
43
.github/scripts/publish_docker_manifest.sh
vendored
普通文件
43
.github/scripts/publish_docker_manifest.sh
vendored
普通文件
@@ -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
可执行文件
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
可执行文件
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
可执行文件
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
|
||||
在新工单中引用
屏蔽一个用户