mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-06 17:40:58 +00:00
194 lines
5 KiB
Go
194 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
|
||
|
}
|