From f3fa8359e3f4bd3520ac820ac0125a7abc954e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 10 Mar 2026 16:00:23 +0700 Subject: [PATCH 1/4] core/model: implement json/v2 `UnmarshalerFrom` --- core/pkg/model/actions/runs_serde.go | 53 +- core/pkg/model/serde.go | 28 + core/pkg/model/workflows/container_serde.go | 25 +- core/pkg/model/workflows/evaluable_serde.go | 134 +++-- core/pkg/model/workflows/event.go | 569 +------------------- core/pkg/model/workflows/event_serde.go | 385 ++----------- core/pkg/model/workflows/job.go | 7 +- core/pkg/model/workflows/job_serde.go | 170 +++--- core/pkg/model/workflows/step_serde.go | 52 +- core/pkg/model/workflows/workflow.go | 16 +- core/pkg/model/workflows/workflow_serde.go | 83 ++- 11 files changed, 329 insertions(+), 1193 deletions(-) diff --git a/core/pkg/model/actions/runs_serde.go b/core/pkg/model/actions/runs_serde.go index ca9234cf..435a7cad 100644 --- a/core/pkg/model/actions/runs_serde.go +++ b/core/pkg/model/actions/runs_serde.go @@ -7,54 +7,35 @@ package actions import ( + "encoding/json/jsontext" + "encoding/json/v2" "fmt" - "reflect" "strings" "drassi.run/core/pkg/model" ) -var typeRuns = reflect.TypeFor[Runs]() - -func DecodeRunsHook(from reflect.Value, to reflect.Value) (any, error) { - if !from.IsValid() || !to.Type().Implements(typeRuns) { - return valueOf(from), nil - } - - f := from.Interface() - m, ok := f.(map[string]any) - if !ok { - return f, nil +func discriminateRuns(raw jsontext.Value) (Runs, error) { + var dis struct { + Using string `json:"using,omitempty"` } - - using, ok := m["using"].(string) - if !ok { - return nil, fmt.Errorf("`using` is required, and MUST be a string") + if err := json.Unmarshal(raw, &dis); err != nil { + return nil, err } - t := to.Interface() - if using == "composite" { - if t == nil { - to.Set(reflect.ValueOf(&CompositeRuns{})) - } else if _, ok := t.(*CompositeRuns); !ok { - return nil, fmt.Errorf(`map with using=%q CAN'T be decode to %T`, using, t) - } - } else if using == "docker" { - if t == nil { - to.Set(reflect.ValueOf(&DockerRuns{})) - } else if _, ok := t.(*DockerRuns); !ok { - return nil, fmt.Errorf(`map with using=%q CAN'T be decode to %T`, using, t) - } - } else if strings.HasPrefix(using, "node") { - if t == nil { - to.Set(reflect.ValueOf(&NodeRuns{})) - } else if _, ok := t.(*NodeRuns); !ok { - return nil, fmt.Errorf(`map with using=%q CAN'T be decode to %T`, using, t) + switch u := dis.Using; u { + case "composite": + return new(CompositeRuns), nil + case "docker": + return new(DockerRuns), nil + default: + if strings.HasPrefix(u, "node") { + return new(NodeRuns), nil } + return nil, fmt.Errorf(`unknown runs with using=%q`, u) } - return m, nil } func init() { - model.RegisterDecodeHook(DecodeRunsHook) + model.RegisterUnmarshalInterface(discriminateRuns) } diff --git a/core/pkg/model/serde.go b/core/pkg/model/serde.go index 87dff841..c5fcdcea 100644 --- a/core/pkg/model/serde.go +++ b/core/pkg/model/serde.go @@ -7,6 +7,8 @@ package model import ( + "encoding/json/jsontext" + "encoding/json/v2" "slices" "github.com/go-viper/mapstructure/v2" @@ -53,3 +55,29 @@ func DecodeWithOptions(source any, target any, opts ...DecodeOption) error { } return d.Decode(source) } + +var unmarshalers []*json.Unmarshalers + +func RegisterUnmarshalInterface[T any](dis func(raw jsontext.Value) (T, error)) { + u := unmarshalInterface(dis) + unmarshalers = append(unmarshalers, u) +} + +func unmarshalInterface[T any](dis func(raw jsontext.Value) (T, error)) *json.Unmarshalers { + fn := func(d *jsontext.Decoder, val *T) error { + var raw jsontext.Value + if err := json.UnmarshalDecode(d, &raw); err != nil { + return err + } + + if t, err := dis(raw); err != nil { + return err + } else if err = json.Unmarshal(raw, t, d.Options()); err != nil { + return err + } else { + *val = t + return nil + } + } + return json.UnmarshalFromFunc(fn) +} diff --git a/core/pkg/model/workflows/container_serde.go b/core/pkg/model/workflows/container_serde.go index 69971678..3bdd9952 100644 --- a/core/pkg/model/workflows/container_serde.go +++ b/core/pkg/model/workflows/container_serde.go @@ -6,13 +6,24 @@ package workflows -import "drassi.run/core/pkg/model" +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" +) -func (c *Container) DecodeMapstructure(input any) (any, error) { - if image, ok := model.Stringify(input); ok { - m := map[string]any{"image": image} - return m, nil +func (c *Container) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand string format (e.g., "ubuntu:22.04") + case jsontext.KindString: + return json.UnmarshalDecode(d, &c.Image) + + // 2. Standard object format (e.g., {"image": "ubuntu:22.04", "env": {...}}) + case jsontext.KindBeginObject: + type alias Container + return json.UnmarshalDecode(d, (*alias)(c)) + + default: + return fmt.Errorf("expected string or object for Container, got kind %v", k) } - // process Container normal way - return input, nil } diff --git a/core/pkg/model/workflows/evaluable_serde.go b/core/pkg/model/workflows/evaluable_serde.go index 9009b775..933af4ab 100644 --- a/core/pkg/model/workflows/evaluable_serde.go +++ b/core/pkg/model/workflows/evaluable_serde.go @@ -7,78 +7,100 @@ package workflows import ( - "reflect" + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" "strings" - - "drassi.run/core/pkg/model" ) -var typeToken = reflect.TypeFor[Token]() +func UnmarshalToken(d *jsontext.Decoder, t *Token) error { + switch d.PeekKind() { + case jsontext.KindNull: + *t = nil -func DecodeTokenHook(from reflect.Value, to reflect.Value) (any, error) { - if !from.IsValid() || !to.Type().Implements(typeToken) || to.Interface() != nil { - return valueOf(from), nil - } + case jsontext.KindTrue, jsontext.KindFalse: + if tok, err := d.ReadToken(); err != nil { + return err + } else { + b := tok.Bool() + *t = NewLiteralToken(b) + } - var ( - token Token = nil - data any = nil - ) - switch from.Kind() { - case reflect.Bool: - token = NewLiteralToken(from.Bool()) - case reflect.String: - s := from.String() - if strings.Contains(s, OpenExpression) { - token = NewExpressionToken(s) + case jsontext.KindNumber: + if tok, err := d.ReadToken(); err != nil { + return err } else { - token = NewLiteralToken(s) + f := tok.Float() + *t = NewLiteralToken(f) } - case reflect.Slice, reflect.Array: - token = NewSequenceToken(nil) - data = from.Interface() - case reflect.Map: - token = NewMappingToken(nil) - data = from.Interface() - default: - if from.CanInt() { - token = NewLiteralToken(from.Int()) - } else if from.CanUint() { - token = NewLiteralToken(from.Uint()) - } else if from.CanFloat() { - token = NewLiteralToken(from.Float()) + + case jsontext.KindString: + if tok, err := d.ReadToken(); err != nil { + return err } else { - data = from.Interface() + s := tok.String() + if strings.Contains(s, OpenExpression) { + *t = NewExpressionToken(s) + } else { + *t = NewLiteralToken(s) + } } + + case jsontext.KindBeginArray: + var seq = make(sequenceToken, 0) + if err := json.UnmarshalDecode(d, &seq); err != nil { + return err + } + *t = seq + + case jsontext.KindBeginObject: + var dic = make(mappingToken, 0) + if err := dic.unmarshalJsonMap(d); err != nil { + return err + } + *t = dic + + default: + return fmt.Errorf("unknown token type %v", d.PeekKind()) } - if token != nil { - to.Set(reflect.ValueOf(token)) - } - if data != nil { - return data, nil - } else { - return token, nil + return nil +} + +func (m *mappingToken) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + case jsontext.KindBeginObject: + *m = make(mappingToken, 0) + return m.unmarshalJsonMap(d) + default: + return fmt.Errorf("unknown token type %v", k) } } -func (m mappingToken) DecodeMapstructure(input any) (any, error) { - inputVal := reflect.ValueOf(input) - if inputVal.Kind() != reflect.Map { - return input, nil +func (m *mappingToken) unmarshalJsonMap(d *jsontext.Decoder) error { + // Consume "{" + if _, err := d.ReadToken(); err != nil { + return err } - a := make([][2]any, 0, inputVal.Len()) - mapIter := inputVal.MapRange() - for mapIter.Next() { - key := mapIter.Key() - val := mapIter.Value() + for d.PeekKind() != jsontext.KindEndObject { + var pair [2]Token - pair := [2]any{key.Interface(), val.Interface()} - a = append(a, pair) + // Object key + if err := json.UnmarshalDecode(d, &pair[0]); err != nil { + return err + } + + // Object value. + if err := json.UnmarshalDecode(d, &pair[1]); err != nil { + return err + } + + *m = append(*m, pair) } - return a, nil -} -func init() { - model.RegisterDecodeHook(DecodeTokenHook) + // Consume "}". + if _, err := d.ReadToken(); err != nil { + return err + } + return nil } diff --git a/core/pkg/model/workflows/event.go b/core/pkg/model/workflows/event.go index 34a618da..d4091883 100644 --- a/core/pkg/model/workflows/event.go +++ b/core/pkg/model/workflows/event.go @@ -6,573 +6,14 @@ package workflows -type empty = struct{} -type onEventWithTypes[T ~string] struct { - Types []T `json:"types,omitempty" yaml:"types,omitempty" actions:"types,omitempty"` -} -type onEventWithRef struct { - Branches []string `json:"branches,omitempty" yaml:"branches,omitempty" actions:"branches,omitempty"` - Tags []string `json:"tags,omitempty" yaml:"tags,omitempty" actions:"tags,omitempty"` - Paths []string `json:"paths,omitempty" yaml:"paths,omitempty" actions:"paths,omitempty"` - BranchesIgnore []string `json:"branches-ignore,omitempty" yaml:"branches-ignore,omitempty" actions:"branches-ignore,omitempty"` - TagsIgnore []string `json:"tags-ignore,omitempty" yaml:"tags-ignore,omitempty" actions:"tags-ignore,omitempty"` - PathsIgnore []string `json:"paths-ignore,omitempty" yaml:"paths-ignore,omitempty" actions:"paths-ignore,omitempty"` -} +import "encoding/json/jsontext" // The name of the GitHub event that triggers the workflow. // You can provide a single event string, array of events, array of event types, or an event configuration map // that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. -// For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows. +// For a list of available events, see https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on -type On struct { - // Runs your workflow anytime the branch_protection_rule event occurs. More than one activity type triggers this event. - // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#branch_protection_rule - BranchProtectionRule *OnBranchProtectionRule `json:"branch_protection_rule,omitempty" yaml:"branch_protection_rule,omitempty" actions:"branch_protection_rule,omitempty"` +type On map[string]Event - // Runs your workflow anytime the check_run event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/checks/runs. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#check_run - CheckRun *OnCheckRun `json:"check_run,omitempty" yaml:"check_run,omitempty" actions:"check_run,omitempty"` - - // Runs your workflow anytime the check_suite event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/checks/suites/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#check_suite - CheckSuite *OnCheckSuite `json:"check_suite,omitempty" yaml:"check_suite,omitempty" actions:"check_suite,omitempty"` - - // Runs your workflow anytime someone creates a branch or tag, which triggers the create event. - // For information about the REST API, see https://developer.github.com/v3/git/refs/#create-a-reference. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#create - Create *OnCreate `json:"create,omitempty" yaml:"create,omitempty" actions:"create,omitempty"` - - // Runs your workflow anytime someone deletes a branch or tag, which triggers the delete event. - // For information about the REST API, see https://developer.github.com/v3/git/refs/#delete-a-reference. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete - Delete *OnDelete `json:"delete,omitempty" yaml:"delete,omitempty" actions:"delete,omitempty"` - - // Runs your workflow anytime someone creates a deployment, which triggers the deployment event. - // Deployments created with a commit SHA may not have a Git ref. - // For information about the REST API, see https://developer.github.com/v3/repos/deployments/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#deployment - Deployment *OnDeployment `json:"deployment,omitempty" yaml:"deployment,omitempty" actions:"deployment,omitempty"` - - // Runs your workflow anytime a third party provides a deployment status, which triggers the deployment_status event. - // Deployments created with a commit SHA may not have a Git ref. - // For information about the REST API, see https://developer.github.com/v3/repos/deployments/#create-a-deployment-status. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#deployment_status - DeploymentStatus *OnDeploymentStatus `json:"deployment_status,omitempty" yaml:"deployment_status,omitempty" actions:"deployment_status,omitempty"` - - // Runs your workflow anytime the discussion event occurs. More than one activity type triggers this event. - // For information about the GraphQL API, see https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#discussion - Discussion *OnDiscussion `json:"discussion,omitempty" yaml:"discussion,omitempty" actions:"discussion,omitempty"` - - // Runs your workflow anytime the discussion_comment event occurs. More than one activity type triggers this event. - // For information about the GraphQL API, see https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#discussion_comment - DiscussionComment *OnDiscussionComment `json:"discussion_comment,omitempty" yaml:"discussion_comment,omitempty" actions:"discussion_comment,omitempty"` - - // Runs your workflow anytime when someone forks a repository, which triggers the fork event. - // For information about the REST API, see https://developer.github.com/v3/repos/forks/#create-a-fork. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork - Fork *OnFork `json:"fork,omitempty" yaml:"fork,omitempty" actions:"fork,omitempty"` - - // Runs your workflow when someone creates or updates a Wiki page, which triggers the gollum event. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum - Gollum *OnGollum `json:"gollum,omitempty" yaml:"gollum,omitempty" actions:"gollum,omitempty"` - - // Runs your workflow anytime the issue_comment event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/issues/comments/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment - IssueComment *OnIssueComment `json:"issue_comment,omitempty" yaml:"issue_comment,omitempty" actions:"issue_comment,omitempty"` - - // Runs your workflow anytime the issues event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/issues. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues - Issues *OnIssues `json:"issues,omitempty" yaml:"issues,omitempty" actions:"issues,omitempty"` - - // Runs your workflow anytime the label event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/issues/labels/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#label - Label *OnLabel `json:"label,omitempty" yaml:"label,omitempty" actions:"label,omitempty"` - - // Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group. - // For information about the merge queue, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/merging-a-pull-request-with-a-merge-queue. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#merge_group - MergeGroup *OnMergeGroup `json:"merge_group,omitempty" yaml:"merge_group,omitempty" actions:"merge_group,omitempty"` - - // Runs your workflow anytime the milestone event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/issues/milestones/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#milestone - Milestone *OnMilestone `json:"milestone,omitempty" yaml:"milestone,omitempty" actions:"milestone,omitempty"` - - // Runs your workflow anytime someone pushes to a GitHub Pages-enabled branch, which triggers the page_build event. - // For information about the REST API, see https://developer.github.com/v3/repos/pages/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#page_build - PageBuild *OnPageBuild `json:"page_build,omitempty" yaml:"page_build,omitempty" actions:"page_build,omitempty"` - - // Runs your workflow anytime the project event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/projects/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#project - Project *OnProject `json:"project,omitempty" yaml:"project,omitempty" actions:"project,omitempty"` - - // Runs your workflow anytime the project_card event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/projects/cards. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#project_card - ProjectCard *OnProjectCard `json:"project_card,omitempty" yaml:"project_card,omitempty" actions:"project_card,omitempty"` - - // Runs your workflow anytime the project_column event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/projects/columns. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#project_column - ProjectColumn *OnProjectColumn `json:"project_column,omitempty" yaml:"project_column,omitempty" actions:"project_column,omitempty"` - - // Runs your workflow anytime someone makes a private repository public, which triggers the public event. - // For information about the REST API, see https://developer.github.com/v3/repos/#edit. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#public - Public *OnPublic `json:"public,omitempty" yaml:"public,omitempty" actions:"public,omitempty"` - - // Runs your workflow anytime the pull_request event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/pulls. - // Note: Workflows do not run on private base repositories when you open a pull request from a forked repository. - // When you create a pull request from a forked repository to the base repository, GitHub sends - // the pull_request event to the base repository and no pull request events occur on the forked repository. - // Workflows don't run on forked repositories by default. You must enable GitHub Actions in the Actions tab - // of the forked repository. The permissions for the GITHUB_TOKEN in forked repositories is read-only. - // For more information about the GITHUB_TOKEN, see https://help.github.com/en/articles/virtual-environments-for-github-actions. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request - PullRequest *OnPullRequest `json:"pull_request,omitempty" yaml:"pull_request,omitempty" actions:"pull_request,omitempty"` - - // Runs your workflow anytime the pull_request_review event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/pulls/reviews. - // Note: Workflows do not run on private base repositories when you open a pull request from a forked repository. - // When you create a pull request from a forked repository to the base repository, GitHub sends - // the pull_request event to the base repository and no pull request events occur on the forked repository. - // Workflows don't run on forked repositories by default. You must enable GitHub Actions in the Actions tab - // of the forked repository. The permissions for the GITHUB_TOKEN in forked repositories is read-only. - // For more information about the GITHUB_TOKEN, see https://help.github.com/en/articles/virtual-environments-for-github-actions. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review - PullRequestReview *OnPullRequestReview `json:"pull_request_review,omitempty" yaml:"pull_request_review,omitempty" actions:"pull_request_review,omitempty"` - - // Runs your workflow anytime a comment on a pull request's unified diff is modified, which triggers the pull_request_review_comment event. - // More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/pulls/comments. - // Note: Workflows do not run on private base repositories when you open a pull request from a forked repository. - // When you create a pull request from a forked repository to the base repository, GitHub sends the pull_request - // event to the base repository and no pull request events occur on the forked repository. - // Workflows don't run on forked repositories by default. You must enable GitHub Actions in the Actions tab - // of the forked repository. The permissions for the GITHUB_TOKEN in forked repositories is read-only. - // For more information about the GITHUB_TOKEN, see https://help.github.com/en/articles/virtual-environments-for-github-actions. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review_comment - PullRequestReviewComment *OnPullRequestReviewComment `json:"pull_request_review_comment,omitempty" yaml:"pull_request_review_comment,omitempty" actions:"pull_request_review_comment,omitempty"` - - // This event is similar to pull_request, except that it runs in the context of the base repository of the pull request, rather than in the merge commit. - // This means that you can more safely make your secrets available to the workflows triggered by the pull request, - // because only workflows defined in the commit on the base repository are run. - // For example, this event allows you to create workflows that label and comment on pull requests, based on the contents of the event payload. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target - PullRequestTarget *OnPullRequestTarget `json:"pull_request_target,omitempty" yaml:"pull_request_target,omitempty" actions:"pull_request_target,omitempty"` - - // Runs your workflow when someone pushes to a repository branch, which triggers the push event. - // Note: The webhook payload available to GitHub Actions does not include the added, removed, and - // modified attributes in the commit object. You can retrieve the full commit object using the REST API. - // For more information, see https://developer.github.com/v3/repos/commits/#get-a-single-commit. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push - Push *OnPush `json:"push,omitempty" yaml:"push,omitempty" actions:"push,omitempty"` - - // Runs your workflow anytime a package is published or updated. - // For more information, see https://help.github.com/en/github/managing-packages-with-github-packages. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#registry_package - RegistryPackage *OnRegistryPackage `json:"registry_package,omitempty" yaml:"registry_package,omitempty" actions:"registry_package,omitempty"` - - // Runs your workflow anytime the release event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/repos/releases/ in the GitHub Developer documentation. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release - Release *OnRelease `json:"release,omitempty" yaml:"release,omitempty" actions:"release,omitempty"` - - // You can use the GitHub API to trigger a webhook event called repository_dispatch when you want to trigger - // a workflow for activity that happens outside of GitHub. - // For more information, see https://developer.github.com/v3/repos/#create-a-repository-dispatch-event. - // To trigger the custom repository_dispatch webhook event, you must send a POST request to a GitHub API endpoint and - // provide an event_type name to describe the activity type. - // To trigger a workflow run, you must also configure your workflow to use the repository_dispatch event. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch - RepositoryDispatch *OnRepositoryDispatch `json:"repository_dispatch,omitempty" yaml:"repository_dispatch,omitempty" actions:"repository_dispatch,omitempty"` - - // You can schedule a workflow to run at specific UTC times using POSIX cron syntax - // (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07). - // Scheduled workflows run on the latest commit on the default or base branch. - // The shortest interval you can run scheduled workflows is once every 5 minutes. - // Note: GitHub Actions does not support the non-standard syntax @yearly, @monthly, @weekly, @daily, @hourly, and @reboot. - // You can use crontab guru (https://crontab.guru/). to help generate your cron syntax and confirm what time it will run. - // To help you get started, there is also a list of crontab guru examples (https://crontab.guru/examples.html). - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule - Schedule *OnSchedule `json:"schedule,omitempty" yaml:"schedule,omitempty" actions:"schedule,omitempty"` - - // Runs your workflow anytime the status of a Git commit changes, which triggers the status event. - // For information about the REST API, see https://developer.github.com/v3/repos/statuses/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#status - Status *OnStatus `json:"status,omitempty" yaml:"status,omitempty" actions:"status,omitempty"` - - // Runs your workflow anytime the watch event occurs. More than one activity type triggers this event. - // For information about the REST API, see https://developer.github.com/v3/activity/starring/. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#watch - Watch *OnWatch `json:"watch,omitempty" yaml:"watch,omitempty" actions:"watch,omitempty"` - - // Allows workflows to be reused by other workflows. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_call - WorkflowCall *OnWorkflowCall `json:"workflow_call,omitempty" yaml:"workflow_call,omitempty" actions:"workflow_call,omitempty"` - - // You can now create workflows that are manually triggered with the new workflow_dispatch event. - // You will then see a 'Run workflow' button on the Actions tab, enabling you to easily trigger a run. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch - WorkflowDispatch *OnWorkflowDispatch `json:"workflow_dispatch,omitempty" yaml:"workflow_dispatch,omitempty" actions:"workflow_dispatch,omitempty"` - - // This event occurs when a workflow run is requested or completed, and allows you to execute a workflow - // based on the finished result of another workflow. For example, if your pull_request workflow generates build artifacts, - // you can create a new workflow that uses workflow_run to analyze the results and add a comment to the original pull request. - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run - WorkflowRun *OnWorkflowRun `json:"workflow_run,omitempty" yaml:"workflow_run,omitempty" actions:"workflow_run,omitempty"` -} - -// Event https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows -type Event string - -const ( - EventBranchProtectionRule Event = "branch_protection_rule" - EventCheckRun Event = "check_run" - EventCheckSuite Event = "check_suite" - EventCreate Event = "create" - EventDelete Event = "delete" - EventDeployment Event = "deployment" - EventDeploymentStatus Event = "deployment_status" - EventDiscussion Event = "discussion" - EventDiscussionComment Event = "discussion_comment" - EventFork Event = "fork" - EventGollum Event = "gollum" - EventIssueComment Event = "issue_comment" - EventIssues Event = "issues" - EventLabel Event = "label" - EventMergeGroup Event = "merge_group" - EventMilestone Event = "milestone" - EventPageBuild Event = "page_build" - EventProject Event = "project" - EventProjectCard Event = "project_card" - EventProjectColumn Event = "project_column" - EventPublic Event = "public" - EventPullRequest Event = "pull_request" - EventPullRequestReview Event = "pull_request_review" - EventPullRequestReviewComment Event = "pull_request_review_comment" - EventPullRequestTarget Event = "pull_request_target" - EventPush Event = "push" - EventRegistryPackage Event = "registry_package" - EventRelease Event = "release" - EventRepositoryDispatch Event = "repository_dispatch" - EventSchedule Event = "schedule" - EventStatus Event = "status" - EventWatch Event = "watch" - EventWorkflowCall Event = "workflow_call" - EventWorkflowDispatch Event = "workflow_dispatch" - EventWorkflowRun Event = "workflow_run" -) - -type OnBranchProtectionRule onEventWithTypes[EventBranchProtectionRuleActivity] -type EventBranchProtectionRuleActivity string - -const ( - EventBranchProtectionRuleActivityCreate EventBranchProtectionRuleActivity = "created" - EventBranchProtectionRuleActivityEdited EventBranchProtectionRuleActivity = "edited" - EventBranchProtectionRuleActivityDeleted EventBranchProtectionRuleActivity = "deleted" -) - -type OnCheckRun onEventWithTypes[EventCheckRunActivity] -type EventCheckRunActivity string - -const ( - EventCheckRunActivityCreated EventCheckRunActivity = "created" - EventCheckRunActivityRerequested EventCheckRunActivity = "rerequested" - EventCheckRunActivityCompleted EventCheckRunActivity = "completed" - EventCheckRunActivityRequestedAction EventCheckRunActivity = "requested_action" -) - -type OnCheckSuite onEventWithTypes[EventCheckSuiteActivity] -type EventCheckSuiteActivity string - -const ( - EventCheckSuiteActivityCompleted EventCheckSuiteActivity = "completed" - EventCheckSuiteActivityRequested EventCheckSuiteActivity = "requested" - EventCheckSuiteActivityRerequested EventCheckSuiteActivity = "rerequested" -) - -type OnCreate = empty -type OnDelete = empty -type OnDeployment = empty -type OnDeploymentStatus = empty - -type OnDiscussion onEventWithTypes[EventDiscussionActivity] -type EventDiscussionActivity string - -const ( - EventDiscussionActivityCreated EventDiscussionActivity = "created" - EventDiscussionActivityEdited EventDiscussionActivity = "edited" - EventDiscussionActivityDeleted EventDiscussionActivity = "deleted" - EventDiscussionActivityTransferred EventDiscussionActivity = "transferred" - EventDiscussionActivityPinned EventDiscussionActivity = "pinned" - EventDiscussionActivityUnpinned EventDiscussionActivity = "unpinned" - EventDiscussionActivityLabeled EventDiscussionActivity = "labeled" - EventDiscussionActivityUnlabeled EventDiscussionActivity = "unlabeled" - EventDiscussionActivityLocked EventDiscussionActivity = "locked" - EventDiscussionActivityUnlocked EventDiscussionActivity = "unlocked" - EventDiscussionActivityCategoryChanged EventDiscussionActivity = "category_changed" - EventDiscussionActivityAnswered EventDiscussionActivity = "answered" - EventDiscussionActivityUnanswered EventDiscussionActivity = "unanswered" -) - -type OnDiscussionComment onEventWithTypes[EventDiscussionCommentActivity] -type EventDiscussionCommentActivity string - -const ( - EventDiscussionCommentActivityCreated EventDiscussionCommentActivity = "created" - EventDiscussionCommentActivityEdited EventDiscussionCommentActivity = "edited" - EventDiscussionCommentActivityDeleted EventDiscussionCommentActivity = "deleted" -) - -type OnFork = empty -type OnGollum = empty - -type OnIssueComment onEventWithTypes[EventIssueCommentActivity] -type EventIssueCommentActivity string - -const ( - EventIssueCommentActivityCreated EventIssueCommentActivity = "created" - EventIssueCommentActivityEdited EventIssueCommentActivity = "edited" - EventIssueCommentActivityDeleted EventIssueCommentActivity = "deleted" -) - -type OnIssues onEventWithTypes[EventIssuesActivity] -type EventIssuesActivity string - -const ( - EventIssuesActivityOpened EventIssuesActivity = "opened" - EventIssuesActivityEdited EventIssuesActivity = "edited" - EventIssuesActivityDeleted EventIssuesActivity = "deleted" - EventIssuesActivityTransferred EventIssuesActivity = "transferred" - EventIssuesActivityPinned EventIssuesActivity = "pinned" - EventIssuesActivityUnpinned EventIssuesActivity = "unpinned" - EventIssuesActivityClosed EventIssuesActivity = "closed" - EventIssuesActivityReopened EventIssuesActivity = "reopened" - EventIssuesActivityAssigned EventIssuesActivity = "assigned" - EventIssuesActivityUnassigned EventIssuesActivity = "unassigned" - EventIssuesActivityLabeled EventIssuesActivity = "labeled" - EventIssuesActivityUnlabeled EventIssuesActivity = "unlabeled" - EventIssuesActivityLocked EventIssuesActivity = "locked" - EventIssuesActivityUnlocked EventIssuesActivity = "unlocked" - EventIssuesActivityMilestoned EventIssuesActivity = "milestoned" - EventIssuesActivityDemilestoned EventIssuesActivity = "demilestoned" -) - -type OnLabel onEventWithTypes[EventLabelActivity] -type EventLabelActivity string - -const ( - EventLabelActivityCreated EventLabelActivity = "created" - EventLabelActivityEdited EventLabelActivity = "edited" - EventLabelActivityDeleted EventLabelActivity = "deleted" -) - -type OnMergeGroup onEventWithTypes[EventMergeGroupActivity] -type EventMergeGroupActivity string - -const EventMergeGroupActivityChecksRequested EventMergeGroupActivity = "checks_requested" - -type OnMilestone onEventWithTypes[EventMilestoneActivity] -type EventMilestoneActivity string - -const ( - EventMilestoneActivityCreated EventMilestoneActivity = "created" - EventMilestoneActivityClosed EventMilestoneActivity = "closed" - EventMilestoneActivityOpened EventMilestoneActivity = "opened" - EventMilestoneActivityEdited EventMilestoneActivity = "edited" - EventMilestoneActivityDeleted EventMilestoneActivity = "deleted" -) - -type OnPageBuild = empty - -type OnProject onEventWithTypes[EventProjectActivity] -type EventProjectActivity string - -const ( - EventProjectActivityCreated EventProjectActivity = "created" - EventProjectActivityClosed EventProjectActivity = "closed" - EventProjectActivityReopened EventProjectActivity = "reopened" - EventProjectActivityEdited EventProjectActivity = "edited" - EventProjectActivityDeleted EventProjectActivity = "deleted" -) - -type OnProjectCard onEventWithTypes[EventProjectCardActivity] -type EventProjectCardActivity string - -const ( - EventProjectCardActivityCreated EventProjectCardActivity = "created" - EventProjectCardActivityMoved EventProjectCardActivity = "moved" - EventProjectCardActivityConverted EventProjectCardActivity = "converted" // converted to an issue - EventProjectCardActivityEdited EventProjectCardActivity = "edited" - EventProjectCardActivityDeleted EventProjectCardActivity = "deleted" -) - -type OnProjectColumn onEventWithTypes[EventProjectColumnActivity] -type EventProjectColumnActivity string - -const ( - EventProjectColumnActivityCreated EventProjectColumnActivity = "created" - EventProjectColumnActivityUpdated EventProjectColumnActivity = "updated" - EventProjectColumnActivityMoved EventProjectColumnActivity = "moved" - EventProjectColumnActivityDeleted EventProjectColumnActivity = "deleted" -) - -type OnPublic = empty - -type OnPullRequest struct { - onEventWithTypes[EventPullRequestActivity] `yaml:",inline" actions:",squash"` - onEventWithRef `yaml:",inline" actions:",squash"` -} -type EventPullRequestActivity string - -const ( - EventPullRequestActivityAssigned EventPullRequestActivity = "assigned" - EventPullRequestActivityUnassigned EventPullRequestActivity = "unassigned" - EventPullRequestActivityLabeled EventPullRequestActivity = "labeled" - EventPullRequestActivityUnlabeled EventPullRequestActivity = "unlabeled" - EventPullRequestActivityOpened EventPullRequestActivity = "opened" - EventPullRequestActivityEdited EventPullRequestActivity = "edited" - EventPullRequestActivityClosed EventPullRequestActivity = "closed" - EventPullRequestActivityReopened EventPullRequestActivity = "reopened" - EventPullRequestActivitySynchronize EventPullRequestActivity = "synchronize" - EventPullRequestActivityConvertedToDraft EventPullRequestActivity = "converted_to_draft" - EventPullRequestActivityLocked EventPullRequestActivity = "locked" - EventPullRequestActivityUnlocked EventPullRequestActivity = "unlocked" - EventPullRequestActivityEnqueued EventPullRequestActivity = "enqueued" - EventPullRequestActivityDequeued EventPullRequestActivity = "dequeued" - EventPullRequestActivityMilestoned EventPullRequestActivity = "milestoned" - EventPullRequestActivityDemilestoned EventPullRequestActivity = "demilestoned" - EventPullRequestActivityReadyForReview EventPullRequestActivity = "ready_for_review" - EventPullRequestActivityReviewRequested EventPullRequestActivity = "review_requested" - EventPullRequestActivityReviewRequestRemoved EventPullRequestActivity = "review_request_removed" - EventPullRequestActivityAutoMergeEnabled EventPullRequestActivity = "auto_merge_enabled" - EventPullRequestActivityAutoMergeDisabled EventPullRequestActivity = "auto_merge_disabled" -) - -type OnPullRequestReview onEventWithTypes[EventPullRequestReviewActivity] -type EventPullRequestReviewActivity string - -const ( - EventPullRequestReviewActivitySubmitted EventPullRequestReviewActivity = "submitted" - EventPullRequestReviewActivityEdited EventPullRequestReviewActivity = "edited" - EventPullRequestReviewActivityDismissed EventPullRequestReviewActivity = "dismissed" -) - -type OnPullRequestReviewComment onEventWithTypes[EventPullRequestReviewCommentActivity] -type EventPullRequestReviewCommentActivity string - -const ( - EventPullRequestReviewCommentActivityCreated EventPullRequestReviewCommentActivity = "created" - EventPullRequestReviewCommentActivityEdited EventPullRequestReviewCommentActivity = "edited" - EventPullRequestReviewCommentActivityDeleted EventPullRequestReviewCommentActivity = "deleted" -) - -type OnPullRequestTarget struct { - onEventWithTypes[EventPullRequestTargetActivity] `yaml:",inline" actions:",squash"` - onEventWithRef `yaml:",inline" actions:",squash"` -} -type EventPullRequestTargetActivity string - -const ( - EventPullRequestTargetActivityAssigned EventPullRequestTargetActivity = "assigned" - EventPullRequestTargetActivityUnassigned EventPullRequestTargetActivity = "unassigned" - EventPullRequestTargetActivityLabeled EventPullRequestTargetActivity = "labeled" - EventPullRequestTargetActivityUnlabeled EventPullRequestTargetActivity = "unlabeled" - EventPullRequestTargetActivityOpened EventPullRequestTargetActivity = "opened" - EventPullRequestTargetActivityEdited EventPullRequestTargetActivity = "edited" - EventPullRequestTargetActivityClosed EventPullRequestTargetActivity = "closed" - EventPullRequestTargetActivityReopened EventPullRequestTargetActivity = "reopened" - EventPullRequestTargetActivitySynchronize EventPullRequestTargetActivity = "synchronize" - EventPullRequestTargetActivityConvertedToDraft EventPullRequestTargetActivity = "converted_to_draft" - EventPullRequestTargetActivityReadyForReview EventPullRequestTargetActivity = "ready_for_review" - EventPullRequestTargetActivityLocked EventPullRequestTargetActivity = "locked" - EventPullRequestTargetActivityUnlocked EventPullRequestTargetActivity = "unlocked" - EventPullRequestTargetActivityReviewRequested EventPullRequestTargetActivity = "review_requested" - EventPullRequestTargetActivityReviewRequestRemoved EventPullRequestTargetActivity = "review_request_removed" - EventPullRequestTargetActivityAutoMergeEnabled EventPullRequestTargetActivity = "auto_merge_enabled" - EventPullRequestTargetActivityAutoMergeDisabled EventPullRequestTargetActivity = "auto_merge_disabled" -) - -type OnPush onEventWithRef - -type OnRegistryPackage onEventWithTypes[EventRegistryPackageActivity] -type EventRegistryPackageActivity string - -const ( - EventRegistryPackageActivityPublished EventRegistryPackageActivity = "published" - EventRegistryPackageActivityUpdated EventRegistryPackageActivity = "updated" -) - -type OnRelease onEventWithTypes[EventReleaseActivity] -type EventReleaseActivity string - -const ( - EventReleaseActivityPublished EventReleaseActivity = "published" - EventReleaseActivityUnpublished EventReleaseActivity = "unpublished" - EventReleaseActivityCreated EventReleaseActivity = "created" - EventReleaseActivityEdited EventReleaseActivity = "edited" - EventReleaseActivityDeleted EventReleaseActivity = "deleted" - EventReleaseActivityPrereleased EventReleaseActivity = "prereleased" - EventReleaseActivityReleased EventReleaseActivity = "released" -) - -type OnRepositoryDispatch onEventWithTypes[EventRepositoryDispatchActivity] -type EventRepositoryDispatchActivity string // any strings - -type OnSchedule []struct { - // https://stackoverflow.com/a/57639657/4044345 - Cron string `json:"cron,omitempty" yaml:"cron,omitempty" actions:"cron,omitempty"` -} -type OnStatus = empty - -type OnWatch onEventWithTypes[EventWatchActivity] -type EventWatchActivity string - -const EventWatchActivityStarted EventWatchActivity = "started" - -type OnWorkflowCall struct { - // When using the workflow_call keyword, you can optionally specify inputs that are passed to the called workflow from the caller workflow. - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callinputs - Inputs map[string]Input `json:"inputs,omitempty" yaml:"inputs,omitempty" actions:"inputs,omitempty"` - - // A map of outputs for a called workflow. Called workflow outputs are available to all downstream jobs in the caller workflow. - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_calloutputs - Outputs map[string]Output `json:"outputs,omitempty" yaml:"outputs,omitempty" actions:"outputs,omitempty"` - - // A map of the secrets that can be used in the called workflow. - // Within the called workflow, you can use the secrets context to refer to a secret. - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callsecrets - Secrets map[string]Secret `json:"secrets,omitempty" yaml:"secrets,omitempty" actions:"secrets,omitempty"` -} - -type OnWorkflowDispatch struct { - // Input parameters allow you to specify data that the action expects to use during runtime. - // GitHub stores input parameters as environment variables. - // Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids. - // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs - Inputs map[string]Input `json:"inputs,omitempty" yaml:"inputs,omitempty" actions:"inputs,omitempty"` -} - -type OnWorkflowRun struct { - onEventWithTypes[EventWorkflowRunActivity] `yaml:",inline" actions:",squash"` - onEventWithRef `yaml:",inline" actions:",squash"` - - Workflows []string `json:"workflows,omitempty" yaml:"workflows,omitempty" actions:"workflows,omitempty"` -} -type EventWorkflowRunActivity string - -const ( - EventWorkflowRunActivityCompleted EventWorkflowRunActivity = "completed" - EventWorkflowRunActivityRequested EventWorkflowRunActivity = "requested" - EventWorkflowRunActivityInProgress EventWorkflowRunActivity = "in_progress" -) +// Event https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows +type Event = jsontext.Value diff --git a/core/pkg/model/workflows/event_serde.go b/core/pkg/model/workflows/event_serde.go index 1d6514e9..0bce09af 100644 --- a/core/pkg/model/workflows/event_serde.go +++ b/core/pkg/model/workflows/event_serde.go @@ -6,353 +6,42 @@ package workflows -import "drassi.run/core/pkg/model" - -func (o *On) DecodeMapstructure(input any) (any, error) { - var events []string - if s, ok := model.Stringify(input); ok { - events = []string{s} - } else if l, ok := model.ListStringify(input); ok { - events = l - } else { - // process On normal way - return input, nil - } - - m := map[string]any{} - for _, e := range events { - m[e] = map[string]any{} - } - return m, nil -} - -func (o *OnBranchProtectionRule) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnBranchProtectionRule) defaultActivities() []EventBranchProtectionRuleActivity { - // all activity types - return []EventBranchProtectionRuleActivity{ - EventBranchProtectionRuleActivityCreate, - EventBranchProtectionRuleActivityEdited, - EventBranchProtectionRuleActivityDeleted, - } -} - -func (o *OnCheckRun) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnCheckRun) defaultActivities() []EventCheckRunActivity { - // all activity types - return []EventCheckRunActivity{ - EventCheckRunActivityCreated, - EventCheckRunActivityRerequested, - EventCheckRunActivityCompleted, - EventCheckRunActivityRequestedAction, - } -} - -func (o *OnCheckSuite) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnCheckSuite) defaultActivities() []EventCheckSuiteActivity { - // all activity types - return []EventCheckSuiteActivity{ - EventCheckSuiteActivityCompleted, - EventCheckSuiteActivityRequested, - EventCheckSuiteActivityRerequested, - } -} - -func (o *OnDiscussion) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} -func (o *OnDiscussion) defaultActivities() []EventDiscussionActivity { - // all activity types - return []EventDiscussionActivity{ - EventDiscussionActivityCreated, - EventDiscussionActivityEdited, - EventDiscussionActivityDeleted, - EventDiscussionActivityTransferred, - EventDiscussionActivityPinned, - EventDiscussionActivityUnpinned, - EventDiscussionActivityLabeled, - EventDiscussionActivityUnlabeled, - EventDiscussionActivityLocked, - EventDiscussionActivityUnlocked, - EventDiscussionActivityCategoryChanged, - EventDiscussionActivityAnswered, - EventDiscussionActivityUnanswered, - } -} - -func (o *OnDiscussionComment) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnDiscussionComment) defaultActivities() []EventDiscussionCommentActivity { - // all activity types - return []EventDiscussionCommentActivity{ - EventDiscussionCommentActivityCreated, - EventDiscussionCommentActivityEdited, - EventDiscussionCommentActivityDeleted, - } -} - -func (o *OnIssueComment) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnIssueComment) defaultActivities() []EventIssueCommentActivity { - // all activity types - return []EventIssueCommentActivity{ - EventIssueCommentActivityCreated, - EventIssueCommentActivityEdited, - EventIssueCommentActivityDeleted, - } -} - -func (o *OnIssues) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnIssues) defaultActivities() []EventIssuesActivity { - // all activity types - return []EventIssuesActivity{ - EventIssuesActivityOpened, - EventIssuesActivityEdited, - EventIssuesActivityDeleted, - EventIssuesActivityTransferred, - EventIssuesActivityPinned, - EventIssuesActivityUnpinned, - EventIssuesActivityClosed, - EventIssuesActivityReopened, - EventIssuesActivityAssigned, - EventIssuesActivityUnassigned, - EventIssuesActivityLabeled, - EventIssuesActivityUnlabeled, - EventIssuesActivityLocked, - EventIssuesActivityUnlocked, - EventIssuesActivityMilestoned, - EventIssuesActivityDemilestoned, - } -} - -func (o *OnLabel) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnLabel) defaultActivities() []EventLabelActivity { - // all activity types - return []EventLabelActivity{ - EventLabelActivityCreated, - EventLabelActivityEdited, - EventLabelActivityDeleted, - } -} - -func (o *OnMergeGroup) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnMergeGroup) defaultActivities() []EventMergeGroupActivity { - // all activity types - return []EventMergeGroupActivity{ - EventMergeGroupActivityChecksRequested, - } -} - -func (o *OnMilestone) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnMilestone) defaultActivities() []EventMilestoneActivity { - // all activity types - return []EventMilestoneActivity{ - EventMilestoneActivityCreated, - EventMilestoneActivityClosed, - EventMilestoneActivityOpened, - EventMilestoneActivityEdited, - EventMilestoneActivityDeleted, - } -} - -func (o *OnProject) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnProject) defaultActivities() []EventProjectActivity { - // all activity types - return []EventProjectActivity{ - EventProjectActivityCreated, - EventProjectActivityClosed, - EventProjectActivityReopened, - EventProjectActivityEdited, - EventProjectActivityDeleted, - } -} - -func (o *OnProjectCard) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnProjectCard) defaultActivities() []EventProjectCardActivity { - // all activity types - return []EventProjectCardActivity{ - EventProjectCardActivityCreated, - EventProjectCardActivityMoved, - EventProjectCardActivityConverted, - EventProjectCardActivityEdited, - EventProjectCardActivityDeleted, - } -} - -func (o *OnProjectColumn) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnProjectColumn) defaultActivities() []EventProjectColumnActivity { - // all activity types - return []EventProjectColumnActivity{ - EventProjectColumnActivityCreated, - EventProjectColumnActivityUpdated, - EventProjectColumnActivityMoved, - EventProjectColumnActivityDeleted, - } -} - -func (o *OnPullRequest) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnPullRequest) defaultActivities() []EventPullRequestActivity { - return []EventPullRequestActivity{ - EventPullRequestActivityOpened, - EventPullRequestActivitySynchronize, - EventPullRequestActivityReopened, - } -} - -func (o *OnPullRequestReview) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnPullRequestReview) defaultActivities() []EventPullRequestReviewActivity { - // all activity types - return []EventPullRequestReviewActivity{ - EventPullRequestReviewActivitySubmitted, - EventPullRequestReviewActivityEdited, - EventPullRequestReviewActivityDismissed, - } -} - -func (o *OnPullRequestReviewComment) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnPullRequestReviewComment) defaultActivities() []EventPullRequestReviewCommentActivity { - // all activity types - return []EventPullRequestReviewCommentActivity{ - EventPullRequestReviewCommentActivityCreated, - EventPullRequestReviewCommentActivityEdited, - EventPullRequestReviewCommentActivityDeleted, - } -} - -func (o *OnPullRequestTarget) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnPullRequestTarget) defaultActivities() []EventPullRequestTargetActivity { - return []EventPullRequestTargetActivity{ - EventPullRequestTargetActivityOpened, - EventPullRequestTargetActivitySynchronize, - EventPullRequestTargetActivityReopened, - } -} - -func (o *OnRegistryPackage) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnRegistryPackage) defaultActivities() []EventRegistryPackageActivity { - // all activity types - return []EventRegistryPackageActivity{ - EventRegistryPackageActivityPublished, - EventRegistryPackageActivityUpdated, - } -} - -func (o *OnRelease) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnRelease) defaultActivities() []EventReleaseActivity { - // all activity types - return []EventReleaseActivity{ - EventReleaseActivityPublished, - EventReleaseActivityUnpublished, - EventReleaseActivityCreated, - EventReleaseActivityEdited, - EventReleaseActivityDeleted, - EventReleaseActivityPrereleased, - EventReleaseActivityReleased, - } -} - -func (o *OnWatch) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnWatch) defaultActivities() []EventWatchActivity { - // all activity types - return []EventWatchActivity{ - EventWatchActivityStarted, - } -} - -func (o *OnWorkflowRun) DecodeMapstructure(input any) (any, error) { - input = applyDefaultActivities(input, o.defaultActivities()) - return input, nil -} - -func (o *OnWorkflowRun) defaultActivities() []EventWorkflowRunActivity { - return []EventWorkflowRunActivity{ - EventWorkflowRunActivityCompleted, - EventWorkflowRunActivityRequested, - } -} - -func applyDefaultActivities[S ~string](input any, defaults []S) any { - m, ok := model.ObjectStringify(input) - if !ok { - return input - } - v, ok := m["types"] - if !ok || v == nil { - m["types"] = defaults +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" +) + +func (o *On) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand for single event, e.g: `on: push` + case jsontext.KindString: + if tok, err := d.ReadToken(); err != nil { + return err + } else { + e := tok.String() + *o = map[string]Event{e: nil} + } + return nil + + // 2. Shorthand for multiple events, e.g: `on: [push, fork]` + case jsontext.KindBeginArray: + var events []string + if err := json.UnmarshalDecode(d, &events); err != nil { + return err + } + m := make(map[string]Event, len(events)) + for _, e := range events { + m[e] = nil + } + *o = m + return nil + + // 3. Full object format, e.g: `"on": {"label": {"types": ["created", "edited"]}}` + case jsontext.KindBeginObject: + return json.UnmarshalDecode(d, (*map[string]Event)(o)) + + default: + return fmt.Errorf("expected string, array or object for On, got kind %v", k) } - return m } diff --git a/core/pkg/model/workflows/job.go b/core/pkg/model/workflows/job.go index 2356b543..0142d806 100644 --- a/core/pkg/model/workflows/job.go +++ b/core/pkg/model/workflows/job.go @@ -173,7 +173,10 @@ type ReusableWorkflowCallJob struct { Secrets Evaluable[JobSecrets] `json:"secrets,omitempty" yaml:"secrets,omitempty" actions:"secrets,omitempty"` } -type JobNeeds []string +// array support decode from string shorthand +type array []string + +type JobNeeds = array type JobSecrets struct { // A pair consisting of a string identifier for the secret and the value of the secret. @@ -216,7 +219,7 @@ type RunsOn struct { // // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Labels []string `json:"labels,omitempty" yaml:"labels,omitempty" actions:"labels,omitempty"` + Labels array `json:"labels,omitempty" yaml:"labels,omitempty" actions:"labels,omitempty"` } // A strategy creates a build matrix for your jobs. You can define different variations of an environment to run each job in. diff --git a/core/pkg/model/workflows/job_serde.go b/core/pkg/model/workflows/job_serde.go index 5adecb14..d1bd2a19 100644 --- a/core/pkg/model/workflows/job_serde.go +++ b/core/pkg/model/workflows/job_serde.go @@ -7,119 +7,111 @@ package workflows import ( + "encoding/json/jsontext" + "encoding/json/v2" "fmt" - "reflect" "drassi.run/core/pkg/model" ) -var typeJob = reflect.TypeFor[Job]() +func init() { + model.RegisterUnmarshalInterface(discriminateJob) +} -func DecodeJobHook(from reflect.Value, to reflect.Value) (any, error) { - if !from.IsValid() || !to.Type().Implements(typeJob) { - return valueOf(from), nil +func discriminateJob(raw jsontext.Value) (Job, error) { + var dis struct { + RunsOn string `json:"runs-on,omitempty"` + Uses string `json:"uses,omitempty"` } - - f := from.Interface() - m, ok := model.ObjectStringify(f) - if !ok { - return f, nil + if err := json.Unmarshal(raw, &dis); err != nil { + return nil, err } - _, containsRunsOn := m["runs-on"] - _, containsUses := m["uses"] - if containsRunsOn == containsUses { - return nil, fmt.Errorf("map MUST be contains either `runs-on` or `uses`") + switch { + case dis.RunsOn != "" && dis.Uses != "": + return nil, fmt.Errorf("job MUST be contains either `runs-on` or `uses`") + case dis.Uses != "": + return new(ReusableWorkflowCallJob), nil + case dis.RunsOn != "": + return new(NormalJob), nil + default: + // both RunsOn and Uses are missing + return nil, fmt.Errorf("job MUST be contains either `runs-on` or `uses`") } +} - t := to.Interface() - if containsRunsOn { - if t == nil { - to.Set(reflect.ValueOf(&NormalJob{})) - } else if _, ok := t.(*NormalJob); !ok { - return nil, fmt.Errorf("map contains `runs-on` CAN'T be decode to %T", t) - } - } - if containsUses { - if t == nil { - to.Set(reflect.ValueOf(&ReusableWorkflowCallJob{})) - } else if _, ok := t.(*ReusableWorkflowCallJob); !ok { - return nil, fmt.Errorf("map contains `uses` CAN'T be decode to %T", t) +func (a *array) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand string format (e.g., "first") + case jsontext.KindString: + if tok, err := d.ReadToken(); err != nil { + return err + } else { + *a = []string{tok.String()} + return nil } - } - return m, nil -} -func init() { - model.RegisterDecodeHook(DecodeJobHook) -} + // 2. Standard array format (e.g., [first, second,...]) + case jsontext.KindBeginArray: + type alias array + return json.UnmarshalDecode(d, (*alias)(a)) -func (n *JobNeeds) DecodeMapstructure(input any) (any, error) { - if s, ok := model.Stringify(input); ok { - return []string{s}, nil - } else { - return input, nil + default: + return fmt.Errorf("expected string or array, got kind %v", k) } } -func (s *JobSecrets) DecodeMapstructure(input any) (any, error) { - if input == "inherit" { - s.Inherit = true - return nil, nil - } - if m, ok := input.(map[string]string); ok { - s.Secrets = m - return nil, nil - } - if m, ok := input.(map[string]any); ok { - if secrets, err := castMap[string, string](m); err != nil { - return nil, err +func (s *JobSecrets) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. "inherit" secret + case jsontext.KindString: + if tok, err := d.ReadToken(); err != nil { + return err + } else if t := tok.String(); t == "inherit" { + s.Inherit = true + return nil } else { - s.Secrets = secrets - return nil, nil + return fmt.Errorf("unexpected JobSecrets=%v", t) } - } - // process JobSecrets normal way - return input, nil -} -func (e *Environment) DecodeMapstructure(input any) (any, error) { - if name, ok := model.Stringify(input); ok { - e.Name = name - return nil, nil + // 2. Standard object format (e.g., {"k": "v",...}) + case jsontext.KindBeginObject: + return json.UnmarshalDecode(d, &s.Secrets) + + default: + return fmt.Errorf("expected object for JobSecrets, got kind %v", k) } - // process Environment normal way - return input, nil } -func (r *RunsOn) setLabels(input any, rec bool) (any, error) { - if s, ok := model.Stringify(input); ok { - r.Labels = []string{s} - return nil, nil - } - if l, ok := model.ListStringify(input); ok { - r.Labels = l - return nil, nil - } - if m, ok := model.ObjectStringify(input); ok { - if rec { - if labels, ok := m["labels"]; ok { - remain, err := r.setLabels(labels, false) - if err != nil { - return nil, err - } - if remain == nil { // Labels is set - delete(m, "labels") - } else { - m["labels"] = remain - } - } - } - return m, nil +func (e *Environment) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand string format (e.g., "first") + case jsontext.KindString: + return json.UnmarshalDecode(d, &e.Name) + + // 2. Standard object format (e.g., {"name": "prod", "url": "http://foobar.app"}) + case jsontext.KindBeginObject: + type alias Environment + return json.UnmarshalDecode(d, (*alias)(e)) + + default: + return fmt.Errorf("expected string or object for Environment, got kind %v", k) } - return input, nil } -func (r *RunsOn) DecodeMapstructure(input any) (any, error) { - return r.setLabels(input, true) +func (r *RunsOn) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand string/[]string format (e.g., "first") + case jsontext.KindString, + jsontext.KindBeginArray: + return json.UnmarshalDecode(d, &r.Labels) + + // 2. Full object format (e.g., {"group": "gpu-runner", "labels": ["label1", "label2",...]}) + case jsontext.KindBeginObject: + type alias RunsOn + return json.UnmarshalDecode(d, (*alias)(r)) + + default: + return fmt.Errorf("expected string, array or object for RunsOn, got kind %v", k) + } } diff --git a/core/pkg/model/workflows/step_serde.go b/core/pkg/model/workflows/step_serde.go index f3550ba9..ea7cfdb6 100644 --- a/core/pkg/model/workflows/step_serde.go +++ b/core/pkg/model/workflows/step_serde.go @@ -7,49 +7,35 @@ package workflows import ( + "encoding/json/jsontext" + "encoding/json/v2" "fmt" - "reflect" "drassi.run/core/pkg/model" ) -var typeStep = reflect.TypeFor[Step]() - -func DecodeStepHook(from reflect.Value, to reflect.Value) (any, error) { - if !from.IsValid() || !to.Type().Implements(typeStep) { - return valueOf(from), nil +func discriminateStep(raw jsontext.Value) (Step, error) { + var dis struct { + Run string `json:"run,omitempty"` + Uses string `json:"uses,omitempty"` } - - f := from.Interface() - m, ok := model.ObjectStringify(f) - if !ok { - return f, nil + if err := json.Unmarshal(raw, &dis); err != nil { + return nil, err } - _, containsRun := m["run"] - _, containsUses := m["uses"] - if containsRun == containsUses { - return nil, fmt.Errorf("map MUST be contains either `run` or `uses`") - } - - t := to.Interface() - if containsRun { - if t == nil { - to.Set(reflect.ValueOf(&RunActionStep{})) - } else if _, ok := t.(*RunActionStep); !ok { - return nil, fmt.Errorf("map contains `run` CAN'T be decode to %T", t) - } - } - if containsUses { - if t == nil { - to.Set(reflect.ValueOf(&UsesActionStep{})) - } else if _, ok := t.(*UsesActionStep); !ok { - return nil, fmt.Errorf("map contains `uses` CAN'T be decode to %T", t) - } + switch { + case dis.Run != "" && dis.Uses != "": + return nil, fmt.Errorf("step MUST be contains either `run` or `uses`") + case dis.Run != "": + return new(RunActionStep), nil + case dis.Uses != "": + return new(UsesActionStep), nil + default: + // both Run and Uses are missing + return nil, fmt.Errorf("step MUST be contains either `run` or `uses`") } - return m, nil } func init() { - model.RegisterDecodeHook(DecodeStepHook) + model.RegisterUnmarshalInterface(discriminateStep) } diff --git a/core/pkg/model/workflows/workflow.go b/core/pkg/model/workflows/workflow.go index 753a259c..d2dfaf7b 100644 --- a/core/pkg/model/workflows/workflow.go +++ b/core/pkg/model/workflows/workflow.go @@ -71,21 +71,7 @@ type Workflow struct { // You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, // so that you only allow the minimum required access. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions -type Permissions struct { - Actions PermissionsLevel `json:"actions,omitempty" yaml:"actions,omitempty" actions:"actions,omitempty"` - Checks PermissionsLevel `json:"checks,omitempty" yaml:"checks,omitempty" actions:"checks,omitempty"` - Contents PermissionsLevel `json:"contents,omitempty" yaml:"contents,omitempty" actions:"contents,omitempty"` - Deployments PermissionsLevel `json:"deployments,omitempty" yaml:"deployments,omitempty" actions:"deployments,omitempty"` - Discussions PermissionsLevel `json:"discussions,omitempty" yaml:"discussions,omitempty" actions:"discussions,omitempty"` - IdToken PermissionsLevel `json:"id-token,omitempty" yaml:"id-token,omitempty" actions:"id-token,omitempty"` - Issues PermissionsLevel `json:"issues,omitempty" yaml:"issues,omitempty" actions:"issues,omitempty"` - Packages PermissionsLevel `json:"packages,omitempty" yaml:"packages,omitempty" actions:"packages,omitempty"` - Pages PermissionsLevel `json:"pages,omitempty" yaml:"pages,omitempty" actions:"pages,omitempty"` - PullRequests PermissionsLevel `json:"pull-requests,omitempty" yaml:"pull-requests,omitempty" actions:"pull-requests,omitempty"` - RepositoryProjects PermissionsLevel `json:"repository-projects,omitempty" yaml:"repository-projects,omitempty" actions:"repository-projects,omitempty"` - SecurityEvents PermissionsLevel `json:"security-events,omitempty" yaml:"security-events,omitempty" actions:"security-events,omitempty"` - Statuses PermissionsLevel `json:"statuses,omitempty" yaml:"statuses,omitempty" actions:"statuses,omitempty"` -} +type Permissions map[string]PermissionsLevel type PermissionsLevel string diff --git a/core/pkg/model/workflows/workflow_serde.go b/core/pkg/model/workflows/workflow_serde.go index 86825dde..10bb38f2 100644 --- a/core/pkg/model/workflows/workflow_serde.go +++ b/core/pkg/model/workflows/workflow_serde.go @@ -6,54 +6,51 @@ package workflows -import "drassi.run/core/pkg/model" - -func (p *Permissions) DecodeMapstructure(input any) (any, error) { - switch input { - case "read-all": - *p = Permissions{ - Actions: PermissionsLevelRead, - Checks: PermissionsLevelRead, - Contents: PermissionsLevelRead, - Deployments: PermissionsLevelRead, - Discussions: PermissionsLevelRead, - IdToken: PermissionsLevelRead, - Issues: PermissionsLevelRead, - Packages: PermissionsLevelRead, - Pages: PermissionsLevelRead, - PullRequests: PermissionsLevelRead, - RepositoryProjects: PermissionsLevelRead, - SecurityEvents: PermissionsLevelRead, - Statuses: PermissionsLevelRead, +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" +) + +func (p *Permissions) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand string format: read-all | write-all + case jsontext.KindString: + tok, err := d.ReadToken() + if err != nil { + return err } - case "write-all": - *p = Permissions{ - Actions: PermissionsLevelWrite, - Checks: PermissionsLevelWrite, - Contents: PermissionsLevelWrite, - Deployments: PermissionsLevelWrite, - Discussions: PermissionsLevelWrite, - IdToken: PermissionsLevelWrite, - Issues: PermissionsLevelWrite, - Packages: PermissionsLevelWrite, - Pages: PermissionsLevelWrite, - PullRequests: PermissionsLevelWrite, - RepositoryProjects: PermissionsLevelWrite, - SecurityEvents: PermissionsLevelWrite, - Statuses: PermissionsLevelWrite, + switch l := tok.String(); l { + case "read-all": + *p = Permissions{"*": PermissionsLevelRead} + case "write-all": + *p = Permissions{"*": PermissionsLevelWrite} + default: + return fmt.Errorf("unknown permission %s", l) } + return nil + + // 2. Standard object format (e.g., {"actions": "read"}) + case jsontext.KindBeginObject: + return json.UnmarshalDecode(d, (*map[string]PermissionsLevel)(p)) + default: - // process Permissions normal way - return input, nil + return fmt.Errorf("expected string or object for Permission, got kind %v", k) } - return nil, nil } -func (c *Concurrency) DecodeMapstructure(input any) (any, error) { - if s, ok := model.Stringify(input); ok { - m := map[string]any{"group": s} - return m, nil +func (c *Concurrency) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand string format (e.g., "group1") + case jsontext.KindString: + return json.UnmarshalDecode(d, c.Group) + + // 2. Standard object format (e.g., {"group": "group1", "cancel-in-progress": true}) + case jsontext.KindBeginObject: + type alias Concurrency + return json.UnmarshalDecode(d, (*alias)(c)) + + default: + return fmt.Errorf("expected string or object for Environment, got kind %v", k) } - // process Concurrency normal way - return input, nil } From 02c3b7560f58271d62c3994ebaf5247ebb63043b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 14 Jul 2026 12:31:48 +0700 Subject: [PATCH 2/4] core/model: remove Decoder --- core/pkg/executor/z_converters.go | 4 +- core/pkg/model/actions/action.go | 6 +- .../{workflows/event_io.go => actions/io.go} | 8 +- .../actions/{runs_serde.go => zz_serde.go} | 31 ++- core/pkg/model/actions/zz_util.go | 16 -- core/pkg/model/decoder.go | 51 ----- core/pkg/model/serde.go | 63 +----- core/pkg/model/weak.go | 146 ------------- core/pkg/model/weak_test.go | 201 ------------------ core/pkg/model/workflows/evaluable_serde.go | 10 +- core/pkg/model/workflows/job_serde.go | 3 +- core/pkg/model/workflows/step_serde.go | 9 +- core/pkg/model/workflows/workflow_serde.go | 2 +- core/pkg/model/workflows/zz_serde.go | 18 ++ core/pkg/model/workflows/zz_util.go | 31 --- 15 files changed, 81 insertions(+), 518 deletions(-) rename core/pkg/model/{workflows/event_io.go => actions/io.go} (92%) rename core/pkg/model/actions/{runs_serde.go => zz_serde.go} (56%) delete mode 100644 core/pkg/model/actions/zz_util.go delete mode 100644 core/pkg/model/decoder.go delete mode 100644 core/pkg/model/weak.go delete mode 100644 core/pkg/model/weak_test.go create mode 100644 core/pkg/model/workflows/zz_serde.go delete mode 100644 core/pkg/model/workflows/zz_util.go diff --git a/core/pkg/executor/z_converters.go b/core/pkg/executor/z_converters.go index b4259f73..6542e7ad 100644 --- a/core/pkg/executor/z_converters.go +++ b/core/pkg/executor/z_converters.go @@ -152,7 +152,7 @@ func ToActionSpec(action *actions.Action, repo *repository.Repository) (ActionSp return spec, nil } -func inputToken(m map[string]workflows.Input) workflows.Token { +func inputToken(m map[string]actions.Input) workflows.Token { tokens := make([][2]workflows.Token, 0) for name, input := range m { if input.Default != nil { @@ -173,7 +173,7 @@ func inputToken(m map[string]workflows.Input) workflows.Token { return nil } -func outputToken(m map[string]workflows.Output) workflows.Token { +func outputToken(m map[string]actions.Output) workflows.Token { tokens := make([][2]workflows.Token, 0) for name, output := range m { if output.Value != nil { diff --git a/core/pkg/model/actions/action.go b/core/pkg/model/actions/action.go index bbfe857f..9d6b4639 100644 --- a/core/pkg/model/actions/action.go +++ b/core/pkg/model/actions/action.go @@ -6,8 +6,6 @@ package actions -import "drassi.run/core/pkg/model/workflows" - type Action struct { // The name of your action. GitHub displays the `name` in the Actions tab to help visually identify actions in each job. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#name @@ -25,11 +23,11 @@ type Action struct { // GitHub stores input parameters as environment variables. // Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs - Inputs map[string]workflows.Input `json:"inputs,omitempty" yaml:"inputs,omitempty" actions:"inputs,omitempty"` + Inputs map[string]Input `json:"inputs,omitempty" yaml:"inputs,omitempty" actions:"inputs,omitempty"` // Output parameters allow you to declare data that an action sets. // Actions that run later in a workflow can use the output data set in previously run actions. - Outputs map[string]workflows.Output `json:"outputs,omitempty" yaml:"outputs,omitempty" actions:"outputs,omitempty"` + Outputs map[string]Output `json:"outputs,omitempty" yaml:"outputs,omitempty" actions:"outputs,omitempty"` // Specifies whether this is a JavaScript action, a composite action, or a Docker container action and how the action is executed. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs diff --git a/core/pkg/model/workflows/event_io.go b/core/pkg/model/actions/io.go similarity index 92% rename from core/pkg/model/workflows/event_io.go rename to core/pkg/model/actions/io.go index 1ca6ac4c..e80c4655 100644 --- a/core/pkg/model/workflows/event_io.go +++ b/core/pkg/model/actions/io.go @@ -4,7 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package workflows +package actions + +import "drassi.run/core/pkg/model/workflows" // A string identifier to associate with the input. The value of is a map of the input's metadata. // The must be a unique identifier within the inputs object. @@ -27,7 +29,7 @@ type Input struct { // // Context available: `github`, `inputs`, `vars` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Default Evaluable[any] `json:"default,omitempty" yaml:"default,omitempty" actions:"default,omitempty"` // TODO type args + Default workflows.Evaluable[any] `json:"default,omitempty" yaml:"default,omitempty" actions:"default,omitempty"` // TODO type args // The value of this parameter is a string specifying the data type of the input. // This must be one of: boolean, choice, number, environment or string. @@ -54,7 +56,7 @@ type Output struct { // Context available: `github`, `jobs`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Value Evaluable[string] `json:"value,omitempty" yaml:"value,omitempty" actions:"value,omitempty"` + Value workflows.Evaluable[string] `json:"value,omitempty" yaml:"value,omitempty" actions:"value,omitempty"` } // A string identifier to associate with the secret. diff --git a/core/pkg/model/actions/runs_serde.go b/core/pkg/model/actions/zz_serde.go similarity index 56% rename from core/pkg/model/actions/runs_serde.go rename to core/pkg/model/actions/zz_serde.go index 435a7cad..cf033466 100644 --- a/core/pkg/model/actions/runs_serde.go +++ b/core/pkg/model/actions/zz_serde.go @@ -13,8 +13,35 @@ import ( "strings" "drassi.run/core/pkg/model" + "drassi.run/core/pkg/model/workflows" ) +func init() { + unmarshalers = []*json.Unmarshalers{ + model.UnmarshalInterface(discriminateRuns), + } +} + +var unmarshalers []*json.Unmarshalers + +func JsonUnmarshalers() *json.Unmarshalers { + return json.JoinUnmarshalers(unmarshalers...) +} + +func Decode[M any](content []byte) (*M, error) { + m := new(M) + opt := json.WithUnmarshalers( + json.JoinUnmarshalers( + JsonUnmarshalers(), + workflows.JsonUnmarshalers(), + ), + ) + if err := json.Unmarshal(content, &m, opt); err != nil { + return nil, err + } + return m, nil +} + func discriminateRuns(raw jsontext.Value) (Runs, error) { var dis struct { Using string `json:"using,omitempty"` @@ -35,7 +62,3 @@ func discriminateRuns(raw jsontext.Value) (Runs, error) { return nil, fmt.Errorf(`unknown runs with using=%q`, u) } } - -func init() { - model.RegisterUnmarshalInterface(discriminateRuns) -} diff --git a/core/pkg/model/actions/zz_util.go b/core/pkg/model/actions/zz_util.go deleted file mode 100644 index 1a1552eb..00000000 --- a/core/pkg/model/actions/zz_util.go +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package actions - -import "reflect" - -func valueOf(v reflect.Value) any { - if !v.IsValid() { - return nil - } - return v.Interface() -} diff --git a/core/pkg/model/decoder.go b/core/pkg/model/decoder.go deleted file mode 100644 index 35b98f95..00000000 --- a/core/pkg/model/decoder.go +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package model - -import "reflect" - -// comparable to yaml.Unmarshaler, decoder allow a type to define its own custom logic to convert value -// see https://github.com/go-viper/mapstructure/v2/pull/294 -// see https://github.com/mitchellh/mapstructure/pull/294 -type decoder interface { - DecodeMapstructure(any) (any, error) -} - -// see https://github.com/go-viper/mapstructure/v2/issues/115#issuecomment-735287466 -// see https://github.com/mitchellh/mapstructure/issues/115#issuecomment-735287466 -// adapted to support types derived from built-in types, as DecodeMapstructure would not be able to mutate internal -// value, so need to invoke DecodeMapstructure defined by pointer to type -func decoderHook(from reflect.Value, to reflect.Value) (any, error) { - // If the destination implements the decoder interface - t, ok := to.Interface().(decoder) - if !ok { - // for non-struct types, we need to invoke func (*type) DecodeMapstructure() - if to.CanAddr() { - pto := to.Addr() - t, ok = pto.Interface().(decoder) - } - if !ok { - return from.Interface(), nil - } - } - // If the destination is a nil pointer, create and assign the target value first - if typ := to.Type(); typ.Kind() == reflect.Pointer && to.IsNil() { - to.Set(reflect.New(typ.Elem())) - t = to.Interface().(decoder) - } - - // Call the custom DecodeMapstructure method - f := from.Interface() - if d, err := t.DecodeMapstructure(f); err != nil { - return nil, err - } else if d != nil { - return d, nil - } else { - // d == nil: all inputs already processed - return t, nil - } -} diff --git a/core/pkg/model/serde.go b/core/pkg/model/serde.go index c5fcdcea..f4b13a50 100644 --- a/core/pkg/model/serde.go +++ b/core/pkg/model/serde.go @@ -9,62 +9,19 @@ package model import ( "encoding/json/jsontext" "encoding/json/v2" - "slices" - - "github.com/go-viper/mapstructure/v2" ) -var hooks []mapstructure.DecodeHookFunc - -func RegisterDecodeHook(fn mapstructure.DecodeHookFunc) { - hooks = append(hooks, fn) -} - -type DecodeOption func(config *mapstructure.DecoderConfig) - -func Decode(source any, target any) error { - opt := WithDecodeHook(true) - return DecodeWithOptions(source, target, opt) -} - -func WithDecodeHook(registered bool, h ...mapstructure.DecodeHookFunc) DecodeOption { - if registered { - h = slices.Concat(h, hooks) - h = append(h, WeaklyString) - h = append(h, decoderHook) - } - return func(config *mapstructure.DecoderConfig) { - config.DecodeHook = mapstructure.ComposeDecodeHookFunc(h...) - } -} - -func DecodeWithOptions(source any, target any, opts ...DecodeOption) error { - metadata := mapstructure.Metadata{} - config := &mapstructure.DecoderConfig{ - Result: target, - TagName: "actions", - Metadata: &metadata, - } - for _, o := range opts { - o(config) - } - - d, err := mapstructure.NewDecoder(config) - if err != nil { - return err - } - return d.Decode(source) -} - -var unmarshalers []*json.Unmarshalers - -func RegisterUnmarshalInterface[T any](dis func(raw jsontext.Value) (T, error)) { - u := unmarshalInterface(dis) - unmarshalers = append(unmarshalers, u) -} - -func unmarshalInterface[T any](dis func(raw jsontext.Value) (T, error)) *json.Unmarshalers { +func UnmarshalInterface[T any](dis func(raw jsontext.Value) (T, error)) *json.Unmarshalers { fn := func(d *jsontext.Decoder, val *T) error { + if d.PeekKind() == jsontext.KindNull { + if _, err := d.ReadToken(); err != nil { + return err + } + var zero T + *val = zero + return nil + } + var raw jsontext.Value if err := json.UnmarshalDecode(d, &raw); err != nil { return err diff --git a/core/pkg/model/weak.go b/core/pkg/model/weak.go deleted file mode 100644 index c5056e27..00000000 --- a/core/pkg/model/weak.go +++ /dev/null @@ -1,146 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package model - -import ( - "math" - "reflect" - "strconv" -) - -func WeaklyString(from reflect.Type, to reflect.Type, data any) (any, error) { - //goland:noinspection GoSwitchMissingCasesForIotaConsts - switch to.Kind() { - // Convert primitive values (bool, number) to string - // https://github.com/actions/runner/blob/v2.315.0/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateEvaluator.cs#L370-L380 - case reflect.String: - if s, ok := Stringify(data); ok { - return s, nil - } - // Struct and Map of string also have weakly key - // https://github.com/actions/runner/blob/v2.315.0/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateEvaluator.cs#L207-L211 - // https://github.com/actions/runner/blob/v2.315.0/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateEvaluator.cs#L325-L329 - case reflect.Struct: - if m, ok := StructStringify(data); ok { - return m, nil - } - case reflect.Map: - if to.Key().Kind() != reflect.String { - break - } - if m, ok := MapStringify(data); ok { - return m, nil - } - } - return data, nil -} - -var typeListString = reflect.TypeFor[[]string]() -var typeMapString = reflect.TypeFor[map[string]any]() - -func Stringify(data any) (string, bool) { - v := reflect.ValueOf(data) - switch k := v.Kind(); { - case k == reflect.Invalid: - return "", true - case k == reflect.Bool: - return strconv.FormatBool(v.Bool()), true - case k <= reflect.Int64: - return strconv.FormatInt(v.Int(), 10), true - case k <= reflect.Uint64: - return strconv.FormatUint(v.Uint(), 10), true - case k <= reflect.Float64: // see Float.ToString() - f := v.Float() - if math.IsInf(f, 1) { - return "Infinity", true - } else if math.IsInf(f, -1) { - return "-Infinity", true - } - return strconv.FormatFloat(f, 'G', 15, 64), true - case k == reflect.String: - return v.String(), true - default: - return "", false - } -} - -func ListStringify(data any) ([]string, bool) { - val := reflect.ValueOf(data) - if val.Kind() != reflect.Array && val.Kind() != reflect.Slice { - return nil, false - } - - // Use `Convert` to handle the underlying type - if val.CanConvert(typeListString) { - l := val.Convert(typeListString).Interface().([]string) - return l, true - } - - l := make([]string, val.Len()) - for i := 0; i < val.Len(); i++ { - e := val.Index(i).Interface() - s, ok := Stringify(e) - if !ok { - return nil, false - } - - l[i] = s - } - return l, true -} - -func ObjectStringify(data any) (map[string]any, bool) { - val := reflect.ValueOf(data) - switch k := val.Kind(); k { - case reflect.Struct: - return StructStringify(data) - case reflect.Map: - return MapStringify(data) - default: - return nil, false - } -} - -func MapStringify(data any) (map[string]any, bool) { - val := reflect.ValueOf(data) - if val.Kind() != reflect.Map { - return nil, false - } - - // Use `Convert` to handle the underlying type - if val.CanConvert(typeMapString) { - m := val.Convert(typeMapString).Interface().(map[string]any) - return m, true - } - - m := make(map[string]any, val.Len()) - iter := val.MapRange() - for iter.Next() { - k := iter.Key().Interface() - s, ok := Stringify(k) - if !ok { - return nil, false - } - - v := iter.Value().Interface() - m[s] = v - } - return m, true -} - -func StructStringify(data any) (map[string]any, bool) { - val := reflect.ValueOf(data) - if val.Kind() != reflect.Struct { - return nil, false - } - - m := make(map[string]any, val.NumField()) - if err := Decode(data, &m); err != nil { - return nil, false - } - return m, true -} diff --git a/core/pkg/model/weak_test.go b/core/pkg/model/weak_test.go deleted file mode 100644 index dc4dd755..00000000 --- a/core/pkg/model/weak_test.go +++ /dev/null @@ -1,201 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package model - -import ( - "github.com/stretchr/testify/assert" - "math" - "testing" -) - -// sample underlying type -type String string -type Boolean bool -type Int int64 -type UInt uint64 -type Float float64 -type List[R any] []R -type Map[K comparable, V any] map[K]V -type Struct struct { - Name string `actions:"name"` - Age int `actions:"age"` -} - -var stringMap = map[any]string{ - nil: "", - "string": "string", - true: "true", - int8(-8): "-8", - int16(-16): "-16", - int32(-32): "-32", - int64(-64): "-64", - uint(8): "8", - uint16(16): "16", - uint32(32): "32", - uint64(64): "64", - float32(32.5): "32.5", - float64(64.5): "64.5", - math.Inf(1): "Infinity", - math.Inf(-1): "-Infinity", - math.NaN(): "NaN", - - String("string"): "string", - Boolean(false): "false", - Int(123): "123", - UInt(123): "123", - Float(123.4): "123.4", -} - -func TestStringify(t *testing.T) { - t.Run("success", func(t *testing.T) { - for k, v := range stringMap { - r, ok := Stringify(k) - assert.True(t, ok, "Stringify %v (%[1]T)", k) - assert.Equal(t, v, r, "Stringify %v (%[1]T)", k) - } - }) - - t.Run("failure", func(t *testing.T) { - tc := []any{ - []string{}, - []int{}, - []any{}, - map[string]string{}, - map[string]any{}, - map[int]string{}, - map[int]any{}, - map[any]any{}, - - List[string]{}, - List[int]{}, - List[any]{}, - Map[string, string]{}, - Map[string, any]{}, - Map[int, string]{}, - Map[int, any]{}, - Map[any, any]{}, - Struct{}, - } - for _, v := range tc { - _, ok := Stringify(v) - assert.False(t, ok, "Stringify %v (%[1]T)", v) - } - }) -} - -func TestListStringify(t *testing.T) { - t.Run("success", func(t *testing.T) { - l1 := []string{"a", "b", "c"} - l, ok := ListStringify(l1) - assert.True(t, ok, "ListStringify %v (%[1]T)", l1) - assert.Equal(t, l1, l, "ListStringify %v (%[1]T)", l1) - - l2 := List[string](l1) - l, ok = ListStringify(l2) - assert.True(t, ok, "ListStringify %v (%[1]T)", l2) - assert.Equal(t, l1, l, "ListStringify %v (%[1]T)", l2) - - var l3 []any - var r []string - for k, v := range stringMap { - l3 = append(l3, k) - r = append(r, v) - } - l, ok = ListStringify(l3) - assert.True(t, ok, "ListStringify %v (%[1]T)", l3) - assert.Equal(t, r, l, "ListStringify %v (%[1]T)", l3) - - l4 := List[any](l3) - l, ok = ListStringify(l4) - assert.True(t, ok, "ListStringify %v (%[1]T)", l4) - assert.Equal(t, r, l, "ListStringify %v (%[1]T)", l4) - }) - - t.Run("failure", func(t *testing.T) { - for k := range stringMap { - _, ok := ListStringify(k) - assert.False(t, ok, "ListStringify %v (%[1]T)", k) - } - - tc := []any{ - map[string]string{}, - map[string]any{}, - map[int]string{}, - map[int]any{}, - map[any]any{}, - - Map[string, string]{}, - Map[string, any]{}, - Map[int, string]{}, - Map[int, any]{}, - Map[any, any]{}, - Struct{}, - } - for _, v := range tc { - _, ok := ListStringify(v) - assert.False(t, ok, "ListStringify %v (%[1]T)", v) - } - }) -} - -func TestMapStringify(t *testing.T) { - t.Run("success", func(t *testing.T) { - m1 := map[string]any{"a": 1, "b": true, "c": "ccc", "d": nil} - m, ok := MapStringify(m1) - assert.True(t, ok, "MapStringify %v (%[1]T)", m1) - assert.Equal(t, m1, m, "MapStringify %v (%[1]T)", m1) - - m2 := Map[string, any](m1) - m, ok = MapStringify(m2) - assert.True(t, ok, "MapStringify %v (%[1]T)", m2) - assert.Equal(t, m1, m, "MapStringify %v (%[1]T)", m2) - - m3 := stringMap - r := make(map[string]any) - for k, v := range stringMap { - s, _ := Stringify(k) - r[s] = v - } - m, ok = MapStringify(m3) - assert.True(t, ok, "MapStringify %v (%[1]T)", m3) - assert.Equal(t, r, m, "MapStringify %v (%[1]T)", m3) - - m4 := Map[any, string](m3) - m, ok = MapStringify(m4) - assert.True(t, ok, "MapStringify %v (%[1]T)", m4) - assert.Equal(t, r, m, "MapStringify %v (%[1]T)", m4) - }) - - t.Run("failure", func(t *testing.T) { - for k := range stringMap { - _, ok := MapStringify(k) - assert.False(t, ok, "MapStringify %v (%[1]T)", k) - } - - tc := []any{ - []string{}, - []int{}, - []any{}, - - List[string]{}, - List[int]{}, - List[any]{}, - } - for _, v := range tc { - _, ok := MapStringify(v) - assert.False(t, ok, "MapStringify %v (%[1]T)", v) - } - }) -} - -func TestStructStringify(t *testing.T) { - s := Struct{Name: "drassi", Age: 1} - r := map[string]any{"name": "drassi", "age": 1} - m, ok := StructStringify(s) - assert.True(t, ok, "StructStringify %v (%[1]T)", s) - assert.Equal(t, r, m, "StructStringify %v (%[1]T)", s) -} diff --git a/core/pkg/model/workflows/evaluable_serde.go b/core/pkg/model/workflows/evaluable_serde.go index 933af4ab..d39711f0 100644 --- a/core/pkg/model/workflows/evaluable_serde.go +++ b/core/pkg/model/workflows/evaluable_serde.go @@ -13,9 +13,17 @@ import ( "strings" ) -func UnmarshalToken(d *jsontext.Decoder, t *Token) error { +func init() { + u := json.UnmarshalFromFunc(unmarshalToken) + unmarshalers = append(unmarshalers, u) +} + +func unmarshalToken(d *jsontext.Decoder, t *Token) error { switch d.PeekKind() { case jsontext.KindNull: + if _, err := d.ReadToken(); err != nil { + return err + } *t = nil case jsontext.KindTrue, jsontext.KindFalse: diff --git a/core/pkg/model/workflows/job_serde.go b/core/pkg/model/workflows/job_serde.go index d1bd2a19..b5549544 100644 --- a/core/pkg/model/workflows/job_serde.go +++ b/core/pkg/model/workflows/job_serde.go @@ -15,7 +15,8 @@ import ( ) func init() { - model.RegisterUnmarshalInterface(discriminateJob) + u := model.UnmarshalInterface(discriminateJob) + unmarshalers = append(unmarshalers, u) } func discriminateJob(raw jsontext.Value) (Job, error) { diff --git a/core/pkg/model/workflows/step_serde.go b/core/pkg/model/workflows/step_serde.go index ea7cfdb6..c884202b 100644 --- a/core/pkg/model/workflows/step_serde.go +++ b/core/pkg/model/workflows/step_serde.go @@ -14,6 +14,11 @@ import ( "drassi.run/core/pkg/model" ) +func init() { + u := model.UnmarshalInterface(discriminateStep) + unmarshalers = append(unmarshalers, u) +} + func discriminateStep(raw jsontext.Value) (Step, error) { var dis struct { Run string `json:"run,omitempty"` @@ -35,7 +40,3 @@ func discriminateStep(raw jsontext.Value) (Step, error) { return nil, fmt.Errorf("step MUST be contains either `run` or `uses`") } } - -func init() { - model.RegisterUnmarshalInterface(discriminateStep) -} diff --git a/core/pkg/model/workflows/workflow_serde.go b/core/pkg/model/workflows/workflow_serde.go index 10bb38f2..50d2ba8a 100644 --- a/core/pkg/model/workflows/workflow_serde.go +++ b/core/pkg/model/workflows/workflow_serde.go @@ -43,7 +43,7 @@ func (c *Concurrency) UnmarshalJSONFrom(d *jsontext.Decoder) error { switch k := d.PeekKind(); k { // 1. Shorthand string format (e.g., "group1") case jsontext.KindString: - return json.UnmarshalDecode(d, c.Group) + return json.UnmarshalDecode(d, &c.Group) // 2. Standard object format (e.g., {"group": "group1", "cancel-in-progress": true}) case jsontext.KindBeginObject: diff --git a/core/pkg/model/workflows/zz_serde.go b/core/pkg/model/workflows/zz_serde.go new file mode 100644 index 00000000..074e8407 --- /dev/null +++ b/core/pkg/model/workflows/zz_serde.go @@ -0,0 +1,18 @@ +package workflows + +import "encoding/json/v2" + +var unmarshalers []*json.Unmarshalers + +func JsonUnmarshalers() *json.Unmarshalers { + return json.JoinUnmarshalers(unmarshalers...) +} + +func Decode[M any](content []byte) (*M, error) { + m := new(M) + opt := json.WithUnmarshalers(JsonUnmarshalers()) + if err := json.Unmarshal(content, &m, opt); err != nil { + return nil, err + } + return m, nil +} diff --git a/core/pkg/model/workflows/zz_util.go b/core/pkg/model/workflows/zz_util.go deleted file mode 100644 index 7af13631..00000000 --- a/core/pkg/model/workflows/zz_util.go +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package workflows - -import ( - "fmt" - "reflect" -) - -func valueOf(v reflect.Value) any { - if !v.IsValid() { - return nil - } - return v.Interface() -} - -func castMap[K comparable, V any](input map[K]any) (map[K]V, error) { - res := make(map[K]V, len(input)) - for k, v := range input { - if e, ok := v.(V); ok { - res[k] = e - continue - } - return nil, fmt.Errorf("expected map of %s, got a value %#v type %T", reflect.TypeFor[V]().String(), v, v) - } - return res, nil -} From cbffa9ce5b1d84651bb5bd5845c1f6da7a4f97dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 14 Jul 2026 12:31:48 +0700 Subject: [PATCH 3/4] core/model: remove `yaml` and `actions` tags --- core/pkg/model/actions/action.go | 18 +++--- core/pkg/model/actions/io.go | 20 +++---- core/pkg/model/actions/runs.go | 34 +++++------ core/pkg/model/records/context.go | 32 +++++----- core/pkg/model/records/github.go | 84 +++++++++++++-------------- core/pkg/model/records/job.go | 22 +++---- core/pkg/model/records/runner.go | 16 ++--- core/pkg/model/workflows/container.go | 16 ++--- core/pkg/model/workflows/job.go | 58 +++++++++--------- core/pkg/model/workflows/step.go | 26 ++++----- core/pkg/model/workflows/workflow.go | 26 ++++----- 11 files changed, 176 insertions(+), 176 deletions(-) diff --git a/core/pkg/model/actions/action.go b/core/pkg/model/actions/action.go index 9d6b4639..32af0dce 100644 --- a/core/pkg/model/actions/action.go +++ b/core/pkg/model/actions/action.go @@ -9,34 +9,34 @@ package actions type Action struct { // The name of your action. GitHub displays the `name` in the Actions tab to help visually identify actions in each job. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#name - Name string `json:"name,omitempty" yaml:"name,omitempty" actions:"name,omitempty" validate:"required"` + Name string `json:"name,omitempty"` // The name of the action's author. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#author - Author string `json:"author,omitempty" yaml:"author,omitempty" actions:"author,omitempty"` + Author string `json:"author,omitempty"` // A short description of the action. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#description - Description string `json:"description,omitempty" yaml:"description,omitempty" actions:"description,omitempty" validate:"required"` + Description string `json:"description,omitempty"` // Input parameters allow you to specify data that the action expects to use during runtime. // GitHub stores input parameters as environment variables. // Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs - Inputs map[string]Input `json:"inputs,omitempty" yaml:"inputs,omitempty" actions:"inputs,omitempty"` + Inputs map[string]Input `json:"inputs,omitempty"` // Output parameters allow you to declare data that an action sets. // Actions that run later in a workflow can use the output data set in previously run actions. - Outputs map[string]Output `json:"outputs,omitempty" yaml:"outputs,omitempty" actions:"outputs,omitempty"` + Outputs map[string]Output `json:"outputs,omitempty"` // Specifies whether this is a JavaScript action, a composite action, or a Docker container action and how the action is executed. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs - Runs Runs `json:"runs,omitempty" yaml:"runs,omitempty" actions:"runs,omitempty" validate:"required"` + Runs Runs `json:"runs,omitempty"` // You can use a color and Feather icon to create a badge to personalize and distinguish your action. // Badges are shown next to your action name in GitHub Marketplace. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding - Branding Branding `json:"branding,omitempty" yaml:"branding,omitempty" actions:"branding,omitempty"` + Branding Branding `json:"branding,omitempty"` } // You can use a color and Feather icon to create a badge to personalize and distinguish your action. @@ -45,9 +45,9 @@ type Action struct { type Branding struct { // The background color of the badge. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#brandingcolor - Color string `json:"color,omitempty" yaml:"color,omitempty" actions:"color,omitempty"` + Color string `json:"color,omitempty"` // The name of the Feather icon to use. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#brandingicon - Icon string `json:"icon,omitempty" yaml:"icon,omitempty" actions:"icon,omitempty"` + Icon string `json:"icon,omitempty"` } diff --git a/core/pkg/model/actions/io.go b/core/pkg/model/actions/io.go index e80c4655..98ae1012 100644 --- a/core/pkg/model/actions/io.go +++ b/core/pkg/model/actions/io.go @@ -15,30 +15,30 @@ import "drassi.run/core/pkg/model/workflows" type Input struct { // Required A string description of the input parameter. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_iddescription - Description string `json:"description,omitempty" yaml:"description,omitempty" actions:"description,omitempty"` + Description string `json:"description,omitempty"` // A string shown to users using the deprecated input. - DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty" actions:"deprecationMessage,omitempty"` + DeprecationMessage string `json:"deprecationMessage,omitempty"` // A boolean to indicate whether the action requires the input parameter. Set to true when the parameter is required. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_idrequired - Required bool `json:"required,omitempty" yaml:"required,omitempty" actions:"required,omitempty"` + Required bool `json:"required,omitempty"` // A string representing the default value. The default value is used when an input parameter isn't specified in a workflow file. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_iddefault // // Context available: `github`, `inputs`, `vars` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Default workflows.Evaluable[any] `json:"default,omitempty" yaml:"default,omitempty" actions:"default,omitempty"` // TODO type args + Default workflows.Evaluable[any] `json:"default,omitempty"` // TODO type args // The value of this parameter is a string specifying the data type of the input. // This must be one of: boolean, choice, number, environment or string. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputsinput_idtype - Type InputType `json:"type,omitempty" yaml:"type,omitempty" actions:"type,omitempty"` + Type InputType `json:"type,omitempty"` // The options of the dropdown list, if the type is a choice. // https://github.blog/changelog/2021-11-10-github-actions-input-types-for-manual-workflows/ - Options []string `json:"options,omitempty" yaml:"options,omitempty" actions:"options,omitempty"` + Options []string `json:"options,omitempty"` } type InputType string @@ -52,20 +52,20 @@ const ( ) type Output struct { - Description string `json:"description,omitempty" yaml:"description,omitempty" actions:"description,omitempty"` + Description string `json:"description,omitempty"` // Context available: `github`, `jobs`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Value workflows.Evaluable[string] `json:"value,omitempty" yaml:"value,omitempty" actions:"value,omitempty"` + Value workflows.Evaluable[string] `json:"value,omitempty"` } // A string identifier to associate with the secret. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callsecretssecret_id type Secret struct { // Required A string description of the secret parameter. - Description string `json:"description,omitempty" yaml:"description,omitempty" actions:"description,omitempty"` + Description string `json:"description,omitempty"` // A boolean specifying whether the secret must be supplied. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callsecretssecret_idrequired - Required bool `json:"required,omitempty" yaml:"required,omitempty" actions:"required,omitempty"` + Required bool `json:"required,omitempty"` } diff --git a/core/pkg/model/actions/runs.go b/core/pkg/model/actions/runs.go index fc101a9d..f58cbb9b 100644 --- a/core/pkg/model/actions/runs.go +++ b/core/pkg/model/actions/runs.go @@ -15,38 +15,38 @@ type Runs interface { type NodeRuns struct { // The application used to execute the code specified in `main`. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsusing-for-javascript-actions - Using string `json:"using,omitempty" yaml:"using,omitempty" actions:"using,omitempty" validate:"required"` + Using string `json:"using,omitempty"` // The file that contains your action code. The application specified in `using` executes this file. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsmain - Main string `json:"main,omitempty" yaml:"main,omitempty" actions:"main,omitempty" validate:"required"` + Main string `json:"main,omitempty"` // Allows you to run a script at the start of a job, before the `main:` action begins. // For example, you can use `pre:` to run a prerequisite setup script. The application specified with // the `using` syntax will execute this file. // The `pre:` action always runs by default but you can override this using `pre-if`. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspre - Pre string `json:"pre,omitempty" yaml:"pre,omitempty" actions:"pre,omitempty"` + Pre string `json:"pre,omitempty"` // Allows you to define conditions for the pre: action execution. // The pre: action will only run if the conditions in pre-if are met. // If not set, then pre-if defaults to always(). // In pre-if, status check functions evaluate against the job's status, not the action's own status. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspre-if - PreIf workflows.Conditional `json:"pre-if,omitempty" yaml:"pre-if,omitempty" actions:"pre-if,omitempty"` + PreIf workflows.Conditional `json:"pre-if,omitempty"` // Allows you to run a script at the end of a job, once the main: action has completed. // For example, you can use post: to terminate certain processes or remove unneeded files. // The runtime specified with the using syntax will execute this file. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspost - Post string `json:"post,omitempty" yaml:"post,omitempty" actions:"post,omitempty"` + Post string `json:"post,omitempty"` // Allows you to define conditions for the post: action execution. // The post: action will only run if the conditions in post-if are met. // If not set, then post-if defaults to always(). // In post-if, status check functions evaluate against the job's status, not the action's own status. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspost-if - PostIf workflows.Conditional `json:"post-if,omitempty" yaml:"post-if,omitempty" actions:"post-if,omitempty"` + PostIf workflows.Conditional `json:"post-if,omitempty"` } func (r *NodeRuns) isRuns() { @@ -55,14 +55,14 @@ func (r *NodeRuns) isRuns() { type DockerRuns struct { // You must set this value to 'docker'. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsusing-for-docker-container-actions - Using string `json:"using,omitempty" yaml:"using,omitempty" actions:"using,omitempty" validate:"required"` + Using string `json:"using,omitempty"` // The Docker image to use as the container to run the action. The value can be the Docker base image name, // a local `Dockerfile` in your repository, or a public image in Docker Hub or another registry. // To reference a `Dockerfile` local to your repository, use a path relative to your action metadata file. // The `docker` application will execute this file. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsimage - Image string `json:"image,omitempty" yaml:"image,omitempty" actions:"image,omitempty" validate:"required"` + Image string `json:"image,omitempty"` // Specifies a key/value map of environment variables to set in the container environment. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsenv @@ -70,7 +70,7 @@ type DockerRuns struct { // To set custom environment variables, you need to specify the variables in the workflow file. // You can define environment variables for a step, job, or entire workflow using the jobs..steps[*].env, jobs..env, and env keywords. // For more information, see https://docs.github.com/en/actions/learn-github-actions/variables - Env workflows.Evaluable[map[string]string] `json:"env,omitempty" yaml:"env,omitempty" actions:"env,omitempty"` + Env workflows.Evaluable[map[string]string] `json:"env,omitempty"` // Overrides the Docker `ENTRYPOINT` in the `Dockerfile`, or sets it if one wasn't already specified. // Use `entrypoint` when the `Dockerfile` does not specify an `ENTRYPOINT` or you want to override the `ENTRYPOINT` instruction. @@ -78,7 +78,7 @@ type DockerRuns struct { // The Docker `ENTRYPOINT instruction has a *shell* form and *exec* form. The Docker `ENTRYPOINT` documentation // recommends using the *exec* form of the `ENTRYPOINT` instruction. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsentrypoint - Entrypoint string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty" actions:"entrypoint,omitempty"` + Entrypoint string `json:"entrypoint,omitempty"` // An array of strings that define the inputs for a Docker container. Inputs can include hardcoded strings. // GitHub passes the `args` to the container's `ENTRYPOINT` when the container starts up. @@ -88,7 +88,7 @@ type DockerRuns struct { // - Use defaults that allow using the action without specifying any `args`. // - If the action exposes a `--help` flag, or something similar, use that to make your action self-documenting. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsargs - Args workflows.Evaluable[[]string] `json:"args,omitempty" yaml:"args,omitempty" actions:"args,omitempty"` + Args workflows.Evaluable[[]string] `json:"args,omitempty"` // Allows you to run a script before the `entrypoint` action begins. For example, you can use `pre-entrypoint:` to run a prerequisite setup script. // GitHub Actions uses `docker run` to launch this action, and runs the script inside a new container that uses the same base image. @@ -96,16 +96,16 @@ type DockerRuns struct { // in either the workspace, `HOME`, or as a `STATE_` variable. // The `pre-entrypoint:` action always runs by default but you can override this using `pre-if`. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspre-entrypoint - PreEntrypoint string `json:"pre-entrypoint,omitempty" yaml:"pre-entrypoint,omitempty" actions:"pre-entrypoint,omitempty"` - PreIf workflows.Conditional `json:"pre-if,omitempty" yaml:"pre-if,omitempty" actions:"pre-if,omitempty"` + PreEntrypoint string `json:"pre-entrypoint,omitempty"` + PreIf workflows.Conditional `json:"pre-if,omitempty"` // Allows you to run a cleanup script once the `runs.entrypoint` action has completed. GitHub Actions uses `docker run` to launch this action. // Because GitHub Actions runs the script inside a new container using the same base image, the runtime state is different from the main `entrypoint` container. // You can access any state you need in either the workspace, `HOME`, or as a `STATE_` variable. // The `post-entrypoint:` action always runs by default but you can override this using `post-if`. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspost-entrypoint - PostEntrypoint string `json:"post-entrypoint,omitempty" yaml:"post-entrypoint,omitempty" actions:"post-entrypoint,omitempty"` - PostIf workflows.Conditional `json:"post-if,omitempty" yaml:"post-if,omitempty" actions:"post-if,omitempty"` + PostEntrypoint string `json:"post-entrypoint,omitempty"` + PostIf workflows.Conditional `json:"post-if,omitempty"` } func (r *DockerRuns) isRuns() { @@ -114,11 +114,11 @@ func (r *DockerRuns) isRuns() { type CompositeRuns struct { // To use a composite run steps action, set this to 'composite'. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions - Using string `json:"using,omitempty" yaml:"using,omitempty" actions:"using,omitempty" validate:"required"` + Using string `json:"using,omitempty"` // The steps that you plan to run in this action. These can be either run steps or uses steps. // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runssteps - Steps []workflows.Step `json:"steps,omitempty" yaml:"steps,omitempty" actions:"steps,omitempty" validate:"required"` + Steps []workflows.Step `json:"steps,omitempty"` } func (r *CompositeRuns) isRuns() { diff --git a/core/pkg/model/records/context.go b/core/pkg/model/records/context.go index c5718c63..7b0f9e68 100644 --- a/core/pkg/model/records/context.go +++ b/core/pkg/model/records/context.go @@ -12,62 +12,62 @@ package records type Dossier struct { // The `github` context contains information about the workflow run and the event that triggered the run. // https://docs.github.com/en/actions/learn-github-actions/contexts#github-contex - Github *Github `json:"github" yaml:"github" actions:"github"` + Github *Github `json:"github"` // The `env` context contains variables that have been set in a workflow, job, or step. // It does not contain variables inherited by the runner process. // https://docs.github.com/en/actions/learn-github-actions/contexts#env-context - Env map[string]string `json:"env" yaml:"env" actions:"env"` + Env map[string]string `json:"env"` // The `vars` context contains custom configuration variables set at the organization, repository, and environment levels. // https://docs.github.com/en/actions/learn-github-actions/contexts#vars-context - Variables map[string]string `json:"vars" yaml:"vars" actions:"vars"` + Variables map[string]string `json:"vars"` // The job context contains information about the currently running job. // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context - Job *JobInfo `json:"job" yaml:"job" actions:"job"` + Job *JobInfo `json:"job"` // The `jobs` context is only available in reusable workflows, and can only be used to set outputs for a reusable workflow. // https://docs.github.com/en/actions/learn-github-actions/contexts#jobs-context - Jobs map[string]*JobResult `json:"jobs" yaml:"jobs" actions:"jobs"` + Jobs map[string]*JobResult `json:"jobs"` // The `steps` context contains information about the steps in the current job that have an `id` specified and have already run. // https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context - Steps map[string]*StepResult `json:"steps" yaml:"steps" actions:"steps"` + Steps map[string]*StepResult `json:"steps"` // The `runner` context contains information about the runner that is executing the current job. // https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context - Runner *RunnerInfo `json:"runner" yaml:"runner" actions:"runner"` + Runner *RunnerInfo `json:"runner"` // The secrets context contains the names and values of secrets that are available to a workflow run. // The secrets context is not available for composite actions due to security reasons. // If you want to pass a secret to a composite action, you need to do it explicitly as an input. // https://docs.github.com/en/actions/learn-github-actions/contexts#secrets-context - Secrets map[string]string `json:"secrets" yaml:"secrets" actions:"secrets"` + Secrets map[string]string `json:"secrets"` // For workflows with a matrix, the strategy context contains information about the matrix execution strategy for the current job. // https://docs.github.com/en/actions/learn-github-actions/contexts#strategy-context - Strategy *Strategy `json:"strategy" yaml:"strategy" actions:"strategy"` + Strategy *Strategy `json:"strategy"` // For workflows with a matrix, the matrix context contains the matrix properties defined in the workflow file that apply to the current job. // https://docs.github.com/en/actions/learn-github-actions/contexts#matrix-context - Matrix map[string]string `json:"matrix" yaml:"matrix" actions:"matrix"` + Matrix map[string]string `json:"matrix"` // The needs context contains outputs from all jobs that are defined as a direct dependency of the current job. // Note that this doesn't include implicitly dependent jobs (for example, dependent jobs of a dependent job). // https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context - Needs map[string]*JobResult `json:"needs" yaml:"needs" actions:"needs"` + Needs map[string]*JobResult `json:"needs"` // The inputs context contains input properties passed to an action, to a reusable workflow, or to a manually triggered workflow. // https://docs.github.com/en/actions/learn-github-actions/contexts#inputs-context - Inputs map[string]any `json:"inputs" yaml:"inputs" actions:"inputs"` + Inputs map[string]any `json:"inputs"` } // For workflows with a matrix, the strategy context contains information about the matrix execution strategy for the current job. // https://docs.github.com/en/actions/learn-github-actions/contexts#strategy-context type Strategy struct { - FailFast bool `json:"fail-fast" yaml:"fail-fast" actions:"fail-fast"` - JobIndex int64 `json:"job-index" yaml:"job-index" actions:"job-index"` - JobTotal int64 `json:"job-total" yaml:"job-total" actions:"job-total"` - MaxParallel int64 `json:"max-parallel" yaml:"max-parallel" actions:"max-parallel"` + FailFast bool `json:"fail-fast"` + JobIndex int64 `json:"job-index"` + JobTotal int64 `json:"job-total"` + MaxParallel int64 `json:"max-parallel"` } diff --git a/core/pkg/model/records/github.go b/core/pkg/model/records/github.go index a91b6e22..64a0a01c 100644 --- a/core/pkg/model/records/github.go +++ b/core/pkg/model/records/github.go @@ -9,50 +9,50 @@ package records // The github context contains information about the workflow run and the event that triggered the run. // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context type Github struct { - Action string `json:"action" yaml:"action" actions:"action"` - ActionPath string `json:"action_path" yaml:"action_path" actions:"action_path"` - ActionRef string `json:"action_ref" yaml:"action_ref" actions:"action_ref"` - ActionRepository string `json:"action_repository" yaml:"action_repository" actions:"action_repository"` - ActionStatus Result `json:"action_status" yaml:"action_status" actions:"action_status"` - Actor string `json:"actor" yaml:"actor" actions:"actor"` - ActorId string `json:"actor_id" yaml:"actor_id" actions:"actor_id"` - ApiUrl string `json:"api_url" yaml:"api_url" actions:"api_url"` - BaseRef string `json:"base_ref" yaml:"base_ref" actions:"base_ref"` - Event any `json:"event" yaml:"event" actions:"event"` // TODO data type - EventName string `json:"event_name" yaml:"event_name" actions:"event_name"` - EventPath string `json:"event_path" yaml:"event_path" actions:"event_path"` - GraphqlUrl string `json:"graphql_url" yaml:"graphql_url" actions:"graphql_url"` - HeadRef string `json:"head_ref" yaml:"head_ref" actions:"head_ref"` - Job string `json:"job" yaml:"job" actions:"job"` - Ref string `json:"ref" yaml:"ref" actions:"ref"` - RefName string `json:"ref_name" yaml:"ref_name" actions:"ref_name"` - RefProtected bool `json:"ref_protected" yaml:"ref_protected" actions:"ref_protected"` - RefType RefType `json:"ref_type" yaml:"ref_type" actions:"ref_type"` - Repository string `json:"repository" yaml:"repository" actions:"repository"` - RepositoryId string `json:"repository_id" yaml:"repository_id" actions:"repository_id"` - RepositoryOwner string `json:"repository_owner" yaml:"repository_owner" actions:"repository_owner"` - RepositoryOwnerId string `json:"repository_owner_id" yaml:"repository_owner_id" actions:"repository_owner_id"` - RepositoryUrl string `json:"repositoryUrl" yaml:"repositoryUrl" actions:"repositoryUrl"` // naming convention??? - RetentionDays string `json:"retention_days" yaml:"retention_days" actions:"retention_days"` - RunId string `json:"run_id" yaml:"run_id" actions:"run_id"` - RunNumber string `json:"run_number" yaml:"run_number" actions:"run_number"` - RunAttempt string `json:"run_attempt" yaml:"run_attempt" actions:"run_attempt"` - SecretSource SecretSource `json:"secret_source" yaml:"secret_source" actions:"secret_source"` - ServerUrl string `json:"server_url" yaml:"server_url" actions:"server_url"` - Sha string `json:"sha" yaml:"sha" actions:"sha"` - Token string `json:"token" yaml:"token" actions:"token"` - TriggeringActor string `json:"triggering_actor" yaml:"triggering_actor" actions:"triggering_actor"` - Workflow string `json:"workflow" yaml:"workflow" actions:"workflow"` - WorkflowRef string `json:"workflow_ref" yaml:"workflow_ref" actions:"workflow_ref"` - WorkflowSha string `json:"workflow_sha" yaml:"workflow_sha" actions:"workflow_sha"` - Workspace string `json:"workspace" yaml:"workspace" actions:"workspace"` + Action string `json:"action"` + ActionPath string `json:"action_path"` + ActionRef string `json:"action_ref"` + ActionRepository string `json:"action_repository"` + ActionStatus Result `json:"action_status"` + Actor string `json:"actor"` + ActorId string `json:"actor_id"` + ApiUrl string `json:"api_url"` + BaseRef string `json:"base_ref"` + Event any `json:"event"` // TODO data type + EventName string `json:"event_name"` + EventPath string `json:"event_path"` + GraphqlUrl string `json:"graphql_url"` + HeadRef string `json:"head_ref"` + Job string `json:"job"` + Ref string `json:"ref"` + RefName string `json:"ref_name"` + RefProtected bool `json:"ref_protected"` + RefType RefType `json:"ref_type"` + Repository string `json:"repository"` + RepositoryId string `json:"repository_id"` + RepositoryOwner string `json:"repository_owner"` + RepositoryOwnerId string `json:"repository_owner_id"` + RepositoryUrl string `json:"repositoryUrl"` // naming convention??? + RetentionDays string `json:"retention_days"` + RunId string `json:"run_id"` + RunNumber string `json:"run_number"` + RunAttempt string `json:"run_attempt"` + SecretSource SecretSource `json:"secret_source"` + ServerUrl string `json:"server_url"` + Sha string `json:"sha"` + Token string `json:"token"` + TriggeringActor string `json:"triggering_actor"` + Workflow string `json:"workflow"` + WorkflowRef string `json:"workflow_ref"` + WorkflowSha string `json:"workflow_sha"` + Workspace string `json:"workspace"` // File commands env - Path string `json:"path" yaml:"path" actions:"path"` - Env string `json:"env" yaml:"env" actions:"env"` - Output string `json:"output" yaml:"output" actions:"output"` - State string `json:"state" yaml:"state" actions:"state"` - StepSummary string `json:"step_summary" yaml:"step_summary" actions:"step_summary"` + Path string `json:"path"` + Env string `json:"env"` + Output string `json:"output"` + State string `json:"state"` + StepSummary string `json:"step_summary"` } type RefType string diff --git a/core/pkg/model/records/job.go b/core/pkg/model/records/job.go index cc1e99e1..08c904f6 100644 --- a/core/pkg/model/records/job.go +++ b/core/pkg/model/records/job.go @@ -10,31 +10,31 @@ package records // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context // https://github.com/actions/runner/blob/v2.315.0/src/Runner.Worker/JobContext.cs type JobInfo struct { - Container *ContainerInfo `json:"container" yaml:"container" actions:"container"` - Services map[string]*ContainerInfo `json:"services" yaml:"services" actions:"services"` - Status Result `json:"status" yaml:"status" actions:"status"` + Container *ContainerInfo `json:"container"` + Services map[string]*ContainerInfo `json:"services"` + Status Result `json:"status"` } type ContainerInfo struct { - Id string `json:"id" yaml:"id" actions:"id"` - Network string `json:"network" yaml:"network" actions:"network"` - Ports map[string]string `json:"ports" yaml:"ports" actions:"ports"` + Id string `json:"id"` + Network string `json:"network"` + Ports map[string]string `json:"ports"` } // The `jobs` context is only available in reusable workflows, and can only be used to set outputs for a reusable workflow. // https://docs.github.com/en/actions/learn-github-actions/contexts#jobs-context type JobResult struct { - Result Result `json:"result" yaml:"result" actions:"result"` - Outputs map[string]string `json:"outputs" yaml:"outputs" actions:"outputs"` + Result Result `json:"result"` + Outputs map[string]string `json:"outputs"` } // The `steps` context contains information about the steps in the current job that have an `id` specified and have already run. // https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context // https://github.com/actions/runner/blob/v2.315.0/src/Runner.Worker/StepsContext.cs type StepResult struct { - Outputs map[string]string `json:"outputs" yaml:"outputs" actions:"outputs"` - Conclusion Result `json:"conclusion" yaml:"conclusion" actions:"conclusion"` - Outcome Result `json:"outcome" yaml:"outcome" actions:"outcome"` + Outputs map[string]string `json:"outputs"` + Conclusion Result `json:"conclusion"` + Outcome Result `json:"outcome"` } // https://github.com/actions/runner/blob/v2.315.0/src/Runner.Common/ActionResult.cs diff --git a/core/pkg/model/records/runner.go b/core/pkg/model/records/runner.go index b56ef8b7..110b7f95 100644 --- a/core/pkg/model/records/runner.go +++ b/core/pkg/model/records/runner.go @@ -11,12 +11,12 @@ import "drassi.run/core/pkg/model" // The `runner` context contains information about the runner that is executing the current job. // https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context type RunnerInfo struct { - Name string `json:"name" yaml:"name" actions:"name"` - Os model.Machine `json:"os" yaml:"os" actions:"os"` - Arch model.Architecture `json:"arch" yaml:"arch" actions:"arch"` - Environment string `json:"environment" yaml:"environment" actions:"environment"` - Temp string `json:"temp" yaml:"temp" actions:"temp"` - ToolCache string `json:"tool_cache" yaml:"tool_cache" actions:"tool_cache"` - Workspace string `json:"workspace" yaml:"workspace" actions:"workspace"` - Debug string `json:"debug" yaml:"debug" actions:"debug"` + Name string `json:"name"` + Os model.Machine `json:"os"` + Arch model.Architecture `json:"arch"` + Environment string `json:"environment"` + Temp string `json:"temp"` + ToolCache string `json:"tool_cache"` + Workspace string `json:"workspace"` + Debug string `json:"debug"` } diff --git a/core/pkg/model/workflows/container.go b/core/pkg/model/workflows/container.go index ea775d16..37135487 100644 --- a/core/pkg/model/workflows/container.go +++ b/core/pkg/model/workflows/container.go @@ -13,13 +13,13 @@ type Container struct { // // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Image string `json:"image,omitempty" yaml:"image,omitempty" actions:"image,omitempty" validate:"required"` + Image string `json:"image,omitempty"` // If the image's container registry requires authentication to pull the image, // you can use credentials to set a map of the username and password. // The credentials are the same values that you would provide to the `docker login` command. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainercredentials - Credentials *ContainerCredentials `json:"credentials,omitempty" yaml:"credentials,omitempty" actions:"credentials,omitempty"` + Credentials *ContainerCredentials `json:"credentials,omitempty"` // Sets an array of environment variables in the container. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainerenv @@ -30,28 +30,28 @@ type Container struct { // // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Env map[string]string `json:"env,omitempty" yaml:"env,omitempty" actions:"env,omitempty"` + Env map[string]string `json:"env,omitempty"` // Sets an array of ports to expose on the container. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainerports - Ports []string `json:"ports,omitempty" yaml:"ports,omitempty" actions:"ports,omitempty"` + Ports []string `json:"ports,omitempty"` // Sets an array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. // You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host. // To specify a volume, you specify the source and destination path: : // The is a volume name or an absolute path on the host machine, and is an absolute path in the container. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainervolumes - Volumes []string `json:"volumes,omitempty" yaml:"volumes,omitempty" actions:"volumes,omitempty"` + Volumes []string `json:"volumes,omitempty"` // Additional Docker container resource options. // For a list of options, see https://docs.docker.com/engine/reference/commandline/create/#options. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontaineroptions - Options string `json:"options,omitempty" yaml:"options,omitempty" actions:"options,omitempty"` + Options string `json:"options,omitempty"` } // Context available: `github`, `needs`, `strategy`, `matrix`, `env`, `vars`, `secrets`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability type ContainerCredentials struct { - Username string `json:"username,omitempty" yaml:"username,omitempty" actions:"username,omitempty"` - Password string `json:"password,omitempty" yaml:"password,omitempty" actions:"password,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` } diff --git a/core/pkg/model/workflows/job.go b/core/pkg/model/workflows/job.go index 0142d806..b673c309 100644 --- a/core/pkg/model/workflows/job.go +++ b/core/pkg/model/workflows/job.go @@ -28,17 +28,17 @@ type BaseJob struct { // // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Name Evaluable[string] `json:"name,omitempty" yaml:"name,omitempty" actions:"name,omitempty"` + Name Evaluable[string] `json:"name,omitempty"` // You can modify the default permissions granted to the GITHUB_TOKEN, // adding or removing access as required, so that you only allow the minimum required access. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions - Permissions Permissions `json:"permissions,omitempty" yaml:"permissions,omitempty" actions:"permissions,omitempty"` + Permissions Permissions `json:"permissions,omitempty"` // Identifies any jobs that must complete successfully before this job will run. It can be a string or array of strings. // If a job fails, all jobs that need it are skipped unless the jobs use a conditional statement that causes the job to continue. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds - Needs JobNeeds `json:"needs,omitempty" yaml:"needs,omitempty" actions:"needs,omitempty"` + Needs JobNeeds `json:"needs,omitempty"` // You can use the if conditional to prevent a job from running unless a condition is met. // You can use any supported context and expression to create a conditional. @@ -49,11 +49,11 @@ type BaseJob struct { // Context available: `github`, `needs`, `vars`, `inputs` // Special functions: `always`, `cancelled`, `success`, `failure` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - If Conditional `json:"if,omitempty" yaml:"if,omitempty" actions:"if,omitempty"` + If Conditional `json:"if,omitempty"` // A strategy creates a build matrix for your jobs. You can define different variations of an environment to run each job in. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategy - Strategy Strategy `json:"strategy,omitempty" yaml:"strategy,omitempty" actions:"strategy,omitempty"` + Strategy Strategy `json:"strategy,omitempty"` // Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. // A concurrency group can be any string or expression. The expression can use any context except for the secrets context. @@ -62,14 +62,14 @@ type BaseJob struct { // the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. // To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idconcurrency - Concurrency Concurrency `json:"concurrency,omitempty" yaml:"concurrency,omitempty" actions:"concurrency,omitempty"` + Concurrency Concurrency `json:"concurrency,omitempty"` // Prevents a workflow run from failing when a job fails. Set to true to allow a workflow run to pass when this job fails. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error // // Context available: `github`, `needs`, `strategy`, `vars`, `matrix`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - ContinueOnError Evaluable[bool] `json:"continue-on-error,omitempty" yaml:"continue-on-error,omitempty" actions:"continue-on-error,omitempty"` + ContinueOnError Evaluable[bool] `json:"continue-on-error,omitempty"` } func (j *BaseJob) Base() *BaseJob { @@ -77,22 +77,22 @@ func (j *BaseJob) Base() *BaseJob { } type NormalJob struct { - BaseJob `json:",inline" yaml:",inline" actions:",squash"` + BaseJob `json:",inline"` // The type of machine to run the job on. The machine can be either a GitHub-hosted runner, or a self-hosted runner. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on - RunsOn Evaluable[RunsOn] `json:"runs-on,omitempty" yaml:"runs-on,omitempty" actions:"runs-on,omitempty" validate:"required"` + RunsOn Evaluable[RunsOn] `json:"runs-on,omitempty"` // The environment that the job references. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idenvironment - Environment Evaluable[Environment] `json:"environment,omitempty" yaml:"environment,omitempty" actions:"environment,omitempty"` + Environment Evaluable[Environment] `json:"environment,omitempty"` // A map of outputs for a job. Job outputs are available to all downstream jobs that depend on this job. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idoutputs // // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Outputs Evaluable[map[string]string] `json:"outputs,omitempty" yaml:"outputs,omitempty" actions:"outputs,omitempty"` + Outputs Evaluable[map[string]string] `json:"outputs,omitempty"` // A map of environment variables that are available to all steps in the job. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idenv @@ -103,11 +103,11 @@ type NormalJob struct { // // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `secrets`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Env Evaluable[map[string]string] `json:"env,omitempty" yaml:"env,omitempty" actions:"env,omitempty"` + Env Evaluable[map[string]string] `json:"env,omitempty"` // A map of default settings that will apply to all steps in the job. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iddefaults - Defaults Evaluable[Defaults] `json:"defaults,omitempty" yaml:"defaults,omitempty" actions:"defaults,omitempty"` + Defaults Evaluable[Defaults] `json:"defaults,omitempty"` // A job contains a sequence of tasks called steps. Steps can run commands, run setup tasks, or run an action in your repository, // a public repository, or an action published in a Docker registry. Not all steps run actions, but all actions run as a step. @@ -115,14 +115,14 @@ type NormalJob struct { // Because steps run in their own process, changes to environment variables are not preserved between steps. // GitHub provides built-in steps to set up and complete a job. Must contain either `uses` or `run` // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps - Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty" actions:"steps,omitempty"` + Steps []Step `json:"steps,omitempty"` // The maximum number of minutes to let a workflow run before GitHub automatically cancels it. Default: 360 // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes // // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - TimeoutInMinutes Evaluable[int64] `json:"timeout-minutes,omitempty" yaml:"timeout-minutes,omitempty" actions:"timeout-minutes,omitempty"` + TimeoutInMinutes Evaluable[int64] `json:"timeout-minutes,omitempty"` // A container to run any steps in a job that don't already specify a container. // If you have steps that use both script and container actions, @@ -130,7 +130,7 @@ type NormalJob struct { // If you do not set a container, all steps will run directly on the host specified by runs-on unless a step // refers to an action configured to run in a container. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer - Container Evaluable[*Container] `json:"container,omitempty" yaml:"container,omitempty" actions:"container,omitempty"` + Container Evaluable[*Container] `json:"container,omitempty"` // Additional containers to host services for a job in a workflow. These are useful for creating databases or cache services like redis. // The runner on the virtual machine will automatically create a network and manage the life cycle of the service containers. @@ -140,7 +140,7 @@ type NormalJob struct { // The hostname is automatically mapped to the service name. // When a step does not use a container action, you must access the service using localhost and bind the ports. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservices - Services Evaluable[map[string]*Container] `json:"services,omitempty" yaml:"services,omitempty" actions:"services,omitempty"` + Services Evaluable[map[string]*Container] `json:"services,omitempty"` } // Each job must have an id to associate with the job. @@ -149,13 +149,13 @@ type NormalJob struct { // The must start with a letter or _ and contain only alphanumeric characters, -, or _.", type: "object // https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow type ReusableWorkflowCallJob struct { - BaseJob `json:",inline" yaml:",inline" actions:",squash"` + BaseJob `json:",inline"` // The location and version of a reusable workflow file to run as a job, of the form './{path/to}/{localfile}.yml' // or '{owner}/{repo}/{path}/{filename}@{ref}'. {ref} can be a SHA, a release tag, or a branch name. // Using the commit SHA is the safest for stability and security. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses - Uses string `json:"uses,omitempty" yaml:"uses,omitempty" actions:"uses,omitempty" validate:"required"` + Uses string `json:"uses,omitempty"` // A map of inputs that are passed to the called workflow. // Any inputs that you pass must match the input specifications defined in the called workflow. @@ -165,12 +165,12 @@ type ReusableWorkflowCallJob struct { // // Context available: `github`, `needs`, `strategy`, `matrix`, `inputs`, `vars` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - With Evaluable[map[string]string] `json:"with,omitempty" yaml:"with,omitempty" actions:"with,omitempty"` + With Evaluable[map[string]string] `json:"with,omitempty"` // When a job is used to call a reusable workflow, you can use 'secrets' to provide a map of secrets that are passed to the called workflow. // Any secrets that you pass must match the names defined in the called workflow. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsecrets - Secrets Evaluable[JobSecrets] `json:"secrets,omitempty" yaml:"secrets,omitempty" actions:"secrets,omitempty"` + Secrets Evaluable[JobSecrets] `json:"secrets,omitempty"` } // array support decode from string shorthand @@ -189,7 +189,7 @@ type JobSecrets struct { // Use the inherit keyword to pass all the calling workflow's secrets to the called workflow. // This includes all secrets the calling workflow has access to, namely organization, repository, and environment secrets. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsecretsinherit - Inherit bool `json:"inherit,omitempty" yaml:"inherit,omitempty" actions:"inherit,omitempty"` + Inherit bool `json:"inherit,omitempty"` } // The environment that the job references @@ -198,13 +198,13 @@ type Environment struct { // The name of the environment configured in the repo. // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Name string `json:"name,omitempty" yaml:"name,omitempty" actions:"name,omitempty"` + Name string `json:"name,omitempty"` // A deployment URL // // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `steps`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Url string `json:"url,omitempty" yaml:"url,omitempty" actions:"url,omitempty"` + Url string `json:"url,omitempty"` } // The type of machine to run the job on. The machine can be either a GitHub-hosted runner, or a self-hosted runner. @@ -213,13 +213,13 @@ type RunsOn struct { // You can use runs-on to target runner groups, so that the job will execute on any runner that is a member of that group. // For more granular control, you can also combine runner groups with labels. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-runners-in-a-group - Group string `json:"group,omitempty" yaml:"group,omitempty" actions:"group,omitempty"` + Group string `json:"group,omitempty"` // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on // // Context available: `github`, `needs`, `strategy`, `matrix`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Labels array `json:"labels,omitempty" yaml:"labels,omitempty" actions:"labels,omitempty"` + Labels array `json:"labels,omitempty"` } // A strategy creates a build matrix for your jobs. You can define different variations of an environment to run each job in. @@ -228,15 +228,15 @@ type RunsOn struct { // Context available: `github`, `needs`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability type Strategy struct { - Matrix Evaluable[Matrix] `json:"matrix,omitempty" yaml:"matrix,omitempty" actions:"matrix,omitempty"` + Matrix Evaluable[Matrix] `json:"matrix,omitempty"` // When set to true, GitHub cancels all in-progress jobs if any matrix job fails. Default: true // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast - FailFast Evaluable[bool] `json:"fail-fast,omitempty" yaml:"fail-fast,omitempty" actions:"fail-fast,omitempty"` // default: true + FailFast Evaluable[bool] `json:"fail-fast,omitempty"` // default: true // The maximum number of jobs that can run simultaneously when using a matrix job strategy. // By default, GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel - MaxParallel Evaluable[int64] `json:"max-parallel,omitempty" yaml:"max-parallel,omitempty" actions:"max-parallel,omitempty"` + MaxParallel Evaluable[int64] `json:"max-parallel,omitempty"` } type Matrix struct { diff --git a/core/pkg/model/workflows/step.go b/core/pkg/model/workflows/step.go index 70eef619..d13bc532 100644 --- a/core/pkg/model/workflows/step.go +++ b/core/pkg/model/workflows/step.go @@ -29,7 +29,7 @@ type ActionStep struct { // A unique identifier for the step. You can use the id to reference the step in contexts. // For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsid - Id string `json:"id,omitempty" yaml:"id,omitempty" actions:"id,omitempty"` + Id string `json:"id,omitempty"` // A name for your step to display on GitHub. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsname @@ -37,7 +37,7 @@ type ActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Name Evaluable[string] `json:"name,omitempty" yaml:"name,omitempty" actions:"name,omitempty"` + Name Evaluable[string] `json:"name,omitempty"` // You can use the if conditional to prevent a step from running unless a condition is met. // You can use any supported context and expression to create a conditional. @@ -48,7 +48,7 @@ type ActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `steps`, `inputs` // Special functions: `always`, `cancelled`, `success`, `failure`, `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - If Conditional `json:"if,omitempty" yaml:"if,omitempty" actions:"if,omitempty"` + If Conditional `json:"if,omitempty"` // Sets environment variables for steps to use in the virtual environment. You can also set environment variables for the entire workflow or a job. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsenv @@ -60,7 +60,7 @@ type ActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Env Evaluable[map[string]string] `json:"env,omitempty" yaml:"env,omitempty" actions:"env,omitempty"` + Env Evaluable[map[string]string] `json:"env,omitempty"` // Prevents a job from failing when a step fails. Set to true to allow a job to pass when this step fails. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error @@ -68,7 +68,7 @@ type ActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - ContinueOnError Evaluable[bool] `json:"continue-on-error,omitempty" yaml:"continue-on-error,omitempty" actions:"continue-on-error,omitempty"` + ContinueOnError Evaluable[bool] `json:"continue-on-error,omitempty"` // The maximum number of minutes to run the step before killing the process. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes @@ -76,7 +76,7 @@ type ActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - TimeoutInMinutes Evaluable[int64] `json:"timeout-minutes,omitempty" yaml:"timeout-minutes,omitempty" actions:"timeout-minutes,omitempty"` + TimeoutInMinutes Evaluable[int64] `json:"timeout-minutes,omitempty"` } func (s *ActionStep) Kind() StepKind { @@ -88,7 +88,7 @@ func (s *ActionStep) ActionBase() *ActionStep { } type UsesActionStep struct { - ActionStep `json:",inline" yaml:",inline" actions:",squash"` + ActionStep `json:",inline"` // Selects an action to run as part of a step in your job. An action is a reusable unit of code. // You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image (https://hub.docker.com/). @@ -102,7 +102,7 @@ type UsesActionStep struct { // Actions are either JavaScript files or Docker containers. If the action you're using is a Docker container you must run the job in a Linux virtual environment. // For more details, see https://help.github.com/en/articles/virtual-environments-for-github-actions. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses - Uses string `json:"uses,omitempty" yaml:"uses,omitempty" actions:"uses,omitempty" validate:"required"` + Uses string `json:"uses,omitempty"` // A map of the input parameters defined by the action. Each input parameter is a key/value pair. // Input parameters are set as environment variables. The variable is prefixed with INPUT_ and converted to upper case. @@ -111,11 +111,11 @@ type UsesActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - With Evaluable[map[string]string] `json:"with,omitempty" yaml:"with,omitempty" actions:"with,omitempty"` + With Evaluable[map[string]string] `json:"with,omitempty"` } type RunActionStep struct { - ActionStep `json:",inline" yaml:",inline" actions:",squash"` + ActionStep `json:",inline"` // Runs command-line programs using the operating system's shell. If you do not provide a name, the step name will default to the text specified in the run command. // Commands run using non-login shells by default. You can choose a different shell and customize the shell used to run commands. @@ -126,12 +126,12 @@ type RunActionStep struct { // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Run Evaluable[string] `json:"run,omitempty" yaml:"run,omitempty" actions:"run,omitempty" validate:"required"` + Run Evaluable[string] `json:"run,omitempty"` - Shell string `json:"shell,omitempty" yaml:"shell,omitempty" actions:"shell,omitempty"` + Shell string `json:"shell,omitempty"` // Context available: `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `secrets`, `steps`, `inputs` // Special functions: `hashFiles` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - WorkingDir Evaluable[string] `json:"working-directory,omitempty" yaml:"working-directory,omitempty" actions:"working-directory,omitempty"` + WorkingDir Evaluable[string] `json:"working-directory,omitempty"` } diff --git a/core/pkg/model/workflows/workflow.go b/core/pkg/model/workflows/workflow.go index d2dfaf7b..456ad89c 100644 --- a/core/pkg/model/workflows/workflow.go +++ b/core/pkg/model/workflows/workflow.go @@ -11,7 +11,7 @@ type Workflow struct { // The name of your workflow. GitHub displays the names of your workflows on your repository's actions page. // If you omit this field, GitHub sets the name to the workflow's filename. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#name - Name string `json:"name,omitempty" yaml:"name,omitempty" actions:"name,omitempty"` + Name string `json:"name,omitempty"` // The name for workflow runs generated from the workflow. // GitHub displays the workflow run name in the list of workflow runs on your repository's 'Actions' tab. @@ -19,20 +19,20 @@ type Workflow struct { // // Context available: `github`, `inputs`, `vars` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - RunName Evaluable[string] `json:"run-name,omitempty" yaml:"run-name,omitempty" actions:"run-name,omitempty"` + RunName Evaluable[string] `json:"run-name,omitempty"` // The name of the GitHub event that triggers the workflow. // You can provide a single event string, array of events, array of event types, or an event configuration map // that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. // For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on - On On `json:"on,omitempty" yaml:"on,omitempty" actions:"on,omitempty"` + On On `json:"on,omitempty"` // You can use permissions to modify the default permissions granted to the GITHUB_TOKEN, // adding or removing access as required, so that you only allow the minimum required access. // For more information, see https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions - Permissions Permissions `json:"permissions,omitempty" yaml:"permissions,omitempty" actions:"permissions,omitempty"` + Permissions Permissions `json:"permissions,omitempty"` // A map of environment variables that are available to all jobs and steps in the workflow. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#env @@ -43,11 +43,11 @@ type Workflow struct { // // Context available: `github`, `secrets`, `inputs`, `vars` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Env Evaluable[map[string]string] `json:"env,omitempty" yaml:"env,omitempty" actions:"env,omitempty"` + Env Evaluable[map[string]string] `json:"env,omitempty"` // A map of default settings that will apply to all jobs in the workflow. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaults - Defaults Evaluable[Defaults] `json:"defaults,omitempty" yaml:"defaults,omitempty" actions:"defaults,omitempty"` + Defaults Evaluable[Defaults] `json:"defaults,omitempty"` // Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. // A concurrency group can be any string or expression. The expression can use any context except for the secrets context. @@ -57,7 +57,7 @@ type Workflow struct { // Any previously pending job or workflow in the concurrency group will be canceled. // To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency - Concurrency Concurrency `json:"concurrency,omitempty" yaml:"concurrency,omitempty" actions:"concurrency,omitempty"` + Concurrency Concurrency `json:"concurrency,omitempty"` // A workflow run is made up of one or more jobs. Jobs run in parallel by default. // To run jobs sequentially, you can define dependencies on other jobs using the jobs..needs keyword. @@ -65,7 +65,7 @@ type Workflow struct { // You can run an unlimited number of jobs as long as you are within the workflow usage limits. // For more information, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#usage-limits. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobs - Jobs map[string]Job `json:"jobs,omitempty" yaml:"jobs,omitempty" actions:"jobs,omitempty"` + Jobs map[string]Job `json:"jobs,omitempty"` } // You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, @@ -87,9 +87,9 @@ type Defaults struct { // - in job level: `github`, `needs`, `strategy`, `matrix`, `env`, `vars`, `inputs` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability Run struct { - Shell string `json:"shell,omitempty" yaml:"shell,omitempty" actions:"shell,omitempty"` - WorkingDir string `json:"working-directory,omitempty" yaml:"working-directory,omitempty" actions:"working-directory,omitempty"` - } `json:"run,omitempty" yaml:"run,omitempty" actions:"run,omitempty"` + Shell string `json:"shell,omitempty"` + WorkingDir string `json:"working-directory,omitempty"` + } `json:"run,omitempty"` } // Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. @@ -109,9 +109,9 @@ type Concurrency struct { // - in workflow level: `github`, `inputs`, `vars` // - in job level: `github`, `needs`, `strategy`, `matrix`, `inputs`, `vars` // https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability - Group Evaluable[string] `json:"group,omitempty" yaml:"group,omitempty" actions:"group,omitempty"` + Group Evaluable[string] `json:"group,omitempty"` // To cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-to-cancel-any-in-progress-job-or-run-1 - CancelInProgress bool `json:"cancel-in-progress,omitempty" yaml:"cancel-in-progress,omitempty" actions:"cancel-in-progress,omitempty"` + CancelInProgress bool `json:"cancel-in-progress,omitempty"` } From f0eb956c392b1821e66f777a79f09d1c0ff05ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 14 Jul 2026 14:19:54 +0700 Subject: [PATCH 4/4] core/model: rewrite test cases --- core/pkg/model/actions/runs_test.go | 185 ----- .../model/actions/{zz_serde.go => serde.go} | 10 +- core/pkg/model/actions/serde_test.go | 76 ++ core/pkg/model/workflows/container_test.go | 92 +-- core/pkg/model/workflows/evaluable.go | 2 +- core/pkg/model/workflows/evaluable_test.go | 184 +++-- core/pkg/model/workflows/event.go | 19 - core/pkg/model/workflows/event_serde.go | 47 -- core/pkg/model/workflows/event_test.go | 653 ------------------ core/pkg/model/workflows/job_test.go | 514 ++++---------- core/pkg/model/workflows/step_test.go | 190 ++--- core/pkg/model/workflows/workflow.go | 12 + core/pkg/model/workflows/workflow_serde.go | 34 + core/pkg/model/workflows/workflow_test.go | 237 +++---- 14 files changed, 548 insertions(+), 1707 deletions(-) delete mode 100644 core/pkg/model/actions/runs_test.go rename core/pkg/model/actions/{zz_serde.go => serde.go} (86%) create mode 100644 core/pkg/model/actions/serde_test.go delete mode 100644 core/pkg/model/workflows/event.go delete mode 100644 core/pkg/model/workflows/event_serde.go delete mode 100644 core/pkg/model/workflows/event_test.go diff --git a/core/pkg/model/actions/runs_test.go b/core/pkg/model/actions/runs_test.go deleted file mode 100644 index 7f5d4628..00000000 --- a/core/pkg/model/actions/runs_test.go +++ /dev/null @@ -1,185 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package actions - -import ( - "drassi.run/core/pkg/model" - "drassi.run/core/pkg/model/workflows" - "fmt" - "github.com/go-viper/mapstructure/v2" - "github.com/mitchellh/copystructure" - "github.com/stretchr/testify/assert" - "reflect" - "testing" -) - -func clone[T any](i T) T { - if o, err := copystructure.Copy(i); err != nil { - return i - } else { - return o.(T) - } -} - -type runsTestStruct struct { - Runs Runs `actions:"runs"` - RunsPtr *Runs `actions:"runsPtr"` - ListOfRuns []Runs `actions:"listOfRuns"` - ListOfRunsPtr []*Runs `actions:"listOfRunsPtr"` - MapOfRuns map[string]Runs `actions:"mapOfRuns"` - MapOfRunsPtr map[string]*Runs `actions:"mapOfRunsPtr"` -} - -func TestDecodeRuns(t *testing.T) { - jsInput := map[string]any{ - "using": "node20", - "main": "main.js", - } - dockerInput := map[string]any{ - "using": "docker", - "image": "docker://debian:stretch-slim", - } - compositeInput := map[string]any{ - "using": "composite", - "steps": []map[string]any{}, - } - - t.Run("js", func(tt *testing.T) { - runs := &NodeRuns{ - Using: "node20", - Main: "main.js", - } - testDecodeRuns(tt, jsInput, runs) - }) - t.Run("docker", func(tt *testing.T) { - runs := &DockerRuns{ - Using: "docker", - Image: "docker://debian:stretch-slim", - } - testDecodeRuns(tt, dockerInput, runs) - }) - - t.Run("composite", func(tt *testing.T) { - runs := &CompositeRuns{ - Using: "composite", - Steps: []workflows.Step{}, - } - testDecodeRuns(tt, compositeInput, runs) - }) - - t.Run("conflict/empty", func(tt *testing.T) { - input := map[string]any{} - var runs Runs = &NodeRuns{} - err := model.Decode(input, &runs) - - assert.ErrorContains(tt, err, "`using` is required, and MUST be a string") - }) - - t.Run("conflict/wrong-map-type/1", func(tt *testing.T) { - var runs Runs = &DockerRuns{} - err := model.Decode(jsInput, &runs) - - assert.ErrorContains(tt, err, fmt.Sprintf(`map with using=%q CAN'T be decode to %T`, jsInput["using"], runs)) - }) - - t.Run("conflict/wrong-map-type/2", func(tt *testing.T) { - var runs Runs = &CompositeRuns{} - err := model.Decode(dockerInput, &runs) - - assert.ErrorContains(tt, err, fmt.Sprintf(`map with using=%q CAN'T be decode to %T`, dockerInput["using"], runs)) - }) - - t.Run("conflict/wrong-map-type/3", func(tt *testing.T) { - var runs Runs = &NodeRuns{} - err := model.Decode(compositeInput, &runs) - - assert.ErrorContains(tt, err, fmt.Sprintf(`map with using=%q CAN'T be decode to %T`, compositeInput["using"], runs)) - }) - - t.Run("absent", func(tt *testing.T) { - type runsStruct struct { - Runs Runs `actions:"runs,omitempty"` - ListOfRuns []Runs `actions:"listOfRuns,omitempty"` - MapOfRuns map[string]Runs `actions:"mapOfRuns,omitempty"` - } - runs := runsStruct{} - err := model.Decode(map[string]any{}, &runs) - - assert.NoError(tt, err) - assert.Nil(tt, runs.Runs) - assert.Nil(tt, runs.ListOfRuns) - assert.Nil(tt, runs.MapOfRuns) - }) - - t.Run("nil", func(tt *testing.T) { - type runsStruct struct { - Runs Runs `actions:"runs,omitempty"` - ListOfRuns []Runs `actions:"listOfRuns,omitempty"` - MapOfRuns map[string]Runs `actions:"mapOfRuns,omitempty"` - } - runs := runsStruct{} - err := model.Decode(map[string]any{ - "runs": nil, - "listOfRuns": nil, - "mapOfRuns": nil, - }, &runs) - - assert.NoError(tt, err) - assert.Nil(tt, runs.Runs) - assert.Nil(tt, runs.ListOfRuns) - assert.Nil(tt, runs.MapOfRuns) - }) - - t.Run("invalid-reflection", func(tt *testing.T) { - opt := func(config *mapstructure.DecoderConfig) { - fault := func(from reflect.Value, to reflect.Value) (any, error) { - if !to.Type().Implements(typeRuns) { - return valueOf(from), nil - } - return nil, nil // fault injection - } - // DecodeRunsHook MUST be after fault - config.DecodeHook = mapstructure.ComposeDecodeHookFunc(fault, DecodeRunsHook) - } - - input := map[string]any{ - "using": "docker", - } - runs := new(NodeRuns) - err := model.DecodeWithOptions(input, &runs, opt) - assert.NoError(tt, err) - }) -} - -func testDecodeRuns[T any](tt *testing.T, value T, runs Runs) { - data := map[string]any{ - "runs": clone(value), - "runsPtr": clone(value), - "listOfRuns": []any{clone(value)}, - "mapOfRuns": map[string]any{ - "key": clone(value), - }, - "listOfRunsPtr": []any{clone(value)}, - "mapOfRunsPtr": map[string]any{ - "key": clone(value), - }, - } - - actual := runsTestStruct{} - err := model.Decode(data, &actual) - - expected := runsTestStruct{ - Runs: runs, - RunsPtr: &runs, - ListOfRuns: []Runs{runs}, - ListOfRunsPtr: []*Runs{&runs}, - MapOfRuns: map[string]Runs{"key": runs}, - MapOfRunsPtr: map[string]*Runs{"key": &runs}, - } - assert.NoError(tt, err) - assert.EqualValues(tt, actual, expected) -} diff --git a/core/pkg/model/actions/zz_serde.go b/core/pkg/model/actions/serde.go similarity index 86% rename from core/pkg/model/actions/zz_serde.go rename to core/pkg/model/actions/serde.go index cf033466..324f18b7 100644 --- a/core/pkg/model/actions/zz_serde.go +++ b/core/pkg/model/actions/serde.go @@ -25,17 +25,13 @@ func init() { var unmarshalers []*json.Unmarshalers func JsonUnmarshalers() *json.Unmarshalers { - return json.JoinUnmarshalers(unmarshalers...) + um := append(unmarshalers, workflows.JsonUnmarshalers()) + return json.JoinUnmarshalers(um...) } func Decode[M any](content []byte) (*M, error) { m := new(M) - opt := json.WithUnmarshalers( - json.JoinUnmarshalers( - JsonUnmarshalers(), - workflows.JsonUnmarshalers(), - ), - ) + opt := json.WithUnmarshalers(JsonUnmarshalers()) if err := json.Unmarshal(content, &m, opt); err != nil { return nil, err } diff --git a/core/pkg/model/actions/serde_test.go b/core/pkg/model/actions/serde_test.go new file mode 100644 index 00000000..ace987c0 --- /dev/null +++ b/core/pkg/model/actions/serde_test.go @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package actions + +import ( + "encoding/json/v2" + "testing" + + "drassi.run/core/pkg/model/workflows" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunsSerde(t *testing.T) { + opt := json.WithUnmarshalers(JsonUnmarshalers()) + + t.Run("null", func(t *testing.T) { + var got Action + input := `{"runs":null}` + err := json.Unmarshal([]byte(input), &got, opt) + assert.NoError(t, err) + + assert.Nil(t, got.Runs) + }) + + t.Run("node", func(t *testing.T) { + var got Action + input := `{"runs":{"using":"node20","main":"dist/index.js"}}` + err := json.Unmarshal([]byte(input), &got, opt) + assert.NoError(t, err) + + runs, ok := got.Runs.(*NodeRuns) + require.True(t, ok) + assert.Equal(t, "node20", runs.Using) + assert.Equal(t, "dist/index.js", runs.Main) + }) + + t.Run("docker", func(t *testing.T) { + var got Action + input := `{"runs":{"using":"docker","image":"Dockerfile","args":["--flag"]}}` + err := json.Unmarshal([]byte(input), &got, opt) + assert.NoError(t, err) + + runs, ok := got.Runs.(*DockerRuns) + require.True(t, ok) + assert.Equal(t, "docker", runs.Using) + assert.Equal(t, "Dockerfile", runs.Image) + }) + + t.Run("composite", func(t *testing.T) { + var got Action + input := `{"runs":{"using":"composite","steps":[{"run":"echo hi"},{"uses":"actions/checkout@v4"}]}}` + err := json.Unmarshal([]byte(input), &got, opt) + assert.NoError(t, err) + + runs, ok := got.Runs.(*CompositeRuns) + require.True(t, ok) + assert.Len(t, runs.Steps, 2) + _, isRun := runs.Steps[0].(*workflows.RunActionStep) + _, isUses := runs.Steps[1].(*workflows.UsesActionStep) + assert.True(t, isRun) + assert.True(t, isUses) + }) + + t.Run("unknown", func(t *testing.T) { + var got Action + input := `{"runs":{"using":"python3","main":"main.py"}}` + err := json.Unmarshal([]byte(input), &got, opt) + + assert.ErrorContains(t, err, `unknown runs with using="python3"`) + }) +} diff --git a/core/pkg/model/workflows/container_test.go b/core/pkg/model/workflows/container_test.go index 8a27d4c1..c4ecd2d6 100644 --- a/core/pkg/model/workflows/container_test.go +++ b/core/pkg/model/workflows/container_test.go @@ -7,70 +7,52 @@ package workflows import ( - "drassi.run/core/pkg/model" - "github.com/stretchr/testify/assert" + "encoding/json/jsontext" + "encoding/json/v2" "testing" -) -type containerTestStruct struct { - Con Container `actions:"con"` - ConPtr *Container `actions:"conPtr"` - ListOfCon []Container `actions:"listOfCon"` - MapOfCon map[string]Container `actions:"mapOfCon"` - ListOfConPtr []*Container `actions:"listOfConPtr"` - MapOfConPtr map[string]*Container `actions:"mapOfConPtr"` -} + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) -func TestDecodeContainer(t *testing.T) { - t.Run("string", func(tt *testing.T) { - con := Container{Image: "ubuntu:22.04"} - testDecodeContainer(tt, "ubuntu:22.04", con) - }) +func unmarshal[M any](in string, fn func(M, *testing.T), opts ...jsontext.Options) func(t *testing.T) { + return func(t *testing.T) { + var got M + err := json.Unmarshal([]byte(in), &got, opts...) + require.NoError(t, err) - t.Run("map", func(tt *testing.T) { - con := Container{ - Image: "ubuntu:22.04", - Credentials: &ContainerCredentials{ - Username: "username", - Password: "password", - }, - } - val := map[string]any{ - "image": "ubuntu:22.04", - "credentials": map[string]any{ - "username": "username", - "password": "password", - }, - } - testDecodeContainer(tt, val, con) - }) + fn(got, t) + } } -func testDecodeContainer[C any](tt *testing.T, value C, con Container) { - data := map[string]any{ - "con": value, - "conPtr": value, - "listOfCon": []C{value}, - "mapOfCon": map[string]any{ - "key": value, +func TestContainerSerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(Container, *testing.T) + }{ + "string shorthand": { + input: `"node:22"`, + fn: func(got Container, t *testing.T) { + assert.Equal(t, "node:22", got.Image) + }, }, - "listOfConPtr": []C{value}, - "mapOfConPtr": map[string]any{ - "key": value, + "full-form": { + input: `{"image":"node:22","env":{"NODE_ENV":"test"},"ports":["3000"]}`, + fn: func(got Container, t *testing.T) { + assert.Equal(t, "node:22", got.Image) + assert.EqualValues(t, map[string]string{"NODE_ENV": "test"}, got.Env) + assert.EqualValues(t, []string{"3000"}, got.Ports) + }, }, } - actual := containerTestStruct{} - err := model.Decode(data, &actual) - assert.NoError(tt, err) - - expected := containerTestStruct{ - Con: con, - ConPtr: &con, - ListOfCon: []Container{con}, - ListOfConPtr: []*Container{&con}, - MapOfCon: map[string]Container{"key": con}, - MapOfConPtr: map[string]*Container{"key": &con}, + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) } - assert.EqualValues(tt, actual, expected) + + t.Run("invalid kind", func(t *testing.T) { + var got Container + err := json.Unmarshal([]byte(`true`), &got) + assert.ErrorContains(t, err, "expected string or object for Container") + }) } diff --git a/core/pkg/model/workflows/evaluable.go b/core/pkg/model/workflows/evaluable.go index 502ed157..cb0526fd 100644 --- a/core/pkg/model/workflows/evaluable.go +++ b/core/pkg/model/workflows/evaluable.go @@ -21,7 +21,7 @@ const ( // GitHub Actions always evaluates it as an expression. type Conditional string -type Evaluable[R any] Token +type Evaluable[R any] = Token type Unraveler interface { UnravelLiteral(val any) (any, error) diff --git a/core/pkg/model/workflows/evaluable_test.go b/core/pkg/model/workflows/evaluable_test.go index a0b449be..d254b0e1 100644 --- a/core/pkg/model/workflows/evaluable_test.go +++ b/core/pkg/model/workflows/evaluable_test.go @@ -7,115 +7,105 @@ package workflows import ( - "drassi.run/core/pkg/model" - "github.com/go-viper/mapstructure/v2" - "github.com/google/go-cmp/cmp" - "github.com/mitchellh/copystructure" - "golang.org/x/exp/maps" - "gotest.tools/v3/assert" - "reflect" - "slices" + "encoding/json/v2" "testing" -) -func clone[T any](i T) T { - if o, err := copystructure.Copy(i); err != nil { - return i - } else { - return o.(T) - } -} + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) -func mapValues[E any](m map[string]E) []E { - keys := maps.Keys(m) - slices.Sort(keys) +func TestTokenSerde(t *testing.T) { + opt := json.WithUnmarshalers(json.UnmarshalFromFunc(unmarshalToken)) - l := make([]E, len(keys)) - for i, k := range keys { - l[i] = m[k] + testcases := map[string]struct { + input string + fn func(Token, *testing.T) + }{ + "null": { + input: "null", + fn: func(got Token, t *testing.T) { + assert.Nil(t, got) + }, + }, + "bool": { + input: `true`, + fn: func(got Token, t *testing.T) { + assert.Equal(t, true, literalValue(t, got)) + }, + }, + "number": { + input: `42`, + fn: func(got Token, t *testing.T) { + assert.EqualValues(t, 42, literalValue(t, got)) + }, + }, + "string": { + input: `"plain"`, + fn: func(got Token, t *testing.T) { + assert.Equal(t, "plain", literalValue(t, got)) + }, + }, + "expression": { + input: `"${{ github.ref }}"`, + fn: func(got Token, t *testing.T) { + expr, ok := Expression(got) + assert.True(t, ok) + assert.Equal(t, "${{ github.ref }}", expr) + }, + }, + "array": { + input: `["ubuntu-latest","x64"]`, + fn: func(got Token, t *testing.T) { + seq, ok := got.(sequenceToken) + require.True(t, ok) + assert.Len(t, seq, 2) + assert.Equal(t, "ubuntu-latest", literalValue(t, seq[0])) + assert.Equal(t, "x64", literalValue(t, seq[1])) + }, + }, + "object": { + input: `{"os":"ubuntu-latest","ref":"${{ github.ref }}"}`, + fn: func(got Token, t *testing.T) { + mapping, ok := got.(mappingToken) + require.True(t, ok) + assert.Len(t, mapping, 2) + assert.Equal(t, "os", literalValue(t, mapping[0][0])) + assert.Equal(t, "ubuntu-latest", literalValue(t, mapping[0][1])) + assert.Equal(t, "ref", literalValue(t, mapping[1][0])) + expr, ok := Expression(mapping[1][1]) + assert.True(t, ok) + assert.Equal(t, "${{ github.ref }}", expr) + }, + }, } - return l -} -func mapTokenPairs[K comparable](m map[K]Token, keyConv func(K) Token) [][2]Token { - r := make([][2]Token, 0) - for k, v := range m { - key := keyConv(k) - r = append(r, [2]Token{key, v}) + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn, opt)) } - return r } -func comparerForLiteralToken(opts ...cmp.Option) cmp.Option { - return cmp.Comparer(func(x, y literalToken) bool { - return cmp.Equal(x.value, y.value, opts...) - }) -} +func TestMappingTokenSerde(t *testing.T) { + opt := json.WithUnmarshalers(json.UnmarshalFromFunc(unmarshalToken)) -func TestDecodeEvaluable(t *testing.T) { - t.Run("kind", func(tt *testing.T) { - type testEvaluable struct { - LitBool Evaluable[bool] `actions:"litBool"` - LitInt Evaluable[int64] `actions:"litInt"` - LitFloat Evaluable[float64] `actions:"litFloat"` - LitString Evaluable[string] `actions:"litString"` - Expr Evaluable[string] `actions:"expr"` - Seq Evaluable[[]any] `actions:"seq"` - Dict Evaluable[map[string]any] `actions:"dict"` - } + t.Run("object", unmarshal(`{"key":"value"}`, func(got mappingToken, t *testing.T) { + assert.Len(t, got, 1) + assert.Equal(t, "key", literalValue(t, got[0][0])) + assert.Equal(t, "value", literalValue(t, got[0][1])) + }, opt)) - scala := map[string]any{ - "litBool": true, - "litInt": int64(123), - "litFloat": float64(1.23), - "litString": "hello world", - "expr": "${{ foo.bar }}", - } - input := clone(scala) - input["seq"] = mapValues(scala) - input["dict"] = clone(scala) + t.Run("invalid kind", func(t *testing.T) { + var got mappingToken + err := json.Unmarshal([]byte(`[]`), &got, opt) - scalaExpected := map[string]Token{ - "litBool": NewLiteralToken(true), - "litInt": NewLiteralToken(int64(123)), - "litFloat": NewLiteralToken(float64(1.23)), - "litString": NewLiteralToken("hello world"), - "expr": NewExpressionToken("${{ foo.bar }}"), - } - expected := &testEvaluable{ - LitBool: NewLiteralToken(true), - LitInt: NewLiteralToken(int64(123)), - LitFloat: NewLiteralToken(float64(1.23)), - LitString: NewLiteralToken("hello world"), - Expr: NewExpressionToken("${{ foo.bar }}"), - Seq: NewSequenceToken(mapValues(scalaExpected)), - Dict: NewMappingToken(mapTokenPairs(scalaExpected, func(s string) Token { - return NewLiteralToken(s) - })), - } - - actual := new(testEvaluable) - err := model.Decode(input, actual) - assert.NilError(tt, err) - - opts := comparerForLiteralToken() - assert.DeepEqual(tt, expected.Seq, actual.Seq, opts) + assert.ErrorContains(t, err, "unknown token type") }) +} - t.Run("invalid-reflection", func(tt *testing.T) { - opt := func(config *mapstructure.DecoderConfig) { - fault := func(from reflect.Value, to reflect.Value) (any, error) { - if !to.Type().Implements(typeToken) { - return valueOf(from), nil - } - return nil, nil // fault injection - } - // DecodeTokenHook MUST be after fault - config.DecodeHook = mapstructure.ComposeDecodeHookFunc(fault, DecodeTokenHook) - } - - token := new(literalToken) - err := model.DecodeWithOptions("foobar", &token, opt) - assert.NilError(tt, err) - }) +func literalValue(t *testing.T, token Token) any { + t.Helper() + literal, ok := token.(*literalToken) + if !assert.Truef(t, ok, "got token %T, want *literalToken", token) { + return nil + } + return literal.value } diff --git a/core/pkg/model/workflows/event.go b/core/pkg/model/workflows/event.go deleted file mode 100644 index d4091883..00000000 --- a/core/pkg/model/workflows/event.go +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package workflows - -import "encoding/json/jsontext" - -// The name of the GitHub event that triggers the workflow. -// You can provide a single event string, array of events, array of event types, or an event configuration map -// that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. -// For a list of available events, see https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows. -// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on -type On map[string]Event - -// Event https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows -type Event = jsontext.Value diff --git a/core/pkg/model/workflows/event_serde.go b/core/pkg/model/workflows/event_serde.go deleted file mode 100644 index 0bce09af..00000000 --- a/core/pkg/model/workflows/event_serde.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package workflows - -import ( - "encoding/json/jsontext" - "encoding/json/v2" - "fmt" -) - -func (o *On) UnmarshalJSONFrom(d *jsontext.Decoder) error { - switch k := d.PeekKind(); k { - // 1. Shorthand for single event, e.g: `on: push` - case jsontext.KindString: - if tok, err := d.ReadToken(); err != nil { - return err - } else { - e := tok.String() - *o = map[string]Event{e: nil} - } - return nil - - // 2. Shorthand for multiple events, e.g: `on: [push, fork]` - case jsontext.KindBeginArray: - var events []string - if err := json.UnmarshalDecode(d, &events); err != nil { - return err - } - m := make(map[string]Event, len(events)) - for _, e := range events { - m[e] = nil - } - *o = m - return nil - - // 3. Full object format, e.g: `"on": {"label": {"types": ["created", "edited"]}}` - case jsontext.KindBeginObject: - return json.UnmarshalDecode(d, (*map[string]Event)(o)) - - default: - return fmt.Errorf("expected string, array or object for On, got kind %v", k) - } -} diff --git a/core/pkg/model/workflows/event_test.go b/core/pkg/model/workflows/event_test.go deleted file mode 100644 index 86b587fb..00000000 --- a/core/pkg/model/workflows/event_test.go +++ /dev/null @@ -1,653 +0,0 @@ -/* - * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package workflows - -import ( - "drassi.run/core/pkg/model" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestOn(t *testing.T) { - t.Run("single-event", func(t *testing.T) { - expected := &On{ - Push: &OnPush{}, - } - - input := "push" - o := new(On) - err := model.Decode(input, o) - assert.NoError(t, err) - assert.EqualValues(t, expected, o) - }) - - t.Run("multiple-event", func(t *testing.T) { - expected := &On{ - Push: &OnPush{}, - Fork: &OnFork{}, - } - - input := []any{"push", "fork"} - o := new(On) - err := model.Decode(input, o) - assert.NoError(t, err) - assert.EqualValues(t, expected, o) - }) - - t.Run("activity-type", func(t *testing.T) { - expected := &On{ - Label: &OnLabel{ - Types: []EventLabelActivity{EventLabelActivityCreated}, - }, - Push: &OnPush{ - Branches: []string{"main"}, - }, - // TODO PageBuild is not null - } - - input := map[string]any{ - "label": map[string]any{ - "types": []any{"created"}, - }, - "push": map[string]any{ - "branches": []any{"main"}, - }, - "page_build": nil, - } - o := new(On) - err := model.Decode(input, o) - assert.NoError(t, err) - assert.EqualValues(t, expected, o) - }) -} - -type klass[E any] struct { - Event *E `actions:"event,omitempty"` -} - -func createMockData(types any) map[string]any { - return map[string]any{ - "event": map[string]any{ - "types": types, - }, - } -} - -func testScenario(t *testing.T, check func(tt *testing.T, data map[string]any, value any), value any) { - t.Run("absent", func(tt *testing.T) { - data := map[string]any{ - "event": map[string]any{}, - } - check(tt, data, "default") - }) - - t.Run("nil", func(tt *testing.T) { - data := createMockData(nil) - check(tt, data, "default") - }) - - t.Run("emptyList", func(tt *testing.T) { - data := createMockData([]string{}) - check(tt, data, "empty") - }) - - t.Run("specifyValue", func(tt *testing.T) { - data := createMockData(value) - check(tt, data, value) - }) - - t.Run("err", func(tt *testing.T) { - data := createMockData("a string") - check(tt, data, "error") - }) -} - -func TestOnBranchProtectionRule(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnBranchProtectionRule]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventBranchProtectionRuleActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventBranchProtectionRuleActivity{ - EventBranchProtectionRuleActivityCreate, - }) -} - -func TestOnCheckRun(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnCheckRun]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventCheckRunActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventCheckRunActivity{ - EventCheckRunActivityCreated, - }) -} - -func TestOnCheckSuite(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnCheckSuite]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventCheckSuiteActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventCheckSuiteActivity{ - EventCheckSuiteActivityCompleted, - }) -} - -func TestOnDiscussion(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnDiscussion]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventDiscussionActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventDiscussionActivity{ - EventDiscussionActivityCreated, - }) -} - -func TestOnDiscussionComment(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnDiscussionComment]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventDiscussionCommentActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventDiscussionCommentActivity{ - EventDiscussionCommentActivityCreated, - }) -} - -func TestOnIssueComment(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnIssueComment]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventIssueCommentActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventIssueCommentActivity{ - EventIssueCommentActivityCreated, - }) -} - -func TestOnIssues(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnIssues]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventIssuesActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventIssuesActivity{ - EventIssuesActivityOpened, - }) -} - -func TestOnLabel(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnLabel]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventLabelActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventLabelActivity{ - EventLabelActivityCreated, - }) -} - -func TestOnMergeGroup(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnMergeGroup]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventMergeGroupActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventMergeGroupActivity{ - EventMergeGroupActivityChecksRequested, - }) -} - -func TestOnMilestone(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnMilestone]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventMilestoneActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventMilestoneActivity{ - EventMilestoneActivityCreated, - }) -} - -func TestOnProject(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnProject]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventProjectActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventProjectActivity{ - EventProjectActivityReopened, - }) -} - -func TestOnProjectCard(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnProjectCard]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventProjectCardActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventProjectCardActivity{ - EventProjectCardActivityCreated, - }) -} - -func TestOnProjectColumn(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnProjectColumn]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventProjectColumnActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventProjectColumnActivity{ - EventProjectColumnActivityMoved, - }) -} - -func TestOnPullRequest(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnPullRequest]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventPullRequestActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventPullRequestActivity{ - EventPullRequestActivityOpened, - }) -} - -func TestOnPullRequestReview(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnPullRequestReview]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventPullRequestReviewActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventPullRequestReviewActivity{ - EventPullRequestReviewActivitySubmitted, - }) -} - -func TestOnPullRequestReviewComment(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnPullRequestReviewComment]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventPullRequestReviewCommentActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventPullRequestReviewCommentActivity{ - EventPullRequestReviewCommentActivityCreated, - }) -} - -func TestOnPullRequestTarget(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnPullRequestTarget]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventPullRequestTargetActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventPullRequestTargetActivity{ - EventPullRequestTargetActivityOpened, - }) -} - -func TestOnRegistryPackage(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnRegistryPackage]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventRegistryPackageActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventRegistryPackageActivity{ - EventRegistryPackageActivityPublished, - }) -} - -func TestOnRelease(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnRelease]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventReleaseActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventReleaseActivity{ - EventReleaseActivityPublished, - }) -} - -func TestOnWatch(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnWatch]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventWatchActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventWatchActivity{ - EventWatchActivityStarted, - }) -} - -func TestOnWorkflowRun(t *testing.T) { - var check = func(tt *testing.T, data map[string]any, value any) { - obj := klass[OnWorkflowRun]{} - err := model.Decode(data, &obj) - - if value != "error" { - assert.NoError(tt, err) - } - - switch value { - case "error": - assert.Error(tt, err) - case "default": - assert.EqualValues(tt, obj.Event.defaultActivities(), obj.Event.Types) - case "empty": - assert.EqualValues(tt, []EventWorkflowRunActivity{}, obj.Event.Types) - default: - assert.EqualValues(tt, value, obj.Event.Types) - } - } - - testScenario(t, check, []EventWorkflowRunActivity{ - EventWorkflowRunActivityCompleted, - }) -} diff --git a/core/pkg/model/workflows/job_test.go b/core/pkg/model/workflows/job_test.go index ac6dde64..581219c4 100644 --- a/core/pkg/model/workflows/job_test.go +++ b/core/pkg/model/workflows/job_test.go @@ -7,423 +7,197 @@ package workflows import ( - "drassi.run/core/pkg/model" - "fmt" - "github.com/go-viper/mapstructure/v2" - "gotest.tools/v3/assert" - "reflect" + "encoding/json/v2" "testing" -) -type jobTestStruct struct { - Job Job `actions:"job"` - JobPtr *Job `actions:"jobPtr"` - ListOfJob []Job `actions:"listOfJob"` - ListOfJobPtr []*Job `actions:"listOfJobPtr"` - MapOfJob map[string]Job `actions:"mapOfJob"` - MapOfJobPtr map[string]*Job `actions:"mapOfJobPtr"` -} + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) -func TestDecodeJob(t *testing.T) { - runsOnInput := map[string]any{ - "runs-on": "ubuntu", - } - usesInput := map[string]any{ - "uses": "./path/to/the/workflow.yaml", +func TestJobSerde(t *testing.T) { + opt := json.WithUnmarshalers(JsonUnmarshalers()) + + testcases := map[string]struct { + input string + fn func(Job, *testing.T) + }{ + "null": { + input: `null`, + fn: func(got Job, t *testing.T) { + assert.Nil(t, got) + }, + }, + "normal job": { + input: `{"runs-on":"ubuntu-latest","steps":[{"run":"go test ./..."}]}`, + fn: func(got Job, t *testing.T) { + job, ok := got.(*NormalJob) + require.True(t, ok) + assert.Equal(t, "ubuntu-latest", literalValue(t, job.RunsOn)) + assert.Len(t, job.Steps, 1) + }, + }, + "reusable workflow call job": { + input: `{"uses":"./.github/workflows/deploy.yml"}`, + fn: func(got Job, t *testing.T) { + job, ok := got.(*ReusableWorkflowCallJob) + require.True(t, ok) + assert.Equal(t, "./.github/workflows/deploy.yml", job.Uses) + }, + }, } - t.Run("normal", func(tt *testing.T) { - job := &NormalJob{ - RunsOn: NewLiteralToken("ubuntu"), - } - testDecodeJob(tt, runsOnInput, job) - }) - t.Run("reusableWorkflowCall", func(tt *testing.T) { - job := &ReusableWorkflowCallJob{ - Uses: "./path/to/the/workflow.yaml", - } - testDecodeJob(tt, usesInput, job) - }) - - t.Run("conflict/empty", func(tt *testing.T) { - input := map[string]any{} - var job Job = &NormalJob{} - err := model.Decode(input, &job) - - assert.ErrorContains(tt, err, "map MUST be contains either `runs-on` or `uses`") - }) - - t.Run("conflict/map-contains-both", func(tt *testing.T) { - input := map[string]any{ - "runs-on": "ubuntu", - "uses": "./path/to/the/workflow.yaml", - } - var job Job = &NormalJob{} - err := model.Decode(input, &job) - - assert.ErrorContains(tt, err, "map MUST be contains either `runs-on` or `uses`") - }) - - t.Run("conflict/runs-on-ReusableWorkflowCallJob", func(tt *testing.T) { - var job Job = &ReusableWorkflowCallJob{} - err := model.Decode(runsOnInput, &job) - - assert.ErrorContains(tt, err, fmt.Sprintf("map contains `runs-on` CAN'T be decode to %T", job)) - }) - - t.Run("conflict/uses-to-NormalJob", func(tt *testing.T) { - var job Job = &NormalJob{} - err := model.Decode(usesInput, &job) - - assert.ErrorContains(tt, err, fmt.Sprintf("map contains `uses` CAN'T be decode to %T", job)) - }) - - t.Run("absent", func(tt *testing.T) { - type jobStruct struct { - Job Job `actions:"job,omitempty"` - ListOfJob []Job `actions:"listOfJob,omitempty"` - MapOfJob map[string]Job `actions:"mapOfJob,omitempty"` - } - job := jobStruct{} - err := model.Decode(map[string]any{}, &job) - - assert.NilError(tt, err) - assert.Check(tt, job.Job == nil) - assert.Check(tt, job.ListOfJob == nil) - assert.Check(tt, job.MapOfJob == nil) - }) + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn, opt)) + } - t.Run("nil", func(tt *testing.T) { - type jobStruct struct { - Job Job `actions:"job,omitempty"` - ListOfJob []Job `actions:"listOfJob,omitempty"` - MapOfJob map[string]Job `actions:"mapOfJob,omitempty"` - } - job := jobStruct{} - err := model.Decode(map[string]any{ - "job": nil, - "listOfJob": nil, - "mapOfJob": nil, - }, &job) + t.Run("both runs-on and uses", func(t *testing.T) { + var got Job + err := json.Unmarshal([]byte(`{"runs-on":"ubuntu-latest","uses":"./workflow.yml"}`), &got, opt) - assert.NilError(tt, err) - assert.Check(tt, job.Job == nil) - assert.Check(tt, job.ListOfJob == nil) - assert.Check(tt, job.MapOfJob == nil) + assert.ErrorContains(t, err, "either `runs-on` or `uses`") }) - t.Run("invalid-reflection", func(tt *testing.T) { - opt := func(config *mapstructure.DecoderConfig) { - fault := func(from reflect.Value, to reflect.Value) (any, error) { - if !to.Type().Implements(typeJob) { - return valueOf(from), nil - } - return nil, nil // fault injection - } - // DecodeJobHook MUST be after fault - config.DecodeHook = mapstructure.ComposeDecodeHookFunc(fault, DecodeJobHook) - } + t.Run("missing runs-on and uses", func(t *testing.T) { + var got Job + err := json.Unmarshal([]byte(`{"name":"bad"}`), &got, opt) - input := map[string]any{ - "runs-on": "ubuntu", - } - job := new(NormalJob) - err := model.DecodeWithOptions(input, &job, opt) - assert.NilError(tt, err) + assert.ErrorContains(t, err, "either `runs-on` or `uses`") }) } -func testDecodeJob[T any](tt *testing.T, value T, job Job) { - data := map[string]any{ - "job": clone(value), - "jobPtr": clone(value), - "listOfJob": []any{clone(value)}, - "mapOfJob": map[string]any{ - "key": clone(value), +func TestArraySerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(array, *testing.T) + }{ + "string": { + input: `"build"`, + fn: func(got array, t *testing.T) { + assert.EqualValues(t, array{"build"}, got) + }, }, - "listOfJobPtr": []any{clone(value)}, - "mapOfJobPtr": map[string]any{ - "key": clone(value), + "array": { + input: `["build","test"]`, + fn: func(got array, t *testing.T) { + assert.EqualValues(t, array{"build", "test"}, got) + }, }, } - - actual := jobTestStruct{} - err := model.Decode(data, &actual) - - opts := comparerForLiteralToken() - expected := jobTestStruct{ - Job: job, - JobPtr: &job, - ListOfJob: []Job{job}, - ListOfJobPtr: []*Job{&job}, - MapOfJob: map[string]Job{"key": job}, - MapOfJobPtr: map[string]*Job{"key": &job}, + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected, opts) -} -type jobNeedsTestStruct struct { - JN JobNeeds `actions:"jn"` - JNPtr *JobNeeds `actions:"jnPtr"` - ListOfJN []JobNeeds `actions:"listOfJn"` - MapOfJN map[string]JobNeeds `actions:"mapOfJn"` - ListOfJNPtr []*JobNeeds `actions:"listOfJnPtr"` - MapOfJNPtr map[string]*JobNeeds `actions:"mapOfJnPtr"` -} + t.Run("invalid kind", func(t *testing.T) { + var got array + err := json.Unmarshal([]byte(`true`), &got) -func TestDecodeJobNeeds(t *testing.T) { - t.Run("single", func(tt *testing.T) { - jn := JobNeeds{"single"} - testDecodeJobNeeds(tt, "single", jn) - }) - t.Run("multi", func(tt *testing.T) { - jn := JobNeeds{"first", "second", "third"} - testDecodeJobNeeds(tt, []string{"first", "second", "third"}, jn) + assert.ErrorContains(t, err, "expected string or array") }) } -func testDecodeJobNeeds[T any](tt *testing.T, value T, jn JobNeeds) { - data := map[string]any{ - "jn": clone(value), - "jnPtr": clone(value), - "listOfJn": []any{clone(value)}, - "mapOfJn": map[string]any{ - "key": clone(value), +func TestJobSecretsSerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(JobSecrets, *testing.T) + }{ + "inherit": { + input: `"inherit"`, + fn: func(got JobSecrets, t *testing.T) { + assert.True(t, got.Inherit) + assert.Nil(t, got.Secrets) + }, }, - "listOfJnPtr": []any{clone(value)}, - "mapOfJnPtr": map[string]any{ - "key": clone(value), + "object": { + input: `{"MESSAGE":"Hello world"}`, + fn: func(got JobSecrets, t *testing.T) { + assert.False(t, got.Inherit) + assert.EqualValues(t, map[string]string{"MESSAGE": "Hello world"}, got.Secrets) + }, }, } - - actual := jobNeedsTestStruct{} - err := model.Decode(data, &actual) - - expected := jobNeedsTestStruct{ - JN: jn, - JNPtr: &jn, - ListOfJN: []JobNeeds{jn}, - MapOfJN: map[string]JobNeeds{"key": jn}, - ListOfJNPtr: []*JobNeeds{&jn}, - MapOfJNPtr: map[string]*JobNeeds{"key": &jn}, + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected) -} -type jobSecretTestStruct struct { - JS JobSecrets `actions:"js"` - JSPtr *JobSecrets `actions:"jsPtr"` - ListOfJS []JobSecrets `actions:"listOfJs"` - MapOfJS map[string]JobSecrets `actions:"mapOfJs"` - ListOfJSPtr []*JobSecrets `actions:"listOfJsPtr"` - MapOfJSPtr map[string]*JobSecrets `actions:"mapOfJsPtr"` -} + t.Run("unexpected string", func(t *testing.T) { + var got JobSecrets + err := json.Unmarshal([]byte(`"all"`), &got) -func TestDecodeJobSecrets(t *testing.T) { - t.Run("inherit", func(tt *testing.T) { - js := JobSecrets{ - Inherit: true, - } - testDecodeJobSecrets(tt, "inherit", js) + assert.ErrorContains(t, err, "unexpected JobSecrets=all") }) - t.Run("map/string", func(tt *testing.T) { - js := JobSecrets{ - Secrets: map[string]string{ - "first": "foobar", - "second": "abcyxz", - }, - } - v := map[string]string{ - "first": "foobar", - "second": "abcyxz", - } - testDecodeJobSecrets(tt, v, js) - }) - t.Run("map/any", func(tt *testing.T) { - js := JobSecrets{ - Secrets: map[string]string{ - "first": "foobar", - "second": "abcyxz", - }, - } - v := map[string]any{ - "first": "foobar", - "second": "abcyxz", - } - testDecodeJobSecrets(tt, v, js) + t.Run("invalid kind", func(t *testing.T) { + var got JobSecrets + err := json.Unmarshal([]byte(`[]`), &got) + + assert.ErrorContains(t, err, "expected object for JobSecrets") }) } -func testDecodeJobSecrets[T any](tt *testing.T, value T, js JobSecrets) { - data := map[string]any{ - "js": clone(value), - "jsPtr": clone(value), - "listOfJs": []any{clone(value)}, - "mapOfJs": map[string]any{ - "key": clone(value), +func TestEnvironmentSerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(Environment, *testing.T) + }{ + "string shorthand": { + input: `"production"`, + fn: func(got Environment, t *testing.T) { + assert.Equal(t, "production", got.Name) + }, }, - "listOfJsPtr": []any{clone(value)}, - "mapOfJsPtr": map[string]any{ - "key": clone(value), + "object": { + input: `{"name":"production","url":"https://example.com"}`, + fn: func(got Environment, t *testing.T) { + assert.Equal(t, "production", got.Name) + assert.Equal(t, "https://example.com", got.Url) + }, }, } - - actual := jobSecretTestStruct{} - err := model.Decode(data, &actual) - - expected := jobSecretTestStruct{ - JS: js, - JSPtr: &js, - ListOfJS: []JobSecrets{js}, - MapOfJS: map[string]JobSecrets{"key": js}, - ListOfJSPtr: []*JobSecrets{&js}, - MapOfJSPtr: map[string]*JobSecrets{"key": &js}, + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected) -} -type environmentTestStruct struct { - Environment Environment `actions:"environment"` - EnvironmentPtr *Environment `actions:"environmentPtr"` - ListOfEnvironment []Environment `actions:"listOfEnvironment"` - MapOfEnvironment map[string]Environment `actions:"mapOfEnvironment"` - ListOfEnvironmentPtr []*Environment `actions:"listOfEnvironmentPtr"` - MapOfEnvironmentPtr map[string]*Environment `actions:"mapOfEnvironmentPtr"` -} + t.Run("invalid kind", func(t *testing.T) { + var got Environment + err := json.Unmarshal([]byte(`42`), &got) -func TestDecodeEnvironment(t *testing.T) { - t.Run("string", func(tt *testing.T) { - env := Environment{ - Name: "name1", - } - testDecodeEnvironment(tt, "name1", env) - }) - - t.Run("map", func(tt *testing.T) { - env := Environment{ - Name: "name1", - Url: "https://example.com", - } - v := map[string]any{ - "name": "name1", - "url": "https://example.com", - } - testDecodeEnvironment(tt, v, env) + assert.ErrorContains(t, err, "expected string or object for Environment") }) } -func testDecodeEnvironment[T any](tt *testing.T, value T, env Environment) { - data := map[string]any{ - "environment": clone(value), - "environmentPtr": clone(value), - "listOfEnvironment": []any{clone(value)}, - "mapOfEnvironment": map[string]any{ - "key": clone(value), +func TestRunsOnSerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(RunsOn, *testing.T) + }{ + "string shorthand": { + input: `"ubuntu-latest"`, + fn: func(got RunsOn, t *testing.T) { + assert.EqualValues(t, array{"ubuntu-latest"}, got.Labels) + }, }, - "listOfEnvironmentPtr": []any{clone(value)}, - "mapOfEnvironmentPtr": map[string]any{ - "key": clone(value), + "array shorthand": { + input: `["self-hosted","x64"]`, + fn: func(got RunsOn, t *testing.T) { + assert.EqualValues(t, array{"self-hosted", "x64"}, got.Labels) + }, + }, + "object": { + input: `{"group":"linux","labels":["self-hosted","x64"]}`, + fn: func(got RunsOn, t *testing.T) { + assert.Equal(t, "linux", got.Group) + assert.EqualValues(t, array{"self-hosted", "x64"}, got.Labels) + }, }, } - - actual := environmentTestStruct{} - err := model.Decode(data, &actual) - - expected := environmentTestStruct{ - Environment: env, - EnvironmentPtr: &env, - ListOfEnvironment: []Environment{env}, - MapOfEnvironment: map[string]Environment{"key": env}, - ListOfEnvironmentPtr: []*Environment{&env}, - MapOfEnvironmentPtr: map[string]*Environment{"key": &env}, + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected) -} - -type runOnTestStruct struct { - RO RunsOn `actions:"ro"` - ROPtr *RunsOn `actions:"roPtr"` - ListOfRO []RunsOn `actions:"listOfRo"` - MapOfRO map[string]RunsOn `actions:"mapOfRo"` - ListOfROPtr []*RunsOn `actions:"listOfRoPtr"` - MapOfROPtr map[string]*RunsOn `actions:"mapOfRoPtr"` -} - -func TestDecodeRunsOn(t *testing.T) { - t.Run("string", func(tt *testing.T) { - ro := RunsOn{ - Labels: []string{"label1"}, - } - testDecodeRunsOn(tt, "label1", ro) - }) - t.Run("list/string", func(tt *testing.T) { - ro := RunsOn{ - Labels: []string{"label1", "label2"}, - } - testDecodeRunsOn(tt, []string{"label1", "label2"}, ro) - }) + t.Run("invalid kind", func(t *testing.T) { + var got RunsOn + err := json.Unmarshal([]byte(`true`), &got) - t.Run("list/any", func(tt *testing.T) { - ro := RunsOn{ - Labels: []string{"label1", "label2"}, - } - testDecodeRunsOn(tt, []any{"label1", "label2"}, ro) + assert.ErrorContains(t, err, "expected string, array or object for RunsOn") }) - - t.Run("map/single_label", func(tt *testing.T) { - ro := RunsOn{ - Labels: []string{"label1"}, - } - v := map[string]any{ - "labels": "label1", - } - testDecodeRunsOn(tt, v, ro) - }) - - t.Run("map/list_labels", func(tt *testing.T) { - ro := RunsOn{ - Group: "foobar", - Labels: []string{"label1", "label1"}, - } - v := map[string]any{ - "group": "foobar", - "labels": []string{"label1", "label1"}, - } - testDecodeRunsOn(tt, v, ro) - }) -} - -func testDecodeRunsOn[T any](tt *testing.T, value T, ro RunsOn) { - data := map[string]any{ - "ro": clone(value), - "roPtr": clone(value), - "listOfRo": []any{clone(value)}, - "mapOfRo": map[string]any{ - "key": clone(value), - }, - "listOfRoPtr": []any{clone(value)}, - "mapOfRoPtr": map[string]any{ - "key": clone(value), - }, - } - - actual := runOnTestStruct{} - err := model.Decode(data, &actual) - - expected := runOnTestStruct{ - RO: ro, - ROPtr: &ro, - ListOfRO: []RunsOn{ro}, - MapOfRO: map[string]RunsOn{"key": ro}, - ListOfROPtr: []*RunsOn{&ro}, - MapOfROPtr: map[string]*RunsOn{"key": &ro}, - } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected) } diff --git a/core/pkg/model/workflows/step_test.go b/core/pkg/model/workflows/step_test.go index 5eb8ae4c..8b61fc35 100644 --- a/core/pkg/model/workflows/step_test.go +++ b/core/pkg/model/workflows/step_test.go @@ -7,159 +7,65 @@ package workflows import ( - "fmt" - "reflect" + "encoding/json/v2" "testing" "drassi.run/core/pkg/model" - "github.com/go-viper/mapstructure/v2" - "gotest.tools/v3/assert" + "github.com/stretchr/testify/assert" ) -type stepTestStruct struct { - Step Step `actions:"step"` - StepPtr *Step `actions:"stepPtr"` - ListOfStep []Step `actions:"listOfStep"` - ListOfStepPtr []*Step `actions:"listOfStepPtr"` - MapOfStep map[string]Step `actions:"mapOfStep"` - MapOfStepPtr map[string]*Step `actions:"mapOfStepPtr"` -} - -func TestDecodeStep(t *testing.T) { - runInput := map[string]any{ - "run": "echo hello world", +func TestStepSerde(t *testing.T) { + opt := json.WithUnmarshalers( + json.JoinUnmarshalers( + model.UnmarshalInterface(discriminateStep), + json.UnmarshalFromFunc(unmarshalToken), + ), + ) + + testcases := map[string]struct { + input string + fn func(Step, *testing.T) + }{ + "null": { + input: `null`, + fn: func(got Step, t *testing.T) { + assert.Nil(t, got) + }, + }, + "run step": { + input: `{"name":"test","run":"go test ./..."}`, + fn: func(got Step, t *testing.T) { + step, ok := got.(*RunActionStep) + assert.True(t, ok) + assert.Equal(t, "go test ./...", literalValue(t, step.Run)) + assert.Equal(t, "test", literalValue(t, step.Name)) + }, + }, + "uses step": { + input: `{"name":"checkout","uses":"actions/checkout@v4"}`, + fn: func(got Step, t *testing.T) { + step, ok := got.(*UsesActionStep) + assert.True(t, ok) + assert.Equal(t, "actions/checkout@v4", step.Uses) + assert.Equal(t, "checkout", literalValue(t, step.Name)) + }, + }, } - usesInput := map[string]any{ - "uses": "action/pass@v0", + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn, opt)) } - t.Run("run", func(tt *testing.T) { - step := &RunActionStep{ - Run: NewLiteralToken("echo hello world"), - } - testDecodeStep(tt, runInput, step) - }) - t.Run("uses", func(tt *testing.T) { - step := &UsesActionStep{ - Uses: "action/pass@v0", - } - testDecodeStep(tt, usesInput, step) - }) - - t.Run("conflict/empty", func(tt *testing.T) { - input := map[string]any{} - var step Step = &RunActionStep{} - err := model.Decode(input, &step) + t.Run("both run and uses", func(t *testing.T) { + var got []Step + err := json.Unmarshal([]byte(`[{"run":"go test ./...","uses":"actions/checkout@v4"}]`), &got, opt) - assert.ErrorContains(tt, err, "map MUST be contains either `run` or `uses`") + assert.ErrorContains(t, err, "either `run` or `uses`") }) - t.Run("conflict/map-contains-both", func(tt *testing.T) { - input := map[string]any{ - "run": "echo hello world", - "uses": "action/pass@v0", - } - var step Step = &RunActionStep{} - err := model.Decode(input, &step) + t.Run("missing run and uses", func(t *testing.T) { + var got []Step + err := json.Unmarshal([]byte(`[{"name":"missing"}]`), &got, opt) - assert.ErrorContains(tt, err, "map MUST be contains either `run` or `uses`") + assert.ErrorContains(t, err, "either `run` or `uses`") }) - - t.Run("conflict/run-to-UsesStep", func(tt *testing.T) { - var step Step = &UsesActionStep{} - err := model.Decode(runInput, &step) - - assert.ErrorContains(tt, err, fmt.Sprintf("map contains `run` CAN'T be decode to %T", step)) - }) - - t.Run("conflict/uses-to-RunStep", func(tt *testing.T) { - var step Step = &RunActionStep{} - err := model.Decode(usesInput, &step) - - assert.ErrorContains(tt, err, fmt.Sprintf("map contains `uses` CAN'T be decode to %T", step)) - }) - - t.Run("absent", func(tt *testing.T) { - type stepStruct struct { - Step Step `actions:"step,omitempty"` - ListOfStep []Step `actions:"listOfStep,omitempty"` - MapOfStep map[string]Step `actions:"mapOfStep,omitempty"` - } - step := stepStruct{} - err := model.Decode(map[string]any{}, &step) - - assert.NilError(tt, err) - assert.Check(tt, step.Step == nil) - assert.Check(tt, step.ListOfStep == nil) - assert.Check(tt, step.MapOfStep == nil) - }) - - t.Run("nil", func(tt *testing.T) { - type stepStruct struct { - Step Step `actions:"step,omitempty"` - ListOfStep []Step `actions:"listOfStep,omitempty"` - MapOfStep map[string]Step `actions:"mapOfStep,omitempty"` - } - step := stepStruct{} - err := model.Decode(map[string]any{ - "step": nil, - "listOfStep": nil, - "mapOfStep": nil, - }, &step) - - assert.NilError(tt, err) - assert.Check(tt, step.Step == nil) - assert.Check(tt, step.ListOfStep == nil) - assert.Check(tt, step.MapOfStep == nil) - }) - - t.Run("invalid-reflection", func(tt *testing.T) { - opt := func(config *mapstructure.DecoderConfig) { - fault := func(from reflect.Value, to reflect.Value) (any, error) { - if !to.Type().Implements(typeStep) { - return valueOf(from), nil - } - return nil, nil // fault injection - } - // DecodeStepHook MUST be after fault - config.DecodeHook = mapstructure.ComposeDecodeHookFunc(fault, DecodeStepHook) - } - - input := map[string]any{ - "run": "true", - } - step := new(RunActionStep) - err := model.DecodeWithOptions(input, &step, opt) - assert.NilError(tt, err) - }) -} - -func testDecodeStep[T any](tt *testing.T, value T, step Step) { - data := map[string]any{ - "step": clone(value), - "stepPtr": clone(value), - "listOfStep": []any{clone(value)}, - "mapOfStep": map[string]any{ - "key": clone(value), - }, - "listOfStepPtr": []any{clone(value)}, - "mapOfStepPtr": map[string]any{ - "key": clone(value), - }, - } - - actual := stepTestStruct{} - err := model.Decode(data, &actual) - - opts := comparerForLiteralToken() - expected := stepTestStruct{ - Step: step, - StepPtr: &step, - ListOfStep: []Step{step}, - ListOfStepPtr: []*Step{&step}, - MapOfStep: map[string]Step{"key": step}, - MapOfStepPtr: map[string]*Step{"key": &step}, - } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected, opts) } diff --git a/core/pkg/model/workflows/workflow.go b/core/pkg/model/workflows/workflow.go index 456ad89c..25d4fc2f 100644 --- a/core/pkg/model/workflows/workflow.go +++ b/core/pkg/model/workflows/workflow.go @@ -6,6 +6,8 @@ package workflows +import "encoding/json/jsontext" + // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions type Workflow struct { // The name of your workflow. GitHub displays the names of your workflows on your repository's actions page. @@ -68,6 +70,16 @@ type Workflow struct { Jobs map[string]Job `json:"jobs,omitempty"` } +// The name of the GitHub event that triggers the workflow. +// You can provide a single event string, array of events, array of event types, or an event configuration map +// that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. +// For a list of available events, see https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows. +// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on +type On map[string]Event + +// Event https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows +type Event = jsontext.Value + // You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, // so that you only allow the minimum required access. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions diff --git a/core/pkg/model/workflows/workflow_serde.go b/core/pkg/model/workflows/workflow_serde.go index 50d2ba8a..68cafc7c 100644 --- a/core/pkg/model/workflows/workflow_serde.go +++ b/core/pkg/model/workflows/workflow_serde.go @@ -12,6 +12,40 @@ import ( "fmt" ) +func (o *On) UnmarshalJSONFrom(d *jsontext.Decoder) error { + switch k := d.PeekKind(); k { + // 1. Shorthand for single event, e.g: `on: push` + case jsontext.KindString: + if tok, err := d.ReadToken(); err != nil { + return err + } else { + e := tok.String() + *o = map[string]Event{e: nil} + } + return nil + + // 2. Shorthand for multiple events, e.g: `on: [push, fork]` + case jsontext.KindBeginArray: + var events []string + if err := json.UnmarshalDecode(d, &events); err != nil { + return err + } + m := make(map[string]Event, len(events)) + for _, e := range events { + m[e] = nil + } + *o = m + return nil + + // 3. Full object format, e.g: `"on": {"label": {"types": ["created", "edited"]}}` + case jsontext.KindBeginObject: + return json.UnmarshalDecode(d, (*map[string]Event)(o)) + + default: + return fmt.Errorf("expected string, array or object for On, got kind %v", k) + } +} + func (p *Permissions) UnmarshalJSONFrom(d *jsontext.Decoder) error { switch k := d.PeekKind(); k { // 1. Shorthand string format: read-all | write-all diff --git a/core/pkg/model/workflows/workflow_test.go b/core/pkg/model/workflows/workflow_test.go index 5c784eba..123eaec7 100644 --- a/core/pkg/model/workflows/workflow_test.go +++ b/core/pkg/model/workflows/workflow_test.go @@ -7,157 +7,132 @@ package workflows import ( - "drassi.run/core/pkg/model" - "gotest.tools/v3/assert" + "encoding/json/v2" "testing" -) - -type concurrencyTestStruct struct { - Con Concurrency `actions:"con"` - ConPtr *Concurrency `actions:"conPtr"` - ListOfCon []Concurrency `actions:"listOfCon"` - MapOfCon map[string]Concurrency `actions:"mapOfCon"` - ListOfConPtr []*Concurrency `actions:"listOfConPtr"` - MapOfConPtr map[string]*Concurrency `actions:"mapOfConPtr"` -} -func TestDecodeConcurrency(t *testing.T) { - t.Run("string", func(tt *testing.T) { - c := Concurrency{ - Group: NewLiteralToken("group1"), - } - testDecodeConcurrency(tt, "group1", c) - }) + "github.com/stretchr/testify/assert" +) - t.Run("expr", func(tt *testing.T) { - c := Concurrency{ - Group: NewExpressionToken("${{ foobar }}"), - } - testDecodeConcurrency(tt, "${{ foobar }}", c) - }) +func TestOnSerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(On, *testing.T) + }{ + "string shorthand": { + input: `"push"`, + fn: func(got On, t *testing.T) { + _, ok := got["push"] + assert.True(t, ok) + assert.Len(t, got, 1) + }, + }, + "array shorthand": { + input: `["push","pull_request"]`, + fn: func(got On, t *testing.T) { + _, hasPush := got["push"] + _, hasPullRequest := got["pull_request"] + assert.True(t, hasPush) + assert.True(t, hasPullRequest) + assert.Len(t, got, 2) + }, + }, + "object": { + input: `{"pull_request":{"types":["opened"]}}`, + fn: func(got On, t *testing.T) { + assert.Len(t, got, 1) + assert.JSONEq(t, `{"types":["opened"]}`, string(got["pull_request"])) + }, + }, + } + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) + } - t.Run("map/group-expr", func(tt *testing.T) { - c := Concurrency{ - Group: NewExpressionToken("${{ foobar }}"), - CancelInProgress: true, - } - input := map[string]any{ - "group": "${{ foobar }}", - "cancel-in-progress": true, - } - testDecodeConcurrency(tt, input, c) - }) - t.Run("map/group-string", func(tt *testing.T) { - c := Concurrency{ - Group: NewLiteralToken("group1"), - CancelInProgress: true, - } - input := map[string]any{ - "group": "group1", - "cancel-in-progress": true, - } - testDecodeConcurrency(tt, input, c) + t.Run("invalid kind", func(t *testing.T) { + var got On + input := `true` + err := json.Unmarshal([]byte(input), &got) + assert.ErrorContains(t, err, "expected string, array or object for On") }) } -func testDecodeConcurrency(tt *testing.T, value any, con Concurrency) { - data := map[string]any{ - "con": value, - "conPtr": value, - "listOfCon": []any{value}, - "mapOfCon": map[string]any{ - "key": value, +func TestPermissionsSerde(t *testing.T) { + testcases := map[string]struct { + input string + fn func(Permissions, *testing.T) + }{ + "read-all shorthand": { + input: `"read-all"`, + fn: func(got Permissions, t *testing.T) { + assert.Equal(t, Permissions{"*": PermissionsLevelRead}, got) + }, + }, + "write-all shorthand": { + input: `"write-all"`, + fn: func(got Permissions, t *testing.T) { + assert.Equal(t, Permissions{"*": PermissionsLevelWrite}, got) + }, }, - "listOfConPtr": []any{value}, - "mapOfConPtr": map[string]any{ - "key": value, + "object": { + input: `{"actions":"read","contents":"write","issues":"none"}`, + fn: func(got Permissions, t *testing.T) { + assert.Equal(t, Permissions{ + "actions": PermissionsLevelRead, + "contents": PermissionsLevelWrite, + "issues": PermissionsLevelNone, + }, got) + }, }, } - - actual := concurrencyTestStruct{} - err := model.Decode(data, &actual) - - opts := comparerForLiteralToken() - expected := concurrencyTestStruct{ - Con: con, - ConPtr: &con, - ListOfCon: []Concurrency{con}, - ListOfConPtr: []*Concurrency{&con}, - MapOfCon: map[string]Concurrency{"key": con}, - MapOfConPtr: map[string]*Concurrency{"key": &con}, + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn)) } - assert.NilError(tt, err) - assert.DeepEqual(tt, actual, expected, opts) -} -type permissionTestStruct struct { - Perm Permissions `actions:"perm"` -} + t.Run("unknown shorthand", func(t *testing.T) { + var got Permissions + err := json.Unmarshal([]byte(`"admin-all"`), &got) -func TestDecodePermissions(t *testing.T) { - t.Run("read-all", func(tt *testing.T) { - p := Permissions{ - Actions: PermissionsLevelRead, - Checks: PermissionsLevelRead, - Contents: PermissionsLevelRead, - Deployments: PermissionsLevelRead, - Discussions: PermissionsLevelRead, - IdToken: PermissionsLevelRead, - Issues: PermissionsLevelRead, - Packages: PermissionsLevelRead, - Pages: PermissionsLevelRead, - PullRequests: PermissionsLevelRead, - RepositoryProjects: PermissionsLevelRead, - SecurityEvents: PermissionsLevelRead, - Statuses: PermissionsLevelRead, - } - testDecodePermissions(tt, "read-all", p) + assert.ErrorContains(t, err, "unknown permission admin-all") }) - t.Run("write-all", func(tt *testing.T) { - p := Permissions{ - Actions: PermissionsLevelWrite, - Checks: PermissionsLevelWrite, - Contents: PermissionsLevelWrite, - Deployments: PermissionsLevelWrite, - Discussions: PermissionsLevelWrite, - IdToken: PermissionsLevelWrite, - Issues: PermissionsLevelWrite, - Packages: PermissionsLevelWrite, - Pages: PermissionsLevelWrite, - PullRequests: PermissionsLevelWrite, - RepositoryProjects: PermissionsLevelWrite, - SecurityEvents: PermissionsLevelWrite, - Statuses: PermissionsLevelWrite, - } - testDecodePermissions(tt, "write-all", p) - }) + t.Run("invalid kind", func(t *testing.T) { + var got Permissions + err := json.Unmarshal([]byte(`true`), &got) - t.Run("specify", func(tt *testing.T) { - p := Permissions{ - Actions: PermissionsLevelWrite, - IdToken: PermissionsLevelRead, - Issues: PermissionsLevelNone, - Statuses: PermissionsLevelWrite, - } - d := map[string]any{ - "actions": PermissionsLevelWrite, - "id-token": PermissionsLevelRead, - "issues": PermissionsLevelNone, - "statuses": PermissionsLevelWrite, - } - testDecodePermissions(tt, d, p) + assert.ErrorContains(t, err, "expected string or object for Permission") }) } -func testDecodePermissions(tt *testing.T, value any, perm Permissions) { - data := map[string]any{ - "perm": value, +func TestConcurrencySerde(t *testing.T) { + opt := json.WithUnmarshalers(json.UnmarshalFromFunc(unmarshalToken)) + + testcases := map[string]struct { + input string + fn func(Concurrency, *testing.T) + }{ + "string shorthand": { + input: `"ci-main"`, + fn: func(got Concurrency, t *testing.T) { + assert.Equal(t, "ci-main", literalValue(t, got.Group)) + assert.False(t, got.CancelInProgress) + }, + }, + "object": { + input: `{"group":"ci-main","cancel-in-progress":true}`, + fn: func(got Concurrency, t *testing.T) { + assert.Equal(t, "ci-main", literalValue(t, got.Group)) + assert.True(t, got.CancelInProgress) + }, + }, + } + for name, tc := range testcases { + t.Run(name, unmarshal(tc.input, tc.fn, opt)) } - obj := permissionTestStruct{} - err := model.Decode(data, &obj) + t.Run("invalid kind", func(t *testing.T) { + var got Concurrency + err := json.Unmarshal([]byte(`42`), &got, opt) - assert.NilError(tt, err) - assert.Equal(tt, obj.Perm, perm) + assert.ErrorContains(t, err, "expected string or object for Environment") + }) }