Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 6 additions & 9 deletions .github/actions/setup/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,16 @@ outputs:
runs:
using: composite
steps:
# The whole tree, not a sparse list: dart_test.yaml (the integration tag
# and its timeout), .pubignore and LICENSE (the real published file
# set), example/ (the `--check` of the generated example) and tool/ are
# all needed, and a list silently goes stale when a file is added.
# Without `sparse-checkout`, checkout@v4 also disables the sparse
# checkout of `.github/` the calling job started with.
- name: 📦 Checkout the repo
uses: actions/checkout@v4
with:
fetch-depth: 1
sparse-checkout-cone-mode: false
sparse-checkout: |
bin/
lib/
test/
analysis_options.yaml
pubspec.yaml
README.md
CHANGELOG.md

- name: 👀 Verify versions
id: versions
Expand Down
72 changes: 72 additions & 0 deletions .github/scripts/check_skips.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Fails when a test run skipped anything other than the tests that need the
// internal real-sheet snapshot. A skip looks like a pass in the summary, so
// a toolchain that silently disappeared from the CI image (node, tsc,
// python, mypy, pyright, flutter) would otherwise go unnoticed. The helpers
// already fail under SHEETY_REQUIRE_TOOLS=1; this catches the probes that do
// not go through them.
//
// Usage: dart .github/scripts/check_skips.dart <json-reporter-output>...
// (produced by `dart test --file-reporter=json:<file>`).
import 'dart:convert';
import 'dart:io';

/// Skip reasons that are expected on every CI run: the real sheet is
/// internal and never available there, and the Linux runners have no
/// case-insensitive file system (the case-only rename tests run on macOS).
final RegExp _allowed = RegExp(
r'SHEETY_REAL_(SNAPSHOT|REFERENCE)'
'|the temporary directory is case-sensitive',
);

void main(List<String> args) {
if (args.isEmpty) {
stderr.writeln('usage: check_skips.dart <json-report>...');
exit(64);
}
final unexpected = <String>[];
var skipped = 0;
for (final path in args) {
final names = <int, String>{};
final reasons = <int, List<String>>{};
for (final line in File(path).readAsLinesSync()) {
if (line.trim().isEmpty) continue;
final Object? decoded;
try {
decoded = jsonDecode(line);
} on FormatException {
continue;
}
if (decoded is! Map<String, Object?>) continue;
switch (decoded['type']) {
case 'testStart':
final test = decoded['test']! as Map<String, Object?>;
final id = test['id']! as int;
names[id] = test['name']! as String;
final metadata = test['metadata'] as Map<String, Object?>?;
if (metadata?['skipReason'] case final String reason) {
(reasons[id] ??= []).add(reason);
}
case 'print' when decoded['messageType'] == 'skip':
final id = decoded['testID']! as int;
(reasons[id] ??= []).add(decoded['message']! as String);
case 'testDone' when decoded['skipped'] == true:
final id = decoded['testID']! as int;
skipped++;
final why = (reasons[id] ?? const ['(no reason)']).join(' / ');
if (!_allowed.hasMatch(why)) {
unexpected.add('${names[id] ?? '#$id'}: $why');
}
}
}
}
if (unexpected.isNotEmpty) {
for (final entry in unexpected) {
stdout.writeln('::error::Unexpected skip: $entry');
}
stdout.writeln('${unexpected.length} of $skipped skipped tests were not '
'expected to skip on the CI (only the real-snapshot and the '
'case-insensitive file system tests may).');
exit(1);
}
stdout.writeln('$skipped skipped tests, all of them expected.');
}
164 changes: 137 additions & 27 deletions .github/workflows/checkout.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,24 @@ on:
branches:
- "main"
- "master"
# Everything that can change a check below: the code, the test data
# (fixtures and goldens), the generated example, the published file set
# (.pubignore, README, CHANGELOG, LICENSE, pubspec) and the CI itself.
# Keep the pull_request list below identical.
paths:
- "bin/**.dart"
- "lib/**.dart"
- "test/**.dart"
- "example/**.dart"
- "example/**.html"
- ".github/workflows/*.yml"
- "pubspec.yaml"
- "bin/**"
- "lib/**"
- "test/**"
- "tool/**"
- "example/**"
- ".github/**"
- "dart_test.yaml"
- "analysis_options.yaml"
- "pubspec.yaml"
- ".pubignore"
- "README.md"
- "CHANGELOG.md"
- "LICENSE"
pull_request:
branches:
- "main"
Expand All @@ -31,14 +40,19 @@ on:
- "hotfix/**"
- "support/**"
paths:
- "bin/**.dart"
- "lib/**.dart"
- "test/**.dart"
- "example/**.dart"
- "example/**.html"
- ".github/workflows/*.yml"
- "pubspec.yaml"
- "bin/**"
- "lib/**"
- "test/**"
- "tool/**"
- "example/**"
- ".github/**"
- "dart_test.yaml"
- "analysis_options.yaml"
- "pubspec.yaml"
- ".pubignore"
- "README.md"
- "CHANGELOG.md"
- "LICENSE"

permissions:
contents: read
Expand All @@ -49,6 +63,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

# Pinned toolchains of the integration job: a new upstream release must not
# change the result of a run. Bump them deliberately.
env:
NODE_VERSION: "22"
PYTHON_VERSION: "3.12"
MYPY_VERSION: "2.4.0"
PYRIGHT_VERSION: "1.1.414"

jobs:
checkout:
name: "Checkout"
Expand All @@ -61,7 +83,7 @@ jobs:
env:
pub-cache: pub
PUB_CACHE: /github/home/.pub-cache
timeout-minutes: 10
timeout-minutes: 20
outputs:
version: ${{ steps.setup.outputs.version }}
concurrency:
Expand Down Expand Up @@ -89,33 +111,121 @@ jobs:
timeout-minutes: 1
run: dart format --set-exit-if-changed --line-length 80 -o none bin/ lib/ test/

- name: 🔍 Check what would be published
- name: 📈 Check analyzer
id: check-analyzer
timeout-minutes: 2
run: dart analyze --fatal-infos --fatal-warnings bin/ lib/ test/

- name: 🔍 Check the package publishes
id: check-published-files
timeout-minutes: 2
# A .pubignore replaces the .gitignore of its own directory for
# `dart pub publish --dry-run` exits 0 when the package is clean and
# 65 on any validation warning or error, so a non-zero exit fails
# the job. The leak scan below is the last line of defence: a
# .pubignore replaces the .gitignore of its own directory for
# `pub publish`, so a rule forgotten there uploads credentials or
# build output to pub.dev. This is the last line of defence.
# build output to pub.dev.
run: |
dart pub publish --dry-run > /tmp/publish-set.txt 2>&1 || true
set +e
dart pub publish --dry-run > /tmp/publish-set.txt 2>&1
status=$?
set -e
cat /tmp/publish-set.txt
sed -E 's/.*[├└]── //; s/ \([^)]*\)$//' /tmp/publish-set.txt |
grep -iE '^([^ ]+\.(key|pem|p12|jks|keystore)|[^ ]+\.vm\.json|credentials\.json|service-account[^ ]*\.json|build|coverage|reports|node_modules)$' \
grep -iE '^([^ ]+\.(key|pem|p12|jks|keystore)|[^ ]+\.vm\.json|credentials\.json|service-account[^ ]*\.json|arch\.z|build|coverage|reports|node_modules|\.dart_tool)$' \
> /tmp/leaked.txt || true
if [ -s /tmp/leaked.txt ]; then
echo "::error::Files that must never reach pub.dev are in the published set:"
cat /tmp/leaked.txt
echo "Add them to .pubignore - it replaces .gitignore for pub."
exit 1
fi
echo "The published file set is clean."
if [ "$status" -ne 0 ]; then
echo "::error::dart pub publish --dry-run failed (exit $status): the package would not publish cleanly."
exit "$status"
fi
echo "The package publishes and the published file set is clean."

- name: 📈 Check analyzer
id: check-analyzer
timeout-minutes: 1
run: dart analyze --fatal-infos --fatal-warnings bin/ lib/ test/
- name: 🧬 Check the generated example is up to date
id: check-example
timeout-minutes: 2
run: dart run sheety_localization:generate --config=example/sheety.yaml --check

- name: 🧪 Run unit tests
id: run-unit-tests
timeout-minutes: 5
timeout-minutes: 10
# The integration tests (real toolchains) run in the job below; the
# Dart image has none of them.
run: |
dart test --color --platform=vm --concurrency=12 \
--timeout=60s --reporter=github
--exclude-tags=integration --timeout=120s --reporter=github

integration:
name: "Integration"
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: integration-${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
env:
# Every toolchain is installed below, so a missing one is a failure,
# not a skip that looks like a pass.
SHEETY_REQUIRE_TOOLS: "1"
SHEETY_PYTHON: python
SHEETY_MYPY_PYTHON: python
SHEETY_PYRIGHT: pyright
steps:
- name: 📦 Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: 🐦 Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: 🧰 Setup Node.js
# The TypeScript locale negotiation test runs the .ts runtime with
# node's type stripping, which needs node >= 22.
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: 🐍 Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: 👷 Install dependencies
run: |
python -m pip install --quiet "mypy==${MYPY_VERSION}"
npm install --global --no-audit --no-fund "pyright@${PYRIGHT_VERSION}"
flutter --version
# The gen-l10n parity test loads flutter_tools from the SDK; the
# first `flutter` run normally resolves its packages already.
tools="$(dirname "$(dirname "$(command -v flutter)")")/packages/flutter_tools"
test -f "$tools/.dart_tool/package_config.json" ||
(cd "$tools" && dart pub get)
node --version && npx --version
python --version && python -m mypy --version && pyright --version
dart pub get --no-example

- name: 🧪 Run all tests with the real toolchains
timeout-minutes: 25
# Everything, not only `--tags=integration`: some untagged tests
# (the CLDR cross-check against node) need a toolchain too.
run: |
mkdir -p reports
dart test --color --platform=vm --timeout=300s --reporter=github \
--file-reporter=json:reports/tests.json

- name: 🚫 Fail on unexpected skips
if: ${{ !cancelled() }}
# A probe that does not go through the test helpers (and so ignores
# SHEETY_REQUIRE_TOOLS) still only skips; only the real-snapshot
# tests are allowed to.
run: dart .github/scripts/check_skips.dart reports/tests.json
18 changes: 15 additions & 3 deletions .pubignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,26 @@ arch.z
*.tar.gz
*.tgz

# Local configuration
# Local configuration and contributor guidance
.vscode/
.claude/
CLAUDE.md
prompt.txt
tool/

# Generated example output
example/lib/
# Example: ship only the configuration, the snapshot and the README (pub.dev
# shows example/README.md). The generated output and the packaging files of
# example/dart, example/ts and example/python live in the repository; they
# would only bloat the archive (and example/dart is a nested Flutter package).
example/dart/
example/ts/
example/python/

# Bulky test data: the expected emitter output (~460 KB) and the CLDR sample
# table (~70 KB) only matter in a checkout of the repository, where the tests
# that read them run; nobody runs the tests of a downloaded package.
test/golden/
test/fixtures/cldr_plural_samples.json

# Build, test and tooling output
build/
Expand Down
Loading
Loading