diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..f346aa0d82 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: [push, pull_request] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Clone Flutter SDK + # We can't do a depth-1 clone, because we need the most recent tag + # so that Flutter knows its version and sees the constraint in our + # pubspec is satisfied. It's uncommon for flutter/flutter to go + # more than 100 commits between tags. Fetch 1000 for good measure. + run: | + git clone --depth=1000 https://github.com/flutter/flutter ~/flutter + TZ=UTC git --git-dir ~/flutter/.git log -1 --format='%h | %ci | %s' --date=iso8601-local + echo ~/flutter/bin >> "$GITHUB_PATH" + + - name: Download Flutter SDK artifacts (flutter precache) + run: flutter precache --universal + + - name: Download our dependencies (flutter pub get) + run: flutter pub get + + - name: Run tools/check + run: TERM=dumb tools/check --all --verbose diff --git a/lib/model/database.dart b/lib/model/database.dart index 5bc257b7f5..d95b6f4817 100644 --- a/lib/model/database.dart +++ b/lib/model/database.dart @@ -52,13 +52,10 @@ class AppDatabase extends _$AppDatabase { // When updating the schema: // * Make the change in the table classes, and bump schemaVersion. - // * Export the new schema: - // $ dart run drift_dev schema dump lib/model/database.dart test/model/schemas/ - // * Generate test migrations from the schemas: - // $ dart run drift_dev schema generate --data-classes --companions test/model/schemas/ test/model/schemas/ + // * Export the new schema and generate test migrations: + // $ tools/check --fix drift // * Write a migration in `onUpgrade` below. // * Write tests. - // TODO run those `drift_dev schema` commands in CI: https://github.com/zulip/zulip-flutter/issues/60 @override int get schemaVersion => 2; // See note. diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index 1d599bfb91..b9553c643d 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -16,7 +16,7 @@ abstract final class ZulipIcons { // or otherwise edit the SVG files there. // The files' names (before ".svg") should be valid Dart identifiers. // - // * Then run the command `scripts/icons/build-icon-font`. + // * Then run the command `tools/icons/build-icon-font`. // That will update this file and the generated icon font, // `assets/icons/ZulipIcons.ttf`. // diff --git a/test/model/schemas/drift_schema_v2.json b/test/model/schemas/drift_schema_v2.json index 4d639ddead..8d184f229b 100644 --- a/test/model/schemas/drift_schema_v2.json +++ b/test/model/schemas/drift_schema_v2.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"acked_push_token","getter_name":"ackedPushToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const UriConverter()","dart_type_name":"Uri"}},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"acked_push_token","getter_name":"ackedPushToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]} \ No newline at end of file diff --git a/tools/check b/tools/check new file mode 100755 index 0000000000..d1a89e854c --- /dev/null +++ b/tools/check @@ -0,0 +1,381 @@ +#!/usr/bin/env bash + +# Careful! `set -e` doesn't do everything you'd think it does. In +# fact, we don't get its benefit in any of the `run_foo` functions. +# +# This is because it has an effect only when it can exit the whole shell. +# (Its full name is `set -o errexit`, and it means "exit" literally.) See: +# https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin +# +# When one test suite fails, we want to go on to run the other suites, so +# we use `||` to prevent the whole script from exiting there, and that +# defeats `set -e`. +# +# For now our workaround is to put `|| return` in the `run_foo` just +# after each nontrivial command that isn't the final command in the +# function. +set -euo pipefail + +this_dir=${BASH_SOURCE[0]%/*} + +# shellcheck source=tools/lib/git.sh +. "${this_dir}"/lib/git.sh + + +## CLI PARSING + +default_suites=(analyze test build_runner drift icons) +extra_suites=( + shellcheck # Requires its own dependency, from outside the pub system. +) + +usage() { + cat >&2 <&2 "There were ${change_description}:" + git_status_short "$@" + else + echo >&2 "Error: there were ${change_description}:" + git_status_short "$@" + git checkout HEAD -- "$@" + git clean -fd --quiet -- "$@" + return 1 + fi +} + +run_analyze() { + # no `flutter analyze --verbose` even when $opt_verbose; it's *very* verbose + flutter analyze +} + +run_test() { + # no `flutter test --verbose` even when $opt_verbose; it's *very* verbose + flutter test +} + +# Whether the build_runner suite should run, given $opt_files. +should_run_build_runner() { + # First, check for changes in relevant metadata. + # Omitted from this files_check: + # pubspec.{yaml,lock} tools/check + if files_check build.yaml; then + return 0; + fi + + # Otherwise, check for changes in the input files of build_runner. + # These input files should have `part "foo.g.dart"` directives. + # Omitted from this check: any files where that isn't yet added. + # (And at this point we must have a meaningful $files_base_commit.) + if git_changed_files "${files_base_commit}" \ + | xargs -r git grep -l '^part .*g\.dart.;$' \ + | grep -q .; then + return 0 + fi + + # No relevant changes found. + return 1 +} + +run_build_runner() { + should_run_build_runner \ + || return 0 + + check_no_uncommitted_or_untracked '*.g.dart' \ + || return + + local build_runner_cmd=( + dart run build_runner build --delete-conflicting-outputs + ) + if [ -n "${opt_verbose}" ]; then + # No --verbose needed; build_runner is verbose enough by default. + "${build_runner_cmd[@]}" \ + || return + else + # build_runner lacks a --quiet, and is fairly verbose to begin with. + # So we filter out "[INFO]" messages ourselves. + "${build_runner_cmd[@]}" \ + | perl -lne ' + BEGIN { my $silence = 0 } + if (/^\[INFO\]/) { $silence = 1 } + elsif (/^\[[A-Z]/) { $silence = 0 } + print if (!$silence) + ' \ + || return + fi + + check_no_changes "updates to *.g.dart files" '*.g.dart' +} + +run_drift() { + local schema_dir=test/model/schemas/ + + # Omitted from this check: + # pubspec.{yaml,lock} tools/check + files_check lib/model/database{,.g}.dart "${schema_dir}" \ + || return 0 + + check_no_uncommitted_or_untracked "${schema_dir}" \ + || return + + dart run drift_dev schema dump \ + lib/model/database.dart "${schema_dir}" \ + || return + dart run drift_dev schema generate --data-classes --companions \ + "${schema_dir}" "${schema_dir}" \ + || return + + check_no_changes "schema updates" "${schema_dir}" +} + +run_icons() { + # Omitted from this check: + # pubspec.{yaml,lock} tools/check + files_check tools/icons/ assets/icons/ lib/widgets/icons.dart \ + || return 0 + + local outputs=( assets/icons/ZulipIcons.ttf lib/widgets/icons.dart ) + + check_no_uncommitted_or_untracked "${outputs[@]}" \ + || return + + tools/icons/build-icon-font + + check_no_changes "icon updates" "${outputs[@]}" +} + +run_shellcheck() { + # Omitted from this check: nothing (nothing known, anyway). + files_check tools/ '!*.'{dart,js,json} \ + || return 0 + + # Shellcheck is fast, <1s; so if we touched any possible targets at all, + # just run on the full list of targets. + # shellcheck disable=SC2207 # filenames in our own tree, assume well-behaved + targets=( + $(git grep -l '#!.*sh\b' -- tools/) + $(git ls-files -- tools/'*.sh') + ) + + if ! type shellcheck >/dev/null 2>&1; then + cat >&2 <&2 "Internal error: unknown suite $suite" ;; + esac || failed+=( "$suite" ) +done +if_verbose echo "${divider_line}" + +if (( ${#failed[@]} )); then + cat >&2 <&2 "There are ${problem}${qualifier}:" + echo >&2 + git_status_short "$@" + echo >&2 + echo >&2 "Aborting, to avoid losing your work." + return 1 +} + +# Compute what remote name is being used for the upstream repo. +git_upstream_remote_name() { + # Out of the names listed by `git remote`, pick one from the + # list below, in preference order. + grep -m1 -xFf <(git remote) <