uploadRecordFor function
Whether command is an invocation that records an upload, and what of.
Three things disqualify a run, and each was learned rather than designed:
- Not
upload. A promote moves a build the store already holds and is not the moment anything was published from this repository. - A subcommand that does not declare
--commit. A parser that never had the option is skipped rather than interrogated. - No artifact in the run.
play upload --metadatapublishes a listing and hands over no build — documented inplay/cli.dartas a first-class case, "listing-only pushes need no artifact". The record's scope is an artifact reached the store, not the upload command ran, and conflating them made a store-listing typo fix demand a commit, a version and a build number for a build that does not exist. Worse, an operator who supplied the previous build's numbers to get past that would be told the upload collided — exit 3, the loudest error here, on a run that uploaded nothing. Takes the three things the decision actually reads rather than aCommand, because aCommand'sargResultsis populated byrun()and is null after a bareparse()— so a signature taking one can only be exercised by actually running a store command, which is why this was never tested.
Implementation
UploadRecordRequest? uploadRecordFor({
required String commandName,
required ArgParser parser,
required ArgResults args,
BuildManifest? manifest,
String? Function()? fallbackVersionName,
}) {
// **`parser.options` is what was *declared*; `ArgResults.options` is what
// was *provided or defaulted*.** This asked the second and meant the first,
// so recording was skipped for every caller that did not type `--commit` —
// which is every caller using `--manifest`, the flag that exists to supply it.
// Nothing failed: the guard is the silent kind, and `record-uploads: true`
// read as working for as long as nobody looked for the tag.
if (commandName != 'upload' || !parser.options.containsKey('commit')) {
return null;
}
String? opt(String name) =>
parser.options.containsKey(name) ? args.option(name) : null;
// `--aab` on Play, `--artifact` on the App Store, or whatever the manifest
// named. Absent from all three means nothing was handed over.
final artifact = opt('aab') ?? opt('artifact') ?? manifest?.artifactPath;
if (artifact == null) {
return null;
}
// **The bytes have to exist before the record claims they were uploaded.**
// The tag is written before the store is contacted, deliberately — the
// documented meaning is "an upload was attempted with this artifact at this
// commit", and the alternative failure is "shipped but unprovable". But it
// was also written before anything opened the file, so a mistyped path left
// a permanent, pushed tag for an upload that was never physically possible:
// no store contacted, no bytes in existence. *Attempted* implies something
// was tried, and nothing was.
//
// A `--manifest` caller was already covered, because `verify()` refuses a
// missing artifact before this runs. A caller passing `--aab` or
// `--artifact` directly — the first thing anyone writes — was not, and both
// known consumers escaped it only by checking the file in their own scripts
// first, which is a property of those scripts rather than of this tool.
//
// Costs one `stat`, no credentials and no network, and forfeits nothing:
// everything the record protects against happens after this point.
if (!File(artifact).existsSync()) {
throw ReleaseException(
'no artifact at $artifact, so there is nothing to record an upload of. '
'The record is written before the store is contacted, and it would '
'otherwise name an upload that could not have happened.',
);
}
return UploadRecordRequest(
version:
opt('version-name') ??
manifest?.versionName ??
fallbackVersionName?.call(),
build: opt('build-number') ?? manifest?.buildNumber,
// The manifest's gitSha is the whole reason --commit exists, so a caller
// that passed one has already answered the question the flag asks.
commit: opt('commit') ?? manifest?.gitSha,
checksum: manifest?.sha256Digest,
// A dry run must not write a record: it deletes its store edit rather than
// committing, so nothing is published and there is nothing to record.
dryRun: args.options.contains('dry-run') && args.flag('dry-run'),
);
}