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
99 changes: 99 additions & 0 deletions .github/workflows/release-prepare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Prepare release
run-name: Prepare Orchestral v${{ inputs.version }}

on:
workflow_dispatch:
inputs:
version:
description: Stable MAJOR.MINOR.PATCH version to prepare from Unreleased notes
required: true
type: string

permissions:
contents: read

defaults:
run:
shell: bash

concurrency:
group: prepare-release
cancel-in-progress: false

jobs:
version-pr:
if: github.repository == 'sizzlecar/orchestral'
runs-on: ubuntu-22.04
timeout-minutes: 30
env:
RELEASE_VERSION: ${{ inputs.version }}
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
steps:
- name: Require main and a release PR credential
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
run: |
if [ "$GITHUB_REF" != refs/heads/main ]; then
echo 'Prepare release must be dispatched on main.' >&2
exit 1
fi
if [ -z "$RELEASE_TOKEN" ]; then
echo 'RELEASE_GITHUB_TOKEN needs contents and pull request write access to this repository.' >&2
exit 1
fi
# Use a PAT or App token for both push and PR creation so ordinary required
# CI starts automatically. Do not fall back to the workflow GITHUB_TOKEN.
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
fetch-depth: 0
- name: Install repository Rust toolchain
run: rustup show active-toolchain
- uses: Swatinem/rust-cache@v2
with:
key: release-tools
- name: Prepare coordinated versions and release notes
run: |
cargo run --locked -p orchestral-release -- prepare-version --version "$RELEASE_VERSION"
git diff --check
if git show-ref --verify --quiet "refs/tags/v$RELEASE_VERSION"; then
echo 'The requested release tag already exists; no release branch will be changed.' >&2
exit 1
fi
git config user.name orchestral-release
git config user.email orchestral-release@users.noreply.github.com
git switch -c "release/v$RELEASE_VERSION"
git add -- Cargo.toml Cargo.lock CHANGELOG.md
# A preparer change outside its declared files must not be omitted.
git diff --exit-code
if git diff --cached --quiet; then
echo 'The release preparer produced no version changes.' >&2
exit 1
fi
# A retry can follow a successful push but a failed PR API request.
# Reuse identical content; never overwrite a different release proposal.
if git show-ref --verify --quiet "refs/remotes/origin/release/v$RELEASE_VERSION"; then
if [ "$(git write-tree)" != "$(git rev-parse "origin/release/v$RELEASE_VERSION^{tree}")" ]; then
echo 'The existing release branch differs from this proposal; review it before retrying.' >&2
exit 1
fi
else
git commit -m "chore: prepare v$RELEASE_VERSION"
git push origin "HEAD:refs/heads/release/v$RELEASE_VERSION"
fi
- name: Open or reuse the release PR
env:
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
body="$RUNNER_TEMP/release-pr.md"
printf 'Prepare Orchestral v%s with coordinated workspace/internal dependency versions, Cargo.lock, and the reviewed Unreleased notes.\n\nNormal PR CI must pass and this change must be reviewed before merge. After merge, push the matching annotated tag to run the existing Release workflow; it creates a draft for review before public distribution.\n' "$RELEASE_VERSION" > "$body"
pr=$(gh pr list --head "release/v$RELEASE_VERSION" --base main --state open --json url --jq '.[0].url // empty')
if [ -z "$pr" ]; then
pr=$(gh pr create --head "release/v$RELEASE_VERSION" --base main \
--title "chore: prepare v$RELEASE_VERSION" --body-file "$body")
fi
test -n "$pr"
printf 'Release PR: %s\n\nReview and merge after its required CI passes. Tagging and public distribution remain separate.\n' "$pr" >> "$GITHUB_STEP_SUMMARY"
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# Changelog

## [Unreleased]

### Model selection and reasoning controls

- `/model` discovers model IDs from the configured service API while preserving its
endpoint, authentication, and protocol settings. Explicit profile selection remains
available through `/model profiles`; model and reasoning switches retain session history.
- `/reasoning`, `--reasoning`, and model profiles support provider-defined effort names
without a client release, preserving their spelling through discovery and requests.
Missing capability metadata no longer produces an invented menu of supported efforts;
explicit values can still be submitted for the service to validate.
- Default omits reasoning controls, `none` requests an explicit effort, and `on`/`off`
control binary thinking. Use `effort:on`, `effort:off`, or `effort:default` for literal
provider efforts that share those names.
- Native Codex choices follow its model catalog and configured defaults. Unknown effort
names remain intact, and missing catalog metadata leaves optional controls for native
validation rather than selecting fixed fallback models or effort levels.
- Web forms can clear optional model and reasoning selections back to the provider default
without weakening required sandbox or approval settings.

### Runtime and Web reliability

- Live session updates and reconciliation continue after the dialog that started them
closes. A recovered session clears superseded error messages while retaining genuinely
new failures and the original history.
- Process sessions wait for both stdout and stderr readers to finish before reporting
completion. Poll deadlines preserve unread output, and session cleanup stops its readers.

### Distribution and compatibility

- Published-release checks exercise public installers and startup of the installed CLI,
including onboarding against a local fixture service without paid model calls.
- The SDK adds optional `AgentConfig.reasoning` and extensible reasoning preference types.
Direct `AgentConfig` struct literals need the new field or a `Default` update; existing
serialized configuration remains valid. Explicit reasoning controls participate in request
and unfinished-run recovery identity; an omitted control preserves the default identity.
- OpenAI discovery adds `DiscoveredModel.reasoning`. Downstream code constructing this public
struct directly must supply `reasoning: None` when capabilities are unknown, or provide the
declared capabilities. Existing model IDs and context-capacity metadata retain their meanings.
- Agent Protocol remains v1. Native Codex validates model/effort combinations; an HTTP effort
named `ultra` does not imply Codex's native preset or multi-agent behavior.

## [0.3.1]

The v0.3.0 tag remains an unpublished candidate; v0.3.1 includes the changes below.
Expand Down
48 changes: 31 additions & 17 deletions tools/orchestral-release/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod formula;
mod registry;
mod version;

use anyhow::{bail, ensure, Context, Result};
use clap::{Parser, Subcommand};
Expand All @@ -21,6 +22,11 @@ struct Args {

#[derive(Subcommand)]
enum Action {
/// Prepare a new stable version and promote nonempty Unreleased notes.
PrepareVersion {
#[arg(long)]
version: String,
},
/// Validate Cargo publication metadata and extract reviewed release notes.
Prepare {
#[arg(long)]
Expand Down Expand Up @@ -67,6 +73,10 @@ fn main() -> Result<()> {
let root = args.workspace.canonicalize().context("workspace path")?;
let inventory = inventory(&root)?;
match args.command {
Action::PrepareVersion { version } => {
version::prepare(&root, &inventory.version, &version)?;
println!("{version}");
}
Action::Prepare { output, tag } => {
if let Some(tag) = tag {
ensure!(
Expand Down Expand Up @@ -126,23 +136,7 @@ fn inventory(root: &Path) -> Result<Inventory> {
.context("workspace version")?
.to_owned();
validate_version(&version)?;
let metadata = Command::new("cargo")
.args([
"metadata",
"--locked",
"--offline",
"--no-deps",
"--format-version",
"1",
])
.current_dir(root)
.output()?;
ensure!(
metadata.status.success(),
"Cargo metadata failed: {}",
String::from_utf8_lossy(&metadata.stderr)
);
let metadata: serde_json::Value = serde_json::from_slice(&metadata.stdout)?;
let metadata = cargo_metadata(root)?;
let members = metadata["workspace_members"]
.as_array()
.context("workspace members")?;
Expand Down Expand Up @@ -190,6 +184,26 @@ fn inventory(root: &Path) -> Result<Inventory> {
})
}

fn cargo_metadata(root: &Path) -> Result<serde_json::Value> {
let metadata = Command::new("cargo")
.args([
"metadata",
"--locked",
"--offline",
"--no-deps",
"--format-version",
"1",
])
.current_dir(root)
.output()?;
ensure!(
metadata.status.success(),
"Cargo metadata failed: {}",
String::from_utf8_lossy(&metadata.stderr)
);
Ok(serde_json::from_slice(&metadata.stdout)?)
}

fn validate_version(version: &str) -> Result<()> {
let parts: Vec<_> = version.split('.').collect();
ensure!(
Expand Down
Loading
Loading