publish method

  1. @override
Future<int> publish()
override

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:

  1. Process variables and configure authentication
  2. Initialize GitHub API client with token
  3. Find existing release or create new one
  4. Upload files/directory contents as release assets
  5. 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 pattern = Glob.hasMagic(resolvedPath);
  final isDirectory =
      !pattern && 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 &&
      !pattern &&
      !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;
  // GitHub attaches every asset it is given, so a pattern here means "all of
  // them" rather than "the best one" — `out/*.apk` uploads each split APK.
  if (pattern) {
    assets = Glob.expand(resolvedPath);
    if (assets.isEmpty) {
      if (JobArguments.dryRun) {
        logger.logNote('nothing matches $resolvedPath yet (dry run)');
        return 0;
      }
      logger.logError('No file matches $resolvedPath');
      return 1;
    }
    logger.logInfo(
      'Pattern matched ${assets.length} asset(s): $resolvedPath',
    );
  } else 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}";
  JobArguments.trackCancellation(
    () => argumentBuilder._cancelToken.cancel('job cancelled'),
  );
  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;
}