diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 0000000..7046d41 --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,82 @@ +name: Publish Packages to pub.dev + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' # matches 0.2.0-beta, 1.0.0, etc. + +permissions: + contents: read + id-token: write # Required for OIDC authentication with pub.dev + +jobs: + publish: + runs-on: ubuntu-latest + environment: pub.dev # Optional: requires approval before publishing + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Prepare packages for publishing + run: dart tools/prepare_publish.dart ${{ steps.version.outputs.VERSION }} + + - name: Verify package contents + run: | + for pkg in dart_node_core dart_node_express dart_node_ws dart_node_react dart_node_react_native; do + echo "=== $pkg pubspec.yaml ===" + cat packages/$pkg/pubspec.yaml + echo "" + done + + # Publish packages in dependency order + # dart-lang/setup-dart handles OIDC token provisioning automatically + # Adding delays between publishes to allow pub.dev to index each package + + - name: Publish dart_node_core + run: | + cd packages/dart_node_core + dart pub get + dart pub publish --force + + - name: Wait for pub.dev to index dart_node_core + run: sleep 30 + + - name: Publish dart_node_express + run: | + cd packages/dart_node_express + dart pub get + dart pub publish --force + + - name: Publish dart_node_ws + run: | + cd packages/dart_node_ws + dart pub get + dart pub publish --force + + - name: Wait for pub.dev to index before dart_node_react + run: sleep 30 + + - name: Publish dart_node_react + run: | + cd packages/dart_node_react + dart pub get + dart pub publish --force + + - name: Wait for pub.dev to index dart_node_react + run: sleep 30 + + - name: Publish dart_node_react_native + run: | + cd packages/dart_node_react_native + dart pub get + dart pub publish --force diff --git a/tools/prepare_publish.dart b/tools/prepare_publish.dart new file mode 100644 index 0000000..ccace5d --- /dev/null +++ b/tools/prepare_publish.dart @@ -0,0 +1,117 @@ +// ignore_for_file: avoid_print +import 'dart:io'; + +/// Package dependency graph - order matters for publishing +/// Packages with no dependencies must be published first +const packageDeps = >{ + 'dart_node_core': [], + 'dart_node_express': ['dart_node_core'], + 'dart_node_ws': ['dart_node_core'], + 'dart_node_react': ['dart_node_core'], + 'dart_node_react_native': ['dart_node_core', 'dart_node_react'], +}; + +/// Publishing order based on dependency graph (topological sort) +const publishOrder = [ + 'dart_node_core', + 'dart_node_express', + 'dart_node_ws', + 'dart_node_react', + 'dart_node_react_native', +]; + +void main(List args) { + if (args.isEmpty) { + print('Usage: dart tools/prepare_publish.dart '); + print(' version - The version to set (e.g., 0.2.0-beta)'); + print(''); + print('This script prepares all packages for publishing by:'); + print(' 1. Setting the version in all pubspec.yaml files'); + print(' 2. Updating interdependencies to use pub.dev versions'); + print(' 3. Removing publish_to: none from all pubspec.yaml files'); + exit(1); + } + + final version = args[0]; + if (!_isValidVersion(version)) { + print('Error: Invalid version format: $version'); + print('Expected format: X.Y.Z or X.Y.Z-prerelease'); + exit(1); + } + + final scriptDir = File(Platform.script.toFilePath()).parent; + final repoRoot = scriptDir.parent; + final packagesDir = Directory('${repoRoot.path}/packages'); + + print('Preparing packages for publishing version $version\n'); + + for (final packageName in publishOrder) { + _preparePackage(packagesDir, packageName, version); + } + + print('\nAll packages prepared for publishing!'); + print('Publishing order: ${publishOrder.join(' -> ')}'); +} + +void _preparePackage(Directory packagesDir, String packageName, String version) { + final pubspecFile = File('${packagesDir.path}/$packageName/pubspec.yaml'); + if (!pubspecFile.existsSync()) { + print('Error: $packageName/pubspec.yaml not found'); + exit(1); + } + + var content = pubspecFile.readAsStringSync(); + final changes = []; + + // 1. Update version + final versionPattern = RegExp(r'version:\s*[^\n]+'); + if (versionPattern.hasMatch(content)) { + content = content.replaceFirst(versionPattern, 'version: $version'); + changes.add('version -> $version'); + } + + // 2. Remove publish_to: none + final publishToPattern = RegExp(r'publish_to:\s*none\n?'); + if (publishToPattern.hasMatch(content)) { + content = content.replaceFirst(publishToPattern, ''); + changes.add('removed publish_to: none'); + } + + // 3. Update interdependencies to pub.dev versions + final deps = packageDeps[packageName] ?? []; + for (final dep in deps) { + content = _switchToPubDevDependency(content, dep, version); + changes.add('$dep -> ^$version'); + } + + pubspecFile.writeAsStringSync(content); + print('$packageName: ${changes.join(", ")}'); +} + +String _switchToPubDevDependency(String content, String depName, String version) { + // Match path dependency format + final pathPattern = RegExp( + '$depName:\\s*\\n\\s*path:\\s*[^\\n]+', + multiLine: true, + ); + + // Match existing version dependency format + final versionPattern = RegExp('$depName:\\s*\\^[^\\n]+'); + + final replacement = '$depName: ^$version'; + + if (pathPattern.hasMatch(content)) { + return content.replaceFirst(pathPattern, replacement); + } + if (versionPattern.hasMatch(content)) { + return content.replaceFirst(versionPattern, replacement); + } + + return content; +} + +bool _isValidVersion(String version) { + // Match semantic versioning with optional prerelease + final versionRegex = RegExp(r'^\d+\.\d+\.\d+(-[\w.]+)?$'); + return versionRegex.hasMatch(version); +}