~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
309 lines · 9.0 KB Raw History
package workflow

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/models"
)

const (
	virtualOutput = "pe-virtual:GITHUB_OUTPUT"
	virtualEnv    = "pe-virtual:GITHUB_ENV"
	virtualPath   = "pe-virtual:GITHUB_PATH"
	virtualState  = "pe-virtual:GITHUB_STATE"
	virtualCache  = "pe-virtual:GITHUB_CACHE"
	virtualEvent  = "pe-virtual:GITHUB_EVENT"
	resultMarker  = "__PE_RESULT__"
)

type actionRunResult struct {
	Code   int
	VFS    map[string]string
	Stdout string
	Stderr string
}

func (e *Executor) buildStepEnv(ctx context.Context, step *models.WorkflowStep) (map[string]string, error) {
	env := make(map[string]string)
	for key, value := range e.Context.Env {
		env[key] = fmt.Sprintf("%v", value)
	}
	for key, value := range step.Env {
		evaluated, err := e.evaluateExpression(ctx, fmt.Sprintf("%v", value))
		if err != nil {
			return nil, err
		}
		env[key] = evaluated
	}
	for key, value := range step.With {
		evaluated, err := e.evaluateExpression(ctx, fmt.Sprintf("%v", value))
		if err != nil {
			return nil, err
		}
		env["INPUT_"+strings.ToUpper(key)] = evaluated
	}
	env["GITHUB_OUTPUT"] = virtualOutput
	env["GITHUB_ENV"] = virtualEnv
	env["GITHUB_PATH"] = virtualPath
	env["GITHUB_STATE"] = virtualState
	env["GITHUB_CACHE"] = virtualCache
	env["GITHUB_EVENT_PATH"] = virtualEvent
	return env, nil
}

func (e *Executor) executeStepUses(ctx context.Context, step *models.WorkflowStep, env map[string]string) error {
	if e.opts.ResolveAction == nil {
		return fmt.Errorf("no bundled action resolver configured")
	}
	action, err := e.opts.ResolveAction(step.Uses)
	if err != nil {
		return err
	}
	if !strings.HasPrefix(action.Using, "node") {
		if action.Using == "composite" {
			return fmt.Errorf("this evaluator runs gha-lite policies net-only and refuses a composite action")
		}
		return fmt.Errorf("unsupported action type: %s", action.Using)
	}
	for name, def := range action.Inputs {
		key := "INPUT_" + strings.ToUpper(name)
		if _, exists := env[key]; exists {
			continue
		}
		if strings.TrimSpace(def) == "" {
			continue
		}
		evaluated, err := e.evaluateExpression(ctx, def)
		if err != nil {
			return err
		}
		env[key] = evaluated
	}
	env["GITHUB_ACTION_PATH"] = action.Path
	env["GITHUB_ACTION"] = step.ID
	if env["GITHUB_ACTION"] == "" {
		env["GITHUB_ACTION"] = step.Uses
	}
	result, err := e.runAction(ctx, action, env)
	e.applyActionResult(step, result)
	if err != nil {
		return err
	}
	if result.Code != 0 {
		return fmt.Errorf("action exited with code %d", result.Code)
	}
	return nil
}

func (e *Executor) runAction(ctx context.Context, action *Action, env map[string]string) (actionRunResult, error) {
	envJSON, err := json.Marshal(env)
	if err != nil {
		return actionRunResult{}, err
	}
	cacheJSON, err := json.Marshal(e.Context.Cache)
	if err != nil {
		return actionRunResult{}, err
	}
	eventJSON, err := json.Marshal(map[string]any{"inputs": e.Context.Inputs})
	if err != nil {
		return actionRunResult{}, err
	}
	cacheLiteral, _ := json.Marshal(string(cacheJSON))
	eventLiteral, _ := json.Marshal(string(eventJSON))
	shim := strings.ReplaceAll(actionShim, "__PE_ENV_JSON__", string(envJSON))
	shim = strings.ReplaceAll(shim, "__PE_CACHE_JSON__", string(cacheLiteral))
	shim = strings.ReplaceAll(shim, "__PE_EVENT_JSON__", string(eventLiteral))
	shim = strings.ReplaceAll(shim, "__PE_RESULT_MARKER__", fmt.Sprintf("%q", resultMarker))
	script := shim + "\n" + string(action.Source) + "\n" + actionFooter
	stdout, stderr, runErr := e.runDeno(ctx, script, env)
	result := parseActionResult(stdout)
	result.Stdout = stdout
	result.Stderr = stderr
	if runErr != nil && (result.VFS == nil || result.Code == 0) {
		return result, fmt.Errorf("action execution failed: %w: %s", runErr, tail(stderr, 2000))
	}
	return result, nil
}

func parseActionResult(stdout string) actionRunResult {
	result := actionRunResult{}
	found := false
	for line := range strings.SplitSeq(stdout, "\n") {
		line = strings.TrimSpace(line)
		if !strings.HasPrefix(line, resultMarker) {
			continue
		}
		var decoded struct {
			Code int               `json:"code"`
			VFS  map[string]string `json:"vfs"`
		}
		if err := json.Unmarshal([]byte(strings.TrimPrefix(line, resultMarker)), &decoded); err != nil {
			continue
		}
		result.Code = decoded.Code
		result.VFS = decoded.VFS
		found = true
	}
	if !found {
		result.Code = 1
	}
	return result
}

func (e *Executor) applyActionResult(step *models.WorkflowStep, result actionRunResult) {
	for line := range strings.SplitSeq(result.Stderr, "\n") {
		if line == "" {
			continue
		}
		e.emit(line)
		e.parseAnnotations(line)
	}
	for line := range strings.SplitSeq(result.Stdout, "\n") {
		if line == "" || strings.HasPrefix(strings.TrimSpace(line), resultMarker) {
			continue
		}
		e.emit(line)
		e.parseAnnotations(line)
	}
	output := result.VFS[virtualOutput]
	envUpdates := result.VFS[virtualEnv]
	cacheUpdate := result.VFS[virtualCache]
	if step.ID != "" {
		outputs := parseGitHubActionsOutputs(output)
		if e.Context.Outputs[step.ID] == nil {
			e.Context.Outputs[step.ID] = map[string]any{}
		}
		e.Context.Outputs[step.ID]["outputs"] = outputs
	}
	if envUpdates != "" {
		for key, value := range parseGitHubActionsOutputs(envUpdates) {
			e.Context.Env[key] = value
		}
	}
	if cacheUpdate != "" {
		e.Context.Cache = parseCacheContent(cacheUpdate, e.Context.Cache)
	}
}

func parseCacheContent(content string, previous models.Cache) models.Cache {
	if strings.TrimSpace(content) == "" {
		return previous
	}
	var parsed models.Cache
	if err := json.Unmarshal([]byte(content), &parsed); err != nil || parsed == nil {
		return previous
	}
	return parsed
}

func tail(value string, limit int) string {
	if limit <= 0 || len(value) <= limit {
		return value
	}
	return value[len(value)-limit:]
}

const actionShim = `const __pe_env = __PE_ENV_JSON__;
const __pe_vfs = {};
__pe_vfs[__pe_env.GITHUB_CACHE] = __PE_CACHE_JSON__;
__pe_vfs[__pe_env.GITHUB_EVENT_PATH] = __PE_EVENT_JSON__;
const __pe_ghPaths = new Set([
__pe_env.GITHUB_OUTPUT,
__pe_env.GITHUB_ENV,
__pe_env.GITHUB_PATH,
__pe_env.GITHUB_STATE,
__pe_env.GITHUB_CACHE,
__pe_env.GITHUB_EVENT_PATH,
].filter((value) => typeof value === "string" && value.length > 0));
const __pe_realExit = Deno.exit.bind(Deno);
const __pe_encoder = new TextEncoder();
function __pe_write(fd, text) {
  const bytes = __pe_encoder.encode(text);
  if (fd === 1) Deno.stdout.writeSync(bytes);
  else Deno.stderr.writeSync(bytes);
}
function __pe_finish(code) {
  __pe_write(1, __PE_RESULT_MARKER__ + JSON.stringify({ code: code, vfs: __pe_vfs }) + "\n");
}
Object.defineProperty(Deno, "env", {
  configurable: true,
  value: {
    get: (key) => __pe_env[key],
    has: (key) => Object.prototype.hasOwnProperty.call(__pe_env, key),
    set: () => {},
    delete: () => {},
    toObject: () => ({ ...__pe_env }),
  },
});
const __pe_originalWriteTextFile = Deno.writeTextFile.bind(Deno);
const __pe_originalWriteTextFileSync = Deno.writeTextFileSync.bind(Deno);
const __pe_originalReadTextFile = Deno.readTextFile.bind(Deno);
const __pe_originalReadTextFileSync = Deno.readTextFileSync.bind(Deno);
function __pe_append(key, data, options) {
  __pe_vfs[key] = (options && options.append ? (__pe_vfs[key] || "") : "") + data;
}
Deno.writeTextFile = (path, data, options) => {
  const key = String(path);
  if (__pe_ghPaths.has(key)) {
    __pe_append(key, data, options);
    return Promise.resolve();
  }
  return __pe_originalWriteTextFile(path, data, options);
};
Deno.writeTextFileSync = (path, data, options) => {
  const key = String(path);
  if (__pe_ghPaths.has(key)) {
    __pe_append(key, data, options);
    return;
  }
  return __pe_originalWriteTextFileSync(path, data, options);
};
Deno.readTextFile = (path, options) => {
  const key = String(path);
  if (__pe_ghPaths.has(key)) return Promise.resolve(__pe_vfs[key] || "");
  return __pe_originalReadTextFile(path, options);
};
Deno.readTextFileSync = (path) => {
  const key = String(path);
  if (__pe_ghPaths.has(key)) return __pe_vfs[key] || "";
  return __pe_originalReadTextFileSync(path);
};
for (const method of ["log", "error", "warn", "info", "debug", "trace"]) {
  console[method] = (...args) => {
    const line = args.map((value) => typeof value === "string" ? value : Deno.inspect(value)).join(" ");
    __pe_write(2, line + "\n");
  };
}
class __PeExit extends Error {
  constructor(code) {
    super("policy action exited");
    this.code = typeof code === "number" ? code : 0;
  }
}
Deno.exit = (code = 0) => {
  throw new __PeExit(code);
};
globalThis.addEventListener("unhandledrejection", (event) => {
  event.preventDefault();
  const reason = event.reason;
  if (!(reason instanceof __PeExit)) {
    __pe_write(2, (reason && reason.message ? reason.message : String(reason)) + "\n");
  }
  __pe_finish(reason instanceof __PeExit ? reason.code : 1);
  __pe_realExit(0);
});
globalThis.addEventListener("error", (event) => {
  event.preventDefault();
  __pe_write(2, String(event.message) + "\n");
  __pe_finish(1);
  __pe_realExit(0);
});
`

const actionFooter = `
__pe_finish(0);
__pe_realExit(0);
`