Skip to content
Closed
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
91 changes: 91 additions & 0 deletions .github/workflows/release_codex_plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: release_codex_plugin

# Builds and validates the Codex plugin zip for the OpenAI plugin portal.
# - On a codex-plugin-v* tag: builds the zip from the tagged commit, validates
# it, and uploads it as a workflow artifact. tool/codex_release/bin/release.dart
# pushes the tag and downloads the artifact.
# - On pull requests: builds and validates the zip without uploading it, so
# problems are caught before they reach main.

on:
push:
tags: ['codex-plugin-v*']
pull_request:
paths:
- '.codex-plugin/**'
- 'assets/**'
- 'skills/**'
- 'tool/codex_release/**'
- 'pubspec.yaml'
- '.github/workflows/release_codex_plugin.yaml'

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Full history and tags, to find the previous release for the notes.
fetch-depth: 0
persist-credentials: false

- uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d # v1.8.1
with:
sdk: stable

- run: dart pub get

- name: Analyze and test the release tool
working-directory: tool/codex_release
run: |
dart analyze --fatal-infos
dart format --output=none --set-exit-if-changed .
dart test

- name: Build zip
id: build
env:
TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: dart run tool/codex_release/bin/build.dart ${TAG:+--tag "$TAG"}

- name: Validate zip
env:
ZIP_PATH: ${{ steps.build.outputs.zip_path }}
VERSION: ${{ steps.build.outputs.version }}
run: dart run tool/codex_release/bin/validate.dart "$ZIP_PATH" --version "$VERSION"

- name: Upload zip
if: github.ref_type == 'tag'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ steps.build.outputs.zip_name }}
path: ${{ steps.build.outputs.zip_path }}
retention-days: 30
if-no-files-found: error
# The file is already a zip.
compression-level: 0

report_failure:
needs: build
if: failure() && github.ref_type == 'tag'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Open an issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh label create codex-release --repo "$REPO" --color D93F0B \
--description "Codex plugin release" 2>/dev/null || true
gh issue create --repo "$REPO" --label codex-release \
--title "Codex plugin release failed: $TAG" \
--body "The release workflow for \`$TAG\` failed: $RUN_URL

Fix the problem on main, bump the version in \`.codex-plugin/plugin.json\`, and release again. Tags are never moved or reused."
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ __pycache__/
state.json
CLAUDE.md
CLAUDE.local.md

# Codex plugin release zips
dist/
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ environment:
sdk: ^3.10.8

workspace:
- tool/codex_release
- tool/generator
60 changes: 60 additions & 0 deletions tool/codex_release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# codex_release

Builds, validates, and releases the Dart and Flutter Codex plugin zip that's
uploaded to the [OpenAI plugin submission portal](https://platform.openai.com/plugins).

The zip is a skills-only build of the plugin: the release copy of
`.codex-plugin/plugin.json` has every MCP reference removed and release notes
added. The repo manifest is unchanged.

## Releasing

From an up-to-date `main` with no uncommitted changes:

```sh
dart run tool/codex_release/bin/release.dart
```

The script:

1. checks that you're on `main`, it matches the remote, and the tree is clean;
2. reads `version` from `.codex-plugin/plugin.json` and stops if
`codex-plugin-v<version>` already exists;
3. creates and pushes the tag;
4. waits for the `release_codex_plugin` workflow to build and validate the zip;
5. downloads the zip to `dist/`.

Then upload the zip in the portal, resolve any findings, submit it for review,
and publish it after approval.

If the version was already released, bump `version` in
`.codex-plugin/plugin.json` in a PR first. Tags are never moved or reused.

The script needs the [GitHub CLI](https://cli.github.com) (`gh auth login`),
and push access to `flutter/agent-plugins`. It finds the git remote for
`flutter/agent-plugins` automatically; pass `--remote` to choose one.

To check everything without tagging, build and validate the zip locally:

```sh
dart run tool/codex_release/bin/release.dart --dry-run
```

## Building and validating by hand

```sh
dart run tool/codex_release/bin/build.dart # writes dist/dart-flutter-codex-<version>.zip
dart run tool/codex_release/bin/validate.dart dist/dart-flutter-codex-<version>.zip
```

The validator checks the zip against OpenAI's documented
[package rules](https://developers.openai.com/plugins/deploy/submission-errors)
and reports OpenAI's error code for each problem. Checks specific to this repo
use codes starting with `release_`. The portal is still the final authority:
its skill safety scans and review can't be reproduced locally.

## Release notes

The build sets `extensions.com.openai.publication.release_notes` to the skills
added, updated, and removed since the previous `codex-plugin-v*` tag. Pass
`--release-notes` to `build.dart` to override them.
14 changes: 14 additions & 0 deletions tool/codex_release/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
include: package:lints/recommended.yaml

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true

linter:
rules:
- prefer_final_locals
- prefer_single_quotes
- require_trailing_commas
- directives_ordering
93 changes: 93 additions & 0 deletions tool/codex_release/bin/build.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'dart:io';

import 'package:args/args.dart';
import 'package:codex_release/codex_release.dart';
import 'package:path/path.dart' as p;

/// Builds the Codex plugin release zip from the current checkout.
///
/// Usage: dart run tool/codex_release/bin/build.dart [--tag codex-plugin-v1.0.8]
Future<void> main(List<String> args) async {
final parser = ArgParser()
..addOption(
'tag',
help:
'The release tag. The build fails if it does not match the '
'plugin.json version.',
)
..addOption(
'output-dir',
defaultsTo: 'dist',
help: 'Where to write the zip.',
)
..addOption('release-notes', help: 'Overrides the generated release notes.')
..addFlag('help', abbr: 'h', negatable: false);
final ArgResults options;
try {
options = parser.parse(args);
} on FormatException catch (e) {
stderr.writeln('${e.message}\n\n${parser.usage}');
exit(64);
}
if (options.flag('help')) {
stdout.writeln('Builds the Codex plugin release zip.\n\n${parser.usage}');
return;
}

try {
final git = Git(await findRepoRoot());
final tag = options.option('tag');
final build = await buildZip(
git,
tag: tag == null || tag.isEmpty ? null : tag,
releaseNotes: options.option('release-notes'),
);
final outputDir = Directory(p.join(git.root, options.option('output-dir')!))
..createSync(recursive: true);
final zipPath = p.join(outputDir.path, build.fileName);
File(zipPath).writeAsBytesSync(build.bytes);
stdout
..writeln('Built $zipPath (${build.bytes.length} bytes)')
..writeln('Release notes:')
..writeln(releaseNotesOf(build.manifest));
writeGitHubOutput({
'zip_path': zipPath,
'zip_name': build.fileName,
'version': build.version,
});
} on BuildException catch (e) {
stderr.writeln('Error: $e');
exit(1);
} on CommandException catch (e) {
stderr.writeln('Error: $e');
exit(1);
}
}

/// The repository root containing the current directory.
Future<String> findRepoRoot() async {
final result = await Process.run('git', ['rev-parse', '--show-toplevel']);
if (result.exitCode != 0) {
stderr.writeln('Error: run this from inside the agent-plugins repository.');
exit(1);
}
return (result.stdout as String).trim();
}

/// The release notes in a release manifest.
String releaseNotesOf(Map<String, Object?> manifest) {
final extensions = manifest['extensions'] as Map<String, Object?>;
final openai = extensions['com.openai'] as Map<String, Object?>;
final publication = openai['publication'] as Map<String, Object?>;
return publication['release_notes'] as String;
}

/// Writes step outputs when running in GitHub Actions.
void writeGitHubOutput(Map<String, String> outputs) {
final path = Platform.environment['GITHUB_OUTPUT'];
if (path == null) return;
File(path).writeAsStringSync(
outputs.entries.map((e) => '${e.key}=${e.value}\n').join(),
mode: FileMode.append,
);
}
81 changes: 81 additions & 0 deletions tool/codex_release/bin/release.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'dart:io';

import 'package:args/args.dart';
import 'package:codex_release/codex_release.dart';

/// Releases the Codex plugin: tags main, waits for the release workflow, and
/// downloads the zip to upload to the OpenAI plugin portal.
///
/// Usage: dart run tool/codex_release/bin/release.dart [--dry-run]
Future<void> main(List<String> args) async {
final parser = ArgParser()
..addFlag(
'dry-run',
negatable: false,
help:
'Check the checkout and build and validate the zip locally, '
'without creating a tag.',
)
..addOption(
'repo',
defaultsTo: 'flutter/agent-plugins',
help: 'The GitHub repository to release from.',
)
..addOption(
'remote',
help: 'The git remote for --repo. Detected from remote URLs by default.',
)
..addOption(
'branch',
defaultsTo: 'main',
help:
'The branch releases are cut from. Change it only for test runs '
'on a fork.',
)
..addOption(
'output-dir',
defaultsTo: 'dist',
help: 'Where to save the zip.',
)
..addFlag('help', abbr: 'h', negatable: false);
final ArgResults options;
try {
options = parser.parse(args);
} on FormatException catch (e) {
stderr.writeln('${e.message}\n\n${parser.usage}');
exit(64);
}
if (options.flag('help')) {
stdout.writeln('Releases the Codex plugin.\n\n${parser.usage}');
return;
}

final root = await Process.run('git', ['rev-parse', '--show-toplevel']);
if (root.exitCode != 0) {
stderr.writeln('Error: run this from inside the agent-plugins repository.');
exit(1);
}

final release = Release(
Git((root.stdout as String).trim()),
ReleaseOptions(
repo: options.option('repo')!,
remote: options.option('remote'),
branch: options.option('branch')!,
outputDirectory: options.option('output-dir')!,
dryRun: options.flag('dry-run'),
),
);
try {
await release.run();
} on ReleaseException catch (e) {
stderr.writeln('Error: $e');
exit(1);
} on BuildException catch (e) {
stderr.writeln('Error: $e');
exit(1);
} on CommandException catch (e) {
stderr.writeln('Error: $e');
exit(1);
}
}
Loading
Loading