diff --git a/Cargo.lock b/Cargo.lock
index ba1455824..e39f6fd01 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -664,7 +664,7 @@ dependencies = [
[[package]]
name = "capsem"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"blake3",
@@ -699,7 +699,7 @@ dependencies = [
[[package]]
name = "capsem-admin"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"blake3",
@@ -721,7 +721,7 @@ dependencies = [
[[package]]
name = "capsem-agent"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"capsem-foundation",
"capsem-proto",
@@ -735,7 +735,7 @@ dependencies = [
[[package]]
name = "capsem-api"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"base64 0.22.1",
"serde",
@@ -745,7 +745,7 @@ dependencies = [
[[package]]
name = "capsem-app"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"capsem-proto",
"filetime",
@@ -765,7 +765,7 @@ dependencies = [
[[package]]
name = "capsem-archive"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"blake3",
"capsem-foundation",
@@ -778,7 +778,7 @@ dependencies = [
[[package]]
name = "capsem-assets"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"axum",
@@ -798,7 +798,7 @@ dependencies = [
[[package]]
name = "capsem-bench"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"capsem-foundation",
@@ -815,7 +815,7 @@ dependencies = [
[[package]]
name = "capsem-config"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"capsem-proto",
"regex",
@@ -828,7 +828,7 @@ dependencies = [
[[package]]
name = "capsem-core"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"async-compression",
@@ -893,7 +893,7 @@ dependencies = [
[[package]]
name = "capsem-credentials"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"capsem-foundation",
"capsem-proto",
@@ -905,7 +905,7 @@ dependencies = [
[[package]]
name = "capsem-foundation"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"blake3",
"capsem-proto",
@@ -923,7 +923,7 @@ dependencies = [
[[package]]
name = "capsem-gateway"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"axum",
@@ -954,7 +954,7 @@ dependencies = [
[[package]]
name = "capsem-guard"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"capsem-foundation",
"tempfile",
@@ -964,7 +964,7 @@ dependencies = [
[[package]]
name = "capsem-logger"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"blake3",
@@ -990,7 +990,7 @@ dependencies = [
[[package]]
name = "capsem-mcp-aggregator"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"axum",
@@ -1014,7 +1014,7 @@ dependencies = [
[[package]]
name = "capsem-mcp-builtin"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"capsem-core",
@@ -1033,7 +1033,7 @@ dependencies = [
[[package]]
name = "capsem-mock-server"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -1060,7 +1060,7 @@ dependencies = [
[[package]]
name = "capsem-network"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"capsem-proto",
"tokio",
@@ -1068,7 +1068,7 @@ dependencies = [
[[package]]
name = "capsem-process"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"axum",
@@ -1091,7 +1091,7 @@ dependencies = [
[[package]]
name = "capsem-proto"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"blake3",
@@ -1108,7 +1108,7 @@ dependencies = [
[[package]]
name = "capsem-router"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"bytes",
"capsem-foundation",
@@ -1126,7 +1126,7 @@ dependencies = [
[[package]]
name = "capsem-sdk"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"axum",
"capsem-api",
@@ -1139,7 +1139,7 @@ dependencies = [
[[package]]
name = "capsem-service"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"axum",
@@ -1179,7 +1179,7 @@ dependencies = [
[[package]]
name = "capsem-telemetry"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"async-trait",
"bytes",
@@ -1195,7 +1195,7 @@ dependencies = [
[[package]]
name = "capsem-tray"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"capsem-foundation",
@@ -1215,7 +1215,7 @@ dependencies = [
[[package]]
name = "capsem-tui"
-version = "0.6.3"
+version = "0.6.4"
dependencies = [
"anyhow",
"capsem-sdk",
diff --git a/Cargo.toml b/Cargo.toml
index 88d22faaa..e8e2e4dcd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,7 +31,7 @@ members = [
]
[workspace.package]
-version = "0.6.3"
+version = "0.6.4"
edition = "2021"
rust-version = "1.91"
license = "Apache-2.0"
diff --git a/build_system/pyproject.toml b/build_system/pyproject.toml
index 906f038ed..6dc3f4611 100644
--- a/build_system/pyproject.toml
+++ b/build_system/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "capsem-builder"
-version = "0.6.3"
+version = "0.6.4"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
diff --git a/build_system/tests/test_project_boundary.py b/build_system/tests/test_project_boundary.py
index f89aefc36..b931ff0bc 100644
--- a/build_system/tests/test_project_boundary.py
+++ b/build_system/tests/test_project_boundary.py
@@ -17,4 +17,4 @@ def test_editable_install_maps_the_direct_source_directory() -> None:
def test_installed_distribution_uses_the_reserved_builder_name() -> None:
- assert version("capsem-builder") == "0.6.3"
+ assert version("capsem-builder") == "0.6.4"
diff --git a/build_system/uv.lock b/build_system/uv.lock
index ff631ecd8..ba88b28c2 100644
--- a/build_system/uv.lock
+++ b/build_system/uv.lock
@@ -238,7 +238,7 @@ wheels = [
[[package]]
name = "capsem-builder"
-version = "0.6.3"
+version = "0.6.4"
source = { editable = "." }
dependencies = [
{ name = "blake3" },
diff --git a/config/gate.toml b/config/gate.toml
index f26ccb3a6..a80cdcd77 100644
--- a/config/gate.toml
+++ b/config/gate.toml
@@ -1099,7 +1099,7 @@ must_stay_below_lines = 3000
[boundary.rust.oversized_line_counts]
"crates/capsem-admin/src/assets_channel_render.rs" = 1719
"crates/capsem-admin/src/assets_channel_validation.rs" = 1412
-"crates/capsem-admin/src/main.rs" = 2182
+"crates/capsem-admin/src/main.rs" = 2144
"crates/capsem-admin/src/profile_images.rs" = 2188
"crates/capsem-admin/src/release_graph.rs" = 1089
"crates/capsem-admin/src/release_graph/tests.rs" = 1323
diff --git a/config/profiles/code/profile.toml b/config/profiles/code/profile.toml
index 092b1bef4..f95417a67 100644
--- a/config/profiles/code/profile.toml
+++ b/config/profiles/code/profile.toml
@@ -8,7 +8,7 @@ id = "code"
name = "Code"
description = "Optimized for coding and long-running agents."
icon_svg = ""
-revision = "0.6.2"
+revision = "0.6.3"
refresh_policy = "24h"
# The runtimes this profile is the default for; the gateway publishes both in
# GET /status so no client compiles a profile name into itself. Today one
diff --git a/crates/capsem-admin/src/main.rs b/crates/capsem-admin/src/main.rs
index 5514c1545..a0b17734f 100644
--- a/crates/capsem-admin/src/main.rs
+++ b/crates/capsem-admin/src/main.rs
@@ -4,7 +4,6 @@ use std::{
io::Read,
path::{Path, PathBuf},
process::{Command, Stdio},
- thread,
};
use anyhow::{anyhow, Context, Result};
@@ -26,6 +25,7 @@ mod channel_bootstrap;
mod manifest_generation;
mod package_inspection;
mod profile_images;
+mod release_github;
#[allow(dead_code)]
mod release_graph;
mod source_commit;
@@ -37,6 +37,7 @@ use manifest_generation::*;
use profile_images::*;
use package_inspection::binary_files_from_artifacts;
+use release_github::{ensure_publication_identity_is_free, GhProfileWorkflowRunner, ProfileWorkflowRunner};
use source_commit::SourceCommit;
#[derive(Debug, Parser)]
@@ -629,47 +630,6 @@ struct ProfileWorkflowRun {
conclusion: String,
}
-trait ProfileWorkflowRunner {
- fn run(&mut self, args: &[String]) -> Result<()>;
- fn output(&mut self, args: &[String]) -> Result;
- fn wait_before_poll(&mut self);
-}
-
-struct GhProfileWorkflowRunner;
-
-impl ProfileWorkflowRunner for GhProfileWorkflowRunner {
- fn run(&mut self, args: &[String]) -> Result<()> {
- let status = Command::new("gh")
- .args(args)
- .status()
- .with_context(|| format!("run gh {}", args.join(" ")))?;
- if !status.success() {
- return Err(anyhow!("gh {} failed with {}", args.join(" "), status));
- }
- Ok(())
- }
-
- fn output(&mut self, args: &[String]) -> Result {
- let output = Command::new("gh")
- .args(args)
- .output()
- .with_context(|| format!("run gh {}", args.join(" ")))?;
- if !output.status.success() {
- return Err(anyhow!(
- "gh {} failed with {}: {}",
- args.join(" "),
- output.status,
- String::from_utf8_lossy(&output.stderr).trim()
- ));
- }
- String::from_utf8(output.stdout).context("gh workflow listing was not UTF-8")
- }
-
- fn wait_before_poll(&mut self) {
- thread::sleep(std::time::Duration::from_secs(2));
- }
-}
-
fn dispatch_profile_workflow(
runner: &mut R,
workflow: &str,
@@ -1376,6 +1336,8 @@ fn release_command(args: ReleaseArgs) -> Result<()> {
let run_id = if args.dry_run {
None
} else {
+ let identity = &selection.publication_identity;
+ ensure_publication_identity_is_free(&mut GhProfileWorkflowRunner, identity, &args.source_commit)?;
let dispatch_id = format!(
"capsem-admin-{}-{}",
std::process::id(),
diff --git a/crates/capsem-admin/src/release_github.rs b/crates/capsem-admin/src/release_github.rs
new file mode 100644
index 000000000..a98f40031
--- /dev/null
+++ b/crates/capsem-admin/src/release_github.rs
@@ -0,0 +1,117 @@
+//! The GitHub side of `capsem-admin release`: the `gh` runner that dispatches
+//! and watches profile workflows, and the check that a publication identity is
+//! still free before any of that starts.
+
+use std::process::Command;
+use std::thread;
+
+use anyhow::{anyhow, bail, Context, Result};
+
+use crate::source_commit::SourceCommit;
+
+pub(crate) trait ProfileWorkflowRunner {
+ fn run(&mut self, args: &[String]) -> Result<()>;
+ fn output(&mut self, args: &[String]) -> Result;
+ fn wait_before_poll(&mut self);
+}
+
+/// Which commit an existing immutable release was published from.
+pub(crate) trait ReleaseLookup {
+ /// The source commit of the release tagged `tag`, or `None` if none exists.
+ fn release_commit(&mut self, tag: &str) -> Result