run method

  1. @override
void run()
override

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;
}