Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,11 @@ jobs:
with:
persist-credentials: false

- name: Check CHANGELOG has unreleased content
run: |
# Check if CHANGELOG.md exists
if [ ! -f CHANGELOG.md ]; then
echo "❌ CHANGELOG.md not found"
exit 1
fi

# Extract content between ## [Unreleased] and next ## heading
unreleased_content=$(awk '/^## \[Unreleased\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md | grep -v '^[[:space:]]*$' | head -20)

if [ -z "$unreleased_content" ]; then
echo "❌ No content found under ## [Unreleased] section"
echo ""
echo "Please add your changes to the CHANGELOG.md under the ## [Unreleased] section."
echo "If this PR doesn't require a changelog entry, add the 'skip-changelog' label."
exit 1
else
echo "✅ Found unreleased content in CHANGELOG.md:"
echo "$unreleased_content"
fi
- uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c # v1.7.1
with:
sdk: stable

- name: Check CHANGELOG
env:
BRANCH_NAME: ${{ github.head_ref }}
run: dart tool/check_changelog.dart --branch "$BRANCH_NAME"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.1] - 2026-02-04

### Added

- Release automation with tag-based publishing to pub.dev
- Numbered steps in RELEASING.md Quick Reference for easier scanning
- Version bump tooling (`dart tool/version_bump.dart`)
- Pre-release validation (`dart tool/pre_release_check.dart`)
- Release creation script (`dart tool/create_release.dart`) - creates tag, pushes, and creates GitHub release with changelog
- CI changelog check with `skip-changelog` label escape hatch
- Smart changelog validation (`dart tool/check_changelog.dart`) - auto-detects release branches
- `CONTRIBUTING.md` with development workflow and PR guidelines
- `RELEASING.md` documentation for maintainers
- Contributing section in README
Expand Down
15 changes: 9 additions & 6 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ This document describes the process for releasing new versions of dartypod to pu
## Quick Reference

```bash
# Create release branch
# 1. Create release branch
git checkout main && git pull origin main
git checkout -b release/v0.2.0

# Pre-release validation
# 2. Pre-release validation
dart tool/pre_release_check.dart

# Version bump (choose one)
# 3. Version bump (choose one)
dart tool/version_bump.dart patch # 0.1.0 -> 0.1.1
dart tool/version_bump.dart minor # 0.1.0 -> 0.2.0
dart tool/version_bump.dart major # 0.1.0 -> 1.0.0

# Commit
# 4. Commit
git add . && git commit -m "chore: bump version to v0.2.0"

# Post-bump validation
# 5. Post-bump validation (dry-run publish check)
dart tool/pre_release_check.dart --post

# Push and create PR, then after merge:
# 6. Push and create PR
git push origin release/v0.2.0

# 7. After PR merge, create release
git checkout main && git pull origin main
dart tool/create_release.dart
```
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: dartypod
description: A minimal Service Locator with compile-time safe provider references, enabling clean dependency injection patterns.
version: 0.1.0
version: 0.1.1
repository: https://github.com/robert-northmind/dartypod

environment:
Expand Down
170 changes: 170 additions & 0 deletions tool/check_changelog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import 'dart:io';

/// Check CHANGELOG.md for proper content based on branch type.
///
/// For release branches (release/*):
/// - Verifies there's a versioned section matching pubspec.yaml version
///
/// For other branches:
/// - Verifies there's content under [Unreleased] section
Future<void> main(List<String> args) async {
// Parse arguments
String? branchName;

for (var i = 0; i < args.length; i++) {
if (args[i] == '--branch' && i + 1 < args.length) {
branchName = args[i + 1];
i++;
}
}

if (branchName == null) {
stderr.writeln('Usage: dart tool/check_changelog.dart --branch <branch>');
stderr.writeln('');
stderr.writeln('Examples:');
stderr.writeln(
' dart tool/check_changelog.dart --branch feature/add-something');
stderr.writeln(' dart tool/check_changelog.dart --branch release/v0.1.1');
exit(1);
}

// Check if CHANGELOG.md exists
final changelogFile = File('CHANGELOG.md');
if (!changelogFile.existsSync()) {
_fail('CHANGELOG.md not found');
}

final changelog = await changelogFile.readAsString();

// Determine check type based on branch name
if (branchName.startsWith('release/')) {
await _checkReleaseBranch(changelog);
} else {
_checkUnreleasedContent(changelog);
}
}

/// Check that CHANGELOG has a versioned section matching pubspec.yaml version
Future<void> _checkReleaseBranch(String changelog) async {
stdout.writeln('📦 Detected release branch');
stdout.writeln('');

// Get version from pubspec.yaml
final pubspecFile = File('pubspec.yaml');
if (!pubspecFile.existsSync()) {
_fail('pubspec.yaml not found');
}

final pubspec = await pubspecFile.readAsString();
final versionMatch = RegExp(r'version: (\d+\.\d+\.\d+)').firstMatch(pubspec);

if (versionMatch == null) {
_fail('Could not find version in pubspec.yaml');
}

final version = versionMatch.group(1)!;
stdout.writeln('Expected version: $version');
stdout.writeln('');

// Check if CHANGELOG has a section for this version
// Pattern: ## [0.1.1] - YYYY-MM-DD or ## [0.1.1]
final versionPattern = RegExp(r'## \[' + RegExp.escape(version) + r'\]');

if (versionPattern.hasMatch(changelog)) {
// Extract and display the content for this version
final content = _extractVersionContent(changelog, version);
stdout.writeln('✅ Found changelog entry for version $version:');
stdout.writeln('');
for (final line in content.take(20)) {
stdout.writeln(' $line');
}
if (content.length > 20) {
stdout.writeln(' ... (truncated)');
}
} else {
_fail(
'No changelog entry found for version $version\n'
'\n'
'Expected to find: ## [$version] - YYYY-MM-DD\n'
'\n'
'Make sure you ran: dart tool/version_bump.dart <patch|minor|major>',
);
}
}

/// Check that CHANGELOG has content under [Unreleased] section
void _checkUnreleasedContent(String changelog) {
stdout.writeln('🔍 Checking for unreleased content');
stdout.writeln('');

final content = _extractUnreleasedContent(changelog);

if (content.isEmpty) {
_fail(
'No content found under ## [Unreleased] section\n'
'\n'
'Please add your changes to the CHANGELOG.md under the ## [Unreleased] section.\n'
"If this PR doesn't require a changelog entry, add the 'skip-changelog' label.",
);
} else {
stdout.writeln('✅ Found unreleased content in CHANGELOG.md:');
stdout.writeln('');
for (final line in content.take(20)) {
stdout.writeln(' $line');
}
if (content.length > 20) {
stdout.writeln(' ... (truncated)');
}
}
}

/// Extract content between ## [version] and the next ## heading
List<String> _extractVersionContent(String changelog, String version) {
final lines = changelog.split('\n');
final result = <String>[];
var inSection = false;
final versionPattern = RegExp(r'## \[' + RegExp.escape(version) + r'\]');

for (final line in lines) {
if (versionPattern.hasMatch(line)) {
inSection = true;
continue;
}
if (inSection && line.startsWith('## [')) {
break;
}
if (inSection && line.trim().isNotEmpty) {
result.add(line);
}
}

return result;
}

/// Extract content between ## [Unreleased] and the next ## heading
List<String> _extractUnreleasedContent(String changelog) {
final lines = changelog.split('\n');
final result = <String>[];
var inSection = false;

for (final line in lines) {
if (line.startsWith('## [Unreleased]')) {
inSection = true;
continue;
}
if (inSection && line.startsWith('## [')) {
break;
}
if (inSection && line.trim().isNotEmpty) {
result.add(line);
}
}

return result;
}

/// Print error message and exit with failure
Never _fail(String message) {
stderr.writeln('❌ $message');
exit(1);
}