run method
Runs this command.
The return value is wrapped in a Future if necessary and returned by
CommandRunner.runCommand.
Implementation
@override
void run() {
final args = argResults!;
final project = _project(this);
final config = ProjectConfig.read(project.root);
final changelog = args.option('changelog') ?? project.changelog;
// **Declaring the block is what says a project publishes to a store**, and
// it is what the *requirements* hang off — which locales, which screenshot
// types. It is deliberately not what decides whether a tree is looked at.
//
// A tree that is present is checked against the rules intrinsic to it
// whether or not anybody declared it, because an upgrade that silently
// checks less than the version before it is the failure this change exists
// to remove, committed on the way in. An undeclared tree is reported, and
// a declared one additionally has to satisfy what it declared.
final play = args.option('play') ?? project.playMetadata;
final dataSafety = args.option('data-safety') ?? project.dataSafety;
// **Which platform's requirements each App Store tree is checked against.**
//
// A path carries no platform, so without this there is nothing for
// `appstore.screenshots.macos` to be selected *by* — and an earlier
// revision silently applied `ios:` to every tree, failing a macOS listing
// Apple holds for lacking iPhone screenshots. A declared requirement
// enforcing the *other* platform's rules is worse than one enforcing none.
//
// What changed is where the platform comes from when nobody says. It used
// to be a refusal — "pass --platform to say which tree this is" — and that
// was right while `--appstore` was the only way to reach a tree, because
// one path genuinely cannot be two platforms. It is wrong when the paths
// are *derived*, because `store/appstore/macos` is not ambiguous about
// which platform it holds. So a repository with a split layout gets every
// tree checked, each against its own rules, in one run.
//
// The refusal survives for `--appstore`, which is still one path.
final platform = args.option('platform');
final named = args.option('appstore');
final Map<String, String> appStoreTrees;
if (named != null) {
final declaredPlatforms =
config.appstore?.screenshots.keys.toSet() ?? const <String>{};
if (platform == null && declaredPlatforms.length > 1) {
stderr.writeln(
'cux_ship verify: $cuxShipConfigFile declares appstore.screenshots '
'for ${(declaredPlatforms.toList()..sort()).join(' and ')}, and '
'--appstore names one tree — pass --platform to say which of them '
'it is, or the wrong platform\'s requirements would be applied.',
);
exitCode = 1;
return;
}
appStoreTrees = {platform ?? 'ios': named};
} else {
appStoreTrees = project.appStoreTrees(platform: platform);
}
final problems = <ReleaseProblem>[
..._declarationProblems(
config,
args,
appStoreTrees.isEmpty ? null : appStoreTrees.values.first,
play,
),
if (changelog != null) ...checkChangelogFile(changelog),
for (final MapEntry(key: platform, value: tree)
in appStoreTrees.entries) ...[
?_derivationProblem(
args.multiOption('require-screenshot-type').toSet(),
config.appstore,
project,
platform: platform,
),
...checkAppStoreTree(
tree,
requireScreenshotTypes: _appStoreScreenshotTypes(
args,
config.appstore,
project,
platform: platform,
),
requireLocales: _requiredLocales(args, config.appstore),
),
],
if (play != null)
...checkPlayTree(
play,
// Play's own vocabulary, from the config only — see
// _appStoreScreenshotTypes for why the flag does not reach here.
requireScreenshotTypes:
config.play?.screenshotsFor(StoreConfig.anyPlatform) ?? const {},
requireLocales: _requiredLocales(args, config.play),
),
if (dataSafety != null) ...checkDataSafetyFile(dataSafety),
];
// Refused rather than passed: checking nothing and reporting success is the
// failure this command exists to prevent, so having nothing to check is
// itself the finding.
if (changelog == null &&
appStoreTrees.isEmpty &&
play == null &&
dataSafety == null) {
stderr.writeln(
'cux_ship verify: nothing to check — no CHANGELOG.md, no App Store or '
'Play metadata tree, and no data safety declaration were found, and '
'none was named. Name one with --changelog, --appstore, --play or '
'--data-safety.',
);
exitCode = 1;
return;
}
// **What was checked, named, on the way past.**
//
// A clean run used to print one line, so a reader could not tell whether
// the data safety declaration had been validated or silently skipped —
// they had to suspect it and go looking. That is the failure this release
// exists to close, on the success path: absence of output reading as
// coverage. Every artifact says so itself.
for (final line in [
if (changelog != null) 'changelog $changelog',
for (final tree in appStoreTrees.entries) ...[
'appstore ${tree.value} (${tree.key})',
],
if (play != null) 'play $play',
if (dataSafety != null) 'data safe $dataSafety',
]) {
stdout.writeln(' checked $line');
}
if (problems.isEmpty) {
stdout.writeln('==> release inputs are publishable');
return;
}
// Every problem at once. Reported one at a time, the second is found only
// after the first is fixed and pushed.
stderr.writeln('cux_ship verify: ${problems.length} problem(s)');
for (final problem in problems) {
stderr.writeln(' $problem');
}
exitCode = 1;
}