distributeYaml static method
Creates a ConfigParser instance by parsing a YAML file.
This method reads and parses a YAML configuration file, processes variables, validates required fields, and creates a complete ConfigParser instance.
Parameters:
path- The path to the YAML configuration fileglobalResults- Global command line argument results
Returns a new ConfigParser instance with the parsed configuration
Throws a ConfigException describing the offending key when the file is missing, is not valid YAML, or does not match the expected structure.
Implementation
static Future<ConfigParser> distributeYaml(
String path,
ArgResults? globalResults,
) async {
final file = File(path);
if (!file.existsSync()) {
// A directory here is almost always a `--config` pointed one level too
// high; saying "not found" about something that is plainly there sends
// the reader looking for the wrong problem.
if (FileSystemEntity.isDirectorySync(path)) {
throw ConfigException(
"'$path' is a directory, not a configuration file. "
"Point --config at the YAML file itself.",
);
}
throw ConfigException(
"Configuration file '$path' not found. Run `distribute init` to create one.",
);
}
final Map<String, dynamic> configJson;
try {
final decoded = loadYaml(file.readAsStringSync());
if (decoded == null) {
throw ConfigException("Configuration file '$path' is empty.");
}
if (decoded is! Map) {
throw ConfigException(
"Configuration file '$path' must contain a YAML mapping at the root.",
);
}
configJson = Map<String, dynamic>.from(jsonDecode(jsonEncode(decoded)));
} on YamlException catch (e) {
throw ConfigException("Invalid YAML in '$path': ${e.message}");
}
_requireString(configJson, "name", path);
_requireString(configJson, "description", path);
final rawVariables = configJson["variables"];
if (rawVariables != null && rawVariables is! Map) {
throw ConfigException(
"'variables' in '$path' must be a mapping of KEY: value pairs.",
);
}
// `variables` is optional: an empty map keeps every downstream lookup valid.
final yamlVariables = Map<String, dynamic>.from(
(rawVariables as Map?) ?? const {},
);
final environments = Map<String, dynamic>.from(Platform.environment.cast());
// Iterating a copy of the keys: the loop removes entries as it goes.
for (final key in yamlVariables.keys.toList()) {
// A key written with no value — `FIREBASE_TOKEN:` — is a declaration, not
// an assignment. Overwriting the exported variable with an empty string
// used to blank the real credential, resolve the placeholder to nothing,
// and let `validate` report no problems.
if (yamlVariables[key] == null) {
yamlVariables.remove(key);
continue;
}
yamlVariables[key] = await Variables.processBySystem(
yamlVariables[key]?.toString(),
globalResults,
);
}
environments.addAll(yamlVariables);
// `${{CHANGELOG}}` reads the history lazily, but it has to know the range
// and formatting the project asked for before anything resolves it.
final changelogSection =
_parseSection(configJson["changelog"], "changelog", path);
BuiltinVariables.changelogOptions = _changelogOptions(
changelogSection,
path,
);
// `changelog: ai: true` means the variable is polished on every publish.
// Installed through a callback so this file keeps no dependency on the AI
// adapters, and a project that never asks for it never loads them.
try {
installChangelogPolisher(
changelogSection: changelogSection,
aiSection: _parseAi(configJson["ai"], path),
);
} on ArgumentError catch (e) {
throw ConfigException("${e.message} (in '$path')");
}
final variables = Variables(environments, globalResults);
final rawTasks = configJson["tasks"];
if (rawTasks == null) {
throw ConfigException("'tasks' key not found in '$path'.");
}
if (rawTasks is! List) {
throw ConfigException("'tasks' in '$path' must be a list.");
}
if (rawTasks.isEmpty) {
throw ConfigException(
"'tasks' in '$path' must contain at least one task.");
}
final jobTasks = <Task>[];
final seenTaskKeys = <String>{};
for (var taskIndex = 0; taskIndex < rawTasks.length; taskIndex++) {
final rawTask = rawTasks[taskIndex];
final taskLabel = "tasks[$taskIndex]";
if (rawTask is! Map) {
throw ConfigException("$taskLabel in '$path' must be a mapping.");
}
final task = Map<String, dynamic>.from(rawTask);
final taskName = _requireString(task, "name", path, context: taskLabel);
final taskKey = _requireString(task, "key", path, context: taskLabel);
if (!seenTaskKeys.add(taskKey)) {
throw ConfigException(
"Duplicate task key '$taskKey' in '$path'. Task keys must be unique.",
);
}
final rawJobs = task["jobs"];
if (rawJobs is! List || rawJobs.isEmpty) {
throw ConfigException(
"$taskLabel ('$taskKey') in '$path' must define a non-empty 'jobs' list.",
);
}
final jobs = <Job>[];
final seenJobKeys = <String>{};
for (var jobIndex = 0; jobIndex < rawJobs.length; jobIndex++) {
final rawJob = rawJobs[jobIndex];
final jobLabel = "$taskLabel.jobs[$jobIndex]";
if (rawJob is! Map) {
throw ConfigException("$jobLabel in '$path' must be a mapping.");
}
final job = _parseJob(
Map<String, dynamic>.from(rawJob),
variables: variables,
environments: environments,
path: path,
label: jobLabel,
);
if (job.key != null && !seenJobKeys.add(job.key!)) {
throw ConfigException(
"Duplicate job key '${job.key}' in task '$taskKey' of '$path'. "
"Job keys must be unique within a task.",
);
}
jobs.add(job);
}
final rawWorkflows = task["workflows"];
if (rawWorkflows != null && rawWorkflows is! List) {
throw ConfigException(
"Task '$taskKey' in '$path' has a 'workflows' that is not a list. "
"Write it as a list of job keys.",
);
}
final workflows = rawWorkflows == null
? null
: [
for (final entry in rawWorkflows as List)
if (entry is String)
entry
else
throw ConfigException(
"Task '$taskKey' in '$path' lists a workflow entry that is "
"not a job key: '$entry'.",
),
];
if (workflows != null) {
for (final workflow in workflows) {
if (!jobs.any((job) => job.key == workflow)) {
throw ConfigException(
"Task '$taskKey' in '$path' lists workflow '$workflow' but no job "
"with that key exists. Available job keys: "
"${jobs.map((job) => job.key).whereType<String>().join(', ')}.",
);
}
}
}
jobTasks.add(
Task(
name: taskName,
key: taskKey,
jobs: jobs,
workflows: workflows,
description:
_asString(task["description"], "$taskLabel.description", path),
),
);
}
final rawArguments = configJson["arguments"];
if (rawArguments != null && rawArguments is! Map) {
throw ConfigException("'arguments' in '$path' must be a mapping.");
}
return ConfigParser(
globalResults: globalResults,
tasks: jobTasks,
arguments: (rawArguments as Map?)?.map(
(key, value) => MapEntry(key.toString(), value as dynamic),
),
environments: environments,
variables: variables,
notifications: _parseNotifications(configJson["notifications"], path),
ai: _parseAi(configJson["ai"], path),
changelog: changelogSection,
);
}