mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-06 17:40:58 +00:00
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>
This commit is contained in:
parent
86c528e510
commit
20f115fdac
40 changed files with 545 additions and 8 deletions
2
Makefile
2
Makefile
|
@ -122,7 +122,7 @@ install: $(GOFILES)
|
||||||
|
|
||||||
build: go-check $(EXECUTABLE)
|
build: go-check $(EXECUTABLE)
|
||||||
|
|
||||||
$(EXECUTABLE): $(GOFILES)
|
$(EXECUTABLE): $(GOFILES) act/schema/action_schema.json act/schema/workflow_schema.json
|
||||||
$(GO) build -v -tags 'netgo osusergo $(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
$(GO) build -v -tags 'netgo osusergo $(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
||||||
|
|
||||||
.PHONY: deps-tools
|
.PHONY: deps-tools
|
||||||
|
|
|
@ -15,7 +15,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Execute(ctx context.Context) {
|
func Execute(ctx context.Context) {
|
||||||
// ./act_runner
|
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: "forgejo-runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
Use: "forgejo-runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||||
Short: "Run Forgejo Actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
Short: "Run Forgejo Actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||||
|
@ -26,7 +25,6 @@ func Execute(ctx context.Context) {
|
||||||
configFile := ""
|
configFile := ""
|
||||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
||||||
|
|
||||||
// ./act_runner register
|
|
||||||
var regArgs registerArgs
|
var regArgs registerArgs
|
||||||
registerCmd := &cobra.Command{
|
registerCmd := &cobra.Command{
|
||||||
Use: "register",
|
Use: "register",
|
||||||
|
@ -43,7 +41,6 @@ func Execute(ctx context.Context) {
|
||||||
|
|
||||||
rootCmd.AddCommand(createRunnerFileCmd(ctx, &configFile))
|
rootCmd.AddCommand(createRunnerFileCmd(ctx, &configFile))
|
||||||
|
|
||||||
// ./act_runner daemon
|
|
||||||
daemonCmd := &cobra.Command{
|
daemonCmd := &cobra.Command{
|
||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
Short: "Run as a runner daemon",
|
Short: "Run as a runner daemon",
|
||||||
|
@ -52,7 +49,6 @@ func Execute(ctx context.Context) {
|
||||||
}
|
}
|
||||||
rootCmd.AddCommand(daemonCmd)
|
rootCmd.AddCommand(daemonCmd)
|
||||||
|
|
||||||
// ./act_runner job
|
|
||||||
jobCmd := &cobra.Command{
|
jobCmd := &cobra.Command{
|
||||||
Use: "one-job",
|
Use: "one-job",
|
||||||
Short: "Run only one job",
|
Short: "Run only one job",
|
||||||
|
@ -61,10 +57,10 @@ func Execute(ctx context.Context) {
|
||||||
}
|
}
|
||||||
rootCmd.AddCommand(jobCmd)
|
rootCmd.AddCommand(jobCmd)
|
||||||
|
|
||||||
// ./act_runner exec
|
|
||||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||||
|
|
||||||
// ./act_runner config
|
rootCmd.AddCommand(loadValidateCmd(ctx))
|
||||||
|
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: "generate-config",
|
Use: "generate-config",
|
||||||
Short: "Generate an example config file",
|
Short: "Generate an example config file",
|
||||||
|
@ -74,7 +70,6 @@ func Execute(ctx context.Context) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// ./act_runner cache-server
|
|
||||||
var cacheArgs cacheServerArgs
|
var cacheArgs cacheServerArgs
|
||||||
cacheCmd := &cobra.Command{
|
cacheCmd := &cobra.Command{
|
||||||
Use: "cache-server",
|
Use: "cache-server",
|
||||||
|
|
1
internal/app/cmd/testdata/validate/README.txt
vendored
Normal file
1
internal/app/cmd/testdata/validate/README.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Use make-repositories.sh to change good-repository and bad-repository
|
67
internal/app/cmd/testdata/validate/bad-action.yml
vendored
Normal file
67
internal/app/cmd/testdata/validate/bad-action.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
name: 'Forgejo release download and upload'
|
||||||
|
author: 'Forgejo authors'
|
||||||
|
description: |
|
||||||
|
Upload or download the assets of a release to a Forgejo instance.
|
||||||
|
inputs:
|
||||||
|
badinput: scalarinsteadofmap
|
||||||
|
url:
|
||||||
|
description: 'URL of the Forgejo instance'
|
||||||
|
default: '${{ env.FORGEJO_SERVER_URL }}'
|
||||||
|
repo:
|
||||||
|
description: 'owner/project relative to the URL'
|
||||||
|
default: '${{ forge.repository }}'
|
||||||
|
tag:
|
||||||
|
description: 'Tag of the release'
|
||||||
|
default: '${{ forge.ref_name }}'
|
||||||
|
title:
|
||||||
|
description: 'Title of the release (defaults to tag)'
|
||||||
|
sha:
|
||||||
|
description: 'SHA of the release'
|
||||||
|
default: '${{ forge.sha }}'
|
||||||
|
token:
|
||||||
|
description: 'Forgejo application token'
|
||||||
|
default: '${{ forge.token }}'
|
||||||
|
release-dir:
|
||||||
|
description: 'Directory in whichs release assets are uploaded or downloaded'
|
||||||
|
required: true
|
||||||
|
release-notes:
|
||||||
|
description: 'Release notes'
|
||||||
|
direction:
|
||||||
|
description: 'Can either be `download` or `upload`'
|
||||||
|
required: true
|
||||||
|
gpg-private-key:
|
||||||
|
description: 'GPG Private Key to sign the release artifacts'
|
||||||
|
gpg-passphrase:
|
||||||
|
description: 'Passphrase of the GPG Private Key'
|
||||||
|
download-retry:
|
||||||
|
description: 'Number of times to retry if the release is not ready (default 1)'
|
||||||
|
download-latest:
|
||||||
|
description: 'Download the latest release'
|
||||||
|
default: false
|
||||||
|
verbose:
|
||||||
|
description: 'Increase the verbosity level'
|
||||||
|
default: false
|
||||||
|
override:
|
||||||
|
description: 'Override an existing release by the same `{tag}`'
|
||||||
|
default: false
|
||||||
|
prerelease:
|
||||||
|
description: 'Mark Release as Pre-Release'
|
||||||
|
default: false
|
||||||
|
release-notes-assistant:
|
||||||
|
description: 'Generate release notes with Release Notes Assistant'
|
||||||
|
default: false
|
||||||
|
hide-archive-link:
|
||||||
|
description: 'Hide the archive links'
|
||||||
|
default: false
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- if: ${{ inputs.release-notes-assistant }}
|
||||||
|
uses: https://data.forgejo.org/actions/cache@v4
|
||||||
|
with:
|
||||||
|
key: rna-${{ inputs.repo }}
|
||||||
|
path: ${{ forge.action_path }}/rna
|
||||||
|
|
||||||
|
- run: echo "${{ forge.action_path }}" >> $FORGEJO_PATH
|
||||||
|
shell: bash
|
1
internal/app/cmd/testdata/validate/bad-repository/HEAD
vendored
Normal file
1
internal/app/cmd/testdata/validate/bad-repository/HEAD
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/master
|
6
internal/app/cmd/testdata/validate/bad-repository/config
vendored
Normal file
6
internal/app/cmd/testdata/validate/bad-repository/config
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = /tmp/tmp.jyjE6tqWGS/bad
|
1
internal/app/cmd/testdata/validate/bad-repository/description
vendored
Normal file
1
internal/app/cmd/testdata/validate/bad-repository/description
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
6
internal/app/cmd/testdata/validate/bad-repository/info/exclude
vendored
Normal file
6
internal/app/cmd/testdata/validate/bad-repository/info/exclude
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# git ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
# Lines that start with '#' are comments.
|
||||||
|
# For a project mostly in C, the following would be a good set of
|
||||||
|
# exclude patterns (uncomment them if you want to use them):
|
||||||
|
# *.[oa]
|
||||||
|
# *~
|
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/4a/b480aa3a6da70e379c50fb30509cf8acc1bd8c
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/4a/b480aa3a6da70e379c50fb30509cf8acc1bd8c
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/53/ce48939297c1445f1a7e2c4afb24d9e679c933
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/53/ce48939297c1445f1a7e2c4afb24d9e679c933
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/6f/70f2754f4de737955b68705db6ccec1090e12d
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/6f/70f2754f4de737955b68705db6ccec1090e12d
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/71/48e33ec2da861625486f4ac5c72e2197445bbd
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/71/48e33ec2da861625486f4ac5c72e2197445bbd
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/80/2c1a243fbadbedf725ae695d7a37be1748eb2d
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/80/2c1a243fbadbedf725ae695d7a37be1748eb2d
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/99/a66b16c83472b94e5e275ae6bf85ba89a8e201
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/bad-repository/objects/99/a66b16c83472b94e5e275ae6bf85ba89a8e201
vendored
Normal file
Binary file not shown.
2
internal/app/cmd/testdata/validate/bad-repository/packed-refs
vendored
Normal file
2
internal/app/cmd/testdata/validate/bad-repository/packed-refs
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
99a66b16c83472b94e5e275ae6bf85ba89a8e201 refs/heads/master
|
0
internal/app/cmd/testdata/validate/bad-repository/refs/placeholder
vendored
Normal file
0
internal/app/cmd/testdata/validate/bad-repository/refs/placeholder
vendored
Normal file
6
internal/app/cmd/testdata/validate/bad-workflow.yml
vendored
Normal file
6
internal/app/cmd/testdata/validate/bad-workflow.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
ruins-on: docker
|
||||||
|
steps:
|
||||||
|
- run: echo All good!
|
67
internal/app/cmd/testdata/validate/good-action.yml
vendored
Normal file
67
internal/app/cmd/testdata/validate/good-action.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
name: 'Forgejo release download and upload'
|
||||||
|
author: 'Forgejo authors'
|
||||||
|
description: |
|
||||||
|
Upload or download the assets of a release to a Forgejo instance.
|
||||||
|
inputs:
|
||||||
|
url:
|
||||||
|
description: 'URL of the Forgejo instance'
|
||||||
|
default: '${{ env.FORGEJO_SERVER_URL }}'
|
||||||
|
repo:
|
||||||
|
description: 'owner/project relative to the URL'
|
||||||
|
default: '${{ forge.repository }}'
|
||||||
|
tag:
|
||||||
|
description: 'Tag of the release'
|
||||||
|
default: '${{ forge.ref_name }}'
|
||||||
|
title:
|
||||||
|
description: 'Title of the release (defaults to tag)'
|
||||||
|
sha:
|
||||||
|
description: 'SHA of the release'
|
||||||
|
default: '${{ forge.sha }}'
|
||||||
|
token:
|
||||||
|
description: 'Forgejo application token'
|
||||||
|
default: '${{ forge.token }}'
|
||||||
|
release-dir:
|
||||||
|
description: 'Directory in whichs release assets are uploaded or downloaded'
|
||||||
|
required: true
|
||||||
|
release-notes:
|
||||||
|
description: 'Release notes'
|
||||||
|
direction:
|
||||||
|
description: 'Can either be `download` or `upload`'
|
||||||
|
required: true
|
||||||
|
gpg-private-key:
|
||||||
|
description: 'GPG Private Key to sign the release artifacts'
|
||||||
|
gpg-passphrase:
|
||||||
|
description: 'Passphrase of the GPG Private Key'
|
||||||
|
download-retry:
|
||||||
|
description: 'Number of times to retry if the release is not ready (default 1)'
|
||||||
|
download-latest:
|
||||||
|
description: 'Download the latest release'
|
||||||
|
default: false
|
||||||
|
verbose:
|
||||||
|
description: 'Increase the verbosity level'
|
||||||
|
default: false
|
||||||
|
override:
|
||||||
|
description: 'Override an existing release by the same `{tag}`'
|
||||||
|
default: false
|
||||||
|
prerelease:
|
||||||
|
description: 'Mark Release as Pre-Release'
|
||||||
|
default: false
|
||||||
|
release-notes-assistant:
|
||||||
|
description: 'Generate release notes with Release Notes Assistant'
|
||||||
|
default: false
|
||||||
|
hide-archive-link:
|
||||||
|
description: 'Hide the archive links'
|
||||||
|
default: false
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- if: ${{ inputs.release-notes-assistant }}
|
||||||
|
uses: https://data.forgejo.org/actions/cache@v4
|
||||||
|
with:
|
||||||
|
key: rna-${{ inputs.repo }}
|
||||||
|
path: ${{ forge.action_path }}/rna
|
||||||
|
|
||||||
|
- run: echo "${{ forge.action_path }}" >> $FORGEJO_PATH
|
||||||
|
shell: bash
|
1
internal/app/cmd/testdata/validate/good-repository/HEAD
vendored
Normal file
1
internal/app/cmd/testdata/validate/good-repository/HEAD
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/master
|
6
internal/app/cmd/testdata/validate/good-repository/config
vendored
Normal file
6
internal/app/cmd/testdata/validate/good-repository/config
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = /tmp/tmp.jyjE6tqWGS/good
|
1
internal/app/cmd/testdata/validate/good-repository/description
vendored
Normal file
1
internal/app/cmd/testdata/validate/good-repository/description
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
6
internal/app/cmd/testdata/validate/good-repository/info/exclude
vendored
Normal file
6
internal/app/cmd/testdata/validate/good-repository/info/exclude
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# git ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
# Lines that start with '#' are comments.
|
||||||
|
# For a project mostly in C, the following would be a good set of
|
||||||
|
# exclude patterns (uncomment them if you want to use them):
|
||||||
|
# *.[oa]
|
||||||
|
# *~
|
BIN
internal/app/cmd/testdata/validate/good-repository/objects/62/6b6b892539d8c81545a5af6bac1b9996335e4d
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/62/6b6b892539d8c81545a5af6bac1b9996335e4d
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/74/22baa3a822e909afe3c87eaa7646b12f43fdcb
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/74/22baa3a822e909afe3c87eaa7646b12f43fdcb
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/80/2c1a243fbadbedf725ae695d7a37be1748eb2d
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/80/2c1a243fbadbedf725ae695d7a37be1748eb2d
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/ba/e251227a079a7601f20554eb82b3cefcce51c9
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/ba/e251227a079a7601f20554eb82b3cefcce51c9
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/cb/66d230ae7d2c6ca2132b0996cffd8c74de48b1
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/cb/66d230ae7d2c6ca2132b0996cffd8c74de48b1
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/ce/2b2747739d1553c23a48e695732c358faccfaf
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/ce/2b2747739d1553c23a48e695732c358faccfaf
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/ef/dc13fcbbc43a196903b12847fca66d3c6b8d9d
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/ef/dc13fcbbc43a196903b12847fca66d3c6b8d9d
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/f0/9a73e2ddef7b7834661e4c7b388a22f654f164
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/f0/9a73e2ddef7b7834661e4c7b388a22f654f164
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/f1/8ebc1e1151d0b8de9c296f1d8baf9c90fe3fa6
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/f1/8ebc1e1151d0b8de9c296f1d8baf9c90fe3fa6
vendored
Normal file
Binary file not shown.
BIN
internal/app/cmd/testdata/validate/good-repository/objects/f4/2c3901e0beb480edfd5d41670c1a1958d5b33c
vendored
Normal file
BIN
internal/app/cmd/testdata/validate/good-repository/objects/f4/2c3901e0beb480edfd5d41670c1a1958d5b33c
vendored
Normal file
Binary file not shown.
2
internal/app/cmd/testdata/validate/good-repository/packed-refs
vendored
Normal file
2
internal/app/cmd/testdata/validate/good-repository/packed-refs
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
f09a73e2ddef7b7834661e4c7b388a22f654f164 refs/heads/master
|
0
internal/app/cmd/testdata/validate/good-repository/refs/placeholder
vendored
Normal file
0
internal/app/cmd/testdata/validate/good-repository/refs/placeholder
vendored
Normal file
6
internal/app/cmd/testdata/validate/good-workflow.yml
vendored
Normal file
6
internal/app/cmd/testdata/validate/good-workflow.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- run: echo All good!
|
56
internal/app/cmd/testdata/validate/make-repositories.sh
vendored
Executable file
56
internal/app/cmd/testdata/validate/make-repositories.sh
vendored
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
#!/bin/bash -ex
|
||||||
|
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
|
||||||
|
trap "rm -fr $tmpdir" EXIT
|
||||||
|
|
||||||
|
# good
|
||||||
|
|
||||||
|
mkdir $tmpdir/good
|
||||||
|
git -C $tmpdir/good init --quiet
|
||||||
|
|
||||||
|
cp good-action.yml $tmpdir/good/action.yml
|
||||||
|
mkdir -p $tmpdir/good/subaction
|
||||||
|
cp good-action.yml $tmpdir/good/subaction/action.yaml
|
||||||
|
|
||||||
|
mkdir -p $tmpdir/good/.forgejo/workflows
|
||||||
|
cp good-workflow.yml $tmpdir/good/.forgejo/workflows/action.yml
|
||||||
|
cp good-workflow.yml $tmpdir/good/.forgejo/workflows/workflow1.yml
|
||||||
|
cp good-workflow.yml $tmpdir/good/.forgejo/workflows/workflow2.yaml
|
||||||
|
|
||||||
|
# add workflows / actions that won't be good but it does not matter
|
||||||
|
# because they must be ignored
|
||||||
|
for i in .github .gitea; do
|
||||||
|
mkdir -p $tmpdir/good/$i/workflows
|
||||||
|
cp bad-workflow.yml $tmpdir/good/$i/workflows/bad.yml
|
||||||
|
done
|
||||||
|
|
||||||
|
git -C $tmpdir/good config user.email root@example.com
|
||||||
|
git -C $tmpdir/good config user.name username
|
||||||
|
git -C $tmpdir/good add .
|
||||||
|
git -C $tmpdir/good commit -m 'initial'
|
||||||
|
|
||||||
|
rm -fr good-repository
|
||||||
|
git clone --bare $tmpdir/good good-repository
|
||||||
|
rm -fr good-repository/hooks
|
||||||
|
touch good-repository/refs/placeholder
|
||||||
|
|
||||||
|
# bad
|
||||||
|
|
||||||
|
mkdir $tmpdir/bad
|
||||||
|
git -C $tmpdir/bad init --quiet
|
||||||
|
|
||||||
|
cp bad-action.yml $tmpdir/bad/action.yml
|
||||||
|
|
||||||
|
mkdir -p $tmpdir/bad/.forgejo/workflows
|
||||||
|
cp bad-workflow.yml $tmpdir/bad/.forgejo/workflows/workflow1.yml
|
||||||
|
|
||||||
|
git -C $tmpdir/bad config user.email root@example.com
|
||||||
|
git -C $tmpdir/bad config user.name username
|
||||||
|
git -C $tmpdir/bad add .
|
||||||
|
git -C $tmpdir/bad commit -m 'initial'
|
||||||
|
|
||||||
|
rm -fr bad-repository
|
||||||
|
git clone --bare $tmpdir/bad bad-repository
|
||||||
|
rm -fr bad-repository/hooks
|
||||||
|
touch bad-repository/refs/placeholder
|
193
internal/app/cmd/validate.go
Normal file
193
internal/app/cmd/validate.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
// 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
|
||||||
|
}
|
93
internal/app/cmd/validate_test.go
Normal file
93
internal/app/cmd/validate_test.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_validateCmd(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
message string
|
||||||
|
cmdOut string
|
||||||
|
stdOut string
|
||||||
|
stdErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "MissingFlag",
|
||||||
|
args: []string{"--path", "testdata/validate/good-action.yml"},
|
||||||
|
cmdOut: "Usage:",
|
||||||
|
message: "one of --workflow or --action must be set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MutuallyExclusive",
|
||||||
|
args: []string{"--action", "--workflow", "--path", "/tmp"},
|
||||||
|
message: "[action workflow] were all set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PathActionOK",
|
||||||
|
args: []string{"--action", "--path", "testdata/validate/good-action.yml"},
|
||||||
|
stdOut: "schema validation OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PathActionNOK",
|
||||||
|
args: []string{"--action", "--path", "testdata/validate/bad-action.yml"},
|
||||||
|
stdOut: "Expected a mapping got scalar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PathWorkflowOK",
|
||||||
|
args: []string{"--workflow", "--path", "testdata/validate/good-workflow.yml"},
|
||||||
|
stdOut: "schema validation OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PathWorkflowNOK",
|
||||||
|
args: []string{"--workflow", "--path", "testdata/validate/bad-workflow.yml"},
|
||||||
|
stdOut: "Unknown Property ruins-on",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RepositoryOK",
|
||||||
|
args: []string{"--repository", "testdata/validate/good-repository"},
|
||||||
|
stdOut: "action.yml action schema validation OK\nsubaction/action.yaml action schema validation OK\n.forgejo/workflows/action.yml workflow schema validation OK\n.forgejo/workflows/workflow1.yml workflow schema validation OK\n.forgejo/workflows/workflow2.yaml workflow schema validation OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RepositoryActionNOK",
|
||||||
|
args: []string{"--repository", "testdata/validate/bad-repository"},
|
||||||
|
stdOut: "action.yml action schema validation failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RepositoryWorkflowNOK",
|
||||||
|
args: []string{"--repository", "testdata/validate/bad-repository"},
|
||||||
|
stdOut: ".forgejo/workflows/workflow1.yml workflow schema validation failed",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
cmd := loadValidateCmd(ctx)
|
||||||
|
cmdOut, stdOut, stdErr, err := executeCommand(ctx, t, cmd, testCase.args...)
|
||||||
|
if testCase.message != "" {
|
||||||
|
assert.ErrorContains(t, err, testCase.message)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if testCase.stdOut != "" {
|
||||||
|
assert.Contains(t, stdOut, testCase.stdOut)
|
||||||
|
} else {
|
||||||
|
assert.Empty(t, stdOut)
|
||||||
|
}
|
||||||
|
if testCase.stdErr != "" {
|
||||||
|
assert.Contains(t, stdErr, testCase.stdErr)
|
||||||
|
} else {
|
||||||
|
assert.Empty(t, stdErr)
|
||||||
|
}
|
||||||
|
if testCase.cmdOut != "" {
|
||||||
|
assert.Contains(t, cmdOut, testCase.cmdOut)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
release-notes/757.md
Normal file
1
release-notes/757.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
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.
|
20
testutils/file.go
Normal file
20
testutils/file.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package testutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FileExists(pathname string) (bool, error) {
|
||||||
|
_, err := os.Stat(pathname)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue