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
4 changes: 3 additions & 1 deletion api/v1alpha1/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ const (
AnnotationProjectName = "actions.kelos.dev/project-name"
AnnotationWorkflowFile = "actions.kelos.dev/workflow-file"
AnnotationWorkflowPlan = "actions.kelos.dev/workflow-plan"
AnnotationMatrixPlan = "actions.kelos.dev/matrix-plan"
// AnnotationDeferredJobPlan is persisted on planning ConfigMaps and must
// remain stable for stored WorkflowRuns.
AnnotationDeferredJobPlan = "actions.kelos.dev/matrix-plan"
)
8 changes: 8 additions & 0 deletions api/v1alpha1/workflowrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ type WorkflowRunRerun struct {
// +optional
RequestID string `json:"requestID,omitempty"`

// TriggeringActor is the GitHub login that requested this attempt. It is
// omitted when the rerun source cannot identify a GitHub user.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=100
// +kubebuilder:validation:Pattern=`^[A-Za-z0-9][A-Za-z0-9_-]*(\[bot\])?$`
// +optional
TriggeringActor string `json:"triggeringActor,omitempty"`

// JobIDs selects expanded WorkflowJob IDs to rerun. Selected jobs reuse the
// latest available results and outputs of prerequisite jobs from earlier
// attempts. Omit it to execute every job in the workflow.
Expand Down
2 changes: 2 additions & 0 deletions cmd/open-actions-runner/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestRunWritesWorkflowJobResult(t *testing.T) {
Event: runner.Event{Name: "push", DeliveryID: "delivery"},
Revision: runner.Revision{SHA: strings.Repeat("a", 40), Ref: "refs/heads/main", RefName: "main"},
WorkflowName: "CI",
WorkflowPath: ".github/workflows/ci.yml",
JobID: "build",
GitHubTokenPermissions: map[string]string{"contents": "read"},
TimeoutSeconds: int64((6 * time.Hour) / time.Second),
Expand Down Expand Up @@ -78,6 +79,7 @@ func TestRunLoadsNeedsContext(t *testing.T) {
Event: runner.Event{Name: "push", DeliveryID: "delivery"},
Revision: runner.Revision{SHA: strings.Repeat("a", 40), Ref: "refs/heads/main", RefName: "main"},
WorkflowName: "CI",
WorkflowPath: ".github/workflows/ci.yml",
JobID: "report",
GitHubTokenPermissions: map[string]string{"contents": "read"},
TimeoutSeconds: int64((6 * time.Hour) / time.Second),
Expand Down
184 changes: 124 additions & 60 deletions docs/reference.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/controller/runner_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2138,6 +2138,7 @@ func runnerControllerPlanData(t *testing.T, permissions map[string]string, steps
Event: runner.Event{Name: "push", DeliveryID: "delivery"},
Revision: runner.Revision{SHA: strings.Repeat("a", 40), Ref: "refs/heads/main", RefName: "main"},
WorkflowName: "CI",
WorkflowPath: ".github/workflows/ci.yml",
JobID: "build",
GitHubTokenPermissions: permissions,
TimeoutSeconds: int64((6 * time.Hour) / time.Second),
Expand Down
332 changes: 189 additions & 143 deletions internal/controller/workflowrun_controller.go

Large diffs are not rendered by default.

244 changes: 213 additions & 31 deletions internal/controller/workflowrun_controller_test.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/eventsnapshot/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func Decode(data []byte) (map[string]any, error) {
}
document := map[string]any{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&document); err != nil {
return nil, fmt.Errorf("decode GitHub event snapshot: %w", err)
}
Expand Down
7 changes: 6 additions & 1 deletion internal/eventsnapshot/snapshot_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package eventsnapshot

import (
"encoding/json"
"strings"
"testing"
)

func TestDecodeBoundsSnapshot(t *testing.T) {
if _, err := Decode([]byte(`{"repository":{"full_name":"acme/example"}}`)); err != nil {
document, err := Decode([]byte(`{"repository":{"id":9007199254740993,"full_name":"acme/example"}}`))
if err != nil {
t.Fatal(err)
}
if document["repository"].(map[string]any)["id"] != json.Number("9007199254740993") {
t.Fatalf("decoded snapshot = %#v", document)
}
if _, err := Decode([]byte(`{"first":1}{"second":2}`)); err == nil {
t.Fatal("Decode() accepted trailing JSON")
}
Expand Down
12 changes: 12 additions & 0 deletions internal/expression/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package expression

import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -10,6 +11,17 @@ import (
"testing"
)

func TestEvaluationPreservesDecodedJSONNumberType(t *testing.T) {
context := Context{
Availability: NewAvailability("matrix"),
Values: map[string]any{"matrix": map[string]any{"count": json.Number("2")}},
}
result := evaluateForTest(t, "${{ matrix.count }}", context)
if result.Value != float64(2) {
t.Fatalf("value = %#v (%T), want numeric 2", result.Value, result.Value)
}
}

func TestTemplateEvaluationPreservesTypesAndLiteralText(t *testing.T) {
context := Context{
Availability: NewAvailability("matrix"),
Expand Down
7 changes: 7 additions & 0 deletions internal/expression/value.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package expression

import (
"encoding/json"
"fmt"
"math"
"reflect"
Expand Down Expand Up @@ -105,6 +106,12 @@ func normalize(input any, sensitive bool) value {
return value{kind: boolKind, boolean: typed, sensitive: sensitive}
case string:
return value{kind: stringKind, text: typed, sensitive: sensitive}
case json.Number:
number, err := typed.Float64()
if err != nil {
return value{kind: stringKind, text: typed.String(), sensitive: sensitive}
}
return value{kind: numberKind, number: number, sensitive: sensitive}
case float64:
return value{kind: numberKind, number: typed, sensitive: sensitive}
case float32:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ spec:
minLength: 1
pattern: ^[A-Za-z0-9-]+$
type: string
triggeringActor:
description: |-
TriggeringActor is the GitHub login that requested this attempt. It is
omitted when the rerun source cannot identify a GitHub user.
maxLength: 100
minLength: 1
pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*(\[bot\])?$
type: string
required:
- attempt
- originalRunRef
Expand Down
60 changes: 44 additions & 16 deletions internal/runner/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"

"github.com/kelos-dev/open-actions/internal/actionref"
workflowexpression "github.com/kelos-dev/open-actions/internal/expression"
"github.com/kelos-dev/open-actions/internal/gitrepository"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -169,9 +170,6 @@ func (r *actionResolver) resolve(ctx context.Context, uses string) (preparedActi
return preparedAction{}, fmt.Errorf("action %s uses an unsupported lifecycle condition", uses)
}
case "composite":
if err := validateComposite(definition); err != nil {
return preparedAction{}, fmt.Errorf("action %s: %w", uses, err)
}
default:
return preparedAction{}, fmt.Errorf("action %s uses unsupported runtime %q", uses, definition.Runs.Using)
}
Expand All @@ -180,7 +178,7 @@ func (r *actionResolver) resolve(ctx context.Context, uses string) (preparedActi
return action, nil
}

func (r *actionResolver) invocation(step Step, plan *Plan, token string) (*actionInvocation, error) {
func (r *actionResolver) invocation(step Step, plan *Plan, token string, status *workflowexpression.Status) (*actionInvocation, error) {
reference, err := actionref.Parse(step.Uses)
if err != nil {
return nil, err
Expand All @@ -189,7 +187,7 @@ func (r *actionResolver) invocation(step Step, plan *Plan, token string) (*actio
if !found {
return nil, fmt.Errorf("action %s was not prepared", step.Uses)
}
inputs, err := actionInputs(action.definition.Inputs, step.With, plan, r.environment, token)
inputs, err := actionInputsForAction(action.definition.Inputs, step.With, plan, r.environment, token, action, status)
if err != nil {
return nil, fmt.Errorf("configure action %s: %w", step.Uses, err)
}
Expand Down Expand Up @@ -297,10 +295,39 @@ func loadActionDefinition(directory string) (actionDefinition, error) {
return actionDefinition{}, fmt.Errorf("input %q default: %w", name, err)
}
}
for name, output := range definition.Outputs {
if output.Value == nil {
continue
}
value, err := inputString(output.Value)
if err != nil {
return actionDefinition{}, fmt.Errorf("output %q value: %w", name, err)
}
if err := validateActionExpression(value, compositeOutputAvailability); err != nil {
return actionDefinition{}, fmt.Errorf("output %q value: %w", name, err)
}
}
if definition.Runs.Using == "composite" {
if err := validateComposite(definition); err != nil {
return actionDefinition{}, err
}
}
return definition, nil
}

func actionInputs(definitions map[string]actionInput, supplied map[string]string, plan *Plan, environment []string, token string) (map[string]string, error) {
return actionInputsWithContext(definitions, supplied, plan, environment, token, "", "", "", nil)
}

func actionInputsForAction(definitions map[string]actionInput, supplied map[string]string, plan *Plan, environment []string, token string, action preparedAction, status *workflowexpression.Status) (map[string]string, error) {
actionPath := ""
if action.definition.Runs.Using == "composite" {
actionPath = action.directory
}
return actionInputsWithContext(definitions, supplied, plan, environment, token, actionPath, action.reference.Owner+"/"+action.reference.Repository, action.reference.Ref, status)
}

func actionInputsWithContext(definitions map[string]actionInput, supplied map[string]string, plan *Plan, environment []string, token, actionPath, actionRepository, actionRef string, status *workflowexpression.Status) (map[string]string, error) {
result := make(map[string]string, len(definitions)+len(supplied))
definitionNames := make(map[string]string, len(definitions))
for name := range definitions {
Expand Down Expand Up @@ -329,7 +356,7 @@ func actionInputs(definitions map[string]actionInput, supplied map[string]string
if err != nil {
return nil, fmt.Errorf("input %q default: %w", name, err)
}
resolved, err := resolveActionDefault(value, plan, environment, token)
resolved, err := resolveActionDefault(value, plan, environment, token, actionPath, actionRepository, actionRef, status)
if err != nil {
return nil, fmt.Errorf("input %q default: %w", name, err)
}
Expand All @@ -350,11 +377,11 @@ func inputString(value any) (string, error) {
}
}

func resolveActionDefault(value string, plan *Plan, environment []string, token string) (string, error) {
return resolveActionDefaultExpression(value, plan, environment, token)
func resolveActionDefault(value string, plan *Plan, environment []string, token, actionPath, actionRepository, actionRef string, status *workflowexpression.Status) (string, error) {
return resolveActionDefaultExpression(value, plan, environment, token, actionPath, actionRepository, actionRef, status)
}

func (e *Executor) runJavaScriptHook(ctx context.Context, invocation *actionInvocation, hook, entrypoint, temporaryDirectory, workspace string, environment *[]string) error {
func (e *Executor) runJavaScriptHook(ctx context.Context, invocation *actionInvocation, hook, entrypoint string, state *executionState) error {
if entrypoint == "" {
return nil
}
Expand All @@ -368,11 +395,11 @@ func (e *Executor) runJavaScriptHook(ctx context.Context, invocation *actionInvo
if _, err := os.Stat(entrypointPath); err != nil {
return fmt.Errorf("find %s entrypoint: %w", hook, err)
}
files, err := newCommandFiles(filepath.Join(temporaryDirectory, "commands"), fmt.Sprintf("%d-%s", e.commandID.Add(1), hook))
files, err := newCommandFiles(filepath.Join(state.temporaryDirectory, "commands"), fmt.Sprintf("%d-%s", e.commandID.Add(1), hook))
if err != nil {
return err
}
actionEnvironment := append([]string(nil), (*environment)...)
actionEnvironment := append([]string(nil), state.environment...)
actionEnvironment = appendEnvironment(actionEnvironment, invocation.step.Env)
actionEnvironment = setEnvironment(actionEnvironment, "GITHUB_ACTION_PATH", invocation.directory)
actionEnvironment = setEnvironment(actionEnvironment, "GITHUB_ACTION_REPOSITORY", invocation.reference.Owner+"/"+invocation.reference.Repository)
Expand All @@ -385,12 +412,13 @@ func (e *Executor) runJavaScriptHook(ctx context.Context, invocation *actionInvo
actionEnvironment = setEnvironment(actionEnvironment, "STATE_"+name, value)
}
}
executionError := e.executeCommandWithFiles(ctx, invocation.executable, []string{entrypointPath}, workspace, actionEnvironment, &files, workspace)
executionError := e.executeCommandWithFiles(ctx, invocation.executable, []string{entrypointPath}, state.workspace, actionEnvironment, &files, state.workspace)
updates, commandError := files.read()
if commandError != nil {
return errors.Join(executionError, commandError)
}
applyEnvironmentUpdates(environment, updates)
applyEnvironmentUpdates(&state.environment, updates)
applyContextEnvironmentUpdates(&state.contextEnvironment, updates)
for name, value := range updates.state {
invocation.state[name] = value
}
Expand All @@ -403,7 +431,7 @@ func (e *Executor) runJavaScriptHook(ctx context.Context, invocation *actionInvo
}

func (e *Executor) executeAction(ctx context.Context, state *executionState, step Step, cancelled bool) (map[string]string, error) {
invocation, err := state.resolver.invocation(step, state.plan, e.githubToken)
invocation, err := state.resolver.invocation(step, state.plan, e.githubToken, &state.jobStatus)
if err != nil {
return nil, err
}
Expand All @@ -423,10 +451,10 @@ func (e *Executor) executeAction(ctx context.Context, state *executionState, ste
if invocation.definition.Runs.Post != "" {
state.posts = append(state.posts, invocation)
}
if err := e.runJavaScriptHook(ctx, invocation, "pre", invocation.definition.Runs.Pre, state.temporaryDirectory, state.workspace, &state.environment); err != nil {
if err := e.runJavaScriptHook(ctx, invocation, "pre", invocation.definition.Runs.Pre, state); err != nil {
return nil, err
}
if err := e.runJavaScriptHook(ctx, invocation, "main", invocation.definition.Runs.Main, state.temporaryDirectory, state.workspace, &state.environment); err != nil {
if err := e.runJavaScriptHook(ctx, invocation, "main", invocation.definition.Runs.Main, state); err != nil {
return invocation.outputs, err
}
if integrate {
Expand Down
Loading
Loading