removing golang/temporal building

This commit is contained in:
Michael Neale 2025-10-29 12:20:08 +11:00 committed by GitHub
parent b94535b679
commit f0056e6cd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 34 additions and 4999 deletions

View file

@ -78,12 +78,6 @@ jobs:
if: matrix.use-cross
run: source ./bin/activate-hermit && cargo install cross --git https://github.com/cross-rs/cross
# Install Go for building temporal-service
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # pin@v5
with:
go-version: '1.21'
# Cache Cargo registry and git dependencies for Windows builds
- name: Cache Cargo registry (Windows)
if: matrix.use-docker
@ -231,140 +225,7 @@ jobs:
echo "✅ Windows runtime DLLs:"
ls -la ./target/x86_64-pc-windows-gnu/release/*.dll
- name: Build temporal-service for target platform using build.sh script (Linux/macOS)
if: matrix.use-cross
run: |
source ./bin/activate-hermit
export TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}"
# Set Go cross-compilation variables based on target
case "${TARGET}" in
"x86_64-unknown-linux-gnu")
export GOOS=linux
export GOARCH=amd64
BINARY_NAME="temporal-service"
;;
"aarch64-unknown-linux-gnu")
export GOOS=linux
export GOARCH=arm64
BINARY_NAME="temporal-service"
;;
"x86_64-apple-darwin")
export GOOS=darwin
export GOARCH=amd64
BINARY_NAME="temporal-service"
;;
"aarch64-apple-darwin")
export GOOS=darwin
export GOARCH=arm64
BINARY_NAME="temporal-service"
;;
*)
echo "Unsupported target: ${TARGET}"
exit 1
;;
esac
echo "Building temporal-service for ${GOOS}/${GOARCH} using build.sh script..."
cd temporal-service
# Run build.sh with cross-compilation environment
GOOS="${GOOS}" GOARCH="${GOARCH}" ./build.sh
# Move the built binary to the expected location
mv "${BINARY_NAME}" "../target/${TARGET}/release/${BINARY_NAME}"
echo "temporal-service built successfully for ${TARGET}"
- name: Build temporal-service for Windows
if: matrix.use-docker
run: |
echo "Building temporal-service for Windows using build.sh script..."
docker run --rm \
-v "$(pwd)":/usr/src/myapp \
-w /usr/src/myapp/temporal-service \
golang:latest \
sh -c "
# Make build.sh executable
chmod +x build.sh
# Set Windows build environment and run build script
GOOS=windows GOARCH=amd64 ./build.sh
# Move the built binary to the expected location (inside container)
mkdir -p ../target/x86_64-pc-windows-gnu/release
mv temporal-service.exe ../target/x86_64-pc-windows-gnu/release/temporal-service.exe
# Fix permissions for host access
chmod -R 755 ../target/x86_64-pc-windows-gnu
"
echo "temporal-service.exe built successfully for Windows"
- name: Download temporal CLI (Linux/macOS)
if: matrix.use-cross
run: |
export TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}"
TEMPORAL_VERSION="1.3.0"
# Set platform-specific download parameters
case "${TARGET}" in
"x86_64-unknown-linux-gnu")
TEMPORAL_OS="linux"
TEMPORAL_ARCH="amd64"
TEMPORAL_EXT=""
;;
"aarch64-unknown-linux-gnu")
TEMPORAL_OS="linux"
TEMPORAL_ARCH="arm64"
TEMPORAL_EXT=""
;;
"x86_64-apple-darwin")
TEMPORAL_OS="darwin"
TEMPORAL_ARCH="amd64"
TEMPORAL_EXT=""
;;
"aarch64-apple-darwin")
TEMPORAL_OS="darwin"
TEMPORAL_ARCH="arm64"
TEMPORAL_EXT=""
;;
*)
echo "Unsupported target for temporal CLI: ${TARGET}"
exit 1
;;
esac
echo "Downloading temporal CLI for ${TEMPORAL_OS}/${TEMPORAL_ARCH}..."
TEMPORAL_FILE="temporal_cli_${TEMPORAL_VERSION}_${TEMPORAL_OS}_${TEMPORAL_ARCH}.tar.gz"
curl -L "https://github.com/temporalio/cli/releases/download/v${TEMPORAL_VERSION}/${TEMPORAL_FILE}" -o "${TEMPORAL_FILE}"
# Extract temporal CLI
tar -xzf "${TEMPORAL_FILE}"
chmod +x temporal${TEMPORAL_EXT}
# Move to target directory
mv temporal${TEMPORAL_EXT} "target/${TARGET}/release/temporal${TEMPORAL_EXT}"
# Clean up
rm -f "${TEMPORAL_FILE}"
echo "temporal CLI downloaded successfully for ${TARGET}"
- name: Download temporal CLI (Windows)
if: matrix.use-docker
run: |
TEMPORAL_VERSION="1.3.0"
echo "Downloading temporal CLI for Windows..."
curl -L "https://github.com/temporalio/cli/releases/download/v${TEMPORAL_VERSION}/temporal_cli_${TEMPORAL_VERSION}_windows_amd64.zip" -o temporal-cli-windows.zip
unzip -o temporal-cli-windows.zip
chmod +x temporal.exe
# Fix permissions on target directory (created by Docker as root)
sudo chown -R $(whoami):$(whoami) target/x86_64-pc-windows-gnu/ || true
# Move to target directory
mv temporal.exe target/x86_64-pc-windows-gnu/release/temporal.exe
# Clean up
rm -f temporal-cli-windows.zip
echo "temporal CLI downloaded successfully for Windows"
- name: Package CLI with temporal-service (Linux/macOS)
- name: Package CLI (Linux/macOS)
if: matrix.use-cross
run: |
source ./bin/activate-hermit
@ -373,17 +234,15 @@ jobs:
# Create a directory for the package contents
mkdir -p "target/${TARGET}/release/goose-package"
# Copy binaries
# Copy the goose binary
cp "target/${TARGET}/release/goose" "target/${TARGET}/release/goose-package/"
cp "target/${TARGET}/release/temporal-service" "target/${TARGET}/release/goose-package/"
cp "target/${TARGET}/release/temporal" "target/${TARGET}/release/goose-package/"
# Create the tar archive with all binaries
# Create the tar archive
cd "target/${TARGET}/release"
tar -cjf "goose-${TARGET}.tar.bz2" -C goose-package .
echo "ARTIFACT=target/${TARGET}/release/goose-${TARGET}.tar.bz2" >> $GITHUB_ENV
- name: Package CLI with temporal-service (Windows)
- name: Package CLI (Windows)
if: matrix.use-docker
run: |
export TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}"
@ -391,15 +250,13 @@ jobs:
# Create a directory for the package contents
mkdir -p "target/${TARGET}/release/goose-package"
# Copy binaries
# Copy the goose binary
cp "target/${TARGET}/release/goose.exe" "target/${TARGET}/release/goose-package/"
cp "target/${TARGET}/release/temporal-service.exe" "target/${TARGET}/release/goose-package/"
cp "target/${TARGET}/release/temporal.exe" "target/${TARGET}/release/goose-package/"
# Copy Windows runtime DLLs
cp "target/${TARGET}/release/"*.dll "target/${TARGET}/release/goose-package/"
# Create the zip archive with all binaries and DLLs
# Create the zip archive with binary and DLLs
cd "target/${TARGET}/release"
zip -r "goose-${TARGET}.zip" goose-package/
echo "ARTIFACT=target/${TARGET}/release/goose-${TARGET}.zip" >> $GITHUB_ENV

View file

@ -87,11 +87,6 @@ jobs:
restore-keys: |
${{ runner.os }}-intel-cargo-build-
# Install Go for building temporal-service
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # pin@v5
with:
go-version: '1.21'
- name: Build goose-server for Intel macOS (x86_64)
run: |
@ -99,20 +94,7 @@ jobs:
rustup target add x86_64-apple-darwin
cargo build --release -p goose-server --target x86_64-apple-darwin
# Build temporal-service using build.sh script
- name: Build temporal-service
run: |
echo "Building temporal-service using build.sh script..."
cd temporal-service
./build.sh
echo "temporal-service built successfully"
# Install and prepare temporal CLI
- name: Install temporal CLI via hermit
run: |
echo "Installing temporal CLI via hermit..."
./bin/hermit install temporal-cli
echo "temporal CLI installed successfully"
# Post-build cleanup to free space
- name: Post-build cleanup
@ -131,8 +113,6 @@ jobs:
- name: Copy binaries into Electron folder
run: |
cp target/x86_64-apple-darwin/release/goosed ui/desktop/src/bin/goosed
cp temporal-service/temporal-service ui/desktop/src/bin/temporal-service
cp bin/temporal ui/desktop/src/bin/temporal
- name: Cache npm dependencies
uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # pin@v3

View file

@ -99,18 +99,6 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # pin@v5
with:
go-version: '1.21'
- name: Build temporal-service
run: |
echo "Building temporal-service using build.sh script..."
cd temporal-service
./build.sh
echo "temporal-service built successfully"
- name: Build goosed binary
env:
CROSS_NO_WARNINGS: 0
@ -130,9 +118,7 @@ jobs:
export TARGET="x86_64-unknown-linux-gnu"
mkdir -p ui/desktop/src/bin
cp target/$TARGET/release/goosed ui/desktop/src/bin/
cp temporal-service/temporal-service ui/desktop/src/bin/
chmod +x ui/desktop/src/bin/goosed
chmod +x ui/desktop/src/bin/temporal-service
ls -la ui/desktop/src/bin/
- name: Cache npm dependencies

View file

@ -198,32 +198,6 @@ jobs:
ls -la ./target/x86_64-pc-windows-gnu/release/goosed.exe
ls -la ./target/x86_64-pc-windows-gnu/release/*.dll
# 4.5) Build temporal-service for Windows using build.sh script
- name: Build temporal-service for Windows
run: |
echo "Building temporal-service for Windows using build.sh script..."
docker run --rm \
-v "$(pwd)":/usr/src/myapp \
-w /usr/src/myapp/temporal-service \
golang:latest \
sh -c "
# Make build.sh executable
chmod +x build.sh
# Set Windows build environment and run build script
GOOS=windows GOARCH=amd64 ./build.sh
"
echo "temporal-service.exe built successfully"
# 4.6) Download temporal CLI for Windows
- name: Download temporal CLI for Windows
run: |
echo "Downloading temporal CLI for Windows..."
TEMPORAL_VERSION="1.3.0"
curl -L "https://github.com/temporalio/cli/releases/download/v${TEMPORAL_VERSION}/temporal_cli_${TEMPORAL_VERSION}_windows_amd64.zip" -o temporal-cli-windows.zip
unzip -o temporal-cli-windows.zip
chmod +x temporal.exe
echo "temporal CLI downloaded successfully"
# 5) Prepare Windows binary and DLLs
- name: Prepare Windows binary and DLLs
run: |
@ -232,16 +206,6 @@ jobs:
exit 1
fi
if [ ! -f "./temporal-service/temporal-service.exe" ]; then
echo "temporal-service.exe not found."
exit 1
fi
if [ ! -f "./temporal.exe" ]; then
echo "temporal.exe not found."
exit 1
fi
echo "Cleaning destination directory..."
rm -rf ./ui/desktop/src/bin
mkdir -p ./ui/desktop/src/bin
@ -250,12 +214,6 @@ jobs:
cp -f ./target/x86_64-pc-windows-gnu/release/goosed.exe ./ui/desktop/src/bin/
cp -f ./target/x86_64-pc-windows-gnu/release/*.dll ./ui/desktop/src/bin/
echo "Copying temporal-service.exe..."
cp -f ./temporal-service/temporal-service.exe ./ui/desktop/src/bin/
echo "Copying temporal.exe..."
cp -f ./temporal.exe ./ui/desktop/src/bin/
# Copy Windows platform files (tools, scripts, etc.)
if [ -d "./ui/desktop/src/platform/windows/bin" ]; then
echo "Copying Windows platform files..."

View file

@ -131,31 +131,10 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-build-
# Install Go for building temporal-service
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # pin@v5
with:
go-version: '1.21'
# Build the project
- name: Build goosed
run: source ./bin/activate-hermit && cargo build --release -p goose-server
# Build temporal-service using build.sh script
- name: Build temporal-service
run: |
echo "Building temporal-service using build.sh script..."
cd temporal-service
./build.sh
echo "temporal-service built successfully"
# Install and prepare temporal CLI
- name: Install temporal CLI via hermit
run: |
echo "Installing temporal CLI via hermit..."
./bin/hermit install temporal-cli
echo "temporal CLI installed successfully"
# Post-build cleanup to free space
- name: Post-build cleanup
run: |
@ -172,8 +151,6 @@ jobs:
- name: Copy binaries into Electron folder
run: |
cp target/release/goosed ui/desktop/src/bin/goosed
cp temporal-service/temporal-service ui/desktop/src/bin/temporal-service
cp bin/temporal ui/desktop/src/bin/temporal
- name: Cache npm dependencies
uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # pin@v3

101
Justfile
View file

@ -76,20 +76,6 @@ copy-binary BUILD_MODE="release":
echo "goose CLI binary not found in target/{{BUILD_MODE}}"; \
exit 1; \
fi
@if [ -f ./temporal-service/temporal-service ]; then \
echo "Copying temporal-service binary..."; \
cp -p ./temporal-service/temporal-service ./ui/desktop/src/bin/; \
else \
echo "temporal-service binary not found. Building it..."; \
cd temporal-service && ./build.sh && cp -p temporal-service ../ui/desktop/src/bin/; \
fi
@echo "Checking temporal CLI binary..."
@if [ ! -f ./ui/desktop/src/bin/temporal ]; then \
echo "temporal CLI binary not found in ui/desktop/src/bin/"; \
echo "Please ensure temporal CLI is available or will be downloaded at runtime"; \
else \
echo "temporal CLI binary found"; \
fi
# Copy binary command for Intel build
copy-binary-intel:
@ -107,20 +93,6 @@ copy-binary-intel:
echo "Intel goose CLI binary not found."; \
exit 1; \
fi
@if [ -f ./temporal-service/temporal-service ]; then \
echo "Copying temporal-service binary..."; \
cp -p ./temporal-service/temporal-service ./ui/desktop/src/bin/; \
else \
echo "temporal-service binary not found. Building it..."; \
cd temporal-service && ./build.sh && cp -p temporal-service ../ui/desktop/src/bin/; \
fi
@echo "Checking temporal CLI binary..."
@if [ ! -f ./ui/desktop/src/bin/temporal ]; then \
echo "temporal CLI binary not found in ui/desktop/src/bin/"; \
echo "Please ensure temporal CLI is available or will be downloaded at runtime"; \
else \
echo "temporal CLI binary found"; \
fi
# Copy Windows binary command
copy-binary-windows:
@ -132,24 +104,6 @@ copy-binary-windows:
Write-Host 'Windows binary not found.' -ForegroundColor Red; \
exit 1; \
}"
@powershell.exe -Command "if (Test-Path ./target/x86_64-pc-windows-gnu/release/goose-scheduler-executor.exe) { \
Write-Host 'Copying Windows goose-scheduler-executor binary...'; \
Copy-Item -Path './target/x86_64-pc-windows-gnu/release/goose-scheduler-executor.exe' -Destination './ui/desktop/src/bin/' -Force; \
} else { \
Write-Host 'Windows goose-scheduler-executor binary not found.' -ForegroundColor Yellow; \
}"
@powershell.exe -Command "if (Test-Path './temporal-service/temporal-service.exe') { \
Write-Host 'Copying Windows temporal-service binary...'; \
Copy-Item -Path './temporal-service/temporal-service.exe' -Destination './ui/desktop/src/bin/' -Force; \
} else { \
Write-Host 'Windows temporal-service binary not found. Building it...'; \
Push-Location 'temporal-service'; \
$env:GOOS='windows'; $env:GOARCH='amd64'; \
go build -o temporal-service.exe main.go; \
Copy-Item -Path 'temporal-service.exe' -Destination '../ui/desktop/src/bin/' -Force; \
Pop-Location; \
}"
@powershell.exe -Command "Write-Host 'Note: Temporal CLI for Windows will be downloaded at runtime if needed'"
# Run UI with latest
run-ui:
@ -194,16 +148,10 @@ debug-ui-main-process:
npm run start-gui-debug
# Run UI with alpha changes
run-ui-alpha temporal="true":
run-ui-alpha:
@just release-binary
@echo "Running UI with {{ if temporal == "true" { "Temporal" } else { "Legacy" } }} scheduler..."
cd ui/desktop && npm install && ALPHA=true GOOSE_SCHEDULER_TYPE={{ if temporal == "true" { "temporal" } else { "legacy" } }} npm run start-alpha-gui
# Run UI with alpha changes using legacy scheduler (no Temporal dependency)
run-ui-alpha-legacy:
@just release-binary
@echo "Running UI with Legacy scheduler (no Temporal required)..."
cd ui/desktop && npm install && ALPHA=true GOOSE_SCHEDULER_TYPE=legacy npm run start-alpha-gui
@echo "Running UI with alpha features..."
cd ui/desktop && npm install && ALPHA=true npm run start-alpha-gui
# Run UI with latest (Windows version)
run-ui-windows:
@ -272,50 +220,7 @@ make-ui-intel:
@just release-intel
cd ui/desktop && npm run bundle:intel
# Start Temporal services (server and temporal-service)
start-temporal:
@echo "Starting Temporal server..."
@if ! pgrep -f "temporal server start-dev" > /dev/null; then \
echo "Starting Temporal server in background..."; \
nohup temporal server start-dev --db-filename temporal.db --port 7233 --ui-port 8233 --log-level warn > temporal-server.log 2>&1 & \
echo "Waiting for Temporal server to start..."; \
sleep 5; \
else \
echo "Temporal server is already running"; \
fi
@echo "Starting temporal-service..."
@if ! pgrep -f "temporal-service" > /dev/null; then \
echo "Starting temporal-service in background..."; \
cd temporal-service && nohup ./temporal-service > temporal-service.log 2>&1 & \
echo "Waiting for temporal-service to start..."; \
sleep 3; \
else \
echo "temporal-service is already running"; \
fi
@echo "Temporal services started. Check logs: temporal-server.log, temporal-service/temporal-service.log"
# Stop Temporal services
stop-temporal:
@echo "Stopping Temporal services..."
@pkill -f "temporal server start-dev" || echo "Temporal server was not running"
@pkill -f "temporal-service" || echo "temporal-service was not running"
@echo "Temporal services stopped"
# Check status of Temporal services
status-temporal:
@echo "Checking Temporal services status..."
@if pgrep -f "temporal server start-dev" > /dev/null; then \
echo "✓ Temporal server is running"; \
else \
echo "✗ Temporal server is not running"; \
fi
@if pgrep -f "temporal-service" > /dev/null; then \
echo "✓ temporal-service is running"; \
else \
echo "✗ temporal-service is not running"; \
fi
@echo "Testing temporal-service health..."
@curl -s http://localhost:8080/health > /dev/null && echo "✓ temporal-service is responding" || echo "✗ temporal-service is not responding"
# Run UI with debug build
run-dev:

View file

@ -271,11 +271,11 @@ enum SchedulerCommand {
#[arg(long, help = "ID of the schedule to run")] // Explicitly make it --id
id: String,
},
/// Check status of Temporal services (temporal scheduler only)
#[command(about = "Check status of Temporal services")]
/// Check status of scheduler services (deprecated - no external services needed)
#[command(about = "Check status of scheduler services")]
ServicesStatus {},
/// Stop Temporal services (temporal scheduler only)
#[command(about = "Stop Temporal services")]
/// Stop scheduler services (deprecated - no external services needed)
#[command(about = "Stop scheduler services")]
ServicesStop {},
/// Show cron expression examples and help
#[command(about = "Show cron expression examples and help")]

View file

@ -1214,11 +1214,6 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> {
"goose recipe github repo",
"goose will pull recipes from this repo if not found locally.",
)
.item(
"scheduler",
"Scheduler Type",
"Choose between built-in cron scheduler or Temporal workflow engine",
)
.interact()?;
match setting_type {
@ -1243,9 +1238,6 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> {
"recipe" => {
configure_recipe_dialog()?;
}
"scheduler" => {
configure_scheduler_dialog()?;
}
_ => unreachable!(),
};
@ -1580,60 +1572,6 @@ fn configure_recipe_dialog() -> anyhow::Result<()> {
Ok(())
}
fn configure_scheduler_dialog() -> anyhow::Result<()> {
let config = Config::global();
// Check if GOOSE_SCHEDULER_TYPE is set as an environment variable
if std::env::var("GOOSE_SCHEDULER_TYPE").is_ok() {
let _ = cliclack::log::info("Notice: GOOSE_SCHEDULER_TYPE environment variable is set and will override the configuration here.");
}
// Get current scheduler type from config for display
let current_scheduler: String = config
.get_param("GOOSE_SCHEDULER_TYPE")
.unwrap_or_else(|_| "legacy".to_string());
println!(
"Current scheduler type: {}",
style(&current_scheduler).cyan()
);
let scheduler_type = cliclack::select("Which scheduler type would you like to use?")
.items(&[
("legacy", "Built-in Cron (Default)", "Uses goose's built-in cron scheduler. Simple and reliable for basic scheduling needs."),
("temporal", "Temporal", "Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI to be installed.")
])
.interact()?;
match scheduler_type {
"legacy" => {
config.set_param("GOOSE_SCHEDULER_TYPE", Value::String("legacy".to_string()))?;
cliclack::outro(
"Set to Built-in Cron scheduler - simple and reliable for basic scheduling",
)?;
}
"temporal" => {
config.set_param(
"GOOSE_SCHEDULER_TYPE",
Value::String("temporal".to_string()),
)?;
cliclack::outro(
"Set to Temporal scheduler - advanced workflow engine for complex scheduling",
)?;
println!();
println!("📋 {}", style("Note:").bold());
println!(" • Temporal scheduler requires Temporal CLI to be installed");
println!(" • macOS: brew install temporal");
println!(" • Linux/Windows: https://github.com/temporalio/cli/releases");
println!(" • If Temporal is unavailable, goose will automatically fall back to the built-in scheduler");
println!(" • The scheduling engines do not share the list of schedules");
}
_ => unreachable!(),
};
Ok(())
}
pub fn configure_max_turns_dialog() -> anyhow::Result<()> {
let config = Config::global();

View file

@ -5,7 +5,6 @@ use goose::scheduler::{
SchedulerError,
};
use goose::scheduler_factory::SchedulerFactory;
use goose::temporal_scheduler::TemporalScheduler;
use std::path::Path;
// Base64 decoding function - might be needed if recipe_source_arg can be base64
@ -260,74 +259,18 @@ pub async fn handle_schedule_run_now(id: String) -> Result<()> {
}
pub async fn handle_schedule_services_status() -> Result<()> {
// Check if we're using temporal scheduler
let scheduler_type =
std::env::var("GOOSE_SCHEDULER_TYPE").unwrap_or_else(|_| "temporal".to_string());
if scheduler_type != "temporal" {
println!("Service management is only available for temporal scheduler.");
println!("Set GOOSE_SCHEDULER_TYPE=temporal to use Temporal services.");
return Ok(());
}
println!("Checking Temporal services status...");
// Create a temporary TemporalScheduler to check status
match TemporalScheduler::new().await {
Ok(scheduler) => {
let info = scheduler.get_service_info().await;
println!("{}", info);
}
Err(e) => {
println!("❌ Failed to check services: {}", e);
println!();
println!("💡 This might mean:");
println!(" • Temporal CLI is not installed");
println!(" • temporal-service binary is not available");
println!(" • Services are not running");
println!();
println!("🔧 To fix this:");
println!(" 1. Install Temporal CLI:");
println!(" macOS: brew install temporal");
println!(" Linux/Windows: https://github.com/temporalio/cli/releases");
println!(" 2. Or use legacy scheduler: export GOOSE_SCHEDULER_TYPE=legacy");
}
}
println!("Service management has been removed as Temporal scheduler is no longer supported.");
println!(
"The built-in scheduler runs within the goose process and requires no external services."
);
Ok(())
}
pub async fn handle_schedule_services_stop() -> Result<()> {
// Check if we're using temporal scheduler
let scheduler_type =
std::env::var("GOOSE_SCHEDULER_TYPE").unwrap_or_else(|_| "temporal".to_string());
if scheduler_type != "temporal" {
println!("Service management is only available for temporal scheduler.");
println!("Set GOOSE_SCHEDULER_TYPE=temporal to use Temporal services.");
return Ok(());
}
println!("Stopping Temporal services...");
// Create a temporary TemporalScheduler to stop services
match TemporalScheduler::new().await {
Ok(scheduler) => match scheduler.stop_services().await {
Ok(result) => {
println!("{}", result);
println!("\nNote: Services were running independently and have been stopped.");
println!("They will be automatically restarted when needed.");
}
Err(e) => {
println!("Failed to stop services: {}", e);
}
},
Err(e) => {
println!("Failed to initialize scheduler: {}", e);
println!("Services may not be running or may have already been stopped.");
}
}
println!("Service management has been removed as Temporal scheduler is no longer supported.");
println!(
"The built-in scheduler runs within the goose process and requires no external services."
);
Ok(())
}

View file

@ -17,7 +17,6 @@ pub mod scheduler_factory;
pub mod scheduler_trait;
pub mod security;
pub mod session;
pub mod temporal_scheduler;
pub mod token_counter;
pub mod tool_inspection;
pub mod tool_monitor;

View file

@ -1,152 +1,26 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::config::Config;
use crate::scheduler::{Scheduler, SchedulerError};
use crate::scheduler_trait::SchedulerTrait;
use crate::temporal_scheduler::TemporalScheduler;
pub enum SchedulerType {
Legacy,
Temporal,
}
impl SchedulerType {
pub fn from_config() -> Self {
let config = Config::global();
// Debug logging to help troubleshoot environment variable issues
tracing::debug!("Checking scheduler configuration...");
// Check scheduler type preference from GOOSE_SCHEDULER_TYPE
match config.get_param::<String>("GOOSE_SCHEDULER_TYPE") {
Ok(scheduler_type) => {
tracing::debug!(
"Found GOOSE_SCHEDULER_TYPE environment variable: '{}'",
scheduler_type
);
match scheduler_type.to_lowercase().as_str() {
"temporal" => SchedulerType::Temporal,
"legacy" => SchedulerType::Legacy,
_ => {
tracing::warn!(
"Unknown scheduler type '{}', defaulting to legacy scheduler",
scheduler_type
);
SchedulerType::Legacy
}
}
}
Err(_) => {
tracing::debug!("GOOSE_SCHEDULER_TYPE environment variable not found");
// When no explicit scheduler type is set, default to legacy scheduler
tracing::info!("No scheduler type specified, defaulting to legacy scheduler");
SchedulerType::Legacy
}
}
}
}
/// Factory for creating scheduler instances
pub struct SchedulerFactory;
impl SchedulerFactory {
/// Create a scheduler instance based on configuration
/// Create a scheduler instance
pub async fn create(storage_path: PathBuf) -> Result<Arc<dyn SchedulerTrait>, SchedulerError> {
let scheduler_type = SchedulerType::from_config();
match scheduler_type {
SchedulerType::Legacy => {
tracing::info!("Creating legacy scheduler");
let scheduler = Scheduler::new(storage_path).await?;
Ok(scheduler as Arc<dyn SchedulerTrait>)
}
SchedulerType::Temporal => {
tracing::info!("Attempting to create Temporal scheduler");
match TemporalScheduler::new().await {
Ok(scheduler) => {
tracing::info!("Temporal scheduler created successfully");
Ok(scheduler as Arc<dyn SchedulerTrait>)
}
Err(e) => {
tracing::warn!("Failed to create Temporal scheduler: {}", e);
tracing::info!("Falling back to legacy scheduler");
// Print helpful message for users
eprintln!(
"⚠️ Temporal scheduler unavailable, using legacy scheduler instead."
);
eprintln!(" To use Temporal scheduling features:");
eprintln!(" • Install Temporal CLI: brew install temporal (macOS)");
eprintln!(
" • Or download from: https://github.com/temporalio/cli/releases"
);
eprintln!(" • Then restart goose");
eprintln!();
let scheduler = Scheduler::new(storage_path).await?;
Ok(scheduler as Arc<dyn SchedulerTrait>)
}
}
}
}
}
/// Create a legacy scheduler (for testing or explicit use)
pub async fn create_legacy(
storage_path: PathBuf,
) -> Result<Arc<dyn SchedulerTrait>, SchedulerError> {
tracing::info!("Creating legacy scheduler (explicit)");
tracing::info!("Creating scheduler");
let scheduler = Scheduler::new(storage_path).await?;
Ok(scheduler as Arc<dyn SchedulerTrait>)
}
/// Create a Temporal scheduler (for testing or explicit use)
pub async fn create_temporal() -> Result<Arc<dyn SchedulerTrait>, SchedulerError> {
tracing::info!("Creating Temporal scheduler (explicit)");
let scheduler = TemporalScheduler::new().await?;
/// Create a scheduler (for testing or explicit use)
pub async fn create_legacy(
storage_path: PathBuf,
) -> Result<Arc<dyn SchedulerTrait>, SchedulerError> {
tracing::info!("Creating scheduler (explicit)");
let scheduler = Scheduler::new(storage_path).await?;
Ok(scheduler as Arc<dyn SchedulerTrait>)
}
}
#[cfg(test)]
mod tests {
use super::*;
use temp_env::with_vars;
#[test]
fn test_scheduler_type_no_env() {
// Test that without GOOSE_SCHEDULER_TYPE env var, we get Legacy scheduler
with_vars([("GOOSE_SCHEDULER_TYPE", None::<&str>)], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
});
}
#[test]
fn test_scheduler_type_legacy() {
// Test that with GOOSE_SCHEDULER_TYPE=legacy, we get Legacy scheduler
with_vars([("GOOSE_SCHEDULER_TYPE", Some("legacy"))], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
});
}
#[test]
fn test_scheduler_type_temporal() {
// Test that with GOOSE_SCHEDULER_TYPE=temporal, we get Temporal scheduler
with_vars([("GOOSE_SCHEDULER_TYPE", Some("temporal"))], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Temporal));
});
}
#[test]
fn test_scheduler_type_unknown() {
// Test that with unknown scheduler type, we default to Legacy
with_vars([("GOOSE_SCHEDULER_TYPE", Some("unknown"))], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -154,8 +154,6 @@ These variables control how Goose manages conversation sessions and context.
| `GOOSE_MAX_TURNS` | [Maximum number of turns](/docs/guides/sessions/smart-context-management#maximum-turns) allowed without user input | Integer (e.g., 10, 50, 100) | 1000 |
| `CONTEXT_FILE_NAMES` | Specifies custom filenames for [hint/context files](/docs/guides/using-goosehints#custom-context-files) | JSON array of strings (e.g., `["CLAUDE.md", ".goosehints"]`) | `[".goosehints"]` |
| `GOOSE_CLI_THEME` | [Theme](/docs/guides/goose-cli-commands#themes) for CLI response markdown | "light", "dark", "ansi" | "dark" |
| `GOOSE_SCHEDULER_TYPE` | Controls which scheduler Goose uses for [scheduled recipes](/docs/guides/recipes/session-recipes.md#schedule-recipe) | "legacy" or "temporal" | "legacy" (Goose's built-in cron scheduler) |
| `GOOSE_TEMPORAL_BIN` | Optional custom path to your Temporal binary | /path/to/temporal-service | None |
| `GOOSE_RANDOM_THINKING_MESSAGES` | Controls whether to show amusing random messages during processing | "true", "false" | "true" |
| `GOOSE_CLI_SHOW_COST` | Toggles display of model cost estimates in CLI output | "true", "1" (case insensitive) to enable | false |
| `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which Goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0) | 0.8 |
@ -184,12 +182,6 @@ export CONTEXT_FILE_NAMES='["CLAUDE.md", ".goosehints", ".cursorrules", "project
# Set the ANSI theme for the session
export GOOSE_CLI_THEME=ansi
# Use Temporal for scheduled recipes
export GOOSE_SCHEDULER_TYPE=temporal
# Custom Temporal binary (optional)
export GOOSE_TEMPORAL_BIN=/path/to/temporal-service
# Disable random thinking messages for less distraction
export GOOSE_RANDOM_THINKING_MESSAGES=false

View file

@ -326,10 +326,6 @@ Automate recipes by running them on a [schedule](/docs/guides/recipes/session-re
- `sessions`: List sessions created by a scheduled recipe
- `run-now`: Run a scheduled recipe immediately
**Temporal Commands (requires Temporal CLI):**
- `services-status`: Check if any Temporal services are running
- `services-stop`: Stop any running Temporal services
**Options:**
- `--id <NAME>`: A unique ID for the scheduled job (e.g. `daily-report`)
- `--cron "* * * * * *"`: Specifies when a job should run using a [cron expression](https://en.wikipedia.org/wiki/Cron#Cron_expression)

View file

@ -522,13 +522,6 @@ Automate Goose recipes by running them on a schedule.
You can use either a 5, 6, or 7-digit cron expression for full scheduling precision, following the format "seconds minutes hours day-of-month month day-of-week year".
See the [`schedule` command documentation](/docs/guides/goose-cli-commands.md#schedule) for detailed examples and options.
When scheduling Goose recipes with the CLI, you can use Goose's built-in cron scheduler (default), or the [Temporal scheduler](https://docs.temporal.io/evaluate/development-production-features/schedules) (requires the Temporal CLI). Switch from the default legacy scheduler by setting the `GOOSE_SCHEDULER_TYPE` [environment variable](/docs/guides/environment-variables.md#session-management):
```bash
export GOOSE_SCHEDULER_TYPE=temporal
```
Use Temporal scheduling if you want an advanced workflow engine with monitoring features. The scheduling engines do not share schedules, so schedules created with the legacy Goose scheduler cannot be run with the Temporal scheduler, and vice-versa.
</TabItem>
</Tabs>

View file

@ -143,39 +143,7 @@ if (Test-Path $SOURCE_GOOSE) {
exit 1
}
# --- 10) Install temporal-service if it exists ---
$SOURCE_TEMPORAL_SERVICE = Join-Path $EXTRACT_DIR "temporal-service.exe"
if (Test-Path $SOURCE_TEMPORAL_SERVICE) {
$DEST_TEMPORAL_SERVICE = Join-Path $env:GOOSE_BIN_DIR "temporal-service.exe"
Write-Host "Moving temporal-service to $DEST_TEMPORAL_SERVICE" -ForegroundColor Green
try {
# Remove existing file if it exists to avoid conflicts
if (Test-Path $DEST_TEMPORAL_SERVICE) {
Remove-Item -Path $DEST_TEMPORAL_SERVICE -Force
}
Move-Item -Path $SOURCE_TEMPORAL_SERVICE -Destination $DEST_TEMPORAL_SERVICE -Force
} catch {
Write-Warning "Failed to move temporal-service.exe: $($_.Exception.Message)"
}
}
# --- 11) Install temporal CLI if it exists ---
$SOURCE_TEMPORAL = Join-Path $EXTRACT_DIR "temporal.exe"
if (Test-Path $SOURCE_TEMPORAL) {
$DEST_TEMPORAL = Join-Path $env:GOOSE_BIN_DIR "temporal.exe"
Write-Host "Moving temporal CLI to $DEST_TEMPORAL" -ForegroundColor Green
try {
# Remove existing file if it exists to avoid conflicts
if (Test-Path $DEST_TEMPORAL) {
Remove-Item -Path $DEST_TEMPORAL -Force
}
Move-Item -Path $SOURCE_TEMPORAL -Destination $DEST_TEMPORAL -Force
} catch {
Write-Warning "Failed to move temporal.exe: $($_.Exception.Message)"
}
}
# --- 12) Copy Windows runtime DLLs if they exist ---
# --- 10) Copy Windows runtime DLLs if they exist ---
$DLL_FILES = Get-ChildItem -Path $EXTRACT_DIR -Filter "*.dll" -ErrorAction SilentlyContinue
foreach ($dll in $DLL_FILES) {
$DEST_DLL = Join-Path $env:GOOSE_BIN_DIR $dll.Name
@ -191,7 +159,7 @@ foreach ($dll in $DLL_FILES) {
}
}
# --- 13) Clean up temporary directory ---
# --- 11) Clean up temporary directory ---
try {
Remove-Item -Path $TMP_DIR -Recurse -Force
Write-Host "Cleaned up temporary directory." -ForegroundColor Yellow
@ -199,7 +167,7 @@ try {
Write-Warning "Could not clean up temporary directory: $TMP_DIR"
}
# --- 14) Configure goose (Optional) ---
# --- 12) Configure goose (Optional) ---
if ($CONFIGURE -eq "true") {
Write-Host ""
Write-Host "Configuring goose" -ForegroundColor Green
@ -213,7 +181,7 @@ if ($CONFIGURE -eq "true") {
Write-Host "Skipping 'goose configure', you may need to run this manually later" -ForegroundColor Yellow
}
# --- 15) Check PATH and give instructions if needed ---
# --- 13) Check PATH and give instructions if needed ---
$CURRENT_PATH = $env:PATH
if ($CURRENT_PATH -notlike "*$env:GOOSE_BIN_DIR*") {
Write-Host ""

View file

@ -250,41 +250,14 @@ else
mv "$EXTRACT_DIR/goose" "$GOOSE_BIN_DIR/$OUT_FILE"
fi
# Also move temporal-service and temporal CLI if they exist
# Copy Windows runtime DLLs if they exist
if [ "$OS" = "windows" ]; then
if [ -f "$EXTRACT_DIR/temporal-service.exe" ]; then
echo "Moving temporal-service to $GOOSE_BIN_DIR/temporal-service.exe"
mv "$EXTRACT_DIR/temporal-service.exe" "$GOOSE_BIN_DIR/temporal-service.exe"
chmod +x "$GOOSE_BIN_DIR/temporal-service.exe"
fi
# Move temporal CLI if it exists
if [ -f "$EXTRACT_DIR/temporal.exe" ]; then
echo "Moving temporal CLI to $GOOSE_BIN_DIR/temporal.exe"
mv "$EXTRACT_DIR/temporal.exe" "$GOOSE_BIN_DIR/temporal.exe"
chmod +x "$GOOSE_BIN_DIR/temporal.exe"
fi
# Copy Windows runtime DLLs if they exist
for dll in "$EXTRACT_DIR"/*.dll; do
if [ -f "$dll" ]; then
echo "Moving Windows runtime DLL: $(basename "$dll")"
mv "$dll" "$GOOSE_BIN_DIR/"
fi
done
else
if [ -f "$EXTRACT_DIR/temporal-service" ]; then
echo "Moving temporal-service to $GOOSE_BIN_DIR/temporal-service"
mv "$EXTRACT_DIR/temporal-service" "$GOOSE_BIN_DIR/temporal-service"
chmod +x "$GOOSE_BIN_DIR/temporal-service"
fi
# Move temporal CLI if it exists
if [ -f "$EXTRACT_DIR/temporal" ]; then
echo "Moving temporal CLI to $GOOSE_BIN_DIR/temporal"
mv "$EXTRACT_DIR/temporal" "$GOOSE_BIN_DIR/temporal"
chmod +x "$GOOSE_BIN_DIR/temporal"
fi
fi
# skip configuration for non-interactive installs e.g. automation, docker

View file

@ -1 +0,0 @@
temporal-service

View file

@ -1,53 +0,0 @@
#!/bin/bash
# Build script for Temporal service
set -e
echo "Building Temporal service..."
# Change to temporal-service directory
cd "$(dirname "$0")"
# Initialize Go module if not already done
if [ ! -f "go.sum" ]; then
echo "Initializing Go module..."
go mod tidy
fi
# Determine binary name based on target OS
BINARY_NAME="temporal-service"
if [ "${GOOS:-}" = "windows" ]; then
BINARY_NAME="temporal-service.exe"
fi
# Build the service with cross-compilation support
echo "Compiling Go binary..."
if [ -n "${GOOS:-}" ] && [ -n "${GOARCH:-}" ]; then
echo "Cross-compiling for ${GOOS}/${GOARCH}..."
GOOS="${GOOS}" GOARCH="${GOARCH}" go build -buildvcs=false -o "${BINARY_NAME}" .
else
echo "Building for current platform..."
go build -buildvcs=false -o "${BINARY_NAME}" .
fi
# Make it executable (skip on Windows as it's not needed)
if [ "${GOOS:-}" != "windows" ]; then
chmod +x "${BINARY_NAME}"
fi
echo "Build completed successfully!"
echo "Binary location: $(pwd)/${BINARY_NAME}"
# Only show usage info if not cross-compiling
if [ -z "${GOOS:-}" ] || [ "${GOOS}" = "$(go env GOOS)" ]; then
echo ""
echo "Prerequisites:"
echo " 1. Install Temporal CLI: brew install temporal"
echo " 2. Start Temporal server: temporal server start-dev"
echo ""
echo "To run the service:"
echo " ./${BINARY_NAME}"
echo ""
echo "Environment variables:"
echo " PORT - HTTP port (default: 8080)"
fi

View file

@ -1,117 +0,0 @@
#!/bin/bash
# Example usage script for the Temporal service
set -e
echo "Temporal Service Example Usage"
echo "=============================="
echo ""
# Check if service is running
if ! curl -s http://localhost:8080/health > /dev/null; then
echo "Starting Temporal service..."
echo "Please run in another terminal: ./temporal-service"
echo "Then run this script again."
exit 1
fi
echo "✓ Temporal service is running"
echo ""
# Create example recipe
RECIPE_FILE="/tmp/example-recipe.yaml"
cat > $RECIPE_FILE << EOF
version: "1.0.0"
title: "Daily Report Generator"
description: "Generates a daily report"
prompt: |
Generate a daily report with the following information:
- Current date and time
- System status
- Recent activity summary
Please format the output as a structured report.
EOF
echo "Created example recipe: $RECIPE_FILE"
echo ""
# Function to make API calls
make_api_call() {
local action="$1"
local job_id="$2"
local cron="$3"
local recipe_path="$4"
local payload="{\"action\": \"$action\""
if [ -n "$job_id" ]; then
payload="$payload, \"job_id\": \"$job_id\""
fi
if [ -n "$cron" ]; then
payload="$payload, \"cron\": \"$cron\""
fi
if [ -n "$recipe_path" ]; then
payload="$payload, \"recipe_path\": \"$recipe_path\""
fi
payload="$payload}"
echo "API Call: $payload"
curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d "$payload" | jq .
echo ""
}
# Example 1: Create a daily job
echo "1. Creating a daily job (runs at 9 AM every day)..."
make_api_call "create" "daily-report" "0 9 * * *" "$RECIPE_FILE"
# Example 2: Create an hourly job
echo "2. Creating an hourly job..."
make_api_call "create" "hourly-check" "0 * * * *" "$RECIPE_FILE"
# Example 3: List all jobs
echo "3. Listing all scheduled jobs..."
make_api_call "list"
# Example 4: Pause a job
echo "4. Pausing the hourly job..."
make_api_call "pause" "hourly-check"
# Example 5: List jobs again to see paused status
echo "5. Listing jobs to see paused status..."
make_api_call "list"
# Example 6: Unpause the job
echo "6. Unpausing the hourly job..."
make_api_call "unpause" "hourly-check"
# Example 7: Run a job immediately
echo "7. Running daily-report job immediately..."
echo "Note: This will fail without goose-scheduler-executor binary"
make_api_call "run_now" "daily-report"
# Example 8: Delete jobs
echo "8. Cleaning up - deleting jobs..."
make_api_call "delete" "daily-report"
make_api_call "delete" "hourly-check"
# Example 9: Final list (should be empty)
echo "9. Final job list (should be empty)..."
make_api_call "list"
# Clean up
rm -f $RECIPE_FILE
echo "Example completed!"
echo ""
echo "Common cron expressions:"
echo " '0 9 * * *' - Daily at 9 AM"
echo " '0 */6 * * *' - Every 6 hours"
echo " '*/15 * * * *' - Every 15 minutes"
echo " '0 0 * * 0' - Weekly on Sunday at midnight"
echo " '0 0 1 * *' - Monthly on the 1st at midnight"

View file

@ -1,35 +0,0 @@
module temporal-service
go 1.23.0
require (
go.temporal.io/api v1.46.0
go.temporal.io/sdk v1.34.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/nexus-rpc/sdk-go v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/grpc v1.66.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -1,180 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA=
github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.temporal.io/api v1.46.0 h1:O1efPDB6O2B8uIeCDIa+3VZC7tZMvYsMZYQapSbHvCg=
go.temporal.io/api v1.46.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
go.temporal.io/sdk v1.34.0 h1:VLg/h6ny7GvLFVoQPqz2NcC93V9yXboQwblkRvZ1cZE=
go.temporal.io/sdk v1.34.0/go.mod h1:iE4U5vFrH3asOhqpBBphpj9zNtw8btp8+MSaf5A0D3w=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -1,573 +0,0 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/workflow"
"go.temporal.io/sdk/temporal"
"gopkg.in/yaml.v2"
)
// Recipe represents the structure we need from recipe files
type Recipe struct {
Title string `json:"title" yaml:"title"`
Description string `json:"description" yaml:"description"`
Instructions *string `json:"instructions" yaml:"instructions"`
Prompt *string `json:"prompt" yaml:"prompt"`
}
// Workflow definition for executing Goose recipes
func GooseJobWorkflow(ctx workflow.Context, jobID, recipePath string) (string, error) {
logger := workflow.GetLogger(ctx)
logger.Info("Starting Goose job workflow", "jobID", jobID, "recipePath", recipePath)
ao := workflow.ActivityOptions{
StartToCloseTimeout: 2 * time.Hour, // Allow up to 2 hours for job execution
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Minute,
MaximumAttempts: 3,
NonRetryableErrorTypes: []string{"InvalidRecipeError"},
},
}
ctx = workflow.WithActivityOptions(ctx, ao)
var sessionID string
err := workflow.ExecuteActivity(ctx, ExecuteGooseRecipe, jobID, recipePath).Get(ctx, &sessionID)
if err != nil {
logger.Error("Goose job workflow failed", "jobID", jobID, "error", err)
return "", err
}
logger.Info("Goose job workflow completed", "jobID", jobID, "sessionID", sessionID)
return sessionID, nil
}
// Activity definition for executing Goose recipes with proper cancellation handling
func ExecuteGooseRecipe(ctx context.Context, jobID, recipePath string) (string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Executing Goose recipe", "jobID", jobID, "recipePath", recipePath)
// Mark job as running at the start
if globalService != nil {
globalService.markJobAsRunning(jobID)
// Ensure we mark it as not running when we're done
defer globalService.markJobAsNotRunning(jobID)
}
// Resolve the actual recipe path (might be embedded in metadata)
actualRecipePath, err := resolveRecipePath(jobID, recipePath)
if err != nil {
return "", temporal.NewNonRetryableApplicationError(
fmt.Sprintf("failed to resolve recipe: %v", err),
"InvalidRecipeError",
err,
)
}
// Check if recipe file exists
if _, err := os.Stat(actualRecipePath); os.IsNotExist(err) {
return "", temporal.NewNonRetryableApplicationError(
fmt.Sprintf("recipe file not found: %s", actualRecipePath),
"InvalidRecipeError",
err,
)
}
// Create a cancellable context for the subprocess
subCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Monitor for activity cancellation
go func() {
select {
case <-ctx.Done():
logger.Info("Activity cancelled, killing process for job", "jobID", jobID)
globalProcessManager.KillProcess(jobID)
case <-subCtx.Done():
// Normal completion
}
}()
// Check if this is a foreground job
if isForegroundJob(actualRecipePath) {
logger.Info("Executing foreground job with cancellation support", "jobID", jobID)
return executeForegroundJobWithCancellation(subCtx, jobID, actualRecipePath)
}
// For background jobs, execute with cancellation support
logger.Info("Executing background job with cancellation support", "jobID", jobID)
return executeBackgroundJobWithCancellation(subCtx, jobID, actualRecipePath)
}
// resolveRecipePath resolves the actual recipe path, handling embedded recipes
func resolveRecipePath(jobID, recipePath string) (string, error) {
// If the recipe path exists as-is, use it
if _, err := os.Stat(recipePath); err == nil {
return recipePath, nil
}
// Try to get embedded recipe content from schedule metadata
if globalService != nil {
if recipeContent, err := globalService.getEmbeddedRecipeContent(jobID); err == nil && recipeContent != "" {
// Create a temporary file with the embedded content
tempPath := filepath.Join(globalService.recipesDir, fmt.Sprintf("%s-temp.yaml", jobID))
if err := os.WriteFile(tempPath, []byte(recipeContent), 0644); err != nil {
return "", fmt.Errorf("failed to write temporary recipe file: %w", err)
}
log.Printf("Created temporary recipe file for job %s: %s", jobID, tempPath)
return tempPath, nil
}
}
// If no embedded content and original path doesn't exist, return error
return "", fmt.Errorf("recipe not found: %s (and no embedded content available)", recipePath)
}
// executeBackgroundJobWithCancellation handles background job execution with proper process management
func executeBackgroundJobWithCancellation(ctx context.Context, jobID, recipePath string) (string, error) {
log.Printf("Executing background job %s using recipe file: %s", jobID, recipePath)
// Find the goose CLI binary
goosePath, err := findGooseBinary()
if err != nil {
return "", fmt.Errorf("failed to find goose CLI binary: %w", err)
}
// Generate session name for this scheduled job
sessionName := fmt.Sprintf("scheduled-%s", jobID)
// Create command with context for cancellation
cmd := exec.CommandContext(ctx, goosePath, "run",
"--recipe", recipePath,
"--name", sessionName,
"--scheduled-job-id", jobID,
)
// Set up process group for proper cleanup
configureSysProcAttr(cmd)
// Set up environment
cmd.Env = append(os.Environ(),
fmt.Sprintf("GOOSE_JOB_ID=%s", jobID),
)
log.Printf("Starting background CLI job %s with session %s", jobID, sessionName)
// Start the process
if err := cmd.Start(); err != nil {
return "", fmt.Errorf("failed to start background CLI execution: %w", err)
}
// Register the process with the process manager
_, cancel := context.WithCancel(ctx)
globalProcessManager.AddProcess(jobID, cmd.Process, cancel)
// Ensure cleanup
defer func() {
globalProcessManager.RemoveProcess(jobID)
cancel()
}()
// Wait for completion or cancellation
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-ctx.Done():
// Context cancelled - kill the process
log.Printf("Background job %s cancelled, killing process", jobID)
globalProcessManager.KillProcess(jobID)
return "", ctx.Err()
case err := <-done:
if err != nil {
log.Printf("Background CLI job %s failed: %v", jobID, err)
return "", fmt.Errorf("background CLI execution failed: %w", err)
}
log.Printf("Background CLI job %s completed successfully with session %s", jobID, sessionName)
return sessionName, nil
}
}
// executeForegroundJobWithCancellation handles foreground job execution with proper process management
func executeForegroundJobWithCancellation(ctx context.Context, jobID, recipePath string) (string, error) {
log.Printf("Executing foreground job %s with recipe %s", jobID, recipePath)
// Parse the recipe file first
recipe, err := parseRecipeFile(recipePath)
if err != nil {
return "", fmt.Errorf("failed to parse recipe file: %w", err)
}
// Check if desktop app is running
if isDesktopAppRunning() {
log.Printf("Desktop app is running, using GUI mode for job %s", jobID)
return executeForegroundJobGUIWithCancellation(ctx, jobID, recipe)
}
// Desktop app not running, fall back to CLI
log.Printf("Desktop app not running, falling back to CLI mode for job %s", jobID)
return executeForegroundJobCLIWithCancellation(ctx, jobID, recipe, recipePath)
}
// executeForegroundJobGUIWithCancellation handles GUI execution with cancellation
func executeForegroundJobGUIWithCancellation(ctx context.Context, jobID string, recipe *Recipe) (string, error) {
// Generate session name for this scheduled job
sessionName := fmt.Sprintf("scheduled-%s", jobID)
// Generate deep link with session name
deepLink, err := generateDeepLink(recipe, jobID, sessionName)
if err != nil {
return "", fmt.Errorf("failed to generate deep link: %w", err)
}
// Open the deep link
if err := openDeepLink(deepLink); err != nil {
return "", fmt.Errorf("failed to open deep link: %w", err)
}
log.Printf("Foreground GUI job %s initiated with session %s, waiting for completion...", jobID, sessionName)
// Wait for session completion with cancellation support
err = waitForSessionCompletionWithCancellation(ctx, sessionName, 2*time.Hour)
if err != nil {
if ctx.Err() != nil {
log.Printf("GUI session %s cancelled", sessionName)
return "", ctx.Err()
}
return "", fmt.Errorf("GUI session failed or timed out: %w", err)
}
log.Printf("Foreground GUI job %s completed successfully with session %s", jobID, sessionName)
return sessionName, nil
}
// executeForegroundJobCLIWithCancellation handles CLI execution with cancellation
func executeForegroundJobCLIWithCancellation(ctx context.Context, jobID string, recipe *Recipe, recipePath string) (string, error) {
log.Printf("Executing job %s via CLI fallback using recipe file: %s", jobID, recipePath)
// Find the goose CLI binary
goosePath, err := findGooseBinary()
if err != nil {
return "", fmt.Errorf("failed to find goose CLI binary: %w", err)
}
// Generate session name for this scheduled job
sessionName := fmt.Sprintf("scheduled-%s", jobID)
// Create command with context for cancellation
cmd := exec.CommandContext(ctx, goosePath, "run",
"--recipe", recipePath,
"--name", sessionName,
"--scheduled-job-id", jobID,
)
// Set up process group for proper cleanup
configureSysProcAttr(cmd)
// Set up environment
cmd.Env = append(os.Environ(),
fmt.Sprintf("GOOSE_JOB_ID=%s", jobID),
)
log.Printf("Starting foreground CLI job %s with session %s", jobID, sessionName)
// Start the process
if err := cmd.Start(); err != nil {
return "", fmt.Errorf("failed to start foreground CLI execution: %w", err)
}
// Register the process with the process manager
_, cancel := context.WithCancel(ctx)
globalProcessManager.AddProcess(jobID, cmd.Process, cancel)
// Ensure cleanup
defer func() {
globalProcessManager.RemoveProcess(jobID)
cancel()
}()
// Wait for completion or cancellation
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-ctx.Done():
// Context cancelled - kill the process
log.Printf("Foreground CLI job %s cancelled, killing process", jobID)
globalProcessManager.KillProcess(jobID)
return "", ctx.Err()
case err := <-done:
if err != nil {
log.Printf("Foreground CLI job %s failed: %v", jobID, err)
return "", fmt.Errorf("foreground CLI execution failed: %w", err)
}
log.Printf("Foreground CLI job %s completed successfully with session %s", jobID, sessionName)
return sessionName, nil
}
}
// findGooseBinary locates the goose CLI binary
func findGooseBinary() (string, error) {
// Try different possible locations
possiblePaths := []string{
"goose", // In PATH
"./goose", // Current directory
"../goose", // Parent directory
}
// Also try relative to the current executable
if exePath, err := os.Executable(); err == nil {
exeDir := filepath.Dir(exePath)
possiblePaths = append(possiblePaths,
filepath.Join(exeDir, "goose"),
filepath.Join(exeDir, "..", "goose"),
)
}
for _, path := range possiblePaths {
if _, err := exec.LookPath(path); err == nil {
return path, nil
}
// Also check if file exists directly
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", fmt.Errorf("goose CLI binary not found in any of: %v", possiblePaths)
}
// isDesktopAppRunning checks if the Goose desktop app is currently running
func isDesktopAppRunning() bool {
log.Println("Checking if desktop app is running...")
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("pgrep", "-f", "Goose.app")
case "windows":
cmd = exec.Command("tasklist", "/FI", "IMAGENAME eq Goose.exe")
case "linux":
cmd = exec.Command("pgrep", "-f", "goose")
default:
log.Printf("Unsupported OS: %s", runtime.GOOS)
return false
}
output, err := cmd.Output()
if err != nil {
log.Printf("Failed to check if desktop app is running: %v", err)
return false
}
var isRunning bool
switch runtime.GOOS {
case "darwin", "linux":
isRunning = len(output) > 0
case "windows":
isRunning = strings.Contains(string(output), "Goose.exe")
}
log.Printf("Desktop app running: %v", isRunning)
return isRunning
}
// parseRecipeFile parses a recipe file (YAML or JSON)
func parseRecipeFile(recipePath string) (*Recipe, error) {
content, err := os.ReadFile(recipePath)
if err != nil {
return nil, err
}
var recipe Recipe
// Try YAML first, then JSON
if err := yaml.Unmarshal(content, &recipe); err != nil {
if err := json.Unmarshal(content, &recipe); err != nil {
return nil, fmt.Errorf("failed to parse as YAML or JSON: %w", err)
}
}
return &recipe, nil
}
// generateDeepLink creates a deep link for the recipe with session name
func generateDeepLink(recipe *Recipe, jobID, sessionName string) (string, error) {
// Create the recipe config for the deep link
recipeConfig := map[string]interface{}{
"id": jobID,
"title": recipe.Title,
"description": recipe.Description,
"instructions": recipe.Instructions,
"activities": []string{}, // Empty activities array
"prompt": recipe.Prompt,
"sessionName": sessionName, // Include session name for proper tracking
}
// Encode the config as JSON then base64
configJSON, err := json.Marshal(recipeConfig)
if err != nil {
return "", err
}
configBase64 := base64.StdEncoding.EncodeToString(configJSON)
// Create the deep link URL with scheduled job ID parameter
deepLink := fmt.Sprintf("goose://recipe?config=%s&scheduledJob=%s", configBase64, jobID)
log.Printf("Generated deep link for job %s with session %s (length: %d)", jobID, sessionName, len(deepLink))
return deepLink, nil
}
// openDeepLink opens a deep link using the system's default protocol handler
func openDeepLink(deepLink string) error {
log.Printf("Opening deep link: %s", deepLink)
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", deepLink)
case "windows":
cmd = exec.Command("cmd", "/c", "start", "", deepLink)
case "linux":
cmd = exec.Command("xdg-open", deepLink)
default:
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to open deep link: %w", err)
}
log.Println("Deep link opened successfully")
return nil
}
// waitForSessionCompletionWithCancellation polls for session completion with cancellation support
func waitForSessionCompletionWithCancellation(ctx context.Context, sessionName string, timeout time.Duration) error {
log.Printf("Waiting for session %s to complete (timeout: %v)", sessionName, timeout)
start := time.Now()
ticker := time.NewTicker(10 * time.Second) // Check every 10 seconds
defer ticker.Stop()
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-timeoutCtx.Done():
if timeoutCtx.Err() == context.DeadlineExceeded {
return fmt.Errorf("session %s timed out after %v", sessionName, timeout)
}
return timeoutCtx.Err() // Cancelled
case <-ticker.C:
elapsed := time.Since(start)
log.Printf("Checking session %s status (elapsed: %v)", sessionName, elapsed)
// Check if session exists and is complete
complete, err := isSessionComplete(sessionName)
if err != nil {
log.Printf("Error checking session %s status: %v", sessionName, err)
// Continue polling - session might not be created yet
continue
}
if complete {
log.Printf("Session %s completed after %v", sessionName, elapsed)
return nil
}
log.Printf("Session %s still running (elapsed: %v)", sessionName, elapsed)
}
}
}
// isSessionComplete checks if a session is complete by querying the Goose sessions API
func isSessionComplete(sessionName string) (bool, error) {
// Try to find the goose CLI binary to query session status
goosePath, err := findGooseBinary()
if err != nil {
return false, fmt.Errorf("failed to find goose CLI binary: %w", err)
}
// Use goose CLI to list sessions and check if our session exists and is complete
cmd := exec.Command(goosePath, "sessions", "list", "--format", "json")
output, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("failed to list sessions: %w", err)
}
// Parse the JSON output to find our session
var sessions []map[string]interface{}
if err := json.Unmarshal(output, &sessions); err != nil {
return false, fmt.Errorf("failed to parse sessions JSON: %w", err)
}
// Look for our session by name
for _, session := range sessions {
if name, ok := session["name"].(string); ok && name == sessionName {
// Session exists, check if it's complete
// A session is considered complete if it's not currently active
// We can check this by looking for an "active" field or similar
if active, ok := session["active"].(bool); ok {
return !active, nil // Complete if not active
}
// If no active field, check for completion indicators
// This might vary based on the actual Goose CLI output format
if status, ok := session["status"].(string); ok {
return status == "completed" || status == "finished" || status == "done", nil
}
// If we found the session but can't determine status, assume it's still running
return false, nil
}
}
// Session not found - it might not be created yet, so not complete
return false, nil
}
// isForegroundJob checks if a recipe is configured for foreground execution
func isForegroundJob(recipePath string) bool {
// Simple struct to just check the schedule.foreground field
type ScheduleConfig struct {
Foreground bool `json:"foreground" yaml:"foreground"`
}
type MinimalRecipe struct {
Schedule *ScheduleConfig `json:"schedule" yaml:"schedule"`
}
content, err := os.ReadFile(recipePath)
if err != nil {
return false // Default to background if we can't read
}
var recipe MinimalRecipe
// Try YAML first, then JSON
if err := yaml.Unmarshal(content, &recipe); err != nil {
if err := json.Unmarshal(content, &recipe); err != nil {
return false // Default to background if we can't parse
}
}
return recipe.Schedule != nil && recipe.Schedule.Foreground
}

View file

@ -1,411 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client"
)
const (
TaskQueueName = "goose-task-queue"
Namespace = "default"
)
// PortConfig holds the port configuration for Temporal services
type PortConfig struct {
TemporalPort int // Main Temporal server port
UIPort int // Temporal UI port
HTTPPort int // HTTP API port
}
// getManagedRecipesDir returns the proper directory for storing managed recipes
func getManagedRecipesDir() (string, error) {
var baseDir string
switch runtime.GOOS {
case "darwin":
// macOS: ~/Library/Application Support/temporal/managed-recipes
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
baseDir = filepath.Join(homeDir, "Library", "Application Support", "temporal", "managed-recipes")
case "linux":
// Linux: ~/.local/share/temporal/managed-recipes
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
baseDir = filepath.Join(homeDir, ".local", "share", "temporal", "managed-recipes")
case "windows":
// Windows: %APPDATA%\temporal\managed-recipes
appDataDir := os.Getenv("APPDATA")
if appDataDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
appDataDir = filepath.Join(homeDir, "AppData", "Roaming")
}
baseDir = filepath.Join(appDataDir, "temporal", "managed-recipes")
default:
// Fallback for unknown OS
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
baseDir = filepath.Join(homeDir, ".local", "share", "temporal", "managed-recipes")
}
return baseDir, nil
}
// findAvailablePort finds an available port starting from the given port
func findAvailablePort(startPort int) (int, error) {
for port := startPort; port < startPort+100; port++ {
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err == nil {
ln.Close()
return port, nil
}
}
return 0, fmt.Errorf("no available port found starting from %d", startPort)
}
// findAvailablePorts finds available ports for all Temporal services
func findAvailablePorts() (*PortConfig, error) {
// Try to find available ports starting from preferred defaults
temporalPort, err := findAvailablePort(7233)
if err != nil {
return nil, fmt.Errorf("failed to find available port for Temporal server: %w", err)
}
uiPort, err := findAvailablePort(8233)
if err != nil {
return nil, fmt.Errorf("failed to find available port for Temporal UI: %w", err)
}
// For HTTP port, check environment variable first
httpPort := 8080
if portEnv := os.Getenv("PORT"); portEnv != "" {
if parsed, err := strconv.Atoi(portEnv); err == nil {
httpPort = parsed
}
}
// Verify HTTP port is available, find alternative if not
finalHTTPPort, err := findAvailablePort(httpPort)
if err != nil {
return nil, fmt.Errorf("failed to find available port for HTTP server: %w", err)
}
return &PortConfig{
TemporalPort: temporalPort,
UIPort: uiPort,
HTTPPort: finalHTTPPort,
}, nil
}
// isTemporalServerRunning checks if Temporal server is accessible
func isTemporalServerRunning(port int) bool {
// Try to create a client connection to check if server is running
c, err := client.Dial(client.Options{
HostPort: fmt.Sprintf("127.0.0.1:%d", port),
Namespace: Namespace,
})
if err != nil {
return false
}
defer c.Close()
// Try a simple operation to verify the connection works
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err = c.WorkflowService().GetSystemInfo(ctx, &workflowservice.GetSystemInfoRequest{})
return err == nil
}
// findTemporalCLI attempts to find the temporal CLI binary
func findTemporalCLI() (string, error) {
log.Println("Looking for temporal CLI binary...")
// First, try to find temporal in PATH using exec.LookPath
log.Println("Checking PATH for temporal CLI...")
if path, err := exec.LookPath("temporal"); err == nil {
log.Printf("Found temporal in PATH at: %s", path)
// Verify it's the correct temporal CLI by checking version
log.Println("Verifying temporal CLI version...")
cmd := exec.Command(path, "--version")
if err := cmd.Run(); err == nil {
log.Printf("Successfully verified temporal CLI at: %s", path)
return path, nil
} else {
log.Printf("Failed to verify temporal CLI at %s: %v", path, err)
}
} else {
log.Printf("temporal not found in PATH: %v", err)
}
// Try using 'which' command to find temporal
cmd := exec.Command("which", "temporal")
if output, err := cmd.Output(); err == nil {
path := strings.TrimSpace(string(output))
if path != "" {
// Verify it's the correct temporal CLI by checking version
cmd := exec.Command(path, "--version")
if err := cmd.Run(); err == nil {
return path, nil
}
}
}
// If not found in PATH, try different possible locations for the temporal CLI
log.Println("Checking bundled/local locations for temporal CLI...")
currentPaths := []string{
"./temporal",
"./temporal.exe",
}
if path, err := getExistingTemporalCLIFrom(currentPaths); err == nil {
return path, nil
} else {
log.Printf("Attempt to find in local directory failed: %s.", err)
}
// Also try relative to the current executable (most important for bundled apps)
exePath, err := os.Executable()
if err != nil {
log.Printf("Failed to get executable path: %v", err)
}
exeDir := filepath.Dir(exePath)
log.Printf("Executable directory: %s", exeDir)
additionalPaths := []string{
filepath.Join(exeDir, "temporal"),
filepath.Join(exeDir, "temporal.exe"), // Windows
// Also try one level up (for development)
filepath.Join(exeDir, "..", "temporal"),
filepath.Join(exeDir, "..", "temporal.exe"),
}
log.Printf("Will check these additional paths: %v", additionalPaths)
return getExistingTemporalCLIFrom(additionalPaths)
}
// getExistingTemporalCLIFrom gets a list of paths and returns one of those that is an existing and working Temporal CLI binary
func getExistingTemporalCLIFrom(possiblePaths []string) (string, error) {
log.Printf("Checking %d possible paths for temporal CLI", len(possiblePaths))
// Check all possible paths in parallel, pick the first one that works.
pathFound := make(chan string)
var wg sync.WaitGroup
// This allows us to cancel whatever remaining work is done when we find a valid path.
psCtx, psCancel := context.WithCancel(context.Background())
for i, path := range possiblePaths {
wg.Add(1)
go func() {
defer wg.Done()
log.Printf("Checking path %d/%d: %s", i+1, len(possiblePaths), path)
if _, err := os.Stat(path); err != nil {
log.Printf("File does not exist at %s: %v", path, err)
return
}
log.Printf("File exists at: %s", path)
// File exists, test if it's executable and the right binary
cmd := exec.CommandContext(psCtx, path, "--version")
if err := cmd.Run(); err != nil {
log.Printf("Failed to verify temporal CLI at %s: %v", path, err)
return
}
select {
case pathFound <- path:
log.Printf("Successfully verified temporal CLI at: %s", path)
case <-psCtx.Done():
// No need to report the path not chosen.
}
}()
}
// We transform the workgroup wait into a channel so we can wait for either this or pathFound
pathNotFound := make(chan bool)
go func() {
wg.Wait()
pathNotFound <- true
}()
select {
case path := <-pathFound:
psCancel() // Cancel the remaining search functions otherwise they'll just exist eternally.
return path, nil
case <-pathNotFound:
// No need to do anything, this just says that none of the functions were able to do it and there's nothing left to cleanup
}
return "", fmt.Errorf("temporal CLI not found in PATH or any of the expected locations: %v", possiblePaths)
}
// ensureTemporalServerRunning checks if Temporal server is running and starts it if needed
func ensureTemporalServerRunning(ports *PortConfig) error {
log.Println("Checking if Temporal server is running...")
// Check if Temporal server is already running by trying to connect
if isTemporalServerRunning(ports.TemporalPort) {
log.Printf("Temporal server is already running on port %d", ports.TemporalPort)
return nil
}
log.Printf("Temporal server not running, attempting to start it on port %d...", ports.TemporalPort)
// Find the temporal CLI binary
temporalCmd, err := findTemporalCLI()
if err != nil {
log.Printf("ERROR: Could not find temporal CLI: %v", err)
return fmt.Errorf("could not find temporal CLI: %w", err)
}
log.Printf("Using Temporal CLI at: %s", temporalCmd)
// Start Temporal server in background
args := []string{"server", "start-dev",
"--db-filename", "temporal.db",
"--port", strconv.Itoa(ports.TemporalPort),
"--ui-port", strconv.Itoa(ports.UIPort),
"--log-level", "warn"}
log.Printf("Starting Temporal server with command: %s %v", temporalCmd, args)
cmd := exec.Command(temporalCmd, args...)
// Properly detach the process so it survives when the parent exits
configureSysProcAttr(cmd)
// Redirect stdin/stdout/stderr to avoid hanging
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
// Start the process
if err := cmd.Start(); err != nil {
log.Printf("ERROR: Failed to start Temporal server: %v", err)
return fmt.Errorf("failed to start Temporal server: %w", err)
}
log.Printf("Temporal server started with PID: %d (port: %d, UI port: %d)",
cmd.Process.Pid, ports.TemporalPort, ports.UIPort)
// Wait for server to be ready (with timeout)
log.Println("Waiting for Temporal server to be ready...")
timeout := time.After(30 * time.Second)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
attemptCount := 0
for {
select {
case <-timeout:
log.Printf("ERROR: Timeout waiting for Temporal server to start after %d attempts", attemptCount)
return fmt.Errorf("timeout waiting for Temporal server to start")
case <-ticker.C:
attemptCount++
log.Printf("Checking if Temporal server is ready (attempt %d)...", attemptCount)
if isTemporalServerRunning(ports.TemporalPort) {
log.Printf("Temporal server is now ready on port %d", ports.TemporalPort)
return nil
} else {
log.Printf("Temporal server not ready yet (attempt %d)", attemptCount)
}
}
}
}
func main() {
log.Println("Starting Temporal service...")
log.Printf("Runtime OS: %s", runtime.GOOS)
log.Printf("Runtime ARCH: %s", runtime.GOARCH)
// Log current working directory for debugging
if cwd, err := os.Getwd(); err == nil {
log.Printf("Current working directory: %s", cwd)
}
// Log environment variables that might affect behavior
if port := os.Getenv("PORT"); port != "" {
log.Printf("PORT environment variable: %s", port)
}
if rustLog := os.Getenv("RUST_LOG"); rustLog != "" {
log.Printf("RUST_LOG environment variable: %s", rustLog)
}
if temporalLog := os.Getenv("TEMPORAL_LOG_LEVEL"); temporalLog != "" {
log.Printf("TEMPORAL_LOG_LEVEL environment variable: %s", temporalLog)
}
// Create Temporal service (this will find available ports automatically)
log.Println("Creating Temporal service...")
service, err := NewTemporalService()
if err != nil {
log.Printf("ERROR: Failed to create Temporal service: %v", err)
log.Fatalf("Failed to create Temporal service: %v", err)
}
log.Println("✓ Temporal service created successfully")
// Use the dynamically assigned HTTP port
httpPort := service.GetHTTPPort()
temporalPort := service.GetTemporalPort()
uiPort := service.GetUIPort()
log.Printf("Temporal server running on port %d", temporalPort)
log.Printf("Temporal UI available at http://localhost:%d", uiPort)
// Set up HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/jobs", service.handleJobs)
mux.HandleFunc("/health", service.handleHealth)
mux.HandleFunc("/ports", service.handlePorts)
server := &http.Server{
Addr: fmt.Sprintf(":%d", httpPort),
Handler: mux,
}
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Received shutdown signal")
// Kill all managed processes first
globalProcessManager.KillAllProcesses()
// Shutdown HTTP server
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(ctx)
// Stop Temporal service
service.Stop()
os.Exit(0)
}()
log.Printf("Temporal service starting on port %d", httpPort)
log.Printf("Health endpoint: http://localhost:%d/health", httpPort)
log.Printf("Jobs endpoint: http://localhost:%d/jobs", httpPort)
log.Printf("Ports endpoint: http://localhost:%d/ports", httpPort)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server failed: %v", err)
}
}

View file

@ -1,281 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
// ProcessManager tracks and manages spawned processes
type ProcessManager struct {
processes map[string]*ManagedProcess
mutex sync.RWMutex
}
// ManagedProcess represents a process being managed by the ProcessManager
type ManagedProcess struct {
JobID string
Process *os.Process
Cancel context.CancelFunc
StartTime time.Time
}
// Global process manager instance
var globalProcessManager = &ProcessManager{
processes: make(map[string]*ManagedProcess),
}
// AddProcess adds a process to be managed
func (pm *ProcessManager) AddProcess(jobID string, process *os.Process, cancel context.CancelFunc) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.processes[jobID] = &ManagedProcess{
JobID: jobID,
Process: process,
Cancel: cancel,
StartTime: time.Now(),
}
log.Printf("Added process %d for job %s to process manager", process.Pid, jobID)
}
// RemoveProcess removes a process from management
func (pm *ProcessManager) RemoveProcess(jobID string) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
if mp, exists := pm.processes[jobID]; exists {
log.Printf("Removed process %d for job %s from process manager", mp.Process.Pid, jobID)
delete(pm.processes, jobID)
}
}
// KillProcess kills a specific process and its children
func (pm *ProcessManager) KillProcess(jobID string) error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
mp, exists := pm.processes[jobID]
if !exists {
return fmt.Errorf("no process found for job %s", jobID)
}
log.Printf("Killing process %d for job %s", mp.Process.Pid, jobID)
// Cancel the context first
if mp.Cancel != nil {
mp.Cancel()
}
// Kill the process and its children
if err := killProcessGroup(mp.Process); err != nil {
log.Printf("Error killing process group for job %s: %v", jobID, err)
return err
}
delete(pm.processes, jobID)
return nil
}
// KillAllProcesses kills all managed processes
func (pm *ProcessManager) KillAllProcesses() {
pm.mutex.Lock()
defer pm.mutex.Unlock()
log.Printf("Killing all %d managed processes", len(pm.processes))
for jobID, mp := range pm.processes {
log.Printf("Killing process %d for job %s", mp.Process.Pid, jobID)
if mp.Cancel != nil {
mp.Cancel()
}
if err := killProcessGroup(mp.Process); err != nil {
log.Printf("Error killing process group for job %s: %v", jobID, err)
}
}
pm.processes = make(map[string]*ManagedProcess)
}
// ListProcesses returns a copy of the current process map
func (pm *ProcessManager) ListProcesses() map[string]*ManagedProcess {
pm.mutex.RLock()
defer pm.mutex.RUnlock()
result := make(map[string]*ManagedProcess)
for k, v := range pm.processes {
result[k] = v
}
return result
}
// killProcessGroup kills a process and all its children
func killProcessGroup(process *os.Process) error {
if process == nil {
return nil
}
pid := process.Pid
log.Printf("Attempting to kill process group for PID %d", pid)
switch runtime.GOOS {
case "windows":
// On Windows, kill the process tree
return killProcessGroupByPID(pid, 0) // signal parameter not used on Windows
default:
// On Unix-like systems, kill the process group more aggressively
log.Printf("Killing Unix process group for PID %d", pid)
// First, try to kill the entire process group with SIGTERM
if err := killProcessGroupByPID(pid, syscall.SIGTERM); err != nil {
log.Printf("Failed to send SIGTERM to process group -%d: %v", pid, err)
} else {
log.Printf("Sent SIGTERM to process group -%d", pid)
}
// Also try to kill the main process directly
if err := killProcessByPID(pid, syscall.SIGTERM); err != nil {
log.Printf("Failed to send SIGTERM to process %d: %v", pid, err)
} else {
log.Printf("Sent SIGTERM to process %d", pid)
}
// Give processes a brief moment to terminate gracefully
time.Sleep(1 * time.Second)
// Force kill the process group with SIGKILL
if err := killProcessGroupByPID(pid, syscall.SIGKILL); err != nil {
log.Printf("Failed to send SIGKILL to process group -%d: %v", pid, err)
} else {
log.Printf("Sent SIGKILL to process group -%d", pid)
}
// Force kill the main process with SIGKILL
if err := killProcessByPID(pid, syscall.SIGKILL); err != nil {
log.Printf("Failed to send SIGKILL to process %d: %v", pid, err)
} else {
log.Printf("Sent SIGKILL to process %d", pid)
}
// Also try using the process.Kill() method as a fallback
if err := process.Kill(); err != nil {
log.Printf("Failed to kill process using process.Kill(): %v", err)
} else {
log.Printf("Successfully killed process using process.Kill()")
}
log.Printf("Completed kill attempts for process group %d", pid)
return nil
}
}
// FindAndKillProcessesByPattern finds and kills processes related to a job by searching for patterns
func FindAndKillProcessesByPattern(jobID string) int {
log.Printf("Searching for additional processes to kill for job %s", jobID)
killedCount := 0
switch runtime.GOOS {
case "darwin", "linux":
// Search for goose processes that might be related to this job
patterns := []string{
fmt.Sprintf("scheduled-%s", jobID), // Session name pattern
fmt.Sprintf("GOOSE_JOB_ID=%s", jobID), // Environment variable pattern
jobID, // Job ID itself
}
for _, pattern := range patterns {
// Use pgrep to find processes
cmd := exec.Command("pgrep", "-f", pattern)
output, err := cmd.Output()
if err != nil {
log.Printf("No processes found for pattern '%s': %v", pattern, err)
continue
}
pidStr := strings.TrimSpace(string(output))
if pidStr == "" {
continue
}
pids := strings.Split(pidStr, "\n")
for _, pidStr := range pids {
if pidStr == "" {
continue
}
pid, err := strconv.Atoi(pidStr)
if err != nil {
log.Printf("Invalid PID '%s': %v", pidStr, err)
continue
}
log.Printf("Found process %d matching pattern '%s' for job %s", pid, pattern, jobID)
// Kill the process
if err := killProcessByPID(pid, syscall.SIGTERM); err != nil {
log.Printf("Failed to send SIGTERM to PID %d: %v", pid, err)
} else {
log.Printf("Sent SIGTERM to PID %d", pid)
killedCount++
}
// Wait a moment then force kill
time.Sleep(500 * time.Millisecond)
if err := killProcessByPID(pid, syscall.SIGKILL); err != nil {
log.Printf("Failed to send SIGKILL to PID %d: %v", pid, err)
} else {
log.Printf("Sent SIGKILL to PID %d", pid)
}
}
}
case "windows":
// On Windows, search for goose.exe processes
sessionPattern := fmt.Sprintf("scheduled-%s", jobID)
// Use tasklist to find processes
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq goose.exe", "/FO", "CSV")
output, err := cmd.Output()
if err != nil {
log.Printf("Failed to list Windows processes: %v", err)
return killedCount
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, sessionPattern) || strings.Contains(line, jobID) {
// Extract PID from CSV format
fields := strings.Split(line, ",")
if len(fields) >= 2 {
pidStr := strings.Trim(fields[1], "\"")
if pid, err := strconv.Atoi(pidStr); err == nil {
log.Printf("Found Windows process %d for job %s", pid, jobID)
// Kill the process
killCmd := exec.Command("taskkill", "/F", "/PID", fmt.Sprintf("%d", pid))
if err := killCmd.Run(); err != nil {
log.Printf("Failed to kill Windows process %d: %v", pid, err)
} else {
log.Printf("Killed Windows process %d", pid)
killedCount++
}
}
}
}
}
}
log.Printf("Killed %d additional processes for job %s", killedCount, jobID)
return killedCount
}

View file

@ -1,716 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"go.temporal.io/sdk/client"
)
type JobStatus struct {
ID string `json:"id"`
CronExpr string `json:"cron"`
RecipePath string `json:"recipe_path"`
LastRun *string `json:"last_run,omitempty"`
NextRun *string `json:"next_run,omitempty"`
CurrentlyRunning bool `json:"currently_running"`
Paused bool `json:"paused"`
CreatedAt time.Time `json:"created_at"`
ExecutionMode *string `json:"execution_mode,omitempty"` // "foreground" or "background"
LastManualRun *string `json:"last_manual_run,omitempty"` // Track manual runs separately
}
// Request/Response types for HTTP API
type JobRequest struct {
Action string `json:"action"` // create, delete, pause, unpause, list, run_now, kill_job, update
JobID string `json:"job_id"`
CronExpr string `json:"cron"`
RecipePath string `json:"recipe_path"`
ExecutionMode string `json:"execution_mode,omitempty"` // "foreground" or "background"
}
type JobResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Jobs []JobStatus `json:"jobs,omitempty"`
Data interface{} `json:"data,omitempty"`
}
type RunNowResponse struct {
SessionID string `json:"session_id"`
}
// createSchedule handles the creation of a new schedule
func (ts *TemporalService) createSchedule(req JobRequest) JobResponse {
if req.JobID == "" || req.CronExpr == "" || req.RecipePath == "" {
return JobResponse{Success: false, Message: "Missing required fields: job_id, cron, recipe_path"}
}
// Check if job already exists
if _, exists := ts.scheduleJobs[req.JobID]; exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job with ID '%s' already exists", req.JobID)}
}
// Validate and copy recipe file to managed storage
managedRecipePath, recipeContent, err := ts.storeRecipeForSchedule(req.JobID, req.RecipePath)
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to store recipe: %v", err)}
}
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
// Prepare metadata to store with the schedule as a JSON string in the Note field
executionMode := req.ExecutionMode
if executionMode == "" {
executionMode = "background" // Default to background if not specified
}
scheduleMetadata := map[string]interface{}{
"job_id": req.JobID,
"cron_expr": req.CronExpr,
"recipe_path": managedRecipePath, // Use managed path
"original_path": req.RecipePath, // Keep original for reference
"execution_mode": executionMode,
"created_at": time.Now().Format(time.RFC3339),
}
// For small recipes, embed content directly in metadata
if len(recipeContent) < 8192 { // 8KB limit for embedding
scheduleMetadata["recipe_content"] = string(recipeContent)
log.Printf("Embedded recipe content in metadata for job %s (size: %d bytes)", req.JobID, len(recipeContent))
} else {
log.Printf("Recipe too large for embedding, using managed file for job %s (size: %d bytes)", req.JobID, len(recipeContent))
}
metadataJSON, err := json.Marshal(scheduleMetadata)
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to encode metadata: %v", err)}
}
// Create Temporal schedule with metadata in Note field
schedule := client.ScheduleOptions{
ID: scheduleID,
Spec: client.ScheduleSpec{
CronExpressions: []string{req.CronExpr},
},
Action: &client.ScheduleWorkflowAction{
ID: fmt.Sprintf("workflow-%s-{{.ScheduledTime.Unix}}", req.JobID),
Workflow: GooseJobWorkflow,
Args: []interface{}{req.JobID, req.RecipePath},
TaskQueue: TaskQueueName,
},
Note: string(metadataJSON), // Store metadata as JSON in the Note field
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err = ts.client.ScheduleClient().Create(ctx, schedule)
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to create schedule: %v", err)}
}
// Track job in memory - ensure execution mode has a default value
jobStatus := &JobStatus{
ID: req.JobID,
CronExpr: req.CronExpr,
RecipePath: req.RecipePath,
CurrentlyRunning: false,
Paused: false,
CreatedAt: time.Now(),
ExecutionMode: &executionMode,
}
ts.scheduleJobs[req.JobID] = jobStatus
log.Printf("Created schedule for job: %s", req.JobID)
return JobResponse{Success: true, Message: "Schedule created successfully"}
}
// deleteSchedule handles the deletion of a schedule
func (ts *TemporalService) deleteSchedule(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
err := handle.Delete(ctx)
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to delete schedule: %v", err)}
}
// Clean up managed recipe files
ts.cleanupManagedRecipe(req.JobID)
// Remove from memory
delete(ts.scheduleJobs, req.JobID)
log.Printf("Deleted schedule for job: %s", req.JobID)
return JobResponse{Success: true, Message: "Schedule deleted successfully"}
}
// pauseSchedule handles pausing a schedule
func (ts *TemporalService) pauseSchedule(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
err := handle.Pause(ctx, client.SchedulePauseOptions{
Note: "Paused via API",
})
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to pause schedule: %v", err)}
}
// Update in memory
if job, exists := ts.scheduleJobs[req.JobID]; exists {
job.Paused = true
}
log.Printf("Paused schedule for job: %s", req.JobID)
return JobResponse{Success: true, Message: "Schedule paused successfully"}
}
// unpauseSchedule handles unpausing a schedule
func (ts *TemporalService) unpauseSchedule(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
err := handle.Unpause(ctx, client.ScheduleUnpauseOptions{
Note: "Unpaused via API",
})
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to unpause schedule: %v", err)}
}
// Update in memory
if job, exists := ts.scheduleJobs[req.JobID]; exists {
job.Paused = false
}
log.Printf("Unpaused schedule for job: %s", req.JobID)
return JobResponse{Success: true, Message: "Schedule unpaused successfully"}
}
// updateSchedule handles updating a schedule
func (ts *TemporalService) updateSchedule(req JobRequest) JobResponse {
if req.JobID == "" || req.CronExpr == "" {
return JobResponse{Success: false, Message: "Missing required fields: job_id, cron"}
}
// Check if job exists
job, exists := ts.scheduleJobs[req.JobID]
if !exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job with ID '%s' not found", req.JobID)}
}
// Check if job is currently running
if job.CurrentlyRunning {
return JobResponse{Success: false, Message: fmt.Sprintf("Cannot update schedule '%s' while it's currently running", req.JobID)}
}
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Get the existing schedule handle
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
// Update the schedule with new cron expression while preserving metadata
err := handle.Update(ctx, client.ScheduleUpdateOptions{
DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) {
// Update the cron expression
input.Description.Schedule.Spec.CronExpressions = []string{req.CronExpr}
// Update the cron expression in metadata stored in Note field
if input.Description.Schedule.State.Note != "" {
var metadata map[string]interface{}
if err := json.Unmarshal([]byte(input.Description.Schedule.State.Note), &metadata); err == nil {
metadata["cron_expr"] = req.CronExpr
if updatedMetadataJSON, err := json.Marshal(metadata); err == nil {
input.Description.Schedule.State.Note = string(updatedMetadataJSON)
}
}
}
return &client.ScheduleUpdate{
Schedule: &input.Description.Schedule,
}, nil
},
})
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to update schedule: %v", err)}
}
// Update in memory
job.CronExpr = req.CronExpr
log.Printf("Updated schedule for job: %s with new cron: %s", req.JobID, req.CronExpr)
return JobResponse{Success: true, Message: "Schedule updated successfully"}
}
// listSchedules lists all schedules
func (ts *TemporalService) listSchedules() JobResponse {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// List all schedules from Temporal
iter, err := ts.client.ScheduleClient().List(ctx, client.ScheduleListOptions{})
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to list schedules: %v", err)}
}
var jobs []JobStatus
for iter.HasNext() {
schedule, err := iter.Next()
if err != nil {
log.Printf("Error listing schedules: %v", err)
continue
}
// Extract job ID from schedule ID
if strings.HasPrefix(schedule.ID, "goose-job-") {
jobID := strings.TrimPrefix(schedule.ID, "goose-job-")
// Get detailed schedule information to access metadata
scheduleHandle := ts.client.ScheduleClient().GetHandle(ctx, schedule.ID)
desc, err := scheduleHandle.Describe(ctx)
if err != nil {
log.Printf("Warning: Could not get detailed info for schedule %s: %v", schedule.ID, err)
continue
}
// Initialize job status with defaults
jobStatus := JobStatus{
ID: jobID,
CurrentlyRunning: ts.isJobCurrentlyRunning(ctx, jobID),
Paused: desc.Schedule.State.Paused,
CreatedAt: time.Now(), // Fallback if not in metadata
}
// Extract metadata from the schedule's Note field (stored as JSON)
if desc.Schedule.State.Note != "" {
var metadata map[string]interface{}
if err := json.Unmarshal([]byte(desc.Schedule.State.Note), &metadata); err == nil {
// Extract cron expression
if cronExpr, ok := metadata["cron_expr"].(string); ok {
jobStatus.CronExpr = cronExpr
} else if len(desc.Schedule.Spec.CronExpressions) > 0 {
// Fallback to spec if not in metadata
jobStatus.CronExpr = desc.Schedule.Spec.CronExpressions[0]
}
// Extract recipe path
if recipePath, ok := metadata["recipe_path"].(string); ok {
jobStatus.RecipePath = recipePath
}
// Extract execution mode
if executionMode, ok := metadata["execution_mode"].(string); ok {
jobStatus.ExecutionMode = &executionMode
}
// Extract creation time
if createdAtStr, ok := metadata["created_at"].(string); ok {
if createdAt, err := time.Parse(time.RFC3339, createdAtStr); err == nil {
jobStatus.CreatedAt = createdAt
}
}
} else {
log.Printf("Failed to parse metadata from Note field for schedule %s: %v", schedule.ID, err)
// Fallback to spec values
if len(desc.Schedule.Spec.CronExpressions) > 0 {
jobStatus.CronExpr = desc.Schedule.Spec.CronExpressions[0]
}
defaultMode := "background"
jobStatus.ExecutionMode = &defaultMode
}
} else {
// Fallback for schedules without metadata (legacy schedules)
log.Printf("Schedule %s has no metadata, using fallback values", schedule.ID)
if len(desc.Schedule.Spec.CronExpressions) > 0 {
jobStatus.CronExpr = desc.Schedule.Spec.CronExpressions[0]
}
// For legacy schedules, we can't recover recipe path or execution mode
defaultMode := "background"
jobStatus.ExecutionMode = &defaultMode
}
// Update last run time - use the most recent between scheduled and manual runs
var mostRecentRun *string
// Check scheduled runs from Temporal
if len(desc.Info.RecentActions) > 0 {
lastAction := desc.Info.RecentActions[len(desc.Info.RecentActions)-1]
if !lastAction.ActualTime.IsZero() {
scheduledRunStr := lastAction.ActualTime.Format(time.RFC3339)
mostRecentRun = &scheduledRunStr
log.Printf("Job %s scheduled run: %s", jobID, scheduledRunStr)
}
}
// Check manual runs from our in-memory tracking (if available)
if tracked, exists := ts.scheduleJobs[jobID]; exists && tracked.LastManualRun != nil {
log.Printf("Job %s manual run: %s", jobID, *tracked.LastManualRun)
// Compare times if we have both
if mostRecentRun != nil {
scheduledTime, err1 := time.Parse(time.RFC3339, *mostRecentRun)
manualTime, err2 := time.Parse(time.RFC3339, *tracked.LastManualRun)
if err1 == nil && err2 == nil {
if manualTime.After(scheduledTime) {
mostRecentRun = tracked.LastManualRun
log.Printf("Job %s: manual run is more recent", jobID)
} else {
log.Printf("Job %s: scheduled run is more recent", jobID)
}
}
} else {
// Only manual run available
mostRecentRun = tracked.LastManualRun
log.Printf("Job %s: only manual run available", jobID)
}
}
if mostRecentRun != nil {
jobStatus.LastRun = mostRecentRun
} else {
log.Printf("Job %s has no runs (scheduled or manual)", jobID)
}
// Update in-memory tracking with latest info for manual run tracking
ts.scheduleJobs[jobID] = &jobStatus
jobs = append(jobs, jobStatus)
}
}
return JobResponse{Success: true, Jobs: jobs}
}
// runNow executes a job immediately
func (ts *TemporalService) runNow(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
// Get job details
job, exists := ts.scheduleJobs[req.JobID]
if !exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' not found", req.JobID)}
}
// Record the manual run time
now := time.Now()
manualRunStr := now.Format(time.RFC3339)
job.LastManualRun = &manualRunStr
log.Printf("Recording manual run for job %s at %s", req.JobID, manualRunStr)
// Execute workflow immediately
workflowOptions := client.StartWorkflowOptions{
ID: fmt.Sprintf("manual-%s-%d", req.JobID, now.Unix()),
TaskQueue: TaskQueueName,
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
we, err := ts.client.ExecuteWorkflow(ctx, workflowOptions, GooseJobWorkflow, req.JobID, job.RecipePath)
if err != nil {
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to start workflow: %v", err)}
}
// Track the workflow for this job
ts.addRunningWorkflow(req.JobID, we.GetID())
// Don't wait for completion in run_now, just return the workflow ID
log.Printf("Manual execution started for job: %s, workflow: %s", req.JobID, we.GetID())
return JobResponse{
Success: true,
Message: "Job execution started",
Data: RunNowResponse{SessionID: we.GetID()}, // Return workflow ID as session ID for now
}
}
// killJob kills a running job
func (ts *TemporalService) killJob(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
// Check if job exists
_, exists := ts.scheduleJobs[req.JobID]
if !exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' not found", req.JobID)}
}
// Check if job is currently running
if !ts.isJobCurrentlyRunning(context.Background(), req.JobID) {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' is not currently running", req.JobID)}
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Printf("Starting kill process for job %s", req.JobID)
// Step 1: Kill managed processes first
processKilled := false
if err := globalProcessManager.KillProcess(req.JobID); err != nil {
log.Printf("Failed to kill managed process for job %s: %v", req.JobID, err)
} else {
log.Printf("Successfully killed managed process for job %s", req.JobID)
processKilled = true
}
// Step 2: Terminate Temporal workflows
workflowsKilled := 0
workflowIDs, exists := ts.runningWorkflows[req.JobID]
if exists && len(workflowIDs) > 0 {
for _, workflowID := range workflowIDs {
// Terminate the workflow
err := ts.client.TerminateWorkflow(ctx, workflowID, "", "Killed by user request")
if err != nil {
log.Printf("Error terminating workflow %s for job %s: %v", workflowID, req.JobID, err)
continue
}
log.Printf("Terminated workflow %s for job %s", workflowID, req.JobID)
workflowsKilled++
}
log.Printf("Terminated %d workflow(s) for job %s", workflowsKilled, req.JobID)
}
// Step 3: Find and kill any remaining processes by name/pattern
additionalKills := FindAndKillProcessesByPattern(req.JobID)
// Step 4: Mark job as not running in our tracking
ts.markJobAsNotRunning(req.JobID)
// Prepare response message
var messages []string
if processKilled {
messages = append(messages, "killed managed process")
}
if workflowsKilled > 0 {
messages = append(messages, fmt.Sprintf("terminated %d workflow(s)", workflowsKilled))
}
if additionalKills > 0 {
messages = append(messages, fmt.Sprintf("killed %d additional process(es)", additionalKills))
}
if len(messages) == 0 {
messages = append(messages, "no active processes found but marked as not running")
}
log.Printf("Killed job: %s (%s)", req.JobID, strings.Join(messages, ", "))
return JobResponse{
Success: true,
Message: fmt.Sprintf("Successfully killed job '%s': %s", req.JobID, strings.Join(messages, ", ")),
}
}
// inspectJob inspects a running job
func (ts *TemporalService) inspectJob(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
// Check if job exists
_, exists := ts.scheduleJobs[req.JobID]
if !exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' not found", req.JobID)}
}
// Check if job is currently running
if !ts.isJobCurrentlyRunning(context.Background(), req.JobID) {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' is not currently running", req.JobID)}
}
// Get process information
processes := globalProcessManager.ListProcesses()
if mp, exists := processes[req.JobID]; exists {
duration := time.Since(mp.StartTime)
inspectData := map[string]interface{}{
"job_id": req.JobID,
"process_id": mp.Process.Pid,
"running_duration": duration.String(),
"running_duration_seconds": int(duration.Seconds()),
"start_time": mp.StartTime.Format(time.RFC3339),
}
// Try to get session ID from workflow tracking
if workflowIDs, exists := ts.runningWorkflows[req.JobID]; exists && len(workflowIDs) > 0 {
inspectData["session_id"] = workflowIDs[0] // Use the first workflow ID as session ID
}
return JobResponse{
Success: true,
Message: fmt.Sprintf("Job '%s' is running", req.JobID),
Data: inspectData,
}
}
// If no managed process found, check workflows only
if workflowIDs, exists := ts.runningWorkflows[req.JobID]; exists && len(workflowIDs) > 0 {
inspectData := map[string]interface{}{
"job_id": req.JobID,
"session_id": workflowIDs[0],
"message": "Job is running but process information not available",
}
return JobResponse{
Success: true,
Message: fmt.Sprintf("Job '%s' is running (workflow only)", req.JobID),
Data: inspectData,
}
}
return JobResponse{
Success: false,
Message: fmt.Sprintf("Job '%s' appears to be running but no process or workflow information found", req.JobID),
}
}
// markCompleted marks a job as completed
func (ts *TemporalService) markCompleted(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
// Check if job exists
_, exists := ts.scheduleJobs[req.JobID]
if !exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' not found", req.JobID)}
}
log.Printf("Marking job %s as completed (requested by Rust scheduler)", req.JobID)
// Mark job as not running in our tracking
ts.markJobAsNotRunning(req.JobID)
// Also try to clean up any lingering processes
if err := globalProcessManager.KillProcess(req.JobID); err != nil {
log.Printf("No process to clean up for job %s: %v", req.JobID, err)
}
return JobResponse{
Success: true,
Message: fmt.Sprintf("Job '%s' marked as completed", req.JobID),
}
}
// getJobStatus gets the status of a job
func (ts *TemporalService) getJobStatus(req JobRequest) JobResponse {
if req.JobID == "" {
return JobResponse{Success: false, Message: "Missing job_id"}
}
// Check if job exists
job, exists := ts.scheduleJobs[req.JobID]
if !exists {
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' not found", req.JobID)}
}
// Update the currently running status based on our tracking
job.CurrentlyRunning = ts.isJobCurrentlyRunning(context.Background(), req.JobID)
// Return the job as a single-item array for consistency with list endpoint
jobs := []JobStatus{*job}
return JobResponse{
Success: true,
Message: fmt.Sprintf("Status for job '%s'", req.JobID),
Jobs: jobs,
}
}
// storeRecipeForSchedule copies a recipe file to managed storage and returns the managed path and content
func (ts *TemporalService) storeRecipeForSchedule(jobID, originalPath string) (string, []byte, error) {
// Validate original recipe file exists
if _, err := os.Stat(originalPath); os.IsNotExist(err) {
return "", nil, fmt.Errorf("recipe file not found: %s", originalPath)
}
// Read the original recipe content
recipeContent, err := os.ReadFile(originalPath)
if err != nil {
return "", nil, fmt.Errorf("failed to read recipe file: %w", err)
}
// Validate it's a valid recipe by trying to parse it
if _, err := ts.parseRecipeContent(recipeContent); err != nil {
return "", nil, fmt.Errorf("invalid recipe file: %w", err)
}
// Create managed file path
originalFilename := filepath.Base(originalPath)
ext := filepath.Ext(originalFilename)
if ext == "" {
ext = ".yaml" // Default to yaml if no extension
}
managedFilename := fmt.Sprintf("%s%s", jobID, ext)
managedPath := filepath.Join(ts.recipesDir, managedFilename)
// Write to managed storage
if err := os.WriteFile(managedPath, recipeContent, 0644); err != nil {
return "", nil, fmt.Errorf("failed to write managed recipe file: %w", err)
}
log.Printf("Stored recipe for job %s: %s -> %s (size: %d bytes)",
jobID, originalPath, managedPath, len(recipeContent))
return managedPath, recipeContent, nil
}
// cleanupManagedRecipe removes managed recipe files for a job
func (ts *TemporalService) cleanupManagedRecipe(jobID string) {
// Clean up both permanent and temporary files
patterns := []string{
fmt.Sprintf("%s.*", jobID), // Permanent files (jobID.yaml, jobID.json, etc.)
fmt.Sprintf("%s-temp.*", jobID), // Temporary files
}
for _, pattern := range patterns {
matches, err := filepath.Glob(filepath.Join(ts.recipesDir, pattern))
if err != nil {
log.Printf("Error finding recipe files for cleanup: %v", err)
continue
}
for _, filePath := range matches {
if err := os.Remove(filePath); err != nil {
log.Printf("Warning: Failed to remove recipe file %s: %v", filePath, err)
} else {
log.Printf("Cleaned up recipe file: %s", filePath)
}
}
}
}

View file

@ -1,283 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"gopkg.in/yaml.v2"
)
// Global service instance for activities to access
var globalService *TemporalService
// TemporalService manages the Temporal client and provides HTTP API
type TemporalService struct {
client client.Client
worker worker.Worker
scheduleJobs map[string]*JobStatus // In-memory job tracking
runningJobs map[string]bool // Track which jobs are currently running
runningWorkflows map[string][]string // Track workflow IDs for each job
recipesDir string // Directory for managed recipe storage
ports *PortConfig // Port configuration
}
// NewTemporalService creates a new Temporal service and ensures Temporal server is running
func NewTemporalService() (*TemporalService, error) {
// First, find available ports
ports, err := findAvailablePorts()
if err != nil {
return nil, fmt.Errorf("failed to find available ports: %w", err)
}
log.Printf("Using ports - Temporal: %d, UI: %d, HTTP: %d",
ports.TemporalPort, ports.UIPort, ports.HTTPPort)
// Ensure Temporal server is running
if err := ensureTemporalServerRunning(ports); err != nil {
return nil, fmt.Errorf("failed to ensure Temporal server is running: %w", err)
}
// Set up managed recipes directory in user data directory
recipesDir, err := getManagedRecipesDir()
if err != nil {
return nil, fmt.Errorf("failed to determine managed recipes directory: %w", err)
}
if err := os.MkdirAll(recipesDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create managed recipes directory: %w", err)
}
log.Printf("Using managed recipes directory: %s", recipesDir)
// Create client (Temporal server should now be running)
c, err := client.Dial(client.Options{
HostPort: fmt.Sprintf("127.0.0.1:%d", ports.TemporalPort),
Namespace: Namespace,
})
if err != nil {
return nil, fmt.Errorf("failed to create temporal client: %w", err)
}
// Create worker
w := worker.New(c, TaskQueueName, worker.Options{})
w.RegisterWorkflow(GooseJobWorkflow)
w.RegisterActivity(ExecuteGooseRecipe)
if err := w.Start(); err != nil {
c.Close()
return nil, fmt.Errorf("failed to start worker: %w", err)
}
log.Printf("Connected to Temporal server successfully on port %d", ports.TemporalPort)
service := &TemporalService{
client: c,
worker: w,
scheduleJobs: make(map[string]*JobStatus),
runningJobs: make(map[string]bool),
runningWorkflows: make(map[string][]string),
recipesDir: recipesDir,
ports: ports,
}
// Set global service for activities
globalService = service
return service, nil
}
// Stop gracefully shuts down the Temporal service
func (ts *TemporalService) Stop() {
log.Println("Shutting down Temporal service...")
if ts.worker != nil {
ts.worker.Stop()
}
if ts.client != nil {
ts.client.Close()
}
log.Println("Temporal service stopped")
}
// GetHTTPPort returns the HTTP port for this service
func (ts *TemporalService) GetHTTPPort() int {
return ts.ports.HTTPPort
}
// GetTemporalPort returns the Temporal server port for this service
func (ts *TemporalService) GetTemporalPort() int {
return ts.ports.TemporalPort
}
// GetUIPort returns the Temporal UI port for this service
func (ts *TemporalService) GetUIPort() int {
return ts.ports.UIPort
}
// HTTP API handlers
func (ts *TemporalService) handleJobs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
ts.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req JobRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ts.writeErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Invalid JSON: %v", err))
return
}
var resp JobResponse
switch req.Action {
case "create":
resp = ts.createSchedule(req)
case "delete":
resp = ts.deleteSchedule(req)
case "pause":
resp = ts.pauseSchedule(req)
case "unpause":
resp = ts.unpauseSchedule(req)
case "update":
resp = ts.updateSchedule(req)
case "list":
resp = ts.listSchedules()
case "run_now":
resp = ts.runNow(req)
case "kill_job":
resp = ts.killJob(req)
case "inspect_job":
resp = ts.inspectJob(req)
case "mark_completed":
resp = ts.markCompleted(req)
case "status":
resp = ts.getJobStatus(req)
default:
resp = JobResponse{Success: false, Message: fmt.Sprintf("Unknown action: %s", req.Action)}
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}
func (ts *TemporalService) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
}
func (ts *TemporalService) handlePorts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
portInfo := map[string]int{
"http_port": ts.ports.HTTPPort,
"temporal_port": ts.ports.TemporalPort,
"ui_port": ts.ports.UIPort,
}
json.NewEncoder(w).Encode(portInfo)
}
// markJobAsRunning sets a job as currently running and tracks the workflow ID
func (ts *TemporalService) markJobAsRunning(jobID string) {
ts.runningJobs[jobID] = true
log.Printf("Marked job %s as running", jobID)
}
// markJobAsNotRunning sets a job as not currently running and clears workflow tracking
func (ts *TemporalService) markJobAsNotRunning(jobID string) {
delete(ts.runningJobs, jobID)
delete(ts.runningWorkflows, jobID)
log.Printf("Marked job %s as not running", jobID)
}
// addRunningWorkflow tracks a workflow ID for a job
func (ts *TemporalService) addRunningWorkflow(jobID, workflowID string) {
if ts.runningWorkflows[jobID] == nil {
ts.runningWorkflows[jobID] = make([]string, 0)
}
ts.runningWorkflows[jobID] = append(ts.runningWorkflows[jobID], workflowID)
log.Printf("Added workflow %s for job %s", workflowID, jobID)
}
// removeRunningWorkflow removes a workflow ID from job tracking
func (ts *TemporalService) removeRunningWorkflow(jobID, workflowID string) {
if workflows, exists := ts.runningWorkflows[jobID]; exists {
for i, id := range workflows {
if id == workflowID {
ts.runningWorkflows[jobID] = append(workflows[:i], workflows[i+1:]...)
break
}
}
if len(ts.runningWorkflows[jobID]) == 0 {
delete(ts.runningWorkflows, jobID)
ts.runningJobs[jobID] = false
}
}
}
// getEmbeddedRecipeContent retrieves embedded recipe content from schedule metadata
func (ts *TemporalService) getEmbeddedRecipeContent(jobID string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
scheduleID := fmt.Sprintf("goose-job-%s", jobID)
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
desc, err := handle.Describe(ctx)
if err != nil {
return "", fmt.Errorf("failed to get schedule description: %w", err)
}
if desc.Schedule.State.Note == "" {
return "", fmt.Errorf("no metadata found in schedule")
}
var metadata map[string]interface{}
if err := json.Unmarshal([]byte(desc.Schedule.State.Note), &metadata); err != nil {
return "", fmt.Errorf("failed to parse schedule metadata: %w", err)
}
if recipeContent, ok := metadata["recipe_content"].(string); ok {
return recipeContent, nil
}
return "", fmt.Errorf("no embedded recipe content found")
}
// writeErrorResponse writes a standardized error response
func (ts *TemporalService) writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(JobResponse{Success: false, Message: message})
}
// isJobCurrentlyRunning checks if there are any running workflows for the given job ID
func (ts *TemporalService) isJobCurrentlyRunning(ctx context.Context, jobID string) bool {
// Check our in-memory tracking of running jobs
if running, exists := ts.runningJobs[jobID]; exists && running {
return true
}
return false
}
// parseRecipeContent parses recipe content from bytes (YAML or JSON)
func (ts *TemporalService) parseRecipeContent(content []byte) (*Recipe, error) {
var recipe Recipe
// Try YAML first, then JSON
if err := yaml.Unmarshal(content, &recipe); err != nil {
if err := json.Unmarshal(content, &recipe); err != nil {
return nil, fmt.Errorf("failed to parse as YAML or JSON: %w", err)
}
}
return &recipe, nil
}

View file

@ -1,92 +0,0 @@
#!/bin/bash
# Startup script for Temporal service with integrated Temporal server
set -e
echo "Starting Temporal development environment..."
# Check if temporal CLI is available
if ! command -v temporal &> /dev/null; then
echo "Error: Temporal CLI not found!"
echo "Please install it first:"
echo " brew install temporal"
echo " # or download from https://github.com/temporalio/cli/releases"
exit 1
fi
# Check if temporal-service binary exists
if [ ! -f "./temporal-service" ]; then
echo "Error: temporal-service binary not found!"
echo "Please build it first: ./build.sh"
exit 1
fi
# Set data directory
DATA_DIR="${GOOSE_DATA_DIR:-./data}"
mkdir -p "$DATA_DIR"
echo "Data directory: $DATA_DIR"
echo "Starting Temporal server..."
# Start Temporal server in background
temporal server start-dev \
--db-filename "$DATA_DIR/temporal.db" \
--port 7233 \
--ui-port 8233 \
--log-level warn &
TEMPORAL_PID=$!
echo "Temporal server started with PID: $TEMPORAL_PID"
# Function to cleanup on exit
cleanup() {
echo ""
echo "Shutting down..."
if [ ! -z "$SERVICE_PID" ]; then
echo "Stopping temporal-service (PID: $SERVICE_PID)..."
kill $SERVICE_PID 2>/dev/null || true
fi
echo "Stopping Temporal server (PID: $TEMPORAL_PID)..."
kill $TEMPORAL_PID 2>/dev/null || true
wait $TEMPORAL_PID 2>/dev/null || true
echo "Shutdown complete"
}
# Set trap for cleanup
trap cleanup EXIT INT TERM
# Wait for Temporal server to be ready
echo "Waiting for Temporal server to be ready..."
for i in {1..30}; do
if curl -s http://localhost:7233/api/v1/namespaces > /dev/null 2>&1; then
echo "Temporal server is ready!"
break
fi
if [ $i -eq 30 ]; then
echo "Error: Temporal server failed to start within 30 seconds"
exit 1
fi
sleep 1
done
# Start the temporal service
echo "Starting temporal-service..."
PORT="${PORT:-8080}" ./temporal-service &
SERVICE_PID=$!
echo ""
echo "🎉 Temporal development environment is running!"
echo ""
echo "Services:"
echo " - Temporal Server: http://localhost:7233 (gRPC)"
echo " - Temporal Web UI: http://localhost:8233"
echo " - Goose Scheduler API: http://localhost:${PORT:-8080}"
echo ""
echo "API Endpoints:"
echo " - Health: http://localhost:${PORT:-8080}/health"
echo " - Jobs: http://localhost:${PORT:-8080}/jobs"
echo ""
echo "Press Ctrl+C to stop all services"
# Wait for the service to exit
wait $SERVICE_PID

View file

@ -1,27 +0,0 @@
//go:build !windows
// +build !windows
package main
import (
"os/exec"
"syscall"
)
// configureSysProcAttr configures the SysProcAttr for Unix-like systems
func configureSysProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // Create new process group
Pgid: 0, // Use process ID as group ID
}
}
// killProcessByPID kills a process using Unix syscalls
func killProcessByPID(pid int, signal syscall.Signal) error {
return syscall.Kill(pid, signal)
}
// killProcessGroupByPID kills a process group using Unix syscalls
func killProcessGroupByPID(pid int, signal syscall.Signal) error {
return syscall.Kill(-pid, signal)
}

View file

@ -1,32 +0,0 @@
//go:build windows
// +build windows
package main
import (
"fmt"
"os/exec"
"syscall"
)
// configureSysProcAttr configures the SysProcAttr for Windows
func configureSysProcAttr(cmd *exec.Cmd) {
// Windows doesn't support Setpgid/Pgid, so we use different approach
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
}
// killProcessByPID kills a process on Windows
func killProcessByPID(pid int, signal syscall.Signal) error {
// On Windows, we use taskkill command instead of syscall.Kill
cmd := exec.Command("taskkill", "/F", "/PID", fmt.Sprintf("%d", pid))
return cmd.Run()
}
// killProcessGroupByPID kills a process group on Windows
func killProcessGroupByPID(pid int, signal syscall.Signal) error {
// On Windows, kill the process tree
cmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", pid))
return cmd.Run()
}

View file

@ -1,123 +0,0 @@
#!/bin/bash
# Test script for Temporal service
set -e
echo "Testing Temporal service..."
# Check if service is running
if ! curl -s http://localhost:8080/health > /dev/null; then
echo "Error: Temporal service is not running on port 8080"
echo "Please start it with: ./temporal-service"
exit 1
fi
echo "✓ Service is running"
# Test health endpoint
echo "Testing health endpoint..."
HEALTH_RESPONSE=$(curl -s http://localhost:8080/health)
if [[ $HEALTH_RESPONSE == *"healthy"* ]]; then
echo "✓ Health check passed"
else
echo "✗ Health check failed: $HEALTH_RESPONSE"
exit 1
fi
# Test list schedules (should be empty initially)
echo "Testing list schedules..."
LIST_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d '{"action": "list"}')
if [[ $LIST_RESPONSE == *"\"success\":true"* ]]; then
echo "✓ List schedules works"
else
echo "✗ List schedules failed: $LIST_RESPONSE"
exit 1
fi
# Create a test recipe file
TEST_RECIPE="/tmp/test-recipe.yaml"
cat > $TEST_RECIPE << EOF
version: "1.0.0"
title: "Test Recipe"
description: "A test recipe for the scheduler"
prompt: "This is a test prompt for scheduled execution."
EOF
echo "Created test recipe at $TEST_RECIPE"
# Test create schedule
echo "Testing create schedule..."
CREATE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d "{\"action\": \"create\", \"job_id\": \"test-job\", \"cron\": \"0 */6 * * *\", \"recipe_path\": \"$TEST_RECIPE\"}")
if [[ $CREATE_RESPONSE == *"\"success\":true"* ]]; then
echo "✓ Create schedule works"
else
echo "✗ Create schedule failed: $CREATE_RESPONSE"
exit 1
fi
# Test list schedules again (should have one job)
echo "Testing list schedules with job..."
LIST_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d '{"action": "list"}')
if [[ $LIST_RESPONSE == *"test-job"* ]]; then
echo "✓ Job appears in list"
else
echo "✗ Job not found in list: $LIST_RESPONSE"
exit 1
fi
# Test pause schedule
echo "Testing pause schedule..."
PAUSE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d '{"action": "pause", "job_id": "test-job"}')
if [[ $PAUSE_RESPONSE == *"\"success\":true"* ]]; then
echo "✓ Pause schedule works"
else
echo "✗ Pause schedule failed: $PAUSE_RESPONSE"
exit 1
fi
# Test unpause schedule
echo "Testing unpause schedule..."
UNPAUSE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d '{"action": "unpause", "job_id": "test-job"}')
if [[ $UNPAUSE_RESPONSE == *"\"success\":true"* ]]; then
echo "✓ Unpause schedule works"
else
echo "✗ Unpause schedule failed: $UNPAUSE_RESPONSE"
exit 1
fi
# Test delete schedule
echo "Testing delete schedule..."
DELETE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
-H "Content-Type: application/json" \
-d '{"action": "delete", "job_id": "test-job"}')
if [[ $DELETE_RESPONSE == *"\"success\":true"* ]]; then
echo "✓ Delete schedule works"
else
echo "✗ Delete schedule failed: $DELETE_RESPONSE"
exit 1
fi
# Clean up
rm -f $TEST_RECIPE
echo ""
echo "🎉 All tests passed!"
echo ""
echo "The Temporal service is working correctly."
echo "You can now integrate it with the Rust scheduler."

View file

@ -16,8 +16,6 @@ const windowsFiles = [
const macosFiles = [
'goosed',
'goose',
'temporal',
'temporal-service',
'jbang',
'npx',
'uvx',