mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-06 17:40:58 +00:00
<!--start release-notes-assistant--> <!--URL:https://code.forgejo.org/forgejo/runner--> - features - [PR](https://code.forgejo.org/forgejo/runner/pulls/757): <!--number 757 --><!--line 0 --><!--description ZmVhdDogdGhlIG5ldyBgZm9yZ2Vqby1ydW5uZXIgdmFsaWRhdGVgIGNvbW1hbmQgY2FuIGJlIHVzZWQgdG8gdmVyaWZ5IGlmIGFuIGFjdGlvbiBvciBhIHdvcmtmbG93IGlzIGNvbmZvcm1hbnQgd2l0aCB0aGUgZXhwZWN0ZWQgc2NoZW1hLiBgZm9yZ2Vqby1ydW5uZXIgdmFsaWRhdGUgLS1yZXBvc2l0b3J5IGh0dHBzOi8vZXhhbXBsZS5jb20vbXkvcmVwb3NpdG9yeWAgd2lsbCB2YWxpZGF0ZSBhbGwgdGhlIHdvcmtmbG93cyBhbmQgYWN0aW9ucyBhIEdpdCByZXBvc2l0b3J5IGNvbnRhaW5zLiBBbHRlcm5hdGl2ZWx5ICBgZm9yZ2Vqby1ydW5uZXIgdmFsaWRhdGUgLS1wYXRoIG15YWN0aW9uL2FjdGlvbi55bWwgLS1hY3Rpb25gIG9yIGBmb3JnZWpvLXJ1bm5lciB2YWxpZGF0ZSAtLXBhdGggLmZvcmdlam8vd29ya2Zsb3dzL3Rlc3QueW1sIC0td29ya2Zsb3dgIGNhbiBiZSB1c2VkIHRvIHZhbGlkYXRlIGEgc2luZ2xlIGZpbGUuIEl0IGlzIHJlY29tbWVuZGVkIHRvIHVzZSB0aGVzZSBjb21tYW5kcyB0byB2ZXJpZnkgZXhpc3RpbmcgYWN0aW9ucyBhbmQgd29ya2Zsb3dzIHBhc3MgYmVmb3JlIHVwZ3JhZGluZyB0byBbRm9yZ2VqbyBydW5uZXIgdjguMC4wXShodHRwczovL2NvZGUuZm9yZ2Vqby5vcmcvZm9yZ2Vqby9ydW5uZXIvc3JjL2JyYW5jaC9tYWluL1JFTEVBU0UtTk9URVMubWQjOC0wLTApIG9yIGFib3ZlIHRvIG5vdCBkaXNydXB0IGV4aXN0aW5nIHdvcmtmbG93cy4=-->feat: the new `forgejo-runner validate` command can be used to verify if an action or a workflow is conformant with the expected schema. `forgejo-runner validate --repository https://example.com/my/repository` will validate all the workflows and actions a Git repository contains. Alternatively `forgejo-runner validate --path myaction/action.yml --action` or `forgejo-runner validate --path .forgejo/workflows/test.yml --workflow` can be used to validate a single file. It is recommended to use these commands to verify existing actions and workflows pass before upgrading to [Forgejo runner v8.0.0](https://code.forgejo.org/forgejo/runner/src/branch/main/RELEASE-NOTES.md#8-0-0) or above to not disrupt existing workflows.<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/757 Reviewed-by: Michael Kriese <michael.kriese@gmx.de> Co-authored-by: Earl Warren <contact@earl-warren.org> Co-committed-by: Earl Warren <contact@earl-warren.org>
193 lines
5 KiB
Go
193 lines
5 KiB
Go
// Copyright 2025 The Forgejo Authors
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"code.forgejo.org/forgejo/runner/act/model"
|
|
"code.forgejo.org/forgejo/runner/testutils"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type validateArgs struct {
|
|
path string
|
|
repository string
|
|
clonedir string
|
|
workflow bool
|
|
action bool
|
|
}
|
|
|
|
func validate(dir, path string, isWorkflow, isAction bool) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %v", path, err)
|
|
}
|
|
defer func() { f.Close() }()
|
|
|
|
if isWorkflow {
|
|
_, err = model.ReadWorkflow(f, true)
|
|
} else if isAction {
|
|
_, err = model.ReadAction(f)
|
|
}
|
|
|
|
if len(dir) > 0 {
|
|
dir += "/"
|
|
}
|
|
shortPath := strings.TrimPrefix(path, dir)
|
|
kind := "workflow"
|
|
if isAction {
|
|
kind = "action"
|
|
}
|
|
if err != nil {
|
|
fmt.Printf("%s %s schema validation failed:\n%s\n", shortPath, kind, err.Error())
|
|
} else {
|
|
fmt.Printf("%s %s schema validation OK\n", shortPath, kind)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validatePath(validateArgs *validateArgs) error {
|
|
if !validateArgs.workflow && !validateArgs.action {
|
|
return errors.New("one of --workflow or --action must be set")
|
|
}
|
|
return validate("", validateArgs.path, validateArgs.workflow, validateArgs.action)
|
|
}
|
|
|
|
func validateHasYamlSuffix(s, suffix string) bool {
|
|
return strings.HasSuffix(s, suffix+".yml") || strings.HasSuffix(s, suffix+".yaml")
|
|
}
|
|
|
|
func validateRepository(validateArgs *validateArgs) error {
|
|
clonedir := validateArgs.clonedir
|
|
if len(clonedir) == 0 {
|
|
tmpdir, err := os.MkdirTemp("", "runner-validate")
|
|
if err != nil {
|
|
return fmt.Errorf("MkdirTemp: %v", err)
|
|
}
|
|
clonedir = filepath.Join(tmpdir, "clonedir")
|
|
defer os.RemoveAll(tmpdir)
|
|
}
|
|
|
|
exists, err := testutils.FileExists(clonedir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
git := "git"
|
|
args := []string{"clone", "--depth=1", validateArgs.repository, clonedir}
|
|
cmd := exec.Command(git, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s %s: %s", git, args, output)
|
|
return err
|
|
}
|
|
for _, dir := range []string{".git", ".github", ".gitea"} {
|
|
exists, err := testutils.FileExists(clonedir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
if err := os.RemoveAll(filepath.Join(clonedir, dir)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := filepath.Walk(clonedir, func(path string, fi fs.FileInfo, err error) error {
|
|
if validateHasYamlSuffix(path, "/.forgejo/workflows/action") {
|
|
return nil
|
|
}
|
|
isWorkflow := false
|
|
isAction := true
|
|
if validateHasYamlSuffix(path, "/action") {
|
|
if err := validate(clonedir, path, isWorkflow, isAction); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
workflowdir := clonedir + "/.forgejo/workflows"
|
|
exists, err = testutils.FileExists(workflowdir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if exists {
|
|
if err := filepath.Walk(workflowdir, func(path string, fi fs.FileInfo, err error) error {
|
|
isWorkflow := true
|
|
isAction := false
|
|
if validateHasYamlSuffix(path, "") {
|
|
if err := validate(clonedir, path, isWorkflow, isAction); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runValidate(_ context.Context, validateArgs *validateArgs) func(cmd *cobra.Command, args []string) error {
|
|
return func(cmd *cobra.Command, args []string) error {
|
|
if len(validateArgs.path) > 0 {
|
|
return validatePath(validateArgs)
|
|
} else if len(validateArgs.repository) > 0 {
|
|
return validateRepository(validateArgs)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func loadValidateCmd(ctx context.Context) *cobra.Command {
|
|
validateArgs := validateArgs{}
|
|
|
|
validateCmd := &cobra.Command{
|
|
Use: "validate",
|
|
Short: "Validate workflows or actions with a schema",
|
|
Long: `
|
|
Validate workflows or actions with a schema verifying they are conformant.
|
|
|
|
The --path argument is a filename that will be validated as a workflow
|
|
(if the --workflow flag is set) or as an action (if the --action flag is set).
|
|
|
|
The --repository argument is a URL to a Git repository. It will be
|
|
cloned (in the --clonedir directory or a temporary location removed
|
|
when the validation completes). The following files will be validated:
|
|
|
|
- All .forgejo/workflows/*.{yml,yaml} files as workflows
|
|
- All **/action.{yml,yaml} files as actions
|
|
`,
|
|
Args: cobra.MaximumNArgs(20),
|
|
RunE: runValidate(ctx, &validateArgs),
|
|
}
|
|
|
|
validateCmd.Flags().BoolVar(&validateArgs.workflow, "workflow", false, "use the workflow schema")
|
|
validateCmd.Flags().BoolVar(&validateArgs.action, "action", false, "use the action schema")
|
|
validateCmd.MarkFlagsMutuallyExclusive("workflow", "action")
|
|
|
|
validateCmd.Flags().StringVar(&validateArgs.clonedir, "clonedir", "", "directory in which the repository will be cloned")
|
|
validateCmd.Flags().StringVar(&validateArgs.repository, "repository", "", "URL to a repository to validate")
|
|
validateCmd.Flags().StringVar(&validateArgs.path, "path", "", "path to the file")
|
|
validateCmd.MarkFlagsOneRequired("repository", "path")
|
|
validateCmd.MarkFlagsMutuallyExclusive("repository", "path")
|
|
|
|
return validateCmd
|
|
}
|