Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
18 changes: 8 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,15 @@ jobs:
- name: Setup Dart
uses: dart-lang/setup-dart@v1

- name: Create archive (macOS, Linux)
if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest'
- name: Create archive
run: |
bash ./scripts/build_release.sh
shell: bash

- name: Create archive (Windows)
if: matrix.os == 'windows-latest'
run: |
./scripts/build_release.ps1
shell: pwsh
cd scripts
dart pub get
cd ..
dart run scripts/bin/build_release.dart
env:
GITHUB_MATRIX_OS: ${{ matrix.os }}
RUNNER_ARCH: ${{ runner.arch }}

- name: Upload artifact
uses: actions/upload-artifact@v4
Expand Down
12 changes: 12 additions & 0 deletions scripts/bin/build_release.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2025 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:release_scripts/src/build_release_command.dart';
import 'package:release_scripts/src/utils.dart';

Future<void> main(List<String> args) async {
await runScript((context) async {
await BuildReleaseCommand(context).run();
});
}
15 changes: 15 additions & 0 deletions scripts/bin/bump_version.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2025 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:release_scripts/src/bump_version_command.dart';
import 'package:release_scripts/src/utils.dart';

Future<void> main(List<String> args) async {
await runScript((context) async {
if (args.isEmpty) {
throw ExitException('Usage: bump_version <new_version>');
}
await BumpVersionCommand(context, args[0]).run();
});
}
12 changes: 12 additions & 0 deletions scripts/bin/update_local.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2025 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:release_scripts/src/update_local_command.dart';
import 'package:release_scripts/src/utils.dart';

Future<void> main(List<String> args) async {
await runScript((context) async {
await UpdateLocalCommand(context).run();
});
}
7 changes: 0 additions & 7 deletions scripts/build_release.ps1

This file was deleted.

43 changes: 0 additions & 43 deletions scripts/build_release.sh

This file was deleted.

36 changes: 0 additions & 36 deletions scripts/bump_version.sh

This file was deleted.

97 changes: 97 additions & 0 deletions scripts/lib/src/build_release_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'package:file/file.dart';

import 'utils.dart';

/// A command that builds a release archive for the Flutter extension.
///
/// This command:
/// 1. Detects the current OS and architecture (or uses GITHUB_MATRIX_OS if set).
/// 2. Identifies the repository root.
/// 3. Creates a `.tar.gz` (Linux/macOS) or `.zip` (Windows) archive of the
/// extension source.
/// 4. Sets the `ARCHIVE_NAME` environment variable for GitHub Actions.
class BuildReleaseCommand {
/// The script execution context.
final ScriptContext context;

/// Creates a [BuildReleaseCommand] with the given [context].
BuildReleaseCommand(this.context);

/// Executes the build release process.
Future<void> run() async {
final fs = context.fs;
final platform = context.platform;

final platformInfo = await getPlatformInfo(context);
final os = platformInfo.os;
final arch = platformInfo.arch;
final ext = platformInfo.ext;

final archiveName = '$os.$arch.flutter.$ext';

// Find the repository root by looking for 'gemini-extension.json'.
final repoRoot = findRepoRoot(context);
final repoPath = repoRoot.path;
print('Repository root: $repoPath');

String githubRef = platform.environment['GITHUB_REF'] ?? 'refs/tags/HEAD';
String tagName = githubRef.replaceFirst('refs/tags/', '');
if (tagName.isEmpty) tagName = 'HEAD';

if (fs.isFileSync(fs.path.join(repoPath, archiveName))) {
fs.file(fs.path.join(repoPath, archiveName)).deleteSync();
}

print('Creating archive $archiveName from $tagName...');

final filesToArchive = [
'gemini-extension.json',
'commands/',
'LICENSE',
'README.md',
'flutter.md',
];

if (os == 'windows') {
await runProcess(context,
[
'git',
'archive',
'--format=zip',
'-o',
archiveName,
tagName,
...filesToArchive
],
workingDirectory: repoPath);
} else {
final tarName = '$os.$arch.flutter.tar';
await runProcess(context,
[
'git',
'archive',
'--format=tar',
'-o',
tarName,
tagName,
...filesToArchive
],
workingDirectory: repoPath);

await runProcess(context, [
'gzip',
'--force',
tarName,
], workingDirectory: repoPath);
}

// Set output env var for GitHub Actions.
final githubEnv = platform.environment['GITHUB_ENV'];
if (githubEnv != null && fs.isFileSync(githubEnv)) {
fs.file(githubEnv).writeAsStringSync('ARCHIVE_NAME=$archiveName\n',
mode: FileMode.append);
} else {
print('Archive written to $archiveName');
}
}
}
116 changes: 116 additions & 0 deletions scripts/lib/src/bump_version_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2025 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'utils.dart';

/// A command that updates the extension version.
///
/// This command:
/// 1. Updates the "version" field in `gemini-extension.json`.
/// 2. Prepends a new version section to `CHANGELOG.md`.
class BumpVersionCommand {
/// The script execution context.
final ScriptContext context;

/// The new version string (e.g., "1.0.1").
final String newVersion;

/// Creates a [BumpVersionCommand] with the given [context] and [newVersion].
BumpVersionCommand(this.context, this.newVersion);

/// Executes the version bump process.
Future<void> run() async {
if (newVersion.isEmpty) {
throw ExitException('Usage: bump_version <new_version>');
}

final repoRoot = findRepoRoot(context);
final repoPath = repoRoot.path;

_updateExtensionJson(repoPath);
_updateChangelog(repoPath);

print('Version bumped to $newVersion');
}

void _updateExtensionJson(String repoPath) {
final fs = context.fs;
final jsonFile = fs.file(fs.path.join(repoPath, 'gemini-extension.json'));

if (!jsonFile.existsSync()) {
throw ExitException(
'gemini-extension.json not found at ${jsonFile.path}',
);
}

final String content = jsonFile.readAsStringSync();
final Map<String, dynamic> json;
try {
json = jsonDecode(content) as Map<String, dynamic>;
} on FormatException catch (e) {
throw ExitException(
'Failed to parse gemini-extension.json: ${e.message}',
);
}

if (!json.containsKey('version')) {
throw ExitException(
'Could not find "version" field in gemini-extension.json',
);
}

json['version'] = newVersion;

// Use an encoder with indentation for readability and add a trailing newline.
const encoder = JsonEncoder.withIndent(' ');
jsonFile.writeAsStringSync('${encoder.convert(json)}\n');
}

void _updateChangelog(String repoPath) {
final fs = context.fs;
final changelogFile = fs.file(fs.path.join(repoPath, 'CHANGELOG.md'));

if (!changelogFile.existsSync()) {
print('Warning: CHANGELOG.md not found.');
return;
}

final changelogContent = changelogFile.readAsStringSync();
if (changelogContent.contains('## $newVersion')) {
// Version already exists, no need to add again.
return;
}

print('Adding version $newVersion to CHANGELOG.md');

final newSection =
'## $newVersion\n\n- TODO: Describe the changes in this version.\n\n';

int insertIndex = 0;

// Strategy:
// 1. Insert before the first `## Version` header.
// 2. If no version headers, insert after the main title `# Title`.
// 3. Otherwise, prepend to file.

final firstHeaderMatch = RegExp(
r'^##\s',
multiLine: true,
).firstMatch(changelogContent);

if (firstHeaderMatch != null) {
insertIndex = firstHeaderMatch.start;
changelogFile.writeAsStringSync(
changelogContent.substring(0, insertIndex) +
newSection +
changelogContent.substring(insertIndex),
);
} else {
// No existing version headers, so prepend to the file.
changelogFile.writeAsStringSync(newSection + changelogContent);
}
}
}
Loading