publish method
Executes the GitHub Releases publishing workflow.
Performs the complete GitHub release publishing process including variable processing, release management, and asset uploads. Handles both file and directory uploads with proper error handling.
Publishing workflow:
- Process variables and configure authentication
- Initialize GitHub API client with token
- Find existing release or create new one
- Upload files/directory contents as release assets
- Handle errors and provide detailed logging
File handling:
- Single files: Upload directly as release asset
- Directories: Upload all matching binary files
- Binary type filtering: Only files matching
binaryType
Returns exit code:
- 0 = Success (all assets uploaded)
- 1 = Error (upload failed or file issues)
Throws exception for configuration or API errors.
Implementation
@override
Future<int> publish() async {
await registerSecrets();
final argumentBuilder = Arguments.fromJson(
await variables.processMap(toJson()),
variables: variables,
);
await argumentBuilder.printJob();
final resolvedPath = argumentBuilder.filePath;
final isDirectory = await FileSystemEntity.isDirectory(resolvedPath);
// During a dry run the build step never produced anything, so a missing
// artifact is expected. Every other publisher already rehearses cleanly;
// this one failed the whole run, which made `--dry-run` unusable for any
// configuration containing a GitHub job.
if (JobArguments.dryRun &&
!isDirectory &&
!await File(resolvedPath).exists()) {
logger.logNote(
"no ${argumentBuilder.binaryType} artifact yet (dry run)",
);
return 0;
}
// Resolve the set of assets before touching the API, so a missing artifact
// never leaves an empty release behind.
final List<File> assets;
if (isDirectory) {
final suffix = argumentBuilder.binaryType.isEmpty
? ""
: ".${argumentBuilder.binaryType}";
assets = Directory(resolvedPath)
.listSync()
.whereType<File>()
.where((file) => file.path.endsWith(suffix))
.toList();
if (assets.isEmpty) {
if (JobArguments.dryRun) {
logger.logNote(
"no ${argumentBuilder.binaryType} artifact yet (dry run)",
);
return 0;
}
logger.logError(
"No ${suffix.isEmpty ? "files" : "$suffix files"} found in $resolvedPath",
);
return 1;
}
logger.logInfo(
"Directory detected on path: $resolvedPath (${assets.length} asset(s))",
);
} else {
final file = File(resolvedPath);
if (!await file.exists()) {
logger.logError("File does not exist: $resolvedPath");
return 1;
}
assets = [file];
logger.logInfo("File detected on path: $resolvedPath");
}
if (JobArguments.dryRun) {
logger.logInfo(
"[dry-run] would upload ${assets.length} asset(s) to "
"${argumentBuilder.repoOwner}/${argumentBuilder.repoName} "
"release '${argumentBuilder.releaseName}'",
);
for (final asset in assets) {
logger.logInfo("[dry-run] - ${asset.path}");
}
return 0;
}
argumentBuilder._dio.options.headers["Authorization"] =
"Bearer ${argumentBuilder.token}";
logger.logDebug.call("Initializing Github API client");
final uploadUrl = await argumentBuilder._resolveUploadUrl();
if (uploadUrl == null) {
logger.logError(
"Failed to resolve a GitHub release upload URL for "
"${argumentBuilder.repoOwner}/${argumentBuilder.repoName}. "
"Check the token scopes and that the repository exists.",
);
return 1;
}
var failures = 0;
for (final file in assets) {
final downloadUrl = await argumentBuilder.uploadFile(uploadUrl, file);
if (downloadUrl == null) {
failures++;
logger.logError("Failed to upload ${file.path}");
continue;
}
logger.logDebug.call("${file.path} uploaded successfully: $downloadUrl");
}
if (failures > 0) {
logger
.logError("$failures of ${assets.length} asset(s) failed to upload");
return 1;
}
return 0;
}