feat: add approval step

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-15 19:46:33 +01:00
parent 533b738692
commit 7eb6ae7cbb
41 changed files with 7886 additions and 1724 deletions

View File

@@ -0,0 +1,109 @@
syntax = "proto3";
package forest.v1;
import "google/protobuf/timestamp.proto";
service AppService {
// App lifecycle
rpc CreateApp(CreateAppRequest) returns (CreateAppResponse);
rpc GetApp(GetAppRequest) returns (GetAppResponse);
rpc ListApps(ListAppsRequest) returns (ListAppsResponse);
rpc DeleteApp(DeleteAppRequest) returns (DeleteAppResponse);
rpc SuspendApp(SuspendAppRequest) returns (SuspendAppResponse);
// App tokens
rpc CreateAppToken(CreateAppTokenRequest) returns (CreateAppTokenResponse);
rpc ListAppTokens(ListAppTokensRequest) returns (ListAppTokensResponse);
rpc RevokeAppToken(RevokeAppTokenRequest) returns (RevokeAppTokenResponse);
}
// ─── Core types ──────────────────────────────────────────────────────
message App {
string app_id = 1;
string organisation_id = 2;
string name = 3;
string description = 4;
repeated string permissions = 5;
bool suspended = 6;
google.protobuf.Timestamp created_at = 7;
}
message AppToken {
string token_id = 1;
string name = 2;
google.protobuf.Timestamp expires_at = 3;
google.protobuf.Timestamp last_used = 4;
bool revoked = 5;
google.protobuf.Timestamp created_at = 6;
}
// ─── App lifecycle ───────────────────────────────────────────────────
message CreateAppRequest {
string organisation_id = 1;
string name = 2;
string description = 3;
repeated string permissions = 4;
}
message CreateAppResponse {
App app = 1;
}
message GetAppRequest {
string app_id = 1;
}
message GetAppResponse {
App app = 1;
}
message ListAppsRequest {
string organisation_id = 1;
}
message ListAppsResponse {
repeated App apps = 1;
}
message DeleteAppRequest {
string app_id = 1;
}
message DeleteAppResponse {}
message SuspendAppRequest {
string app_id = 1;
bool suspended = 2;
}
message SuspendAppResponse {}
// ─── App tokens ──────────────────────────────────────────────────────
message CreateAppTokenRequest {
string app_id = 1;
string name = 2;
int64 expires_in_seconds = 3; // 0 = no expiry
}
message CreateAppTokenResponse {
AppToken token = 1;
string raw_token = 2; // only returned on creation
}
message ListAppTokensRequest {
string app_id = 1;
}
message ListAppTokensResponse {
repeated AppToken tokens = 1;
}
message RevokeAppTokenRequest {
string token_id = 1;
}
message RevokeAppTokenResponse {}

View File

@@ -0,0 +1,62 @@
syntax = "proto3";
package forest.v1;
message BeginUploadArtifactRequest {}
message BeginUploadArtifactResponse {
string upload_id = 1;
}
message UploadArtifactRequest {
string upload_id = 1;
string env = 2;
string destination = 3;
string file_name = 4;
string file_content = 5;
// Category of the file: "deployment" (default), "spec", or "attachment"
string category = 6;
}
message UploadArtifactResponse {}
message CommitArtifactRequest{
string upload_id = 1;
}
message CommitArtifactResponse {
string artifact_id = 1;
}
message GetArtifactFilesRequest {
// The artifact_id (UUID from annotations/artifacts table)
string artifact_id = 1;
// Optional filter: "deployment", "spec", "attachment". Empty = all categories.
optional string category = 2;
}
message GetArtifactFilesResponse {
repeated ArtifactFile files = 1;
}
message ArtifactFile {
string file_name = 1;
string category = 2;
string env = 3;
string destination = 4;
string content = 5;
}
message GetArtifactSpecRequest {
string artifact_id = 1;
}
message GetArtifactSpecResponse {
// The spec file content (forest.cue), empty string if no spec was uploaded.
string content = 1;
}
service ArtifactService {
rpc BeginUploadArtifact(BeginUploadArtifactRequest) returns (BeginUploadArtifactResponse);
rpc UploadArtifact(stream UploadArtifactRequest) returns (UploadArtifactResponse);
rpc CommitArtifact(CommitArtifactRequest) returns (CommitArtifactResponse);
rpc GetArtifactFiles(GetArtifactFilesRequest) returns (GetArtifactFilesResponse);
rpc GetArtifactSpec(GetArtifactSpecRequest) returns (GetArtifactSpecResponse);
}

View File

@@ -0,0 +1,119 @@
syntax = "proto3";
package forest.v1;
// ── Event streaming ───────────────────────────────────────────────
service EventService {
// Ephemeral server-streaming subscription. Client manages its own cursor.
rpc Subscribe(SubscribeEventsRequest) returns (stream OrgEvent);
// Durable subscription: resumes from the subscription's persisted cursor.
// Events are streamed, and the cursor is advanced as events are sent.
// Client should call AcknowledgeEvents to confirm processing.
rpc SubscribeDurable(SubscribeDurableRequest) returns (stream OrgEvent);
// Acknowledge that events up to (and including) the given sequence have
// been processed. Advances the subscription's cursor. Idempotent.
rpc AcknowledgeEvents(AcknowledgeEventsRequest) returns (AcknowledgeEventsResponse);
}
message SubscribeEventsRequest {
string organisation = 1;
string project = 2; // optional — empty means all projects in org
repeated string resource_types = 3; // optional filter: "release", "destination", etc.
repeated string actions = 4; // optional filter: "created", "updated", etc.
int64 since_sequence = 5; // 0 = latest only, >0 = replay from that sequence
}
message SubscribeDurableRequest {
string organisation = 1;
string subscription_name = 2; // the registered subscription name
}
message AcknowledgeEventsRequest {
string organisation = 1;
string subscription_name = 2;
int64 sequence = 3; // advance cursor to this sequence
}
message AcknowledgeEventsResponse {
int64 cursor = 1; // the new cursor value
}
message OrgEvent {
int64 sequence = 1; // monotonic cursor — client stores this for reconnect
string event_id = 2; // UUID, dedup key
string timestamp = 3; // RFC 3339
string organisation = 4;
string project = 5; // empty for org-level events
string resource_type = 6; // "release", "destination", "environment", "pipeline", "artifact", "policy", "app", "organisation"
string action = 7; // "created", "updated", "deleted", "status_changed"
string resource_id = 8; // ID of the changed resource
map<string, string> metadata = 9; // lightweight context (e.g. "status" → "SUCCEEDED")
}
// ── Subscription management ───────────────────────────────────────
service EventSubscriptionService {
rpc CreateEventSubscription(CreateEventSubscriptionRequest) returns (CreateEventSubscriptionResponse);
rpc UpdateEventSubscription(UpdateEventSubscriptionRequest) returns (UpdateEventSubscriptionResponse);
rpc DeleteEventSubscription(DeleteEventSubscriptionRequest) returns (DeleteEventSubscriptionResponse);
rpc ListEventSubscriptions(ListEventSubscriptionsRequest) returns (ListEventSubscriptionsResponse);
}
message EventSubscription {
string id = 1;
string organisation = 2;
string name = 3;
repeated string resource_types = 4;
repeated string actions = 5;
repeated string projects = 6;
string status = 7; // "active", "paused"
int64 cursor = 8; // last acknowledged sequence
string created_at = 9;
string updated_at = 10;
}
message CreateEventSubscriptionRequest {
string organisation = 1;
string name = 2;
repeated string resource_types = 3; // empty = all
repeated string actions = 4; // empty = all
repeated string projects = 5; // empty = all projects in org
}
message CreateEventSubscriptionResponse {
EventSubscription subscription = 1;
}
message UpdateEventSubscriptionRequest {
string organisation = 1;
string name = 2;
optional string status = 3; // "active" or "paused"
// To update filters, set update_filters = true and provide new values.
// Empty arrays mean "all" (no filter).
bool update_filters = 4;
repeated string resource_types = 5;
repeated string actions = 6;
repeated string projects = 7;
}
message UpdateEventSubscriptionResponse {
EventSubscription subscription = 1;
}
message DeleteEventSubscriptionRequest {
string organisation = 1;
string name = 2;
}
message DeleteEventSubscriptionResponse {}
message ListEventSubscriptionsRequest {
string organisation = 1;
}
message ListEventSubscriptionsResponse {
repeated EventSubscription subscriptions = 1;
}

View File

@@ -0,0 +1,864 @@
syntax = "proto3";
package forest.v1;
import "google/protobuf/duration.proto";
// ---------------------------------------------------------------------------
// ForageService — the control plane RPC surface that forest-server uses to
// drive deployments against a forage cluster. The scheduler calls
// ApplyResources with the full desired-state bundle; forage reconciles.
// ---------------------------------------------------------------------------
service ForageService {
// Apply a batch of resources (create / update / delete).
// This is the main entry-point used by the forage/containers@1 destination.
rpc ApplyResources(ApplyResourcesRequest) returns (ApplyResourcesResponse);
// Poll / stream the rollout status of a previous apply.
rpc WatchRollout(WatchRolloutRequest) returns (stream RolloutEvent);
// Tear down all resources associated with a release / project.
rpc DeleteResources(DeleteResourcesRequest) returns (DeleteResourcesResponse);
}
// ---------------------------------------------------------------------------
// Apply
// ---------------------------------------------------------------------------
message ApplyResourcesRequest {
// Caller-chosen idempotency key (release_state id works well).
string apply_id = 1;
// Namespace / tenant isolation — maps to the forest organisation.
string namespace = 2;
// The ordered list of resources to reconcile. Forage processes them in
// order so that dependencies (e.g. Service before HTTPRoute) are met.
repeated ForageResource resources = 3;
// Labels propagated to every resource for bookkeeping.
map<string, string> labels = 4;
}
message ApplyResourcesResponse {
// Server-generated rollout id for status tracking.
string rollout_id = 1;
}
message WatchRolloutRequest {
string rollout_id = 1;
}
message RolloutEvent {
string resource_name = 1;
string resource_kind = 2;
RolloutStatus status = 3;
string message = 4;
}
enum RolloutStatus {
ROLLOUT_STATUS_UNSPECIFIED = 0;
ROLLOUT_STATUS_PENDING = 1;
ROLLOUT_STATUS_IN_PROGRESS = 2;
ROLLOUT_STATUS_SUCCEEDED = 3;
ROLLOUT_STATUS_FAILED = 4;
ROLLOUT_STATUS_ROLLED_BACK = 5;
}
message DeleteResourcesRequest {
string namespace = 1;
// Selector labels — all resources matching these labels are removed.
map<string, string> labels = 2;
}
message DeleteResourcesResponse {}
// ===========================================================================
// Resource envelope — every item in the apply list is one of these.
// ===========================================================================
message ForageResource {
// Unique name within the namespace (e.g. "my-api", "my-api-worker").
string name = 1;
oneof spec {
ContainerServiceSpec container_service = 10;
ServiceSpec service = 11;
RouteSpec route = 12;
CronJobSpec cron_job = 13;
JobSpec job = 14;
}
}
// ===========================================================================
// ContainerServiceSpec — the primary workload.
// Combines the concerns of Deployment + Pod in a single cohesive spec.
// ===========================================================================
message ContainerServiceSpec {
// ---- Scheduling & scaling ------------------------------------------------
ScalingPolicy scaling = 1;
// ---- Pod-level settings --------------------------------------------------
// Main application container (exactly one required).
Container container = 2;
// Optional sidecar containers that share the pod network.
repeated Container sidecars = 3;
// Init containers run sequentially before the main container starts.
repeated Container init_containers = 4;
// ---- Volumes available to all containers in the pod ----------------------
repeated Volume volumes = 5;
// ---- Update strategy -----------------------------------------------------
UpdateStrategy update_strategy = 6;
// ---- Pod-level configuration ---------------------------------------------
PodConfig pod_config = 7;
}
// ---------------------------------------------------------------------------
// Container — describes a single OCI container.
// ---------------------------------------------------------------------------
message Container {
// Human-readable name (must be unique within the pod).
string name = 1;
// OCI image reference, e.g. "registry.forage.sh/org/app:v1.2.3".
string image = 2;
// Override the image entrypoint.
repeated string command = 3;
// Arguments passed to the entrypoint.
repeated string args = 4;
// Working directory inside the container.
string working_dir = 5;
// Environment variables — static values and references.
repeated EnvVar env = 6;
// Ports the container listens on.
repeated ContainerPort ports = 7;
// Resource requests and limits.
ResourceRequirements resources = 8;
// Volume mounts into this container's filesystem.
repeated VolumeMount volume_mounts = 9;
// Health probes.
Probe liveness_probe = 10;
Probe readiness_probe = 11;
Probe startup_probe = 12;
// Lifecycle hooks.
Lifecycle lifecycle = 13;
// Security context for this container.
ContainerSecurityContext security_context = 14;
// Image pull policy: "Always", "IfNotPresent", "Never".
string image_pull_policy = 15;
// Whether stdin / tty are allocated (usually false for services).
bool stdin = 16;
bool tty = 17;
}
// ---------------------------------------------------------------------------
// Environment variables
// ---------------------------------------------------------------------------
message EnvVar {
string name = 1;
oneof value_source {
// Literal value.
string value = 2;
// Reference to a secret key.
SecretKeyRef secret_ref = 3;
// Reference to a config-map key.
ConfigKeyRef config_ref = 4;
// Downward-API field (e.g. "metadata.name", "status.podIP").
string field_ref = 5;
// Resource field (e.g. "limits.cpu").
string resource_field_ref = 6;
}
}
message SecretKeyRef {
string secret_name = 1;
string key = 2;
}
message ConfigKeyRef {
string config_name = 1;
string key = 2;
}
// ---------------------------------------------------------------------------
// Ports
// ---------------------------------------------------------------------------
message ContainerPort {
// Friendly name (e.g. "http", "grpc", "metrics").
string name = 1;
// The port number inside the container.
uint32 container_port = 2;
// Protocol: TCP (default), UDP, SCTP.
string protocol = 3;
}
// ---------------------------------------------------------------------------
// Resources
// ---------------------------------------------------------------------------
message ResourceRequirements {
ResourceList requests = 1;
ResourceList limits = 2;
}
message ResourceList {
// CPU in Kubernetes quantity format: "100m", "0.5", "2".
string cpu = 1;
// Memory in Kubernetes quantity format: "128Mi", "1Gi".
string memory = 2;
// Ephemeral storage: "1Gi".
string ephemeral_storage = 3;
// GPU / accelerator requests (e.g. "nvidia.com/gpu": "1").
map<string, string> extended = 4;
}
// ---------------------------------------------------------------------------
// Volumes & mounts
// ---------------------------------------------------------------------------
message Volume {
// Volume name referenced by VolumeMount.name.
string name = 1;
oneof source {
EmptyDirVolume empty_dir = 10;
SecretVolume secret = 11;
ConfigMapVolume config_map = 12;
PVCVolume pvc = 13;
HostPathVolume host_path = 14;
NfsVolume nfs = 15;
}
}
message EmptyDirVolume {
// "Memory" for tmpfs, empty for node disk.
string medium = 1;
// Size limit (e.g. "256Mi"). Empty means node default.
string size_limit = 2;
}
message SecretVolume {
string secret_name = 1;
// Optional: mount only specific keys.
repeated KeyToPath items = 2;
// Octal file mode (e.g. 0644). Default 0644.
uint32 default_mode = 3;
bool optional = 4;
}
message ConfigMapVolume {
string config_map_name = 1;
repeated KeyToPath items = 2;
uint32 default_mode = 3;
bool optional = 4;
}
message KeyToPath {
string key = 1;
string path = 2;
uint32 mode = 3;
}
message PVCVolume {
string claim_name = 1;
bool read_only = 2;
}
message HostPathVolume {
string path = 1;
// "Directory", "File", "DirectoryOrCreate", "FileOrCreate", etc.
string type = 2;
}
message NfsVolume {
string server = 1;
string path = 2;
bool read_only = 3;
}
message VolumeMount {
// Must match a Volume.name.
string name = 1;
// Absolute path inside the container.
string mount_path = 2;
// Optional sub-path within the volume.
string sub_path = 3;
bool read_only = 4;
}
// ---------------------------------------------------------------------------
// Probes
// ---------------------------------------------------------------------------
message Probe {
oneof handler {
HttpGetProbe http_get = 1;
TcpSocketProbe tcp_socket = 2;
ExecProbe exec = 3;
GrpcProbe grpc = 4;
}
uint32 initial_delay_seconds = 10;
uint32 period_seconds = 11;
uint32 timeout_seconds = 12;
uint32 success_threshold = 13;
uint32 failure_threshold = 14;
}
message HttpGetProbe {
string path = 1;
uint32 port = 2;
string scheme = 3; // "HTTP" or "HTTPS"
repeated HttpHeader http_headers = 4;
}
message HttpHeader {
string name = 1;
string value = 2;
}
message TcpSocketProbe {
uint32 port = 1;
}
message ExecProbe {
repeated string command = 1;
}
message GrpcProbe {
uint32 port = 1;
string service = 2;
}
// ---------------------------------------------------------------------------
// Lifecycle hooks
// ---------------------------------------------------------------------------
message Lifecycle {
LifecycleHandler post_start = 1;
LifecycleHandler pre_stop = 2;
}
message LifecycleHandler {
oneof action {
ExecProbe exec = 1;
HttpGetProbe http_get = 2;
TcpSocketProbe tcp_socket = 3;
}
}
// ---------------------------------------------------------------------------
// Security
// ---------------------------------------------------------------------------
message ContainerSecurityContext {
bool run_as_non_root = 1;
int64 run_as_user = 2;
int64 run_as_group = 3;
bool read_only_root_filesystem = 4;
bool privileged = 5;
bool allow_privilege_escalation = 6;
Capabilities capabilities = 7;
// SELinux options (optional).
string se_linux_type = 8;
// Seccomp profile: "RuntimeDefault", "Unconfined", or a localhost path.
string seccomp_profile = 9;
}
message Capabilities {
repeated string add = 1;
repeated string drop = 2;
}
message PodSecurityContext {
int64 run_as_user = 1;
int64 run_as_group = 2;
bool run_as_non_root = 3;
int64 fs_group = 4;
// Supplemental groups for all containers.
repeated int64 supplemental_groups = 5;
// "OnRootMismatch" or "Always".
string fs_group_change_policy = 6;
string seccomp_profile = 7;
}
// ---------------------------------------------------------------------------
// Scaling
// ---------------------------------------------------------------------------
message ScalingPolicy {
// Fixed replica count (used when autoscaling is not configured).
uint32 replicas = 1;
// Optional horizontal autoscaler.
AutoscalingPolicy autoscaling = 2;
}
message AutoscalingPolicy {
uint32 min_replicas = 1;
uint32 max_replicas = 2;
// Target average CPU utilisation percentage (e.g. 70).
uint32 target_cpu_utilization_percent = 3;
// Target average memory utilisation percentage.
uint32 target_memory_utilization_percent = 4;
// Custom metrics (e.g. queue depth, RPS).
repeated CustomMetric custom_metrics = 5;
// Scale-down stabilisation window.
google.protobuf.Duration scale_down_stabilization = 6;
}
message CustomMetric {
// Metric name as exposed by the metrics adapter.
string name = 1;
// One of "Value", "AverageValue", "Utilization".
string target_type = 2;
// Target threshold (interpretation depends on target_type).
string target_value = 3;
}
// ---------------------------------------------------------------------------
// Update strategy
// ---------------------------------------------------------------------------
message UpdateStrategy {
// "RollingUpdate" (default) or "Recreate".
string type = 1;
RollingUpdateConfig rolling_update = 2;
}
message RollingUpdateConfig {
// Absolute number or percentage (e.g. "1", "25%").
string max_unavailable = 1;
string max_surge = 2;
}
// ---------------------------------------------------------------------------
// Pod-level configuration
// ---------------------------------------------------------------------------
message PodConfig {
// Service account name for RBAC / workload identity.
string service_account_name = 1;
// Restart policy: "Always" (default for services), "OnFailure", "Never".
string restart_policy = 2;
// Graceful shutdown window.
uint32 termination_grace_period_seconds = 3;
// DNS policy: "ClusterFirst" (default), "Default", "None".
string dns_policy = 4;
PodDnsConfig dns_config = 5;
// Host networking (rare, but needed for some infra workloads).
bool host_network = 6;
// Node scheduling.
map<string, string> node_selector = 7;
repeated Toleration tolerations = 8;
Affinity affinity = 9;
// Topology spread constraints for HA.
repeated TopologySpreadConstraint topology_spread_constraints = 10;
// Image pull secrets.
repeated string image_pull_secrets = 11;
// Pod-level security context.
PodSecurityContext security_context = 12;
// Priority class name for preemption.
string priority_class_name = 13;
// Runtime class (e.g. "gvisor", "kata").
string runtime_class_name = 14;
// Annotations passed to the pod template (not the workload resource).
map<string, string> annotations = 15;
// Labels passed to the pod template.
map<string, string> labels = 16;
}
message PodDnsConfig {
repeated string nameservers = 1;
repeated string searches = 2;
repeated DnsOption options = 3;
}
message DnsOption {
string name = 1;
string value = 2;
}
message Toleration {
string key = 1;
// "Equal" or "Exists".
string operator = 2;
string value = 3;
// "NoSchedule", "PreferNoSchedule", "NoExecute".
string effect = 4;
// Toleration seconds for NoExecute.
int64 toleration_seconds = 5;
}
message Affinity {
NodeAffinity node_affinity = 1;
PodAffinity pod_affinity = 2;
PodAntiAffinity pod_anti_affinity = 3;
}
message NodeAffinity {
repeated PreferredSchedulingTerm preferred = 1;
NodeSelector required = 2;
}
message PreferredSchedulingTerm {
int32 weight = 1;
NodeSelectorTerm preference = 2;
}
message NodeSelector {
repeated NodeSelectorTerm terms = 1;
}
message NodeSelectorTerm {
repeated NodeSelectorRequirement match_expressions = 1;
repeated NodeSelectorRequirement match_fields = 2;
}
message NodeSelectorRequirement {
string key = 1;
// "In", "NotIn", "Exists", "DoesNotExist", "Gt", "Lt".
string operator = 2;
repeated string values = 3;
}
message PodAffinity {
repeated WeightedPodAffinityTerm preferred = 1;
repeated PodAffinityTerm required = 2;
}
message PodAntiAffinity {
repeated WeightedPodAffinityTerm preferred = 1;
repeated PodAffinityTerm required = 2;
}
message WeightedPodAffinityTerm {
int32 weight = 1;
PodAffinityTerm term = 2;
}
message PodAffinityTerm {
LabelSelector label_selector = 1;
string topology_key = 2;
repeated string namespaces = 3;
}
message LabelSelector {
map<string, string> match_labels = 1;
repeated LabelSelectorRequirement match_expressions = 2;
}
message LabelSelectorRequirement {
string key = 1;
// "In", "NotIn", "Exists", "DoesNotExist".
string operator = 2;
repeated string values = 3;
}
message TopologySpreadConstraint {
// Max difference in spread (e.g. 1 for even distribution).
int32 max_skew = 1;
// "zone", "hostname", or any node label.
string topology_key = 2;
// "DoNotSchedule" or "ScheduleAnyway".
string when_unsatisfiable = 3;
LabelSelector label_selector = 4;
}
// ===========================================================================
// ServiceSpec — L4 load balancing & service discovery.
// Combines Service + optional gateway route into one resource when desired.
// ===========================================================================
message ServiceSpec {
// The ContainerServiceSpec name this service fronts.
string target = 1;
// Service type: "ClusterIP" (default), "NodePort", "LoadBalancer", "Headless".
string type = 2;
repeated ServicePort ports = 3;
// Session affinity: "None" (default), "ClientIP".
string session_affinity = 4;
// Optional: expose this service externally via the gateway.
// Setting this is equivalent to creating a separate RouteSpec.
// Allows combining Service + Route into one resource for simpler configs.
InlineRoute inline_route = 5;
// Extra annotations on the Service object (e.g. cloud LB configs).
map<string, string> annotations = 6;
}
message ServicePort {
string name = 1;
uint32 port = 2;
uint32 target_port = 3;
string protocol = 4; // TCP, UDP, SCTP
// Only for NodePort type.
uint32 node_port = 5;
}
message InlineRoute {
// Hostname(s) to match (e.g. "api.example.com").
repeated string hostnames = 1;
// Path matching rules. If empty, matches all paths to the first port.
repeated RouteRule rules = 2;
// TLS configuration.
RouteTls tls = 3;
}
// ===========================================================================
// RouteSpec — Gateway API HTTPRoute (standalone).
// Use this when you need routing rules separate from the service definition.
// ===========================================================================
message RouteSpec {
// The ServiceSpec name this route targets.
string target_service = 1;
// Hostname(s) this route matches.
repeated string hostnames = 2;
// Matching & routing rules.
repeated RouteRule rules = 3;
// TLS termination config.
RouteTls tls = 4;
// Which gateway to attach to (empty = cluster default).
string gateway_ref = 5;
// Route priority / ordering.
int32 priority = 6;
}
message RouteRule {
// Path matching.
repeated RouteMatch matches = 1;
// Backend(s) traffic is sent to.
repeated RouteBackend backends = 2;
// Request / response filters applied to this rule.
repeated RouteFilter filters = 3;
// Timeout for the entire request.
google.protobuf.Duration timeout = 4;
}
message RouteMatch {
// Path match.
PathMatch path = 1;
// Header conditions.
repeated HeaderMatch headers = 2;
// Query parameter conditions.
repeated QueryParamMatch query_params = 3;
// HTTP method constraint.
string method = 4;
}
message PathMatch {
// "Exact", "PathPrefix" (default), "RegularExpression".
string type = 1;
string value = 2;
}
message HeaderMatch {
// "Exact" (default), "RegularExpression".
string type = 1;
string name = 2;
string value = 3;
}
message QueryParamMatch {
string type = 1;
string name = 2;
string value = 3;
}
message RouteBackend {
// Service name.
string service = 1;
// Port on the backend service.
uint32 port = 2;
// Traffic weight for canary / blue-green (1-100).
uint32 weight = 3;
}
message RouteFilter {
oneof filter {
RequestHeaderModifier request_header_modifier = 1;
ResponseHeaderModifier response_header_modifier = 2;
RequestRedirect request_redirect = 3;
UrlRewrite url_rewrite = 4;
RequestMirror request_mirror = 5;
}
}
message RequestHeaderModifier {
map<string, string> set = 1;
map<string, string> add = 2;
repeated string remove = 3;
}
message ResponseHeaderModifier {
map<string, string> set = 1;
map<string, string> add = 2;
repeated string remove = 3;
}
message RequestRedirect {
string scheme = 1;
string hostname = 2;
uint32 port = 3;
string path = 4;
uint32 status_code = 5; // 301, 302, etc.
}
message UrlRewrite {
string hostname = 1;
PathMatch path = 2;
}
message RequestMirror {
string service = 1;
uint32 port = 2;
}
message RouteTls {
// "Terminate" (default) or "Passthrough".
string mode = 1;
// Secret name containing the TLS certificate.
string certificate_ref = 2;
}
// ===========================================================================
// CronJobSpec — scheduled workload.
// ===========================================================================
message CronJobSpec {
// Cron schedule (e.g. "*/5 * * * *").
string schedule = 1;
// Timezone (e.g. "Europe/Copenhagen"). Empty = UTC.
string timezone = 2;
// Container that runs the job.
Container container = 3;
// Volumes for the job pod.
repeated Volume volumes = 4;
// Job-level config.
JobConfig job_config = 5;
// Pod-level config (node selector, tolerations, etc.).
PodConfig pod_config = 6;
// "Allow", "Forbid", "Replace".
string concurrency_policy = 7;
// Number of successful/failed jobs to retain.
uint32 successful_jobs_history_limit = 8;
uint32 failed_jobs_history_limit = 9;
// Suspend the cron schedule.
bool suspend = 10;
// Deadline in seconds for starting the job if it missed its schedule.
int64 starting_deadline_seconds = 11;
}
// ===========================================================================
// JobSpec — one-shot workload.
// ===========================================================================
message JobSpec {
// Container that runs the job.
Container container = 1;
// Volumes for the job pod.
repeated Volume volumes = 2;
// Job-level config.
JobConfig job_config = 3;
// Pod-level config.
PodConfig pod_config = 4;
}
message JobConfig {
// Number of times the job should complete successfully.
uint32 completions = 1;
// Max parallel pods.
uint32 parallelism = 2;
// "NonIndexed" (default) or "Indexed".
string completion_mode = 3;
// Number of retries before marking failed.
uint32 backoff_limit = 4;
// Active deadline (seconds) — job killed if it runs longer.
int64 active_deadline_seconds = 5;
// TTL after finished (seconds) — auto-cleanup.
int64 ttl_seconds_after_finished = 6;
// Restart policy: "OnFailure" (default) or "Never".
string restart_policy = 7;
}

View File

@@ -0,0 +1,10 @@
syntax = "proto3";
package forest.v1;
service StatusService {
rpc Status(GetStatusRequest) returns (GetStatusResponse) {}
}
message GetStatusRequest {}
message GetStatusResponse {}

View File

@@ -0,0 +1,98 @@
syntax = "proto3";
package forest.v1;
enum NotificationType {
NOTIFICATION_TYPE_UNSPECIFIED = 0;
NOTIFICATION_TYPE_RELEASE_ANNOTATED = 1;
NOTIFICATION_TYPE_RELEASE_STARTED = 2;
NOTIFICATION_TYPE_RELEASE_SUCCEEDED = 3;
NOTIFICATION_TYPE_RELEASE_FAILED = 4;
}
enum NotificationChannel {
NOTIFICATION_CHANNEL_UNSPECIFIED = 0;
NOTIFICATION_CHANNEL_CLI = 1;
NOTIFICATION_CHANNEL_SLACK = 2;
}
// Rich context about the release that triggered the notification.
// Integrations decide which fields to use.
message ReleaseContext {
string slug = 1;
string organisation = 2;
string project = 3;
string artifact_id = 4;
string release_intent_id = 5;
string destination = 6;
string environment = 7;
// Source info
string source_username = 8;
string source_email = 9;
string source_user_id = 17;
// Git ref
string commit_sha = 10;
string commit_branch = 11;
// Artifact context
string context_title = 12;
string context_description = 13;
string context_web = 14;
// Error info (populated on failure)
string error_message = 15;
// Number of destinations involved
int32 destination_count = 16;
}
message Notification {
string id = 1;
NotificationType notification_type = 2;
string title = 3;
string body = 4;
string organisation = 5;
string project = 6;
ReleaseContext release_context = 7;
string created_at = 8;
}
message NotificationPreference {
NotificationType notification_type = 1;
NotificationChannel channel = 2;
bool enabled = 3;
}
message GetNotificationPreferencesRequest {}
message GetNotificationPreferencesResponse {
repeated NotificationPreference preferences = 1;
}
message SetNotificationPreferenceRequest {
NotificationType notification_type = 1;
NotificationChannel channel = 2;
bool enabled = 3;
}
message SetNotificationPreferenceResponse {
NotificationPreference preference = 1;
}
message ListenNotificationsRequest {
optional string organisation = 1;
optional string project = 2;
}
message ListNotificationsRequest {
int32 page_size = 1;
string page_token = 2;
optional string organisation = 3;
optional string project = 4;
}
message ListNotificationsResponse {
repeated Notification notifications = 1;
string next_page_token = 2;
}
service NotificationService {
rpc GetNotificationPreferences(GetNotificationPreferencesRequest) returns (GetNotificationPreferencesResponse);
rpc SetNotificationPreference(SetNotificationPreferenceRequest) returns (SetNotificationPreferenceResponse);
rpc ListenNotifications(ListenNotificationsRequest) returns (stream Notification);
rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse);
}

View File

@@ -0,0 +1,178 @@
syntax = "proto3";
package forest.v1;
import "forest/v1/releases.proto";
// ── Policy types ────────────────────────────────────────────────────
enum PolicyType {
POLICY_TYPE_UNSPECIFIED = 0;
POLICY_TYPE_SOAK_TIME = 1;
POLICY_TYPE_BRANCH_RESTRICTION = 2;
POLICY_TYPE_EXTERNAL_APPROVAL = 3;
}
message SoakTimeConfig {
// Environment that must have a successful deploy before target is allowed
string source_environment = 1;
// Environment that is gated by this policy
string target_environment = 2;
// Seconds to wait after source environment succeeds
int64 duration_seconds = 3;
}
message BranchRestrictionConfig {
// Environment that is restricted
string target_environment = 1;
// Regex that source branch must match
string branch_pattern = 2;
}
message ExternalApprovalConfig {
string target_environment = 1;
int32 required_approvals = 2;
}
// ── External approval state ─────────────────────────────────────────
message ExternalApprovalState {
int32 required_approvals = 1;
int32 current_approvals = 2;
repeated ExternalApprovalDecisionEntry decisions = 3;
}
message ExternalApprovalDecisionEntry {
string user_id = 1;
string username = 2;
string decision = 3;
string decided_at = 4;
optional string comment = 5;
}
// ── Policy resource ─────────────────────────────────────────────────
message Policy {
string id = 1;
string name = 2;
bool enabled = 3;
PolicyType policy_type = 4;
oneof config {
SoakTimeConfig soak_time = 10;
BranchRestrictionConfig branch_restriction = 11;
ExternalApprovalConfig external_approval = 12;
}
string created_at = 20;
string updated_at = 21;
}
// ── Policy evaluation result ────────────────────────────────────────
message PolicyEvaluation {
string policy_name = 1;
PolicyType policy_type = 2;
bool passed = 3;
// Human-readable explanation when blocked
string reason = 4;
optional ExternalApprovalState approval_state = 10;
}
// ── CRUD messages ───────────────────────────────────────────────────
message CreatePolicyRequest {
Project project = 1;
string name = 2;
PolicyType policy_type = 3;
oneof config {
SoakTimeConfig soak_time = 10;
BranchRestrictionConfig branch_restriction = 11;
ExternalApprovalConfig external_approval = 12;
}
}
message CreatePolicyResponse {
Policy policy = 1;
}
message UpdatePolicyRequest {
Project project = 1;
string name = 2;
optional bool enabled = 3;
oneof config {
SoakTimeConfig soak_time = 10;
BranchRestrictionConfig branch_restriction = 11;
ExternalApprovalConfig external_approval = 12;
}
}
message UpdatePolicyResponse {
Policy policy = 1;
}
message DeletePolicyRequest {
Project project = 1;
string name = 2;
}
message DeletePolicyResponse {}
message ListPoliciesRequest {
Project project = 1;
}
message ListPoliciesResponse {
repeated Policy policies = 1;
}
message EvaluatePoliciesRequest {
Project project = 1;
string target_environment = 2;
// For branch restriction checks
optional string branch = 3;
optional string release_intent_id = 4;
}
message EvaluatePoliciesResponse {
repeated PolicyEvaluation evaluations = 1;
bool all_passed = 2;
}
// ── External approval RPC messages ──────────────────────────────────
message ExternalApproveReleaseRequest {
Project project = 1;
string release_intent_id = 2;
string target_environment = 3;
optional string comment = 4;
bool force_bypass = 5;
}
message ExternalApproveReleaseResponse {
ExternalApprovalState state = 1;
}
message ExternalRejectReleaseRequest {
Project project = 1;
string release_intent_id = 2;
string target_environment = 3;
optional string comment = 4;
}
message ExternalRejectReleaseResponse {
ExternalApprovalState state = 1;
}
message GetExternalApprovalStateRequest {
Project project = 1;
string release_intent_id = 2;
string target_environment = 3;
}
message GetExternalApprovalStateResponse {
ExternalApprovalState state = 1;
}
service PolicyService {
rpc CreatePolicy(CreatePolicyRequest) returns (CreatePolicyResponse);
rpc UpdatePolicy(UpdatePolicyRequest) returns (UpdatePolicyResponse);
rpc DeletePolicy(DeletePolicyRequest) returns (DeletePolicyResponse);
rpc ListPolicies(ListPoliciesRequest) returns (ListPoliciesResponse);
rpc EvaluatePolicies(EvaluatePoliciesRequest) returns (EvaluatePoliciesResponse);
rpc ExternalApproveRelease(ExternalApproveReleaseRequest) returns (ExternalApproveReleaseResponse);
rpc ExternalRejectRelease(ExternalRejectReleaseRequest) returns (ExternalRejectReleaseResponse);
rpc GetExternalApprovalState(GetExternalApprovalStateRequest) returns (GetExternalApprovalStateResponse);
}

View File

@@ -0,0 +1,80 @@
syntax = "proto3";
package forest.v1;
service RegistryService {
rpc GetComponents(GetComponentsRequest) returns (GetComponentsResponse) {}
rpc GetComponent(GetComponentRequest) returns (GetComponentResponse) {}
rpc GetComponentVersion(GetComponentVersionRequest) returns (GetComponentVersionResponse) {}
rpc BeginUpload(BeginUploadRequest) returns (BeginUploadResponse) {}
rpc UploadFile(UploadFileRequest) returns (UploadFileResponse) {}
rpc CommitUpload(CommitUploadRequest) returns (CommitUploadResponse) {}
rpc GetComponentFiles(GetComponentFilesRequest) returns (stream GetComponentFilesResponse) {}
}
message GetComponentsRequest {}
message GetComponentsResponse {}
message GetComponentRequest {
string name = 1;
string organisation = 2;
}
message GetComponentResponse {
optional Component component = 1;
}
message Component {
string id = 1;
string version = 2;
}
// ComponentVersion
message GetComponentVersionRequest {
string name = 1;
string organisation = 2;
string version = 3;
}
message GetComponentVersionResponse {
optional Component component = 1;
}
// BeginUpload
message BeginUploadRequest {
string name = 1;
string organisation = 2;
string version = 3;
}
message BeginUploadResponse {
string upload_context = 1;
}
message UploadFileRequest {
string upload_context = 1;
string file_path = 2;
bytes file_content = 3;
}
message UploadFileResponse {}
message CommitUploadRequest {
string upload_context = 1;
}
message CommitUploadResponse {}
// Get component files
message GetComponentFilesRequest {
string component_id = 1;
}
message GetComponentFilesResponse {
oneof msg {
Done done = 1;
ComponentFile component_file = 2;
}
}
message ComponentFile {
string file_path = 1;
bytes file_content = 2;
}
message Done {}

View File

@@ -10,6 +10,7 @@ enum StageType {
STAGE_TYPE_UNSPECIFIED = 0;
STAGE_TYPE_DEPLOY = 1;
STAGE_TYPE_WAIT = 2;
STAGE_TYPE_PLAN = 3;
}
// ── Per-type config messages ─────────────────────────────────────────
@@ -22,6 +23,11 @@ message WaitStageConfig {
int64 duration_seconds = 1;
}
message PlanStageConfig {
string environment = 1;
bool auto_approve = 2;
}
// ── A single pipeline stage ──────────────────────────────────────────
message PipelineStage {
@@ -31,6 +37,7 @@ message PipelineStage {
oneof config {
DeployStageConfig deploy = 10;
WaitStageConfig wait = 11;
PlanStageConfig plan = 12;
}
}
@@ -43,6 +50,7 @@ enum PipelineStageStatus {
PIPELINE_STAGE_STATUS_SUCCEEDED = 3;
PIPELINE_STAGE_STATUS_FAILED = 4;
PIPELINE_STAGE_STATUS_CANCELLED = 5;
PIPELINE_STAGE_STATUS_AWAITING_APPROVAL = 6;
}
// ── Pipeline resource ────────────────────────────────────────────────

View File

@@ -35,6 +35,8 @@ message ReleaseRequest {
// When true, use the project's release pipeline (DAG) instead of
// deploying directly to the specified destinations/environments.
bool use_pipeline = 5;
// When true, create a plan-only pipeline (single Plan stage, no deploy).
bool prepare_only = 6;
}
message ReleaseResponse {
// List of release intents created (one per destination)
@@ -55,9 +57,23 @@ message WaitReleaseEvent {
oneof event {
ReleaseStatusUpdate status_update = 1;
ReleaseLogLine log_line = 2;
PipelineStageUpdate stage_update = 3;
}
}
// Streamed in WaitRelease for pipeline releases: reports stage status changes.
message PipelineStageUpdate {
string stage_id = 1;
string stage_type = 2; // "deploy", "wait"
string status = 3; // PENDING, ACTIVE, SUCCEEDED, FAILED, CANCELLED
optional string queued_at = 4;
optional string started_at = 5;
optional string completed_at = 6;
optional string wait_until = 7;
optional string error_message = 8;
optional string approval_status = 9;
}
message ReleaseStatusUpdate {
string destination = 1;
string status = 2;
@@ -90,6 +106,13 @@ message GetProjectsResponse {
repeated string projects = 1;
}
message CreateProjectRequest {
string organisation = 1;
string project = 2;
}
message CreateProjectResponse {
Project project = 1;
}
message GetReleasesByActorRequest {
@@ -125,6 +148,67 @@ message GetDestinationStatesRequest {
message GetDestinationStatesResponse {
repeated DestinationState destinations = 1;
// Active pipeline runs affecting these destinations (if any).
repeated PipelineRunState pipeline_runs = 2;
}
// ── Release intent states (release-centric view) ─────────────────────
message GetReleaseIntentStatesRequest {
string organisation = 1;
optional string project = 2;
// When true, also include recently completed release intents.
bool include_completed = 3;
}
message GetReleaseIntentStatesResponse {
repeated ReleaseIntentState release_intents = 1;
}
// Full state of a release intent: pipeline stages + individual release steps.
message ReleaseIntentState {
string release_intent_id = 1;
string artifact_id = 2;
string project = 3;
string created_at = 4;
// Pipeline stages (empty for non-pipeline releases).
repeated PipelineStageState stages = 5;
// All release_states rows for this intent (deploy steps).
repeated ReleaseStepState steps = 6;
}
// Status of a single pipeline stage (saga coordinator view).
message PipelineStageState {
string stage_id = 1;
repeated string depends_on = 2;
PipelineRunStageType stage_type = 3;
PipelineRunStageStatus status = 4;
// Consistent timestamps for all stage types.
optional string queued_at = 5;
optional string started_at = 6;
optional string completed_at = 7;
optional string error_message = 8;
// Type-specific context.
optional string environment = 9; // deploy/plan stages
optional int64 duration_seconds = 10; // wait stages
optional string wait_until = 11; // wait stages
repeated string release_ids = 12; // deploy/plan stages: individual release IDs
optional string approval_status = 13; // plan stages: AWAITING_APPROVAL, APPROVED, REJECTED
optional bool auto_approve = 14; // plan stages
}
// Status of a single release step (release_states row).
message ReleaseStepState {
string release_id = 1;
optional string stage_id = 2;
string destination_name = 3;
string environment = 4;
string status = 5;
optional string queued_at = 6;
optional string assigned_at = 7;
optional string started_at = 8;
optional string completed_at = 9;
optional string error_message = 10;
}
message DestinationState {
@@ -138,6 +222,83 @@ message DestinationState {
optional string queued_at = 8;
optional string completed_at = 9;
optional int32 queue_position = 10;
// Pipeline context: set when this release was created by a pipeline stage.
optional string release_intent_id = 11;
optional string stage_id = 12;
// When a runner was assigned to this release.
optional string assigned_at = 13;
// When the runner actually started executing.
optional string started_at = 14;
}
// ── Pipeline run progress ────────────────────────────────────────────
// Snapshot of an active (or recently completed) pipeline run.
message PipelineRunState {
string release_intent_id = 1;
string artifact_id = 2;
string created_at = 3;
repeated PipelineRunStage stages = 4;
}
// Status of a single stage within a pipeline run.
message PipelineRunStage {
string stage_id = 1;
repeated string depends_on = 2;
PipelineRunStageType stage_type = 3;
PipelineRunStageStatus status = 4;
// Type-specific context
optional string environment = 5; // deploy stages
optional int64 duration_seconds = 6; // wait stages
optional string queued_at = 7; // when dependencies were met
optional string started_at = 8;
optional string completed_at = 9;
optional string error_message = 10;
optional string wait_until = 11;
repeated string release_ids = 12; // deploy stages: individual release IDs
optional string approval_status = 13; // plan stages: AWAITING_APPROVAL, APPROVED, REJECTED
optional bool auto_approve = 14; // plan stages
}
enum PipelineRunStageType {
PIPELINE_RUN_STAGE_TYPE_UNSPECIFIED = 0;
PIPELINE_RUN_STAGE_TYPE_DEPLOY = 1;
PIPELINE_RUN_STAGE_TYPE_WAIT = 2;
PIPELINE_RUN_STAGE_TYPE_PLAN = 3;
}
enum PipelineRunStageStatus {
PIPELINE_RUN_STAGE_STATUS_UNSPECIFIED = 0;
PIPELINE_RUN_STAGE_STATUS_PENDING = 1;
PIPELINE_RUN_STAGE_STATUS_ACTIVE = 2;
PIPELINE_RUN_STAGE_STATUS_SUCCEEDED = 3;
PIPELINE_RUN_STAGE_STATUS_FAILED = 4;
PIPELINE_RUN_STAGE_STATUS_CANCELLED = 5;
PIPELINE_RUN_STAGE_STATUS_AWAITING_APPROVAL = 6;
}
// ── Plan stage approval ──────────────────────────────────────────────
message ApprovePlanStageRequest {
string release_intent_id = 1;
string stage_id = 2;
}
message ApprovePlanStageResponse {}
message RejectPlanStageRequest {
string release_intent_id = 1;
string stage_id = 2;
optional string reason = 3;
}
message RejectPlanStageResponse {}
message GetPlanOutputRequest {
string release_intent_id = 1;
string stage_id = 2;
}
message GetPlanOutputResponse {
string plan_output = 1;
string status = 2; // RUNNING, AWAITING_APPROVAL, APPROVED, REJECTED
}
service ReleaseService {
@@ -150,7 +311,13 @@ service ReleaseService {
rpc GetReleasesByActor(GetReleasesByActorRequest) returns (GetReleasesByActorResponse);
rpc GetOrganisations(GetOrganisationsRequest) returns (GetOrganisationsResponse);
rpc GetProjects(GetProjectsRequest) returns (GetProjectsResponse);
rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse);
rpc GetDestinationStates(GetDestinationStatesRequest) returns (GetDestinationStatesResponse);
rpc GetReleaseIntentStates(GetReleaseIntentStatesRequest) returns (GetReleaseIntentStatesResponse);
rpc ApprovePlanStage(ApprovePlanStageRequest) returns (ApprovePlanStageResponse);
rpc RejectPlanStage(RejectPlanStageRequest) returns (RejectPlanStageResponse);
rpc GetPlanOutput(GetPlanOutputRequest) returns (GetPlanOutputResponse);
}
message Source {
@@ -158,6 +325,8 @@ message Source {
optional string email = 2;
optional string source_type = 3;
optional string run_url = 4;
// The actor ID (user, app, or service account UUID) that created this annotation.
optional string user_id = 5;
}
message ArtifactContext {
@@ -177,6 +346,7 @@ message Artifact {
Project project = 7;
repeated ArtifactDestination destinations = 8;
string created_at = 9;
Ref ref = 10;
}
message ArtifactDestination {

View File

@@ -0,0 +1,212 @@
syntax = "proto3";
package forest.v1;
// RunnerService is exposed by the forest-server. Runners (workers) call these
// RPCs to register for work, fetch release artifacts, stream logs, and report
// completion. Authentication for all post-assignment RPCs uses a release-scoped
// opaque token rather than the regular JWT flow.
service RunnerService {
// Bidirectional stream used for runner registration and work assignment.
// The runner sends a RunnerRegister as its first message, then periodic
// RunnerHeartbeat messages. The server responds with a RegisterAck followed
// by WorkAssignment messages when releases matching the runner's capabilities
// become available.
rpc RegisterRunner(stream RunnerMessage) returns (stream ServerMessage);
// Fetch the artifact files for a release assigned to this runner.
// Scoped by the release_token received in the WorkAssignment.
rpc GetReleaseFiles(GetReleaseFilesRequest) returns (stream ReleaseFile);
// Stream log lines back to the server for real-time display.
// Each message must include the release_token for authentication.
rpc PushLogs(stream PushLogRequest) returns (PushLogResponse);
// Fetch the original spec files for a release.
// Scoped by the release_token received in the WorkAssignment.
rpc GetSpecFiles(GetSpecFilesRequest) returns (stream ReleaseFile);
// Fetch the annotation (metadata context) for a release.
rpc GetReleaseAnnotation(GetReleaseAnnotationRequest) returns (ReleaseAnnotationResponse);
// Fetch project info (organisation + project name) for a release.
rpc GetProjectInfo(GetProjectInfoRequest) returns (ProjectInfoResponse);
// Report the final outcome of a release (success or failure).
// This commits the release status and revokes the token.
rpc CompleteRelease(CompleteReleaseRequest) returns (CompleteReleaseResponse);
}
// ============================================================================
// Connect stream: Runner → Server
// ============================================================================
message RunnerMessage {
oneof message {
RunnerRegister register = 1;
RunnerHeartbeat heartbeat = 2;
WorkAck work_ack = 3;
}
}
// First message a runner sends on the Connect stream.
message RunnerRegister {
// Runner-chosen unique identifier. If empty, the server assigns one.
string runner_id = 1;
// Destination types this runner can handle.
repeated DestinationCapability capabilities = 2;
// Maximum number of simultaneous releases this runner can process.
int32 max_concurrent = 3;
}
// Describes a destination type the runner supports.
message DestinationCapability {
string organisation = 1;
string name = 2;
uint64 version = 3;
}
// Periodic keepalive sent by the runner (recommended every 10s).
message RunnerHeartbeat {
// Current number of in-progress releases on this runner.
int32 active_releases = 1;
}
// Runner's response to a WorkAssignment.
message WorkAck {
string release_token = 1;
// false = runner rejects the work (e.g., overloaded). The server will
// reassign or fall back to in-process execution.
bool accepted = 2;
}
// ============================================================================
// Connect stream: Server → Runner
// ============================================================================
message ServerMessage {
oneof message {
RegisterAck register_ack = 1;
WorkAssignment work_assignment = 2;
}
}
// Server response to RunnerRegister.
message RegisterAck {
// Server-confirmed (or server-assigned) runner ID.
string runner_id = 1;
bool accepted = 2;
string reason = 3;
}
// Work assignment pushed to a runner when a matching release is available.
message WorkAssignment {
// Scoped opaque auth token. Use this for GetReleaseFiles, PushLogs,
// and CompleteRelease. The token restricts access to only the data
// associated with this specific release.
string release_token = 1;
string release_id = 2;
string release_intent_id = 3;
string artifact_id = 4;
string destination_id = 5;
// Full destination configuration including metadata.
DestinationInfo destination = 6;
}
// Destination configuration sent with the work assignment.
message DestinationInfo {
string name = 1;
string environment = 2;
map<string, string> metadata = 3;
DestinationCapability type = 4;
string organisation = 5;
}
// ============================================================================
// GetReleaseFiles
// ============================================================================
message GetReleaseFilesRequest {
string release_token = 1;
}
message ReleaseFile {
string file_name = 1;
string file_content = 2;
}
// ============================================================================
// GetSpecFiles
// ============================================================================
message GetSpecFilesRequest {
string release_token = 1;
}
// ============================================================================
// GetReleaseAnnotation
// ============================================================================
message GetReleaseAnnotationRequest {
string release_token = 1;
}
message ReleaseAnnotationResponse {
string slug = 1;
string source_username = 2;
string source_email = 3;
string context_title = 4;
string context_description = 5;
string context_web = 6;
string reference_version = 7;
string reference_commit_sha = 8;
string reference_commit_branch = 9;
string reference_commit_message = 10;
string created_at = 11;
}
// ============================================================================
// GetProjectInfo
// ============================================================================
message GetProjectInfoRequest {
string release_token = 1;
}
message ProjectInfoResponse {
string organisation = 1;
string project = 2;
}
// ============================================================================
// PushLogs
// ============================================================================
message PushLogRequest {
string release_token = 1;
// "stdout" or "stderr"
string channel = 2;
string line = 3;
uint64 timestamp = 4;
}
message PushLogResponse {}
// ============================================================================
// CompleteRelease
// ============================================================================
message CompleteReleaseRequest {
string release_token = 1;
ReleaseOutcome outcome = 2;
// Error description when outcome is FAILURE.
string error_message = 3;
}
enum ReleaseOutcome {
RELEASE_OUTCOME_UNSPECIFIED = 0;
RELEASE_OUTCOME_SUCCESS = 1;
RELEASE_OUTCOME_FAILURE = 2;
}
message CompleteReleaseResponse {}

View File

@@ -0,0 +1,79 @@
syntax = "proto3";
package forest.v1;
import "forest/v1/releases.proto";
message Trigger {
string id = 1;
string name = 2;
bool enabled = 3;
optional string branch_pattern = 4;
optional string title_pattern = 5;
optional string author_pattern = 6;
optional string commit_message_pattern = 7;
optional string source_type_pattern = 8;
repeated string target_environments = 9;
repeated string target_destinations = 10;
bool force_release = 11;
string created_at = 12;
string updated_at = 13;
// When true, trigger the project's release pipeline instead of
// deploying directly to target destinations/environments.
bool use_pipeline = 14;
}
message CreateTriggerRequest {
Project project = 1;
string name = 2;
optional string branch_pattern = 3;
optional string title_pattern = 4;
optional string author_pattern = 5;
optional string commit_message_pattern = 6;
optional string source_type_pattern = 7;
repeated string target_environments = 8;
repeated string target_destinations = 9;
bool force_release = 10;
bool use_pipeline = 11;
}
message CreateTriggerResponse {
Trigger trigger = 1;
}
message UpdateTriggerRequest {
Project project = 1;
string name = 2;
optional bool enabled = 3;
optional string branch_pattern = 4;
optional string title_pattern = 5;
optional string author_pattern = 6;
optional string commit_message_pattern = 7;
optional string source_type_pattern = 8;
repeated string target_environments = 9;
repeated string target_destinations = 10;
optional bool force_release = 11;
optional bool use_pipeline = 12;
}
message UpdateTriggerResponse {
Trigger trigger = 1;
}
message DeleteTriggerRequest {
Project project = 1;
string name = 2;
}
message DeleteTriggerResponse {}
message ListTriggersRequest {
Project project = 1;
}
message ListTriggersResponse {
repeated Trigger triggers = 1;
}
service TriggerService {
rpc CreateTrigger(CreateTriggerRequest) returns (CreateTriggerResponse);
rpc UpdateTrigger(UpdateTriggerRequest) returns (UpdateTriggerResponse);
rpc DeleteTrigger(DeleteTriggerRequest) returns (DeleteTriggerResponse);
rpc ListTriggers(ListTriggersRequest) returns (ListTriggersResponse);
}