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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ CF_DATAPLANE_LOCAL_IMAGE=contextforge-org/contextforge-data-plane:local
# CF_DATAPLANE_IMAGE=ghcr.io/contextforge-org/contextforge-data-plane:<tag>
CF_DATAPLANE_VERSION=latest

# Registry refresh policy for prebuilt images. Default: always.
# Set to never only after loading the exact image into the local Docker daemon.
# CF_CONTROLPLANE_PULL_POLICY=never
# CF_DATAPLANE_PULL_POLICY=never

# cf-dataplane image platform. Default: auto.
# auto resolves to linux/amd64 in published image mode, or the Docker server platform
# when CF_DATAPLANE_REF is set for a local source build.
Expand Down
31 changes: 27 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ jobs:
if-no-files-found: error
retention-days: 1

- name: Upload release orchestration CLI
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-orchestration-cli
path: dist/package/cf-integration
if-no-files-found: error
retention-days: 1

publish:
name: Publish crate and tag
needs: [quality, build-binaries]
Expand All @@ -167,6 +176,21 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.97.0

- name: Download release orchestration CLI
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-orchestration-cli
path: .integration/release-cli

- name: Make release orchestration CLI executable
run: chmod +x .integration/release-cli/cf-integration

- name: Replace stale unpublished release state
id: release-state
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: .integration/release-cli/cf-integration ci prepare-release

- name: Publish root package
id: release-plz
uses: release-plz/action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 # v0.5.131
Expand All @@ -178,12 +202,10 @@ jobs:

- name: Select CLI release
id: root-release
shell: bash
env:
CANDIDATE_TAG: ${{ steps.release-state.outputs.tag }}
RELEASES: ${{ steps.release-plz.outputs.releases }}
run: |
tag=$(jq -r 'first(.[] | select(.package_name == "cf-integration") | .tag) // ""' <<<"$RELEASES")
echo "tag=$tag" >> "$GITHUB_OUTPUT"
run: .integration/release-cli/cf-integration ci select-release

publish-binaries:
name: Publish release binaries
Expand All @@ -209,6 +231,7 @@ jobs:

- name: Upload assets and publish release
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.publish.outputs.tag }}
run: |
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cf-integration"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ upstream reserves `:latest` for releases. Stack startup pulls changes;
incompatible main images make the workflow fail instead of selecting an older
pair.

CI jobs that package the code under test as a local image can opt out of
registry access with `CF_CONTROLPLANE_PULL_POLICY=never` or
`CF_DATAPLANE_PULL_POLICY=never`. This is never the default: the selected image
must already be loaded in Docker, and startup fails if it is absent.

Compose requires `JWT_SECRET_KEY` and `AUTH_ENCRYPTION_SECRET`. If either is
unset, a runtime-backed action generates stable values under
`CF_INTEGRATION_DIR`. Canonical configuration is exported internally as the
Expand Down
3 changes: 3 additions & 0 deletions docker/docker-compose.cf-controlplane-build-labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

services:
gateway:
pull_policy: ${CF_CONTROLPLANE_PULL_POLICY:-always}
labels:
name: cf-controlplane
environment:
Expand All @@ -19,6 +20,7 @@ services:
org.opencontainers.image.ref.name: ${CF_CONTROLPLANE_CHECKOUT_REF:-unknown}

migration:
pull_policy: ${CF_CONTROLPLANE_PULL_POLICY:-always}
labels:
name: cf-migration
build:
Expand All @@ -27,6 +29,7 @@ services:
org.opencontainers.image.ref.name: ${CF_CONTROLPLANE_CHECKOUT_REF:-unknown}

register_fast_time:
pull_policy: ${CF_CONTROLPLANE_PULL_POLICY:-always}
labels:
name: cf-register-fast-time

Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.cf-dataplane.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ services:

dataplane:
image: ${CF_DATAPLANE_IMAGE:?Set CF_DATAPLANE_IMAGE to the cf-dataplane image tag}
pull_policy: ${CF_DATAPLANE_PULL_POLICY:-always}
platform: ${CF_DATAPLANE_PLATFORM:?Set CF_DATAPLANE_PLATFORM to the cf-dataplane image platform}
labels:
name: cf-dataplane
Expand Down
67 changes: 65 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::collections::BTreeSet;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use std::path::{Component, PathBuf};
use std::str::FromStr;

use crate::conformance::DEFAULT_MCP_SPEC_VERSION;
Expand All @@ -16,7 +16,7 @@ use crate::performance::LoadRequest;
use anyhow::{Result, bail};

use crate::cli::{
Cli, CliLane, CliTopology, Command, ConformanceCommand, DebugCommand, LiveGroup,
CiCommand, Cli, CliLane, CliTopology, Command, ConformanceCommand, DebugCommand, LiveGroup,
ProtocolVersion, StackCommand, TokenKind, TopologySelection,
};
const STACK_MODE_ENV: &str = "CF_MCP_STACK_MODE";
Expand All @@ -38,6 +38,7 @@ pub(crate) enum Action {
},
Conformance(ConformanceAction),
Debug(DebugAction),
Ci(CiAction),
}

impl Action {
Expand All @@ -57,6 +58,9 @@ impl Action {
Self::Conformance(ConformanceAction::Report { .. }) => "conformance report",
Self::Debug(DebugAction::Inspect { .. }) => "debug inspect",
Self::Debug(DebugAction::Token { .. }) => "debug token",
Self::Ci(CiAction::PrepareImage { .. }) => "prepare prebuilt CI image",
Self::Ci(CiAction::PrepareRelease) => "prepare release state",
Self::Ci(CiAction::SelectRelease) => "select release tag",
}
}

Expand Down Expand Up @@ -100,6 +104,13 @@ impl Action {
Self::Debug(DebugAction::Token { .. }) => {
String::from("Topology: not applicable (token only)")
}
Self::Ci(CiAction::PrepareImage { .. }) => {
String::from("CI operation: prepare prebuilt image")
}
Self::Ci(CiAction::PrepareRelease) => {
String::from("CI operation: prepare release state")
}
Self::Ci(CiAction::SelectRelease) => String::from("CI operation: select release tag"),
}
}

Expand All @@ -121,6 +132,7 @@ impl Action {
self,
Self::Conformance(ConformanceAction::Report { .. })
| Self::Debug(DebugAction::Token { .. })
| Self::Ci(_)
)
}
}
Expand Down Expand Up @@ -251,6 +263,23 @@ pub(crate) enum DebugAction {
},
}

/// Repository CI operation executed by the published CLI.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CiAction {
PrepareImage {
artifact: String,
binary: PathBuf,
image: String,
repository: String,
revision: Option<String>,
dockerfile: PathBuf,
target: String,
download_dir: PathBuf,
},
PrepareRelease,
SelectRelease,
}

/// Resolves a parsed CLI without starting child processes or mutating global state.
///
/// # Errors
Expand Down Expand Up @@ -346,9 +375,43 @@ pub(crate) fn resolve_action(cli: Cli, environment: &Environment) -> Result<Acti
}
}
})),
Command::Ci(args) => Ok(Action::Ci(match args.command {
CiCommand::PrepareImage(args) => {
let mut components = args.binary.components();
if !matches!(components.next(), Some(Component::Normal(_)))
|| components.next().is_some()
{
bail!("--binary must be one filename at the artifact root");
}
let repository = args
.repository
.or_else(|| environment_utf8(environment, "GITHUB_REPOSITORY"))
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow::anyhow!("set --repository or GITHUB_REPOSITORY"))?;
CiAction::PrepareImage {
artifact: args.artifact,
binary: args.binary,
image: args.image,
repository,
revision: args.revision,
dockerfile: args.dockerfile,
target: args.target,
download_dir: args.download_dir,
}
}
CiCommand::PrepareRelease => CiAction::PrepareRelease,
CiCommand::SelectRelease => CiAction::SelectRelease,
})),
}
}

fn environment_utf8(environment: &Environment, key: &str) -> Option<String> {
environment
.get(std::ffi::OsStr::new(key))
.and_then(|value| value.to_str())
.map(str::to_owned)
}

fn resolve_live_lane(lane: Option<CliLane>, environment: &Environment) -> Result<SemanticLane> {
Ok(match lane {
Some(CliLane::FixtureDirect) => SemanticLane::FixtureDirect,
Expand Down
53 changes: 52 additions & 1 deletion src/app_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;

use cf_integration::app::{
Action, ConformanceAction, DebugAction, ResolvedLoadArgs, StackAction, resolve_action,
Action, CiAction, ConformanceAction, DebugAction, ResolvedLoadArgs, StackAction, resolve_action,
};
use cf_integration::cli::{Cli, LiveGroup, ProtocolVersion, TokenKind, TopologySelection};
use cf_integration::conformance::results::{ConformanceServerEra, SemanticLane};
Expand Down Expand Up @@ -51,6 +51,57 @@ fn every_subcommand_has_a_stable_progress_description() {
}
}

#[test]
fn ci_image_preparation_is_read_only_until_execution() {
let action = action(
&[
"cf-integration",
"ci",
"prepare-image",
"--artifact",
"contextforge-data-plane-conformance",
"--binary",
"contextforge-data-plane",
"--image",
"contextforge-data-plane:conformance",
],
&[(
"GITHUB_REPOSITORY",
"contextforge-org/contextforge-data-plane",
)],
);

assert!(matches!(&action, Action::Ci(CiAction::PrepareImage { .. })));
assert_eq!(action.description(), "prepare prebuilt CI image");
assert!(!action.requires_runtime_assets());
}

#[test]
fn ci_image_preparation_rejects_nested_artifact_paths() {
let cli = Cli::try_parse_from([
"cf-integration",
"ci",
"prepare-image",
"--artifact",
"artifact",
"--binary",
"nested/binary",
"--image",
"service:test",
"--repository",
"owner/repository",
])
.expect("CLI syntax should parse before path validation");

let error = resolve_action(cli, &Environment::new())
.expect_err("artifact binary must remain inside its download root");

assert_eq!(
error.to_string(),
"--binary must be one filename at the artifact root"
);
}

#[test]
fn every_subcommand_reports_its_resolved_topology_at_startup() {
let cases: &[(&[&str], &str)] = &[
Expand Down
58 changes: 58 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,64 @@ pub(crate) enum Command {
Conformance(ConformanceArgs),
/// Run manual debugging utilities.
Debug(DebugArgs),
/// Repository CI orchestration used by ContextForge workflows.
#[command(hide = true)]
Ci(CiArgs),
}

/// Internal CI command selection.
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct CiArgs {
/// CI operation to run.
#[command(subcommand)]
pub(crate) command: CiCommand,
}

/// Internal CI operations kept in the published binary instead of workflow scripts.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub(crate) enum CiCommand {
/// Download an exact CI artifact and package it as a local Docker image.
PrepareImage(CiPrepareImageArgs),
/// Remove stale unpublished release state before release-plz runs.
PrepareRelease,
/// Select the release tag produced by or recoverable after release-plz.
SelectRelease,
}

/// Options for packaging a prebuilt service binary from GitHub Actions.
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct CiPrepareImageArgs {
/// GitHub Actions artifact prefix; the exact checkout revision is appended.
#[arg(long)]
pub(crate) artifact: String,

/// Binary filename at the root of the downloaded artifact.
#[arg(long)]
pub(crate) binary: PathBuf,

/// Local Docker image tag to create.
#[arg(long)]
pub(crate) image: String,

/// GitHub owner/repository; defaults to GITHUB_REPOSITORY.
#[arg(long)]
pub(crate) repository: Option<String>,

/// Exact artifact revision; defaults to the current Git checkout.
#[arg(long)]
pub(crate) revision: Option<String>,

/// Dockerfile containing the prebuilt image target.
#[arg(long, default_value = "docker/Dockerfile")]
pub(crate) dockerfile: PathBuf,

/// Dockerfile target that copies from the prebuilt build context.
#[arg(long, default_value = "conformance-prebuilt")]
pub(crate) target: String,

/// Generated artifact download directory.
#[arg(long, default_value = ".integration/ci/prebuilt")]
pub(crate) download_dir: PathBuf,
}

/// Stack command selection.
Expand Down
Loading