build: add earthly deployment workflow

This commit is contained in:
Ashley Venn 2026-07-01 14:16:36 -07:00
parent d65ebf0bd1
commit 06e8e8d355
6 changed files with 319 additions and 35 deletions

13
.earthlyignore Normal file
View 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
View file

@ -1,9 +1,13 @@
# SPDX-FileCopyrightText: 2026 FocusFlow contributors
# SPDX-License-Identifier: AGPL-3.0-only
# Dart
.dart_tool/
.packages
pubspec.lock
!apps/focus_flow_flutter/pubspec.lock
build/
builds/
coverage/
doc/api/

102
Earthfile Normal file
View 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

View file

@ -1,9 +1,26 @@
<!-- SPDX-FileCopyrightText: 2026 FocusFlow contributors -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# ADHD Scheduler Workspace
SQLite-first Dart workspace for the ADHD scheduler backend, persistence
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
scripts/test.sh
@ -12,18 +29,17 @@ scripts/test.sh
The test gate runs analyzer, tests with coverage, combines LCOV output, and
fails below 80% package `lib/` coverage.
## Development
Package a local desktop release with the host toolchain:
```sh
scripts/bootstrap_dev.sh
scripts/dev.sh
scripts/package_release.sh --platform current --output build/releases
```
The shell scripts prepare the default SQLite path and delegate to the Dart
development runner.
Equivalent Dart entry points:
```sh
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
@ -42,6 +58,70 @@ Optional flags:
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
The provisional UI app lives outside the root Dart workspace at

View file

@ -116,23 +116,16 @@ Future<List<String>> _trackedDartFilesMissingDeclarationDocs() async {
/// Returns tracked Dart files in stable path order.
///
/// Git is the source of truth so local build outputs, generated docs, and
/// editor scratch files do not affect the documentation gate.
/// Git is preferred so local build outputs, generated docs, and editor scratch
/// 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 {
final result = await Process.run(
'git',
<String>['ls-files', '*.dart'],
runInShell: Platform.isWindows,
);
if (result.exitCode != 0) {
throw ProcessException(
'git',
<String>['ls-files', '*.dart'],
'Unable to list tracked Dart files.',
result.exitCode,
);
}
if (result.exitCode == 0) {
return (result.stdout as String)
.split('\n')
.where((path) => path.trim().isNotEmpty)
@ -140,6 +133,58 @@ Future<List<String>> _trackedDartFiles() async {
..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.
///
/// Part files and generated files are excluded from this specific check because

View file

@ -137,22 +137,16 @@ List<String> _spdxHeaderFailures() {
/// Returns tracked repository files in stable path order.
///
/// Git is used so ignored build output and local editor files do not affect the
/// metadata check.
/// Git is preferred so ignored build output and local editor files do not affect
/// 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() {
final result = Process.runSync(
'git',
<String>['ls-files'],
runInShell: Platform.isWindows,
);
if (result.exitCode != 0) {
throw ProcessException(
'git',
<String>['ls-files'],
'Unable to list tracked files.',
result.exitCode,
);
}
if (result.exitCode == 0) {
return (result.stdout as String)
.split('\n')
.where((path) => path.trim().isNotEmpty)
@ -160,6 +154,45 @@ List<String> _trackedFiles() {
..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.
///
/// 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')) {
return false;
}
return path.endsWith('.dart') ||
return path == '.earthlyignore' ||
path == '.gitignore' ||
path == 'Earthfile' ||
path.endsWith('.dart') ||
path.endsWith('.yaml') ||
path.endsWith('.yml') ||
path.endsWith('.sh') ||
@ -187,8 +223,12 @@ bool _isComplianceScope(String path) {
path.startsWith('apps/focus_flow_flutter/') ||
path.startsWith('scripts/') ||
path.startsWith('tool/') ||
path == '.earthlyignore' ||
path == '.gitignore' ||
path == 'AGENTS.md' ||
path == 'Earthfile' ||
path == 'Focus Flow Contributors.md' ||
path == 'README.md' ||
path == 'pubspec.yaml' ||
path == 'analysis_options.yaml' ||
path == '.github/workflows/ci.yml';