1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-08-06 17:40:58 +00:00
forgejo-runner/internal/app/cmd/validate.go
Earl Warren 20f115fdac
feat: add the runner validate subcommand (#757)
<!--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>
2025-07-31 05:37:12 +00:00

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
}