forked from eva/focus-flow
Merge branch 'earthly-deployment-improvements'
This commit is contained in:
commit
f29fb3779e
6 changed files with 319 additions and 35 deletions
13
.earthlyignore
Normal file
13
.earthlyignore
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.dart_tool/
|
||||||
|
.git/
|
||||||
|
.idea/
|
||||||
|
build/
|
||||||
|
builds/
|
||||||
|
coverage/
|
||||||
|
doc/api/
|
||||||
|
apps/focus_flow_flutter/.dart_tool/
|
||||||
|
apps/focus_flow_flutter/.plugin_symlinks/
|
||||||
|
apps/focus_flow_flutter/build/
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,9 +1,13 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
# Dart
|
# Dart
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.packages
|
.packages
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
!apps/focus_flow_flutter/pubspec.lock
|
!apps/focus_flow_flutter/pubspec.lock
|
||||||
build/
|
build/
|
||||||
|
builds/
|
||||||
coverage/
|
coverage/
|
||||||
doc/api/
|
doc/api/
|
||||||
|
|
||||||
|
|
|
||||||
102
Earthfile
Normal file
102
Earthfile
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
VERSION 0.8
|
||||||
|
|
||||||
|
# Earthly build pipeline for FocusFlow.
|
||||||
|
#
|
||||||
|
# The primary application artifact is the Flutter Linux desktop release bundle.
|
||||||
|
# The +build target packages that bundle into builds/focus_flow_linux_x64.zip
|
||||||
|
# and also exports the unzipped runnable bundle under builds/focus_flow_linux_x64.
|
||||||
|
|
||||||
|
setup:
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV FLUTTER_VERSION=3.44.3
|
||||||
|
ENV FLUTTER_HOME=/opt/flutter
|
||||||
|
ENV PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:$PATH
|
||||||
|
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
clang \
|
||||||
|
cmake \
|
||||||
|
curl \
|
||||||
|
dbus-x11 \
|
||||||
|
git \
|
||||||
|
libgtk-3-dev \
|
||||||
|
liblzma-dev \
|
||||||
|
libstdc++-12-dev \
|
||||||
|
libglu1-mesa \
|
||||||
|
ninja-build \
|
||||||
|
pkg-config \
|
||||||
|
unzip \
|
||||||
|
xz-utils \
|
||||||
|
xvfb \
|
||||||
|
zip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" -o /tmp/flutter.tar.xz \
|
||||||
|
&& mkdir -p /opt \
|
||||||
|
&& tar -xJf /tmp/flutter.tar.xz -C /opt \
|
||||||
|
&& rm /tmp/flutter.tar.xz
|
||||||
|
WORKDIR /workspace
|
||||||
|
RUN git config --global --add safe.directory /opt/flutter
|
||||||
|
RUN flutter config --no-analytics
|
||||||
|
RUN flutter config --enable-linux-desktop
|
||||||
|
RUN flutter --version
|
||||||
|
|
||||||
|
code:
|
||||||
|
FROM +setup
|
||||||
|
COPY . .
|
||||||
|
RUN dart pub get
|
||||||
|
RUN cd apps/focus_flow_flutter && flutter pub get
|
||||||
|
|
||||||
|
format:
|
||||||
|
FROM +code
|
||||||
|
RUN dart format --set-exit-if-changed packages apps scripts tool test
|
||||||
|
|
||||||
|
dart-analyze:
|
||||||
|
FROM +code
|
||||||
|
RUN dart analyze
|
||||||
|
|
||||||
|
flutter-analyze:
|
||||||
|
FROM +code
|
||||||
|
RUN cd apps/focus_flow_flutter && flutter analyze
|
||||||
|
|
||||||
|
reuse:
|
||||||
|
FROM +code
|
||||||
|
RUN dart run tool/check_reuse.dart
|
||||||
|
|
||||||
|
docs:
|
||||||
|
FROM +code
|
||||||
|
RUN dart run tool/check_docs.dart --skip-dartdoc
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
FROM +code
|
||||||
|
RUN .githooks/pre-commit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
FROM +code
|
||||||
|
BUILD +format
|
||||||
|
BUILD +dart-analyze
|
||||||
|
BUILD +flutter-analyze
|
||||||
|
BUILD +reuse
|
||||||
|
BUILD +docs
|
||||||
|
BUILD +pre-commit
|
||||||
|
|
||||||
|
build:
|
||||||
|
FROM +code
|
||||||
|
RUN cd apps/focus_flow_flutter && flutter build linux --release
|
||||||
|
RUN rm -rf builds \
|
||||||
|
&& mkdir -p builds/focus_flow_linux_x64 \
|
||||||
|
&& cp -R apps/focus_flow_flutter/build/linux/x64/release/bundle/. builds/focus_flow_linux_x64/
|
||||||
|
RUN cd builds && zip -qr focus_flow_linux_x64.zip focus_flow_linux_x64
|
||||||
|
SAVE ARTIFACT --keep-ts builds AS LOCAL builds
|
||||||
|
|
||||||
|
run:
|
||||||
|
FROM +build
|
||||||
|
RUN test -x builds/focus_flow_linux_x64/focus_flow_flutter
|
||||||
|
RUN status=0; timeout 10s xvfb-run -a builds/focus_flow_linux_x64/focus_flow_flutter || status=$?; if [ "$status" = "124" ]; then exit 0; fi; exit "$status"
|
||||||
|
|
||||||
|
all:
|
||||||
|
BUILD +lint
|
||||||
|
BUILD +build
|
||||||
92
README.md
92
README.md
|
|
@ -1,9 +1,26 @@
|
||||||
|
<!-- SPDX-FileCopyrightText: 2026 FocusFlow contributors -->
|
||||||
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
# ADHD Scheduler Workspace
|
# ADHD Scheduler Workspace
|
||||||
|
|
||||||
SQLite-first Dart workspace for the ADHD scheduler backend, persistence
|
SQLite-first Dart workspace for the ADHD scheduler backend, persistence
|
||||||
adapters, notifications, exports, backups, integration tests, and CI scripts.
|
adapters, notifications, exports, backups, integration tests, and CI scripts.
|
||||||
|
|
||||||
## Test
|
## Local Workflow
|
||||||
|
|
||||||
|
Set up the workspace and local SQLite path:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/bootstrap_dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the local app during development:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the full local quality gate:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scripts/test.sh
|
scripts/test.sh
|
||||||
|
|
@ -12,18 +29,17 @@ scripts/test.sh
|
||||||
The test gate runs analyzer, tests with coverage, combines LCOV output, and
|
The test gate runs analyzer, tests with coverage, combines LCOV output, and
|
||||||
fails below 80% package `lib/` coverage.
|
fails below 80% package `lib/` coverage.
|
||||||
|
|
||||||
## Development
|
Package a local desktop release with the host toolchain:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scripts/bootstrap_dev.sh
|
scripts/package_release.sh --platform current --output build/releases
|
||||||
scripts/dev.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The shell scripts prepare the default SQLite path and delegate to the Dart
|
Equivalent Dart entry points:
|
||||||
development runner.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dart run scripts/dev.dart
|
dart run scripts/dev.dart
|
||||||
|
dart run scripts/build.dart --platform current --output build/releases
|
||||||
```
|
```
|
||||||
|
|
||||||
The dev script prints the SQLite path, starts `dart run build_runner watch`, and
|
The dev script prints the SQLite path, starts `dart run build_runner watch`, and
|
||||||
|
|
@ -42,6 +58,70 @@ Optional flags:
|
||||||
dart run scripts/dev.dart --device linux --sqlite /tmp/scheduler.sqlite
|
dart run scripts/dev.dart --device linux --sqlite /tmp/scheduler.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Earthly Workflow
|
||||||
|
|
||||||
|
Earthly is the supported containerized build path going forward. It builds a
|
||||||
|
Linux desktop Flutter bundle and exports a runnable application under `builds/`.
|
||||||
|
|
||||||
|
Install prerequisites on the host:
|
||||||
|
|
||||||
|
- Earthly
|
||||||
|
- Docker or another Earthly-compatible container runtime
|
||||||
|
|
||||||
|
Set up the Earthly build image:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +setup
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy code and resolve Dart/Flutter dependencies inside Earthly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +code
|
||||||
|
```
|
||||||
|
|
||||||
|
Run all style and metadata checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and package the runnable Linux desktop app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run lint, build, and package in one command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +all
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke-run the Earthly-built Linux desktop app under a virtual display:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +run
|
||||||
|
```
|
||||||
|
|
||||||
|
Earthly outputs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
builds/focus_flow_linux_x64/
|
||||||
|
builds/focus_flow_linux_x64.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful focused lint targets:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
earthly +format
|
||||||
|
earthly +dart-analyze
|
||||||
|
earthly +flutter-analyze
|
||||||
|
earthly +reuse
|
||||||
|
earthly +docs
|
||||||
|
earthly +pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
## Flutter UI
|
## Flutter UI
|
||||||
|
|
||||||
The provisional UI app lives outside the root Dart workspace at
|
The provisional UI app lives outside the root Dart workspace at
|
||||||
|
|
|
||||||
|
|
@ -116,23 +116,16 @@ Future<List<String>> _trackedDartFilesMissingDeclarationDocs() async {
|
||||||
|
|
||||||
/// Returns tracked Dart files in stable path order.
|
/// Returns tracked Dart files in stable path order.
|
||||||
///
|
///
|
||||||
/// Git is the source of truth so local build outputs, generated docs, and
|
/// Git is preferred so local build outputs, generated docs, and editor scratch
|
||||||
/// editor scratch files do not affect the documentation gate.
|
/// files do not affect the documentation gate. Container build contexts may omit
|
||||||
|
/// `.git`, so the tool falls back to a conservative walk over source folders.
|
||||||
Future<List<String>> _trackedDartFiles() async {
|
Future<List<String>> _trackedDartFiles() async {
|
||||||
final result = await Process.run(
|
final result = await Process.run(
|
||||||
'git',
|
'git',
|
||||||
<String>['ls-files', '*.dart'],
|
<String>['ls-files', '*.dart'],
|
||||||
runInShell: Platform.isWindows,
|
runInShell: Platform.isWindows,
|
||||||
);
|
);
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode == 0) {
|
||||||
throw ProcessException(
|
|
||||||
'git',
|
|
||||||
<String>['ls-files', '*.dart'],
|
|
||||||
'Unable to list tracked Dart files.',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (result.stdout as String)
|
return (result.stdout as String)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.where((path) => path.trim().isNotEmpty)
|
.where((path) => path.trim().isNotEmpty)
|
||||||
|
|
@ -140,6 +133,58 @@ Future<List<String>> _trackedDartFiles() async {
|
||||||
..sort();
|
..sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _walkDartFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walks source folders for Dart files when Git metadata is unavailable.
|
||||||
|
///
|
||||||
|
/// This fallback supports Earthly and other copied build contexts while still
|
||||||
|
/// avoiding generated output and tool caches that are not hand-maintained docs.
|
||||||
|
List<String> _walkDartFiles() {
|
||||||
|
final files = <String>[];
|
||||||
|
for (final entity in Directory.current.listSync(recursive: true)) {
|
||||||
|
if (entity is! File) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final path = entity.path.replaceAll('\\', '/').replaceFirst('./', '');
|
||||||
|
if (!path.endsWith('.dart') ||
|
||||||
|
!_isDartAuditScope(path) ||
|
||||||
|
_isIgnoredGeneratedPath(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
files.add(path);
|
||||||
|
}
|
||||||
|
return files..sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports whether [path] belongs to source folders covered by Dartdoc policy.
|
||||||
|
///
|
||||||
|
/// The root package, packages, scripts, tools, tests, and Flutter app are the
|
||||||
|
/// source surfaces where contributors add hand-maintained Dart code.
|
||||||
|
bool _isDartAuditScope(String path) {
|
||||||
|
return path.startsWith('packages/') ||
|
||||||
|
path.startsWith('apps/focus_flow_flutter/') ||
|
||||||
|
path.startsWith('scripts/') ||
|
||||||
|
path.startsWith('tool/') ||
|
||||||
|
path.startsWith('test/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports whether [path] belongs to generated output or local tooling caches.
|
||||||
|
///
|
||||||
|
/// The fallback walker skips these paths because they are not source files and
|
||||||
|
/// usually are not present in Git's tracked file list.
|
||||||
|
bool _isIgnoredGeneratedPath(String path) {
|
||||||
|
return path.startsWith('.dart_tool/') ||
|
||||||
|
path.startsWith('.git/') ||
|
||||||
|
path.startsWith('build/') ||
|
||||||
|
path.startsWith('builds/') ||
|
||||||
|
path.startsWith('coverage/') ||
|
||||||
|
path.startsWith('doc/api/') ||
|
||||||
|
path.contains('/.dart_tool/') ||
|
||||||
|
path.contains('/build/') ||
|
||||||
|
path.contains('/.plugin_symlinks/');
|
||||||
|
}
|
||||||
|
|
||||||
/// Finds tracked Dart files that are missing file-level library docs.
|
/// Finds tracked Dart files that are missing file-level library docs.
|
||||||
///
|
///
|
||||||
/// Part files and generated files are excluded from this specific check because
|
/// Part files and generated files are excluded from this specific check because
|
||||||
|
|
|
||||||
|
|
@ -137,22 +137,16 @@ List<String> _spdxHeaderFailures() {
|
||||||
|
|
||||||
/// Returns tracked repository files in stable path order.
|
/// Returns tracked repository files in stable path order.
|
||||||
///
|
///
|
||||||
/// Git is used so ignored build output and local editor files do not affect the
|
/// Git is preferred so ignored build output and local editor files do not affect
|
||||||
/// metadata check.
|
/// the metadata check. Container build contexts may omit `.git`, so the checker
|
||||||
|
/// falls back to a conservative tree walk over the compliance scope.
|
||||||
List<String> _trackedFiles() {
|
List<String> _trackedFiles() {
|
||||||
final result = Process.runSync(
|
final result = Process.runSync(
|
||||||
'git',
|
'git',
|
||||||
<String>['ls-files'],
|
<String>['ls-files'],
|
||||||
runInShell: Platform.isWindows,
|
runInShell: Platform.isWindows,
|
||||||
);
|
);
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode == 0) {
|
||||||
throw ProcessException(
|
|
||||||
'git',
|
|
||||||
<String>['ls-files'],
|
|
||||||
'Unable to list tracked files.',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (result.stdout as String)
|
return (result.stdout as String)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.where((path) => path.trim().isNotEmpty)
|
.where((path) => path.trim().isNotEmpty)
|
||||||
|
|
@ -160,6 +154,45 @@ List<String> _trackedFiles() {
|
||||||
..sort();
|
..sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _walkComplianceFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walks source-controlled compliance paths when Git metadata is unavailable.
|
||||||
|
///
|
||||||
|
/// This path is used by Earthly and other copied build contexts. Generated
|
||||||
|
/// output and tool caches are skipped explicitly so stale local artifacts do not
|
||||||
|
/// create false failures.
|
||||||
|
List<String> _walkComplianceFiles() {
|
||||||
|
final files = <String>[];
|
||||||
|
for (final entity in Directory.current.listSync(recursive: true)) {
|
||||||
|
if (entity is! File) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final path = entity.path.replaceAll('\\', '/').replaceFirst('./', '');
|
||||||
|
if (_isIgnoredGeneratedPath(path) || !_isComplianceScope(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
files.add(path);
|
||||||
|
}
|
||||||
|
return files..sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports whether [path] belongs to generated output or local tooling caches.
|
||||||
|
///
|
||||||
|
/// These paths are intentionally ignored by the fallback scanner because they
|
||||||
|
/// are not source files and are normally excluded by Git.
|
||||||
|
bool _isIgnoredGeneratedPath(String path) {
|
||||||
|
return path.startsWith('.dart_tool/') ||
|
||||||
|
path.startsWith('.git/') ||
|
||||||
|
path.startsWith('build/') ||
|
||||||
|
path.startsWith('builds/') ||
|
||||||
|
path.startsWith('coverage/') ||
|
||||||
|
path.startsWith('doc/api/') ||
|
||||||
|
path.contains('/.dart_tool/') ||
|
||||||
|
path.contains('/build/') ||
|
||||||
|
path.contains('/.plugin_symlinks/');
|
||||||
|
}
|
||||||
|
|
||||||
/// Reports whether [path] should carry an inline SPDX header.
|
/// Reports whether [path] should carry an inline SPDX header.
|
||||||
///
|
///
|
||||||
/// The allow-list is intentionally conservative and includes only formats where
|
/// The allow-list is intentionally conservative and includes only formats where
|
||||||
|
|
@ -171,7 +204,10 @@ bool _requiresInlineSpdxHeader(String path) {
|
||||||
if (path.endsWith('.g.dart') || path.endsWith('LICENSE.md')) {
|
if (path.endsWith('.g.dart') || path.endsWith('LICENSE.md')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return path.endsWith('.dart') ||
|
return path == '.earthlyignore' ||
|
||||||
|
path == '.gitignore' ||
|
||||||
|
path == 'Earthfile' ||
|
||||||
|
path.endsWith('.dart') ||
|
||||||
path.endsWith('.yaml') ||
|
path.endsWith('.yaml') ||
|
||||||
path.endsWith('.yml') ||
|
path.endsWith('.yml') ||
|
||||||
path.endsWith('.sh') ||
|
path.endsWith('.sh') ||
|
||||||
|
|
@ -187,8 +223,12 @@ bool _isComplianceScope(String path) {
|
||||||
path.startsWith('apps/focus_flow_flutter/') ||
|
path.startsWith('apps/focus_flow_flutter/') ||
|
||||||
path.startsWith('scripts/') ||
|
path.startsWith('scripts/') ||
|
||||||
path.startsWith('tool/') ||
|
path.startsWith('tool/') ||
|
||||||
|
path == '.earthlyignore' ||
|
||||||
|
path == '.gitignore' ||
|
||||||
path == 'AGENTS.md' ||
|
path == 'AGENTS.md' ||
|
||||||
|
path == 'Earthfile' ||
|
||||||
path == 'Focus Flow Contributors.md' ||
|
path == 'Focus Flow Contributors.md' ||
|
||||||
|
path == 'README.md' ||
|
||||||
path == 'pubspec.yaml' ||
|
path == 'pubspec.yaml' ||
|
||||||
path == 'analysis_options.yaml' ||
|
path == 'analysis_options.yaml' ||
|
||||||
path == '.github/workflows/ci.yml';
|
path == '.github/workflows/ci.yml';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue