Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions actrun.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/bin/bash
# actrun - ActionForge graph runner
#
# This script is deployed at:
# https://www.actionforge.dev/actrun.sh
#
# This script is deployed from:
# https://github.com/actionforge/actrun-cli/blob/main/actrun.sh
#
# Usage:
# curl -fsSL https://www.actionforge.dev/actrun.sh | bash -s -- <file.act> [options]
# curl -fsSL https://www.actionforge.dev/actrun.sh | bash -s -- <shared-url> [options]
#
# Examples:
# curl -fsSL https://www.actionforge.dev/actrun.sh | bash -s -- hello-world.act
# curl -fsSL https://www.actionforge.dev/actrun.sh | bash -s -- workflow.act --verbose
# curl -fsSL https://www.actionforge.dev/actrun.sh | bash -s -- https://app.actionforge.dev/shared/wispy-paper-a49c664e.act
# curl -fsSL https://www.actionforge.dev/actrun.sh | bash -s -- --help
#
set -e

DOWNLOAD_BASE="https://www.actionforge.dev/download"
API_URL="https://app.actionforge.dev/api/v2/releases/list"
SHARE_API_URL="https://app.actionforge.dev/api/v2/share/graph/read"
CACHE_DIR="${HOME}/.cache/actrun"

# Detect OS
case "$(uname -s)" in
Linux*) OS="linux" ;;
Darwin*) OS="macos" ;;
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
*) echo "❌ Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac

# Detect architecture
case "$(uname -m)" in
x86_64|amd64) ARCH="x64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) echo "❌ Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

# Get latest version from API (find highest version, excluding prereleases)
VERSION=$(curl -fsSL "$API_URL" | \
grep -o '"version":"v[0-9]*\.[0-9]*\.[0-9]*"' | \
cut -d'"' -f4 | sort -uV | tail -1)

if [ -z "$VERSION" ]; then
echo "❌ Failed to fetch latest version" >&2
exit 1
fi

# Check cache
CACHE_VERSION_DIR="$CACHE_DIR/$VERSION"
if [ "$OS" = "windows" ]; then
CACHED_BINARY="$CACHE_VERSION_DIR/actrun.exe"
else
CACHED_BINARY="$CACHE_VERSION_DIR/actrun"
fi

if [ -x "$CACHED_BINARY" ]; then
echo "✅ Using cached actrun $VERSION"

# Process shared URL if first argument matches the pattern
if [ $# -gt 0 ] && [[ "$1" =~ ^https://app\.actionforge\.dev/shared/([a-zA-Z0-9_-]+\.act)$ ]]; then
share_id="${BASH_REMATCH[1]}"
echo "🔗 Fetching shared graph: $share_id"

graph_tmp=$(mktemp -t "actrun-shared-XXXXXX.act")
trap "rm -f '$graph_tmp'" EXIT

if ! curl -fsSL "${SHARE_API_URL}?id=${share_id}" -o "$graph_tmp"; then
echo "❌ Failed to fetch shared graph: $share_id" >&2
exit 1
fi

shift
exec "$CACHED_BINARY" "$graph_tmp" "$@"
fi

exec "$CACHED_BINARY" "$@"
fi

# Determine file extension
case "$OS" in
linux) EXT="tar.gz" ;;
windows) EXT="zip" ;;
macos) EXT="pkg" ;;
esac

FILENAME="actrun-${VERSION}.cli-${ARCH}-${OS}.${EXT}"
URL="${DOWNLOAD_BASE}/${FILENAME}"

echo "⬇️ Downloading $FILENAME..."

TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT

curl -fsSL "$URL" -o "$TMP_DIR/$FILENAME"

echo "📦 Unpacking..."

# Extract and find binary
case "$OS" in
linux)
tar -xzf "$TMP_DIR/$FILENAME" -C "$TMP_DIR"
BINARY="$TMP_DIR/actrun"
;;
windows)
unzip -q "$TMP_DIR/$FILENAME" -d "$TMP_DIR"
BINARY="$TMP_DIR/actrun.exe"
;;
macos)
pkgutil --expand-full "$TMP_DIR/$FILENAME" "$TMP_DIR/pkg_expanded"
BINARY=$(find "$TMP_DIR/pkg_expanded" -name "actrun" -type f -perm +111 2>/dev/null | head -1)
if [ -z "$BINARY" ]; then
BINARY=$(find "$TMP_DIR/pkg_expanded" -name "actrun" -type f | head -1)
fi
;;
esac

if [ ! -f "$BINARY" ]; then
echo "❌ Failed to find actrun binary" >&2
exit 1
fi

# Cache the binary
mkdir -p "$CACHE_VERSION_DIR"
cp "$BINARY" "$CACHED_BINARY"
chmod +x "$CACHED_BINARY"

echo "✅ Unpacked actrun $VERSION"

# Process shared URL if first argument matches the pattern
if [ $# -gt 0 ] && [[ "$1" =~ ^https://app\.actionforge\.dev/shared/([a-zA-Z0-9_-]+\.act)$ ]]; then
share_id="${BASH_REMATCH[1]}"
echo "🔗 Fetching shared graph: $share_id"

graph_tmp=$(mktemp -t "actrun-shared-XXXXXX.act")
trap "rm -rf '$TMP_DIR'; rm -f '$graph_tmp'" EXIT

if ! curl -fsSL "${SHARE_API_URL}?id=${share_id}" -o "$graph_tmp"; then
echo "❌ Failed to fetch shared graph: $share_id" >&2
exit 1
fi

shift
exec "$CACHED_BINARY" "$graph_tmp" "$@"
fi

exec "$CACHED_BINARY" "$@"
14 changes: 11 additions & 3 deletions cmd/cmd_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ var (
)

var cmdRoot = &cobra.Command{
Use: "actrun [filename] [flags]",
Use: "actrun [filename|url] [flags]",
Short: "actrun is a tool for running action graphs.",
Version: build.GetFulllVersionInfo(),
Args: cobra.ArbitraryArgs,
Expand Down Expand Up @@ -172,12 +172,20 @@ func cmdRootRun(cmd *cobra.Command, args []string) {
return
}

err := core.RunGraphFromFile(context.Background(), finalGraphFile, core.RunOpts{
var err error
opts := core.RunOpts{
ConfigFile: finalConfigFile,
OverrideSecrets: nil,
OverrideInputs: nil,
Args: finalGraphArgs,
}, nil)
}

if core.IsSharedGraphURL(finalGraphFile) {
err = core.RunGraphFromURL(context.Background(), finalGraphFile, opts, nil)
} else {
err = core.RunGraphFromFile(context.Background(), finalGraphFile, opts, nil)
}

if err != nil {
core.PrintError(finalGraphFile, err)
os.Exit(1)
Expand Down
69 changes: 69 additions & 0 deletions core/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -1038,3 +1042,68 @@ func RunGraphFromFile(ctx context.Context, graphFile string, opts RunOpts, debug

return nil
}

// Matches URLs like https://app.actionforge.dev/shared/<id>.act
var sharedURLPattern = regexp.MustCompile(`^https://app\.actionforge\.dev/shared/([a-zA-Z0-9_-]+\.act)$`)

const shareAPIURL = "https://app.actionforge.dev/api/v2/share/graph/read"

func IsSharedGraphURL(graphURL string) bool {
return sharedURLPattern.MatchString(graphURL)
}

func ParseSharedGraphURL(graphURL string) (string, bool) {
matches := sharedURLPattern.FindStringSubmatch(graphURL)
if len(matches) != 2 {
return "", false
}
return matches[1], true
}

// RunGraphFromURL fetches and runs a graph from a shared URL.
// Only urls from app.actionforge.dev are accepted
func RunGraphFromURL(ctx context.Context, graphURL string, opts RunOpts, debugCb DebugCallback) error {
parsedURL, err := url.Parse(graphURL)
if err != nil {
return CreateErr(nil, err, "invalid URL format")
}

if parsedURL.Host != "app.actionforge.dev" {
return CreateErr(nil, nil, "invalid shared graph URL: only URLs from app.actionforge.dev are accepted, got '%s'", parsedURL.Host).
SetHint("Double-check the URL - shared graphs must be hosted on app.actionforge.dev")
}

shareID, ok := ParseSharedGraphURL(graphURL)
if !ok {
return CreateErr(nil, nil, "invalid shared graph URL: expected format https://app.actionforge.dev/shared/<id>.act")
}

apiURL := fmt.Sprintf("%s?id=%s", shareAPIURL, url.QueryEscape(shareID))

utils.LogOut.Debugf("fetching shared graph from: %s\n", apiURL)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
if err != nil {
return CreateErr(nil, err, "failed to create request for shared graph")
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return CreateErr(nil, err, "failed to fetch shared graph")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return CreateErr(nil, nil, "failed to fetch shared graph: server returned status %d", resp.StatusCode)
}

graphContent, err := io.ReadAll(resp.Body)
if err != nil {
return CreateErr(nil, err, "failed to read shared graph content")
}

graphName := shareID

return RunGraphFromString(ctx, graphName, string(graphContent), opts, debugCb)
}
2 changes: 1 addition & 1 deletion tests_e2e/references/reference_app.sh_l10
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
build hasn't expired yet
Error: unknown flag: --flag_doesnt_exist
Usage:
actrun [filename] [flags]
actrun [filename|url] [flags]
actrun [command]

Available Commands:
Expand Down
6 changes: 3 additions & 3 deletions tests_e2e/references/reference_app.sh_l12
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ hint:

stack trace:
github.com/actionforge/actrun-cli/core.RunGraphFromFile
graph.go:1031
graph.go:1035
github.com/actionforge/actrun-cli/cmd.cmdRootRun
cmd_root.go:175
cmd_root.go:186
github.com/spf13/cobra.(*Command).execute
command.go:-1
github.com/spf13/cobra.(*Command).ExecuteC
command.go:-1
github.com/spf13/cobra.(*Command).Execute
command.go:-1
github.com/actionforge/actrun-cli/cmd.Execute
cmd_root.go:190
cmd_root.go:198
main.main
main.go:26
runtime.main
Expand Down
2 changes: 1 addition & 1 deletion tests_e2e/references/reference_contexts_env.sh_l26
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
build hasn't expired yet
Error: unable to load env file: doesnt_exist
Usage:
actrun [filename] [flags]
actrun [filename|url] [flags]
actrun [command]

Available Commands:
Expand Down
10 changes: 5 additions & 5 deletions tests_e2e/references/reference_dir-walk.sh_l56
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,21 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl
github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry
start@v1.go:45
github.com/actionforge/actrun-cli/core.RunGraph
graph.go:426
graph.go:430
github.com/actionforge/actrun-cli/core.RunGraphFromString
graph.go:1016
graph.go:1020
github.com/actionforge/actrun-cli/core.RunGraphFromFile
graph.go:1034
graph.go:1038
github.com/actionforge/actrun-cli/cmd.cmdRootRun
cmd_root.go:175
cmd_root.go:186
github.com/spf13/cobra.(*Command).execute
command.go:-1
github.com/spf13/cobra.(*Command).ExecuteC
command.go:-1
github.com/spf13/cobra.(*Command).Execute
command.go:-1
github.com/actionforge/actrun-cli/cmd.Execute
cmd_root.go:190
cmd_root.go:198
main.main
main.go:26
runtime.main
Expand Down
10 changes: 5 additions & 5 deletions tests_e2e/references/reference_error_no_output.sh_l8
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,21 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl
github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry
start@v1.go:45
github.com/actionforge/actrun-cli/core.RunGraph
graph.go:426
graph.go:430
github.com/actionforge/actrun-cli/core.RunGraphFromString
graph.go:1016
graph.go:1020
github.com/actionforge/actrun-cli/core.RunGraphFromFile
graph.go:1034
graph.go:1038
github.com/actionforge/actrun-cli/cmd.cmdRootRun
cmd_root.go:175
cmd_root.go:186
github.com/spf13/cobra.(*Command).execute
command.go:-1
github.com/spf13/cobra.(*Command).ExecuteC
command.go:-1
github.com/spf13/cobra.(*Command).Execute
command.go:-1
github.com/actionforge/actrun-cli/cmd.Execute
cmd_root.go:190
cmd_root.go:198
main.main
main.go:26
runtime.main
Expand Down
4 changes: 2 additions & 2 deletions tests_e2e/references/reference_group-error.sh_l8
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl
github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry
start@v1.go:45
github.com/actionforge/actrun-cli/core.RunGraph
graph.go:426
graph.go:430
github.com/actionforge/actrun-cli/core.RunGraphFromString
graph.go:1016
graph.go:1020

Loading
Loading