mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
removing golang/temporal building
This commit is contained in:
parent
b94535b679
commit
f0056e6cd1
32 changed files with 34 additions and 4999 deletions
155
.github/workflows/build-cli.yml
vendored
155
.github/workflows/build-cli.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
20
.github/workflows/bundle-desktop-intel.yml
vendored
20
.github/workflows/bundle-desktop-intel.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
14
.github/workflows/bundle-desktop-linux.yml
vendored
14
.github/workflows/bundle-desktop-linux.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
42
.github/workflows/bundle-desktop-windows.yml
vendored
42
.github/workflows/bundle-desktop-windows.yml
vendored
|
|
@ -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..."
|
||||
|
|
|
|||
23
.github/workflows/bundle-desktop.yml
vendored
23
.github/workflows/bundle-desktop.yml
vendored
|
|
@ -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
101
Justfile
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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(¤t_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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
temporal-service/.gitignore
vendored
1
temporal-service/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
temporal-service
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
|
@ -16,8 +16,6 @@ const windowsFiles = [
|
|||
const macosFiles = [
|
||||
'goosed',
|
||||
'goose',
|
||||
'temporal',
|
||||
'temporal-service',
|
||||
'jbang',
|
||||
'npx',
|
||||
'uvx',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue