Skip to content

feat: pubspec auditing, sibling dep conversion, and CI template updates#22

Merged
tsavo-at-pieces merged 6 commits intomainfrom
feat/audit-commands
Feb 24, 2026
Merged

feat: pubspec auditing, sibling dep conversion, and CI template updates#22
tsavo-at-pieces merged 6 commits intomainfrom
feat/audit-commands

Conversation

@tsavo-at-pieces
Copy link
Contributor

Summary

  • Pubspec audit infrastructure: New audit and audit-all commands that validate pubspec.yaml files against a package registry — checks git URL format, tag patterns, version constraints, and resolution: workspace status
  • Sibling dep conversion for multi-package releases: During create-release, bare version constraints between sub-packages (e.g., custom_lint_core: ^0.8.2) are automatically converted to full git dep blocks with url, tag_pattern, path, and version — enabling standalone consumers to resolve dependencies. Also creates per-package annotated tags (e.g., custom_lint_core-v0.8.3) for tag_pattern resolution.
  • CI template updates: Updated release template, CI skeleton, issue-triage template, and workflow generator with latest patterns. Increased test timeout from 30→45 minutes.

New Files

File Purpose
lib/src/cli/commands/audit_command.dart Single-package pubspec audit command
lib/src/cli/commands/audit_all_command.dart Bulk audit across all registry packages
lib/src/cli/utils/audit/pubspec_auditor.dart Core auditing logic (git URL, tag_pattern, version constraints)
lib/src/cli/utils/audit/package_registry.dart Package registry with expected git URLs and tag patterns
lib/src/cli/utils/audit/audit_finding.dart Typed audit finding model

Modified Files

File Changes
sub_package_utils.dart Added convertSiblingDepsForRelease() — converts bare sibling deps to git format + strips resolution: workspace
create_release_command.dart Step 2c (sibling dep conversion) + Step 5b (per-package tags) + enriched summary
manage_cicd_cli.dart Registered audit and audit-all commands
pubspec.yaml Added yaml: ^3.1.3 and yaml_edit: ^2.2.1 dependencies
Templates/CI Updated release, CI skeleton, issue-triage templates

Release Pipeline Flow (with new steps)

Step 2:   Bump root version                              (existing)
Step 2b:  Bump sub-package versions                      (existing)
Step 2c:  Convert sibling deps + strip resolution  ← NEW
Step 3:   Assemble release notes                         (existing)
Step 4:   Commit and push                                (existing)
Step 5:   Create main tag                                (existing)
Step 5b:  Create per-package tags                   ← NEW
Step 6:   Create GitHub Release                          (existing)

Config Schema

Sub-packages gain an optional tag_pattern field in .runtime_ci/config.json:

"sub_packages": [
  { "name": "custom_lint", "path": "packages/custom_lint", "tag_pattern": "custom_lint-v{{version}}" },
  { "name": "custom_lint_core", "path": "packages/custom_lint_core", "tag_pattern": "custom_lint_core-v{{version}}" }
]

Test plan

  • dart analyze — zero issues
  • Dry-run: create temp pubspecs with bare sibling deps, verify YAML output has correct git blocks
  • Verify resolution: workspace is stripped from all sub-package pubspecs
  • Verify non-sibling deps (e.g., glob) remain untouched
  • Verify per-package tags use correct format from tag_pattern
  • End-to-end: onboard dart_custom_lint with .runtime_ci/config.json and run release dry-run

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 24, 2026 18:18
@cursor
Copy link

cursor bot commented Feb 24, 2026

PR Summary

Medium Risk
Touches release automation and introduces in-place pubspec rewriting/tag creation, which can impact published artifacts and dependency resolution if registry/tag patterns are wrong; CI/template tweaks are comparatively low risk.

Overview
Adds new CLI commands audit and audit-all to validate (and optionally auto-fix) pubspec.yaml dependencies against an external_workspace_packages.yaml registry, enforcing expected git URL/org/repo, tag_pattern, and version constraints.

Enhances create-release for multi-package repos by converting bare sibling deps in sub-package pubspecs into full git dependency blocks (and removing resolution: workspace), and by creating/pushing per-package annotated tags derived from each sub-package’s tag_pattern.

Updates CI/workflow templates and generator (tooling version bump, standardized GH_PAT env usage for git URL rewriting, macOS x64 runner label change, safer env var passing/quoting in release & triage workflows), increases CLI test timeout to 45 minutes, and adds yaml_edit dependency.

Written by Cursor Bugbot for commit 89ec243. Configure here.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

workers.add(worker());
}
await Future.wait(workers);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audit-all concurrency never actually runs

Medium Severity

The worker-pool in AuditAllCommand doesn’t yield inside worker(), so the first scheduled worker processes the entire pubspecs list synchronously and blocks the event loop; the other workers effectively do nothing. This makes --concurrency misleading and can significantly slow large scans.

Fix in Cursor Fix in Web

);
CiProcessRunner.exec('git', ['push', 'origin', pkgTag], cwd: repoRoot, fatal: true, verbose: global.verbose);
pkgTagsCreated.add(pkgTag);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-package tag check ignores remote tags

Medium Severity

The per-package tag existence check uses git rev-parse locally, so an existing remote-only tag isn’t detected. In that case, git tag -a may succeed locally but git push origin <tag> can fail due to the remote tag already existing, interrupting the release pipeline mid-flight.

Fix in Cursor Fix in Web

Comment on lines +116 to +126
index++;

final pubspecPath = pubspecs[currentIndex];
final findings = auditor.auditPubspec(pubspecPath);

// Filter by minimum severity.
final filtered = findings.where((f) => f.severity.index <= severityFilter.index).toList();

results.add(_AuditResult(index: currentIndex, pubspecPath: pubspecPath, findings: filtered));
}
}

This comment was marked as outdated.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive pubspec auditing infrastructure to validate git-sourced dependencies, introduces automatic sibling dependency conversion for multi-package releases, and refactors CI templates for improved security and maintainability.

Changes:

  • New audit and audit-all commands validate pubspec.yaml files against a package registry, checking git URLs, tag patterns, version constraints, and repository ownership
  • During release creation, sibling dependencies between sub-packages are automatically converted from bare version constraints to full git dependency blocks with tag resolution, enabling standalone package consumption
  • CI workflow templates now use step-level environment variables instead of inline secret interpolation, improving security and reducing accidental exposure risk

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated no comments.

Show a summary per file
File Description
lib/src/cli/utils/audit/audit_finding.dart Data model for audit findings with severity levels and category enums
lib/src/cli/utils/audit/package_registry.dart Registry loading and lookup logic for external workspace packages
lib/src/cli/utils/audit/pubspec_auditor.dart Core audit validation and fix logic for pubspec dependencies
lib/src/cli/commands/audit_command.dart Single-package pubspec audit CLI command
lib/src/cli/commands/audit_all_command.dart Bulk recursive pubspec audit CLI command with concurrency support
lib/src/cli/utils/sub_package_utils.dart Added sibling dependency conversion function for multi-package releases
lib/src/cli/commands/create_release_command.dart Integrated sibling dep conversion and per-package tag creation into release flow
lib/src/cli/manage_cicd_cli.dart Registered new audit commands
templates/github/workflows/release.template.yaml Refactored to use env vars for secrets, improved shell variable handling
templates/github/workflows/issue-triage.template.yaml Refactored to use env vars for secrets
templates/github/workflows/ci.skeleton.yaml Refactored to use env vars for secrets
lib/src/cli/utils/workflow_generator.dart Changed macOS x64 runner from macos-15-intel to macos-15-large
lib/src/cli/commands/test_command.dart Increased test timeout from 30 to 45 minutes
templates/config.json Removed example runner overrides for ubuntu-x64 and windows-x64
pubspec.yaml Added yaml and yaml_edit dependencies
.github/workflows/ci.yaml Generated workflow reflecting template updates
.runtime_ci/template_versions.json Updated version tracking metadata
Comments suppressed due to low confidence (3)

lib/src/cli/utils/sub_package_utils.dart:346

  • The sibling dependency conversion always uses ^$newVersion as the version constraint, which may not preserve the original constraint semantics. If the original constraint was a bare string like ^0.8.2 and the new version is 0.8.3, the converted constraint ^0.8.3 is reasonable. However, if the original constraint was more specific (e.g., >=0.8.2 <0.9.0), this conversion loses that information.

Consider preserving the original version constraint when it's a string, or at least documenting this behavior change in the PR description, since it could affect consumers who relied on more specific version ranges between sibling packages.

          final newValue = <String, Object>{'git': gitBlock, 'version': '^$newVersion'};

lib/src/cli/utils/audit/pubspec_auditor.dart:482

  • The new audit infrastructure (AuditCommand, AuditAllCommand, PubspecAuditor, PackageRegistry) lacks test coverage. The codebase demonstrates comprehensive testing patterns for command utilities (see test/consumers_command_test.dart, test/hook_installer_test.dart), and similar test coverage should be added for the audit functionality.

Consider adding unit tests for:

  • PackageRegistry.loadFromString() with various YAML inputs
  • PubspecAuditor.auditPubspec() with different dependency configurations
  • The various audit categories (bare dependency, wrong URL format, stale version, etc.)
  • Fix application logic in PubspecAuditor.fixPubspec()

These tests would help prevent regressions and validate edge cases in the audit logic.

import 'dart:io';

import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';

import '../logger.dart';
import 'audit_finding.dart';
import 'package_registry.dart';

/// RegExp matching SSH-format GitHub URLs: `git@github.com:org/repo.git`.
final _sshUrlPattern = RegExp(r'^git@github\.com:([^/]+)/([^/]+)\.git$');

/// RegExp matching HTTPS-format GitHub URLs so we can detect and flag them.
final _httpsUrlPattern = RegExp(r'^https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?$');

/// Audits pubspec.yaml dependency declarations against a [PackageRegistry].
///
/// For every dependency that matches a registry entry, the auditor checks:
///
/// 1. **bare_dependency** -- plain version string with no git source
/// 2. **wrong_org** -- git URL points to wrong GitHub org
/// 3. **wrong_repo** -- git URL points to wrong repo name
/// 4. **missing_tag_pattern** -- no `tag_pattern` in git block
/// 5. **wrong_tag_pattern** -- `tag_pattern` differs from registry
/// 6. **stale_version** -- version constraint differs from registry
/// 7. **wrong_url_format** -- git URL uses HTTPS instead of SSH
///
/// Dependencies that do not appear in the registry (pub.dev, workspace-
/// internal, path deps) are silently skipped.
class PubspecAuditor {
  /// The package registry to validate against.
  final PackageRegistry registry;

  const PubspecAuditor({required this.registry});

  // ---------------------------------------------------------------------------
  // Audit
  // ---------------------------------------------------------------------------

  /// Audit a single pubspec.yaml file and return all findings.
  ///
  /// Returns an empty list when the pubspec is fully compliant.
  List<AuditFinding> auditPubspec(String pubspecPath) {
    final file = File(pubspecPath);
    if (!file.existsSync()) {
      Logger.error('Pubspec not found: $pubspecPath');
      return [
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: '<file>',
          severity: AuditSeverity.error,
          category: AuditCategory.bareDependency,
          message: 'Pubspec file does not exist',
        ),
      ];
    }

    final String content;
    try {
      content = file.readAsStringSync();
    } on FileSystemException catch (e) {
      Logger.error('Failed to read pubspec: $e');
      return [];
    }

    final YamlMap doc;
    try {
      doc = loadYaml(content) as YamlMap;
    } on YamlException catch (e) {
      Logger.error('Failed to parse pubspec YAML at $pubspecPath: $e');
      return [];
    }

    final findings = <AuditFinding>[];

    // Audit both `dependencies` and `dev_dependencies` sections.
    final deps = doc['dependencies'] as YamlMap?;
    if (deps != null) {
      findings.addAll(_auditDependencyMap(pubspecPath, deps));
    }

    final devDeps = doc['dev_dependencies'] as YamlMap?;
    if (devDeps != null) {
      findings.addAll(_auditDependencyMap(pubspecPath, devDeps));
    }

    return findings;
  }

  /// Walk a dependency map and check each entry that exists in the registry.
  List<AuditFinding> _auditDependencyMap(String pubspecPath, YamlMap deps) {
    final findings = <AuditFinding>[];

    for (final key in deps.keys) {
      final name = key as String;
      final entry = registry.lookup(name);
      if (entry == null) continue; // Not a registry package -- skip.

      final value = deps[name];
      findings.addAll(_auditDependency(pubspecPath, name, value, entry));
    }

    return findings;
  }

  /// Audit a single dependency against its registry entry.
  List<AuditFinding> _auditDependency(String pubspecPath, String depName, Object? value, RegistryEntry entry) {
    final findings = <AuditFinding>[];

    // --- Path dependency -- skip entirely (workspace-local override) ---------
    if (value is YamlMap && value.containsKey('path')) {
      return findings;
    }

    // --- Bare dependency (just a version string or `any`) --------------------
    if (value is String) {
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.error,
          category: AuditCategory.bareDependency,
          message: 'Bare dependency -- should use git source with tag_pattern',
          currentValue: value,
          expectedValue: entry.expectedGitUrl,
        ),
      );

      // Also flag stale version if the bare constraint doesn't match.
      if (value != entry.version) {
        findings.add(
          AuditFinding(
            pubspecPath: pubspecPath,
            dependencyName: depName,
            severity: AuditSeverity.warning,
            category: AuditCategory.staleVersion,
            message: 'Version constraint does not match registry',
            currentValue: value,
            expectedValue: entry.version,
          ),
        );
      }

      return findings;
    }

    // --- Null value (workspace member ref, e.g. `runtime_native_io_core:`) ---
    if (value == null) {
      // A null value means the dep relies on workspace resolution or has no
      // constraints at all. We still flag it as bare if it's in the registry.
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.error,
          category: AuditCategory.bareDependency,
          message:
              'Empty dependency (no source, no version) -- should use git '
              'source with tag_pattern',
          currentValue: null,
          expectedValue: entry.expectedGitUrl,
        ),
      );
      return findings;
    }

    // --- Map dependency (expected for git deps) ------------------------------
    if (value is! YamlMap) {
      Logger.warn(
        'Unexpected dependency type for "$depName" in $pubspecPath: '
        '${value.runtimeType}',
      );
      return findings;
    }

    final depMap = value;

    // Check for git block.
    final gitBlock = depMap['git'];

    if (gitBlock == null) {
      // Has a map (possibly with `version:` or `sdk:` or `hosted:`) but no
      // `git:` block. If it doesn't have `path:` (already handled above),
      // treat it as bare.
      if (!depMap.containsKey('path')) {
        findings.add(
          AuditFinding(
            pubspecPath: pubspecPath,
            dependencyName: depName,
            severity: AuditSeverity.error,
            category: AuditCategory.bareDependency,
            message:
                'Dependency has no git source -- should use git source '
                'with tag_pattern',
            currentValue: depMap.toString(),
            expectedValue: entry.expectedGitUrl,
          ),
        );
      }
      return findings;
    }

    // The git block can be either a string (shorthand URL) or a YamlMap.
    String? gitUrl;
    String? tagPattern;
    String? ref;

    if (gitBlock is String) {
      gitUrl = gitBlock;
    } else if (gitBlock is YamlMap) {
      gitUrl = gitBlock['url'] as String?;
      tagPattern = gitBlock['tag_pattern'] as String?;
      ref = gitBlock['ref'] as String?;
    }

    // --- Rule 7: wrong_url_format (HTTPS instead of SSH) --------------------
    if (gitUrl != null && !_sshUrlPattern.hasMatch(gitUrl)) {
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.error,
          category: AuditCategory.wrongUrlFormat,
          message: 'Git URL is not SSH format',
          currentValue: gitUrl,
          expectedValue: entry.expectedGitUrl,
        ),
      );
    }

    // Parse org/repo from the git URL for org/repo checks.
    final sshMatch = gitUrl != null ? _sshUrlPattern.firstMatch(gitUrl) : null;
    final httpsMatch = gitUrl != null ? _httpsUrlPattern.firstMatch(gitUrl) : null;
    final urlOrg = sshMatch?.group(1) ?? httpsMatch?.group(1);
    final urlRepo = sshMatch?.group(2) ?? httpsMatch?.group(2);

    // --- Rule 2: wrong_org ---------------------------------------------------
    if (urlOrg != null && urlOrg != entry.githubOrg) {
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.error,
          category: AuditCategory.wrongOrg,
          message: 'Git URL org does not match registry',
          currentValue: urlOrg,
          expectedValue: entry.githubOrg,
        ),
      );
    }

    // --- Rule 3: wrong_repo --------------------------------------------------
    if (urlRepo != null && urlRepo != entry.githubRepo) {
      // Strip trailing `.git` from parsed HTTPS repos for comparison.
      final cleanedRepo = urlRepo.replaceAll(RegExp(r'\.git$'), '');
      if (cleanedRepo != entry.githubRepo) {
        findings.add(
          AuditFinding(
            pubspecPath: pubspecPath,
            dependencyName: depName,
            severity: AuditSeverity.error,
            category: AuditCategory.wrongRepo,
            message: 'Git URL repo does not match registry',
            currentValue: urlRepo,
            expectedValue: entry.githubRepo,
          ),
        );
      }
    }

    // --- Rule 4 & 5: missing/wrong tag_pattern -------------------------------
    if (tagPattern == null) {
      // Legacy format might use `ref` instead of `tag_pattern`.
      if (ref != null) {
        findings.add(
          AuditFinding(
            pubspecPath: pubspecPath,
            dependencyName: depName,
            severity: AuditSeverity.warning,
            category: AuditCategory.missingTagPattern,
            message: 'Git dep uses legacy "ref" instead of "tag_pattern"',
            currentValue: 'ref: $ref',
            expectedValue: 'tag_pattern: ${entry.tagPattern}',
          ),
        );
      } else {
        findings.add(
          AuditFinding(
            pubspecPath: pubspecPath,
            dependencyName: depName,
            severity: AuditSeverity.error,
            category: AuditCategory.missingTagPattern,
            message: 'Git dep is missing "tag_pattern" field',
            currentValue: null,
            expectedValue: entry.tagPattern,
          ),
        );
      }
    } else if (tagPattern != entry.tagPattern) {
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.error,
          category: AuditCategory.wrongTagPattern,
          message: 'tag_pattern does not match registry',
          currentValue: tagPattern,
          expectedValue: entry.tagPattern,
        ),
      );
    }

    // --- Rule 6: stale_version -----------------------------------------------
    final versionValue = depMap['version'] as String?;
    if (versionValue != null && versionValue != entry.version) {
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.warning,
          category: AuditCategory.staleVersion,
          message: 'Version constraint does not match registry',
          currentValue: versionValue,
          expectedValue: entry.version,
        ),
      );
    } else if (versionValue == null) {
      findings.add(
        AuditFinding(
          pubspecPath: pubspecPath,
          dependencyName: depName,
          severity: AuditSeverity.warning,
          category: AuditCategory.staleVersion,
          message:
              'No version constraint specified -- should have version: '
              '${entry.version}',
          currentValue: null,
          expectedValue: entry.version,
        ),
      );
    }

    // NOTE: We do not validate `git_path` presence here because not all
    // pubspecs currently use it for multi-package repos. The fix logic will
    // add it when creating new git dep blocks from bare deps.

    return findings;
  }

  // ---------------------------------------------------------------------------
  // Fix
  // ---------------------------------------------------------------------------

  /// Apply fixes for the given [findings] to the pubspec at [pubspecPath].
  ///
  /// Returns `true` if the file was modified, `false` otherwise.
  ///
  /// The fixer uses [YamlEditor] so comments and formatting are preserved
  /// as much as possible.
  bool fixPubspec(String pubspecPath, List<AuditFinding> findings) {
    if (findings.isEmpty) return false;

    final file = File(pubspecPath);
    if (!file.existsSync()) {
      Logger.error('Cannot fix -- pubspec not found: $pubspecPath');
      return false;
    }

    final original = file.readAsStringSync();
    final editor = YamlEditor(original);

    // Track which deps we've already fully rewritten so we don't try to
    // patch individual fields on top of a wholesale replacement.
    final rewritten = <String>{};

    // Group findings by dependency name for efficient processing.
    final byDep = <String, List<AuditFinding>>{};
    for (final f in findings) {
      byDep.putIfAbsent(f.dependencyName, () => []).add(f);
    }

    // Parse the current doc to determine which section each dep lives in.
    final doc = loadYaml(original) as YamlMap;

    for (final depName in byDep.keys) {
      final depFindings = byDep[depName]!;
      final entry = registry.lookup(depName);
      if (entry == null) continue;

      // Determine the section path (`dependencies` or `dev_dependencies`).
      final sectionKey = _findSectionKey(doc, depName);
      if (sectionKey == null) {
        Logger.warn(
          'Could not locate "$depName" in dependencies or '
          'dev_dependencies of $pubspecPath -- skipping fix',
        );
        continue;
      }

      final categories = depFindings.map((f) => f.category).toSet();

      // If the dep is bare (no git source at all), do a full rewrite.
      if (categories.contains(AuditCategory.bareDependency)) {
        _rewriteToFullGitDep(editor, sectionKey, depName, entry);
        rewritten.add(depName);
        continue;
      }

      // Otherwise, patch individual fields.
      if (rewritten.contains(depName)) continue;

      if (categories.contains(AuditCategory.wrongUrlFormat) ||
          categories.contains(AuditCategory.wrongOrg) ||
          categories.contains(AuditCategory.wrongRepo)) {
        _tryUpdate(editor, [sectionKey, depName, 'git', 'url'], entry.expectedGitUrl);
      }

      if (categories.contains(AuditCategory.missingTagPattern) || categories.contains(AuditCategory.wrongTagPattern)) {
        // If the dep has a legacy `ref` field, remove it first.
        _tryRemove(editor, [sectionKey, depName, 'git', 'ref']);
        _tryUpdate(editor, [sectionKey, depName, 'git', 'tag_pattern'], entry.tagPattern);
      }

      if (categories.contains(AuditCategory.staleVersion)) {
        _tryUpdate(editor, [sectionKey, depName, 'version'], entry.version);
      }
    }

    final updated = editor.toString();
    if (updated == original) return false;

    file.writeAsStringSync(updated);
    return true;
  }

  /// Rewrite a dependency to the full git format using registry values.
  void _rewriteToFullGitDep(YamlEditor editor, String sectionKey, String depName, RegistryEntry entry) {
    final gitBlock = <String, Object>{'url': entry.expectedGitUrl, 'tag_pattern': entry.tagPattern};

    // Include `path:` if the registry specifies a git_path (multi-package
    // repos like dart_custom_lint).
    if (entry.gitPath != null) {
      gitBlock['path'] = entry.gitPath!;
    }

    final newValue = <String, Object>{'git': gitBlock, 'version': entry.version};

    _tryUpdate(editor, [sectionKey, depName], newValue);
  }

  /// Safely attempt a [YamlEditor.update]; log and swallow errors.
  void _tryUpdate(YamlEditor editor, List<Object> path, Object value) {
    try {
      editor.update(path, value);
    } on Exception catch (e) {
      Logger.warn('yaml_edit: failed to update $path -- $e');
    }
  }

  /// Safely attempt a [YamlEditor.remove]; log and swallow errors.
  void _tryRemove(YamlEditor editor, List<Object> path) {
    try {
      editor.remove(path);
    } on Exception catch (_) {
      // The key may not exist -- that's fine.
    }
  }

  /// Determine whether [depName] lives under `dependencies` or
  /// `dev_dependencies` in the parsed YAML document.
  String? _findSectionKey(YamlMap doc, String depName) {
    final deps = doc['dependencies'] as YamlMap?;
    if (deps != null && deps.containsKey(depName)) return 'dependencies';

    final devDeps = doc['dev_dependencies'] as YamlMap?;
    if (devDeps != null && devDeps.containsKey(depName)) {
      return 'dev_dependencies';
    }

    return null;
  }
}

lib/src/cli/utils/sub_package_utils.dart:378

  • The sibling dependency conversion function lacks test coverage. Given the complexity of the YAML manipulation and the critical nature of dependency management in releases, this functionality should have comprehensive unit tests.

Consider adding tests for:

  • Converting bare string constraints to git format
  • Converting null/workspace dependencies to git format
  • Preserving non-sibling dependencies unchanged
  • Handling packages without tag_pattern
  • Stripping resolution: workspace from pubspecs
  • Edge cases like self-references and missing pubspec files

The codebase already has good test coverage patterns (see test/ directory), and this critical release functionality would benefit from similar coverage.

  static int convertSiblingDepsForRelease({
    required String repoRoot,
    required String newVersion,
    required String effectiveRepo,
    required List<Map<String, dynamic>> subPackages,
    bool verbose = false,
  }) {
    // Build sibling lookup: {packageName -> {tag_pattern, path}}
    // Only packages WITH tag_pattern participate.
    final siblingMap = <String, Map<String, String>>{};
    for (final pkg in subPackages) {
      final tp = pkg['tag_pattern'] as String?;
      if (tp == null) continue;
      siblingMap[pkg['name'] as String] = {'tag_pattern': tp, 'path': pkg['path'] as String};
    }
    if (siblingMap.isEmpty) return 0;

    final gitUrl = 'git@github.com:$effectiveRepo.git';
    var totalConversions = 0;

    for (final pkg in subPackages) {
      final pkgName = pkg['name'] as String;
      final pubspecFile = File('$repoRoot/${pkg['path']}/pubspec.yaml');
      if (!pubspecFile.existsSync()) {
        Logger.warn('Sub-package pubspec not found: ${pkg['path']}/pubspec.yaml');
        continue;
      }

      final original = pubspecFile.readAsStringSync();
      final editor = YamlEditor(original);
      final doc = loadYaml(original) as YamlMap;
      var conversions = 0;

      // Scan both dependency sections.
      for (final sectionKey in ['dependencies', 'dev_dependencies']) {
        final section = doc[sectionKey] as YamlMap?;
        if (section == null) continue;

        for (final key in section.keys) {
          final depName = key as String;
          if (depName == pkgName) continue; // skip self
          final sibling = siblingMap[depName];
          if (sibling == null) continue; // not a sibling

          final depValue = section[depName];
          // Only convert bare string constraints (e.g., "^0.8.2") and
          // null values (workspace refs). Map deps are already structured.
          if (depValue is! String && depValue != null) continue;

          final gitBlock = <String, Object>{
            'url': gitUrl,
            'tag_pattern': sibling['tag_pattern']!,
            'path': sibling['path']!,
          };
          final newValue = <String, Object>{'git': gitBlock, 'version': '^$newVersion'};

          try {
            editor.update([sectionKey, depName], newValue);
            conversions++;
          } on Exception catch (e) {
            Logger.warn('yaml_edit: failed to update $sectionKey.$depName -- $e');
          }
        }
      }

      // Strip `resolution: workspace` if present.
      if (doc.containsKey('resolution')) {
        try {
          editor.remove(['resolution']);
          if (verbose) {
            Logger.info('Stripped resolution: workspace from ${pkg['name']}');
          }
        } on Exception catch (_) {}
      }

      final updated = editor.toString();
      if (updated != original) {
        pubspecFile.writeAsStringSync(updated);
        totalConversions += conversions;
        if (verbose) {
          Logger.info('Converted $conversions sibling dep(s) in ${pkg['name']}');
        }
      }
    }

    return totalConversions;
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tsavo-at-pieces and others added 6 commits February 24, 2026 14:00
Add two new manage_cicd commands that validate pubspec.yaml dependency
declarations against the external_workspace_packages.yaml registry.

`manage_cicd audit` — validate a single pubspec.yaml:
- 7 validation rules: bare deps, wrong org/repo, missing/wrong
  tag_pattern, stale version, wrong URL format (HTTPS vs SSH)
- Auto-detects registry by walking up directory tree
- --fix flag for auto-remediation using yaml_edit
- --severity filter (error/warning/info)

`manage_cicd audit-all` — batch validate all pubspecs:
- Recursive discovery with configurable exclusions
- Worker pool concurrency (default 4)
- Per-file status lines + detailed findings + aggregate summary
- --fix applies fixes across all discovered pubspecs

Shared engine in lib/src/cli/utils/audit/:
- PackageRegistry — loads registry YAML, O(1) lookup by dep name
- PubspecAuditor — audit + fix logic with yaml_edit
- AuditFinding — severity/category data model

ref open-runtime/aot_monorepo#411

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Windows named pipe tests need more than 30 minutes on CI runners
when running the full stress test suite (13 large payload tests +
500 sequential RPCs + adversarial scenarios).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eases

Add convertSiblingDepsForRelease() to SubPackageUtils that automatically
converts bare sibling dependencies (e.g., `custom_lint_core: ^0.8.2`) to
full git dep blocks with url, tag_pattern, path, and version constraint
during the release pipeline. Also strips `resolution: workspace` from
sub-package pubspecs so standalone consumers can resolve.

Integrates into create-release as Step 2c (after version bumping) and
Step 5b (per-package tag creation after main tag). Driven by optional
`tag_pattern` field in sub_packages config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update release template, CI skeleton, issue-triage template, and
workflow generator with latest patterns. Remove deprecated config
entries from templates/config.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tsavo-at-pieces tsavo-at-pieces merged commit cdaf48c into main Feb 24, 2026
2 checks passed
tsavo-at-pieces pushed a commit that referenced this pull request Feb 24, 2026
## Changelog

## [0.14.0] - 2026-02-24

### Added
- Added `build_runner` feature flag for CI codegen to generate `.g.dart` files in CI instead of relying on local developer builds
- Added `manage_cicd audit` and `audit-all` commands for `pubspec.yaml` dependency validation against `external_workspace_packages.yaml` registry (refs open-runtime/aot_monorepo#411) (#22)
- Added sibling dependency conversion and per-package tag creation for multi-package releases, automatically converting bare sibling dependencies during release (#22)
- Used org-managed runners as default platform definitions for Linux (`ubuntu-x64`/`arm64`) and Windows (`windows-x64`/`arm64`)

### Changed
- Updated CI workflow templates and generator with latest patterns and removed deprecated config entries from templates (#22)

### Fixed
- Fixed cross-platform hashing in `template_manifest.dart` using pure-Dart crypto and normalized CRLF to LF to prevent data loss on Windows (#23, fixes #5, #8, #10, #11, #12, #13)
- Added strict input validation for `dart_sdk`, `feature` keys, `sub_packages`, and hardened triage config `require_file` to prevent path traversal
- Replaced shell-based hashing with pure-Dart crypto in autodoc to ensure caching works on macOS, Windows, and minimal CI images
- Normalized version input by stripping the leading 'v' prefix and validated SemVer in `create-release`
- Fixed passing `allFindings` to `fixPubspec` and added `pub_semver` dependency
- Validated YAML before writing and created backups in pubspec auditor to prevent corruption (#22)
- Fixed tag existence checks by using `refs/tags/` prefix and handled errors for per-package tags (#22)
- Increased test process timeout from 30 to 45 minutes to fix Windows named pipe tests on CI runners (#22)
- Added `yaml_edit` dependency required for pubspec auditor

### Security
- Added `::add-mask::` token masking before `git config` in all CI workflow locations to prevent Personal Access Token (PAT) leaks in logs, added `fetch-depth: 1` to checkout ...

## Files Modified

```
.../audit/v0.14.0/explore/breaking_changes.json    |   4 +
 .../audit/v0.14.0/explore/commit_analysis.json     | 122 +++++++++++++++++++++
 .runtime_ci/audit/v0.14.0/explore/pr_data.json     |  30 +++++
 .runtime_ci/audit/v0.14.0/meta.json                |  82 ++++++++++++++
 .../v0.14.0/version_analysis/version_bump.json     |   1 +
 .../version_analysis/version_bump_rationale.md     |  26 +++++
 .../release_notes/v0.14.0/changelog_entry.md       |  24 ++++
 .../release_notes/v0.14.0/contributors.json        |   5 +
 .../release_notes/v0.14.0/linked_issues.json       |   1 +
 .runtime_ci/release_notes/v0.14.0/release_notes.md |  59 ++++++++++
 .../release_notes/v0.14.0/release_notes_body.md    |  59 ++++++++++
 .runtime_ci/version_bumps/v0.14.0.md               |  26 +++++
 CHANGELOG.md                                       |  26 +++++
 README.md                                          |  14 ++-
 pubspec.yaml                                       |   2 +-
 15 files changed, 475 insertions(+), 6 deletions(-)
```

## Version Bump Rationale

# Version Bump Rationale

**Decision**: minor

**Why**:
The recent commits introduce new capabilities, including a new `build_runner` feature flag for CI codegen and a new `audit-all` CLI command. These are backwards-compatible additive changes that expand the capabilities of the tooling without introducing breaking changes to existing public APIs or workflows, thereby warranting a minor version bump.

**Key Changes**:
- **New Feature**: Added `build_runner` feature flag for generating `.g.dart` files in CI to avoid environment drift and stale codegen risks.
- **New Feature**: Added `audit-all` command to recursively audit all `pubspec.yaml` files under a directory against the package registry.
- **Fix/Improvement**: Replaced shell-based file hashing with pure-Dart crypto for better cross-platform (Windows) compatibility.
- **Security/CI Hardening**: Masked sensitive tokens in CI logs (`::add-mask::`), reduced checkout depth to 1 for faster executions, and migrated away from hardcoded...

## Contributors

- @tsavo-at-pieces

---
Automated release by CI/CD pipeline (Gemini CLI + GitHub Actions)
Commits since v0.13.0: 15
Generated: 2026-02-24T20:32:27.206709Z
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants