├── testdata ├── projects │ ├── issue-136.out │ ├── workflow_call_ok.out │ ├── local_action_empty.out │ ├── recursive_workflow_call.out │ ├── local_action_case_insensitive.out │ ├── workflow_call_inherit_secrets.out │ ├── broken_local_action │ │ ├── action │ │ │ ├── broken_output │ │ │ │ └── action.yml │ │ │ ├── duplicate_input │ │ │ │ └── action.yml │ │ │ ├── duplicate_output │ │ │ │ └── action.yml │ │ │ └── broken_input │ │ │ │ └── action.yml │ │ └── workflows │ │ │ └── test.yaml │ ├── workflow_call_missing_required │ │ └── workflows │ │ │ ├── test.yaml │ │ │ └── reusable.yaml │ ├── broken_reusable_workflow │ │ ├── reusable │ │ │ ├── no_on.yaml │ │ │ ├── no_hook.yaml │ │ │ ├── broken.yaml │ │ │ ├── broken_secrets.yaml │ │ │ └── broken_input.yaml │ │ ├── workflows │ │ │ └── test.yaml │ │ └── README.md │ ├── local_action_empty │ │ ├── workflows │ │ │ └── test.yaml │ │ └── action │ │ │ └── action.yaml │ ├── example_workflow_call_outputs_downstream_jobs.out │ ├── workflow_call_inherit_secrets │ │ └── workflows │ │ │ ├── test.yaml │ │ │ └── reusable.yaml │ ├── issue-136 │ │ └── workflows │ │ │ ├── reusable.yaml │ │ │ └── test.yaml │ ├── workflow_call_ok │ │ └── workflows │ │ │ ├── empty2.yaml │ │ │ ├── empty3.yaml │ │ │ ├── empty1.yaml │ │ │ ├── reusable_all_optional.yaml │ │ │ ├── reusable_all_required.yaml │ │ │ └── test.yaml │ ├── workflow_call_undefined │ │ └── workflows │ │ │ ├── empty_reusable.yaml │ │ │ ├── reusable.yaml │ │ │ └── test.yaml │ ├── workflow_call_not_found.out │ ├── workflow_call_upper_case │ │ ├── workflows │ │ │ ├── missing.yaml │ │ │ ├── ok_lower.yaml │ │ │ ├── ok_upper.yaml │ │ │ ├── undefined.yaml │ │ │ └── output.yaml │ │ └── reusable │ │ │ ├── lower.yaml │ │ │ └── upper.yaml │ ├── issue173 │ │ ├── workflows │ │ │ ├── workflow1.yaml │ │ │ └── workflow2.yaml │ │ └── action │ │ │ └── action.yaml │ ├── workflow_call_missing_required.out │ ├── workflow_call_not_found │ │ └── workflows │ │ │ └── test.yaml │ ├── local_action_case_insensitive │ │ ├── workflows │ │ │ └── test.yaml │ │ └── action │ │ │ └── action.yaml │ ├── workflow_call_input_type_check │ │ └── workflows │ │ │ ├── reusable.yaml │ │ │ └── test.yaml │ ├── example_workflow_call_outputs_downstream_jobs │ │ ├── .github │ │ │ └── workflows │ │ │ │ └── get-build-info.yaml │ │ └── workflows │ │ │ └── test.yaml │ ├── example_inputs_secrets_in_workflow_call │ │ ├── .github │ │ │ └── workflows │ │ │ │ └── reusable.yaml │ │ └── workflows │ │ │ └── test.yaml │ ├── broken_local_action.out │ ├── broken_reusable_workflow.out │ ├── issue173.out │ ├── recursive_workflow_call │ │ └── workflows │ │ │ └── recursive.yaml │ ├── workflow_call_undefined.out │ ├── README.md │ ├── workflow_call_input_type_check.out │ ├── example_inputs_secrets_in_workflow_call.out │ └── workflow_call_upper_case.out ├── config │ ├── broken.yml │ └── ok.yml ├── examples │ ├── not_persistent_matrix_values.out │ ├── .github │ │ └── actions │ │ │ ├── my-action │ │ │ ├── index.js │ │ │ └── action.yaml │ │ │ └── my-action-with-output │ │ │ ├── index.js │ │ │ └── action.yaml │ ├── unexpected_keys.yaml │ ├── broken_yaml.yaml │ ├── expand_object.out │ ├── broken_yaml.out │ ├── reusable_workflow_outputs.out │ ├── runner_label_conflict.yaml │ ├── cyclic_deps_needs.out │ ├── env_var_names.yaml │ ├── runner_label_conflict.out │ ├── contextual_steps_outputs.out │ ├── popular_action_outputs.out │ ├── local_action_outputs.out │ ├── invalid_ids_in_needs.out │ ├── env_var_names.out │ ├── missing_required_keys.out │ ├── invalid_ids_in_needs.yaml │ ├── cron_schedule_check.out │ ├── contextual_matrix_values.out │ ├── hardcoded_credentials.out │ ├── shellcheck_integration.out │ ├── popular_action_inputs.out │ ├── cron_schedule_check.yaml │ ├── missing_required_keys.yaml │ ├── workflow_inputs_secrets_types.out │ ├── job_step_ids_duplicate.out │ ├── popular_action_inputs.yaml │ ├── pyflakes_integration.out │ ├── local_action_inputs.out │ ├── local_action_inputs.yaml │ ├── matrix_checks.out │ ├── matrix_checks.yaml │ ├── unexpected_keys.out │ ├── unexpected_mapping_values.out │ ├── cyclic_deps_needs.yaml │ ├── unexpected_mapping_values.yaml │ ├── invalid_action_format.yaml │ ├── permissions.yaml │ ├── job_step_ids_duplicate.yaml │ ├── shellcheck_integration.yaml │ ├── expression_syntax_error.yaml │ ├── contextual_needs_object.out │ ├── type_checks.out │ ├── glob.yaml │ ├── reusable_workflow_outputs.yaml │ ├── hardcoded_credentials.yaml │ ├── shell_name_validation.out │ ├── expand_object.yaml │ ├── local_action_outputs.yaml │ ├── not_persistent_matrix_values.yaml │ ├── permissions.out │ ├── workflow_call_definitions.out │ ├── id_naming_convention.yaml │ ├── id_naming_convention.out │ ├── popular_action_outputs.yaml │ ├── webhook_checks.yaml │ ├── workflow_call_jobs.yaml │ ├── type_checks.yaml │ ├── invalid_action_format.out │ ├── pyflakes_integration.yaml │ ├── workflow_inputs_secrets_types.yaml │ ├── main.yaml │ ├── runner_label_check.yaml │ ├── workflow_call_jobs.out │ ├── expression_syntax_error.out │ ├── contextual_steps_outputs.yaml │ ├── workflow_dispatch_input_types.out │ ├── shell_name_validation.yaml │ ├── webhook_checks.out │ ├── contexts_and_buitin_funcs.yaml │ ├── workflow_call_definitions.yaml │ ├── contexts_and_buitin_funcs.out │ ├── workflow_dispatch_input_types.yaml │ ├── untrusted_input.yaml │ ├── glob.out │ ├── untrusted_input.out │ ├── contextual_needs_object.yaml │ ├── contextual_matrix_values.yaml │ ├── runner_label_check.out │ └── main.out ├── realworld │ ├── .gitignore │ └── dataset.zip ├── err │ ├── outputs_map_object.out │ ├── issue102.yaml │ ├── workflow_call_secrets.out │ ├── inputs_without_workflow_call_event.out │ ├── reusable_workflow_empty_secrets.out │ ├── issue207_work_dir_with_uses.out │ ├── object_at_runner_label.out │ ├── workflow_call_invalid_secrets.out │ ├── cron_5minutes_limit.out │ ├── .github │ │ └── workflows │ │ │ └── called-workflow.yml │ ├── issue102.out │ ├── issue151_child_of_child_job.out │ ├── inputs_without_workflow_call_event.yaml │ ├── run_name_check_expr.yaml │ ├── one_error.yaml │ ├── workflow_call_invalid_secrets.yaml │ ├── outputs_of_action_skipping_inputs_check.out │ ├── issue193.yaml │ ├── run_name_check_expr.out │ ├── issue170_empty_permissions.out │ ├── workflow_call_inputs.out │ ├── workflow_call_outputs_sema.out │ ├── nested_untrusted_input.yaml │ ├── object_at_runner_label.yaml │ ├── runner_labels_conflict_matrix.yaml │ ├── issue207_work_dir_with_uses.yaml │ ├── outputs_map_object.yaml │ ├── issue193.out │ ├── one_error.out │ ├── issue170_empty_permissions.yaml │ ├── workflow_call_secrets.yaml │ ├── github_script_untrusted_input.out │ ├── cron_5minutes_limit.yaml │ ├── reusable_workflow_empty_secrets.yaml │ ├── invalid_event_filters.yaml │ ├── glob_more.yaml │ ├── runner_labels_conflict_matrix.out │ ├── github_script_untrusted_input.yaml │ ├── invalid_id.yaml │ ├── workflow_call_outputs_sema.yaml │ ├── workflow_call_outputs_syntax.out │ ├── evaluated_template.out │ ├── outputs_of_action_skipping_inputs_check.yaml │ ├── issue155_env_in_job_level_if.yaml │ ├── env_context_banned.out │ ├── workflow_call_inputs.yaml │ ├── exclusive_webhook_filters.yaml │ ├── issue155_env_in_job_level_if.out │ ├── evaluated_template.yaml │ ├── workflow_call_outputs_syntax.yaml │ ├── issue151_child_of_child_job.yaml │ ├── workflow_call_job.yaml │ ├── invalid_id.out │ ├── nested_untrusted_input.out │ ├── workflow_dispatch_type_check_inputs.out │ ├── env_context_banned.yaml │ ├── upper_case_duplicate_keys.yaml │ ├── invalid_event_filters.out │ ├── exclusive_webhook_filters.out │ ├── workflow_dispatch_type_check_inputs.yaml │ ├── workflow_dispatch_input_types.yaml │ ├── workflow_dispatch_input_types.out │ ├── workflow_call_event.yaml │ ├── workflow_call_job.out │ └── workflow_call_event.out ├── reusable_workflow_metadata │ ├── no_hook.yaml │ ├── broken.yaml │ ├── broken_secrets.yaml │ ├── broken_inputs.yaml │ ├── no_on.yaml │ └── ok.yaml ├── action_metadata │ ├── broken │ │ └── action.yaml │ ├── empty │ │ └── action.yml │ ├── input-duplicate │ │ └── action.yml │ ├── output-duplicate │ │ └── action.yml │ ├── action-yaml │ │ └── action.yaml │ ├── action-yml │ │ └── action.yml │ └── uppercase │ │ └── action.yaml ├── ok │ ├── nested_workflow_call.yaml │ ├── issue-101.yaml │ ├── bool_conversion.yaml │ ├── issue-53-shellcheck-sc2154.yaml │ ├── issue-113.yaml │ ├── no_local_action_found.yaml │ ├── issue-67.yaml │ ├── multi_labels_no_conflict.yaml │ ├── issue-205-closing-braces-in-string.yaml │ ├── issue-45.yaml │ ├── matrix_in_array_at_runs_on.yaml │ ├── issue-31.yaml │ ├── shell_name_case_insensitive.yaml │ ├── allow_any_outputs.yaml │ ├── issue-152.yaml │ ├── workflow_dispatch_upper_case_inputs.yaml │ ├── issue-174-secret-description-is-optional.yaml │ ├── issue-104.yaml │ ├── reusable_workflow_inherit_secrets.yaml │ ├── run_name.yaml │ ├── issue-30.yaml │ ├── using_reusable_workflow_call_outputs.yaml │ ├── shellcheck_explicit_shell_name.yaml │ ├── filters_for_specific_events.yaml │ ├── issue-164.yaml │ ├── filters_ignore_for_specific_events.yaml │ ├── operator_outside_expr.yaml │ ├── issue-145.yaml │ ├── issue-87.yaml │ ├── workflow_call_outputs.yaml │ ├── skip_inputs.yaml │ ├── issue-151-child-of-child-job.yaml │ ├── no_description_workflow_call.yaml │ ├── workflow_call_with_strategy.yaml │ ├── workflow_call_job.yaml │ ├── untrusted_inputs.yaml │ ├── issue-66.yaml │ ├── workflow_call_outputs_sema.yaml │ ├── workflow_call_event.yaml │ └── workflow_dispatch_input_types.yaml ├── bench │ ├── minimal.yaml │ └── small.yaml └── format │ ├── test.yaml │ ├── test.md │ ├── test.json │ └── test.jsonl ├── .github ├── codeql │ └── codeql-config.yaml ├── actionlint-matcher.json └── workflows │ ├── download.yaml │ ├── matcher.yaml │ └── codeql.yaml ├── fuzz ├── README.md ├── glob.go ├── expr.go ├── parse.go └── check.go ├── scripts ├── generate-webhook-events │ ├── .gitignore │ ├── testdata │ │ ├── no_heading.md │ │ ├── no_hook_name_link.md │ │ ├── no_hooks.md │ │ └── ok.go │ └── README.md ├── generate-popular-actions │ ├── testdata │ │ ├── broken.jsonl │ │ ├── test.jsonl │ │ ├── skip_inputs.jsonl │ │ ├── skip_outputs.jsonl │ │ ├── skip_inputs_want.go │ │ ├── skip_outputs_want.go │ │ ├── want.go │ │ └── fetched.go │ └── README.md ├── generate-actionlint-matcher │ ├── main.js │ ├── test │ │ ├── no_escape.txt │ │ ├── want.json │ │ └── escape.txt │ ├── README.md │ └── object.js └── yaml-to-playground-url.js ├── playground ├── .gitignore ├── .stylelintrc.json ├── .prettierrc.json ├── lib.d.ts ├── tsconfig.json ├── Makefile ├── post-install.bash ├── style.css ├── .eslintrc.json ├── README.md ├── main.go └── package.json ├── .codecov.yaml ├── .gitattributes ├── cmd └── actionlint │ └── main.go ├── .gitignore ├── .pre-commit-hooks.yaml ├── all_webhooks_test.go ├── Dockerfile ├── go.mod ├── command_test.go ├── docs ├── README.md ├── config.md └── reference.md ├── expr.go ├── popular_actions_test.go ├── LICENSE.txt ├── rule_credentials.go ├── rule_shellcheck_test.go ├── config.go ├── rule_env_var.go ├── .goreleaser.yaml └── quotes.go /testdata/projects/issue-136.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/projects/local_action_empty.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/projects/recursive_workflow_call.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/config/broken.yml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 42 2 | -------------------------------------------------------------------------------- /testdata/examples/not_persistent_matrix_values.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/projects/local_action_case_insensitive.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_inherit_secrets.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/realworld/.gitignore: -------------------------------------------------------------------------------- 1 | /*.yaml 2 | /*.yml 3 | -------------------------------------------------------------------------------- /testdata/err/outputs_map_object.out: -------------------------------------------------------------------------------- 1 | /{string => string}/ 2 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yaml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - testdata 3 | -------------------------------------------------------------------------------- /testdata/reusable_workflow_metadata/no_hook.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | See [CONTRIBUTING.md](../CONTRIBUTING.md) about this directory. 2 | -------------------------------------------------------------------------------- /scripts/generate-webhook-events/.gitignore: -------------------------------------------------------------------------------- 1 | /events-that-trigger-workflows.md 2 | -------------------------------------------------------------------------------- /testdata/examples/.github/actions/my-action/index.js: -------------------------------------------------------------------------------- 1 | console.log(process.env) 2 | -------------------------------------------------------------------------------- /testdata/config/ok.yml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | labels: 3 | - foo 4 | - bar 5 | -------------------------------------------------------------------------------- /testdata/examples/.github/actions/my-action-with-output/index.js: -------------------------------------------------------------------------------- 1 | console.log(process.env) 2 | -------------------------------------------------------------------------------- /testdata/action_metadata/broken/action.yaml: -------------------------------------------------------------------------------- 1 | author: 'rhysd ' 2 | description: foo: 3 | -------------------------------------------------------------------------------- /testdata/reusable_workflow_metadata/broken.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: my cool outputs 4 | -------------------------------------------------------------------------------- /testdata/examples/unexpected_keys.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | step: 6 | -------------------------------------------------------------------------------- /testdata/realworld/dataset.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actionlint/main/testdata/realworld/dataset.zip -------------------------------------------------------------------------------- /testdata/ok/nested_workflow_call.yaml: -------------------------------------------------------------------------------- 1 | on: workflow_call 2 | 3 | jobs: 4 | test: 5 | uses: owner/repo/x.yml@ref 6 | -------------------------------------------------------------------------------- /testdata/err/issue102.yaml: -------------------------------------------------------------------------------- 1 | jobs: 2 | actionlint: 3 | steps: 4 | - 5 | with: 6 | info: test 7 | -------------------------------------------------------------------------------- /testdata/reusable_workflow_metadata/broken_secrets.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | secrets: 4 | foo: my secret 5 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_secrets.out: -------------------------------------------------------------------------------- 1 | /test\.yaml:14:23: property "secret1" is not defined in object type {.*secret0: string.*}/ 2 | -------------------------------------------------------------------------------- /testdata/projects/broken_local_action/action/broken_output/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | description: hello 3 | 4 | outputs: oops 5 | -------------------------------------------------------------------------------- /testdata/bench/minimal.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo hi 8 | -------------------------------------------------------------------------------- /testdata/examples/broken_yaml.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | linux: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: foo: 7 | -------------------------------------------------------------------------------- /testdata/examples/expand_object.out: -------------------------------------------------------------------------------- 1 | test.yaml:19:14: type of expression at "env" must be object but found type string [expression] 2 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /main.wasm 3 | /node_modules 4 | /package-lock.json 5 | /image 6 | /*.js 7 | /*.js.map 8 | /.testtimestamp 9 | -------------------------------------------------------------------------------- /testdata/err/inputs_without_workflow_call_event.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:23: property "some_input" is not defined in object type {} [expression] 2 | -------------------------------------------------------------------------------- /testdata/reusable_workflow_metadata/broken_inputs.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | validate: validate something when true 5 | -------------------------------------------------------------------------------- /playground/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "selector-class-pattern": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/broken.jsonl: -------------------------------------------------------------------------------- 1 | {"spec":"rhysd/action-setup-vim@v1","metadata":{"name":"Setup Vim","inputs":{"neovim":false, 2 | -------------------------------------------------------------------------------- /testdata/examples/broken_yaml.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:0: could not parse as YAML: yaml: line 6: mapping values are not allowed in this context [yaml-syntax] 2 | -------------------------------------------------------------------------------- /testdata/examples/reusable_workflow_outputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:20: property "imagetag" is not defined in object type {image_tag: string} [expression] 2 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_missing_required/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller: 5 | uses: ./workflows/reusable.yaml 6 | -------------------------------------------------------------------------------- /testdata/err/reusable_workflow_empty_secrets.out: -------------------------------------------------------------------------------- 1 | /test\.yaml:12:24: property "calling_workflow_secret" is not defined in object type {.+} \[expression\]/ 2 | -------------------------------------------------------------------------------- /testdata/ok/issue-101.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: echo ${{ runner.arch }} 8 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/reusable/no_on.yaml: -------------------------------------------------------------------------------- 1 | jobs: 2 | test: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - run: echo ... 6 | -------------------------------------------------------------------------------- /testdata/err/issue207_work_dir_with_uses.out: -------------------------------------------------------------------------------- 1 | test.yaml:8:28: "working-directory" is not available with "uses". it is only available with "run" [syntax-check] 2 | -------------------------------------------------------------------------------- /testdata/err/object_at_runner_label.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:14: type of expression at "runs-on" must be string or array but found type "{foo: string}" [expression] 2 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_invalid_secrets.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:14: expected mapping node for secrets or "inherit" string node but found "foo" node [syntax-check] 2 | -------------------------------------------------------------------------------- /testdata/reusable_workflow_metadata/no_on.yaml: -------------------------------------------------------------------------------- 1 | jobs: 2 | test: 3 | foo: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo ... 7 | -------------------------------------------------------------------------------- /testdata/examples/runner_label_conflict.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: [ubuntu-latest, windows-latest] 5 | steps: 6 | - run: echo ... 7 | -------------------------------------------------------------------------------- /testdata/format/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branch: main 4 | jobs: 5 | test: 6 | runs-on: linux-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | -------------------------------------------------------------------------------- /testdata/projects/local_action_empty/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: ./action 8 | -------------------------------------------------------------------------------- /.codecov.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.com/docs/commit-status#disabling-a-status 2 | coverage: 3 | status: 4 | project: off 5 | patch: off 6 | 7 | comment: false 8 | -------------------------------------------------------------------------------- /testdata/err/cron_5minutes_limit.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:13: scheduled job runs too frequently. it runs once per 240 seconds. the shortest interval is once every 5 minutes [events] 2 | -------------------------------------------------------------------------------- /testdata/projects/example_workflow_call_outputs_downstream_jobs.out: -------------------------------------------------------------------------------- 1 | workflows/test.yaml:13:24: property "tag" is not defined in object type {version: string} [expression] 2 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_inherit_secrets/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller: 5 | uses: ./workflows/reusable.yaml 6 | secrets: inherit 7 | -------------------------------------------------------------------------------- /testdata/err/.github/workflows/called-workflow.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hello 9 | -------------------------------------------------------------------------------- /testdata/err/issue102.out: -------------------------------------------------------------------------------- 1 | test.yaml:2:3: "runs-on" section is missing in job "actionlint" [syntax-check] 2 | test.yaml:5:9: "uses" is required to run action in step [syntax-check] 3 | -------------------------------------------------------------------------------- /testdata/err/issue151_child_of_child_job.out: -------------------------------------------------------------------------------- 1 | test.yaml:26:31: property "first" is not defined in object type {second: {outputs: {second: string}; result: string}} [expression] 2 | -------------------------------------------------------------------------------- /testdata/ok/bool_conversion.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo '${{ !!42 }}' 7 | if: github.event_name 8 | -------------------------------------------------------------------------------- /testdata/projects/issue-136/workflows/reusable.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hello 9 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok/workflows/empty2.yaml: -------------------------------------------------------------------------------- 1 | on: workflow_call 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo hello 8 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok/workflows/empty3.yaml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo hello 8 | -------------------------------------------------------------------------------- /testdata/err/inputs_without_workflow_call_event.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - run: echo ${{ inputs.some_input }} 8 | -------------------------------------------------------------------------------- /testdata/err/run_name_check_expr.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | run-name: Deploy to ${{ hoge }} 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hello 9 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok/workflows/empty1.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hello 9 | -------------------------------------------------------------------------------- /testdata/action_metadata/empty/action.yml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | runs: 5 | using: 'node14' 6 | main: 'index.js' 7 | -------------------------------------------------------------------------------- /testdata/err/one_error.yaml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo "Checking commit '${{ github.event.head_commit.message }}'" 7 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/reusable/no_hook.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo ... 8 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_undefined/workflows/empty_reusable.yaml: -------------------------------------------------------------------------------- 1 | on: workflow_call 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo 'hi' 8 | -------------------------------------------------------------------------------- /testdata/projects/broken_local_action/action/duplicate_input/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | description: hello 3 | 4 | inputs: 5 | foo: 6 | description: ... 7 | FOO: 8 | description: ... 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /testdata/** -text 2 | /scripts/generate-popular-actions/testdata/** -text 3 | /scripts/generate-webhook-events/testdata/** -text 4 | /scripts/generate-actionlint-matcher/test/** -text 5 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_invalid_secrets.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | jobs: 5 | pass-secrets-to-workflow: 6 | uses: ./.github/workflows/called-workflow.yml 7 | secrets: foo 8 | -------------------------------------------------------------------------------- /testdata/projects/broken_local_action/action/duplicate_output/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | description: hello 3 | 4 | outputs: 5 | foo: 6 | description: ... 7 | FOO: 8 | description: ... 9 | -------------------------------------------------------------------------------- /testdata/err/outputs_of_action_skipping_inputs_check.out: -------------------------------------------------------------------------------- 1 | test.yaml:16:40: property "this_output_does_not_exist" is not defined in object type {data: string; headers: string; status: string} [expression] 2 | -------------------------------------------------------------------------------- /testdata/examples/cyclic_deps_needs.out: -------------------------------------------------------------------------------- 1 | /test\.yaml:\d+:\d+: cyclic dependencies in "needs" configurations of jobs are detected\. detected cycle is ".+" -> ".+", ".+" -> ".+", ".+" -> ".+" \[job-needs\]/ 2 | -------------------------------------------------------------------------------- /testdata/ok/issue-53-shellcheck-sc2154.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo hello "$thing" 7 | env: 8 | thing: world 9 | -------------------------------------------------------------------------------- /testdata/projects/broken_local_action/action/broken_input/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | description: hello 3 | 4 | inputs: 5 | hello: 6 | required: 7 | this: value 8 | should-be: boolean 9 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_not_found.out: -------------------------------------------------------------------------------- 1 | /workflows/test\.yaml:5:11: could not read reusable workflow file for "\./workflows/this-workflow-does-not-exist\.yaml": .+ \[(expression|workflow-call)\]/ 2 | -------------------------------------------------------------------------------- /testdata/err/issue193.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | foo: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | continue-on-error: ${{ env.OS == "macos-latest" }} 9 | -------------------------------------------------------------------------------- /testdata/projects/issue-136/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | checks: 5 | concurrency: 6 | group: some-group 7 | cancel-in-progress: true 8 | uses: ./workflows/reusable.yaml 9 | -------------------------------------------------------------------------------- /testdata/projects/local_action_empty/action/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | runs: 6 | using: 'node16' 7 | main: 'index.js' 8 | -------------------------------------------------------------------------------- /testdata/err/run_name_check_expr.out: -------------------------------------------------------------------------------- 1 | test.yaml:2:25: undefined variable "hoge". available variables are "env", "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 2 | -------------------------------------------------------------------------------- /testdata/examples/env_var_names.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | env: 6 | FOO=BAR: foo 7 | FOO BAR: foo 8 | steps: 9 | - run: echo 'hello' 10 | -------------------------------------------------------------------------------- /testdata/examples/runner_label_conflict.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:30: label "windows-latest" conflicts with label "ubuntu-latest" defined at line:4,col:15. note: to run your job on each workers, use matrix [runner-label] 2 | -------------------------------------------------------------------------------- /testdata/examples/contextual_steps_outputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:24: property "get_value" is not defined in object type {} [expression] 2 | test.yaml:22:24: property "get_value" is not defined in object type {} [expression] 3 | -------------------------------------------------------------------------------- /testdata/examples/popular_action_outputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:8:23: property "cache" is not defined in object type {} [expression] 2 | test.yaml:18:23: property "cache_hit" is not defined in object type {cache-hit: string} [expression] 3 | -------------------------------------------------------------------------------- /testdata/err/issue170_empty_permissions.out: -------------------------------------------------------------------------------- 1 | test.yaml:12:17: string should not be empty [syntax-check] 2 | test.yaml:12:17: "" is invalid for permission for all the scopes. available values are "read-all" and "write-all" [permissions] 3 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_inputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:9:18: input "input_not_ok" of workflow_call event has the default value "a", but it is also required. if an input is marked as required, its default value will never be used [events] 2 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_outputs_sema.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:20: property "some_output" is not defined in object type {} [expression] 2 | test.yaml:9:20: property "unknown_output" is not defined in object type {foo: string} [expression] 3 | -------------------------------------------------------------------------------- /testdata/examples/local_action_outputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:8:23: property "my_action" is not defined in object type {} [expression] 2 | test.yaml:15:23: property "some-value" is not defined in object type {some_value: string} [expression] 3 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/reusable/broken.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: my cool outputs 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo ... 10 | -------------------------------------------------------------------------------- /testdata/ok/issue-113.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: | 8 | if [[ -z ${{ env.FOO }} ]]; then 9 | echo "FOO is empty" 10 | fi 11 | -------------------------------------------------------------------------------- /testdata/ok/no_local_action_found.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: ./.github/action-does-not-exist 7 | with: 8 | foo: aaa 9 | bar: bbb 10 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/reusable/broken_secrets.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | secrets: my secrets... 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo ... 10 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/workflows/missing.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | upper: 5 | uses: ./reusable/upper.yaml 6 | with: 7 | my_input_1: hello 8 | secrets: 9 | my_secret_1: hello 10 | -------------------------------------------------------------------------------- /testdata/ok/issue-67.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # https://github.com/rhysd/actionlint/pull/67 7 | - run: echo '${{ runner.os }}' 8 | - run: echo '${{ runner.name }}' 9 | -------------------------------------------------------------------------------- /testdata/ok/multi_labels_no_conflict.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | os: [ubuntu-20.04, linux] 7 | runs-on: [ubuntu-latest, '${{matrix.os}}'] 8 | steps: 9 | - run: echo 10 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/reusable/broken_input.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | empty_input: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo ... 11 | -------------------------------------------------------------------------------- /testdata/ok/issue-205-closing-braces-in-string.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: ${{ fromJSON('{"foo":{}}') }} 8 | 9 | steps: 10 | - run: echo 'hello' 11 | -------------------------------------------------------------------------------- /testdata/projects/issue173/workflows/workflow1.yaml: -------------------------------------------------------------------------------- 1 | name: 'workflow 1' 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: ./action 10 | with: 11 | goodbye: bye 12 | -------------------------------------------------------------------------------- /testdata/examples/invalid_ids_in_needs.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:18: job ID "BAR" duplicates in "needs" section. note that job ID is case insensitive [job-needs] 2 | test.yaml:8:3: job "bar" needs job "unknown" which does not exist in this workflow [job-needs] 3 | -------------------------------------------------------------------------------- /testdata/ok/issue-45.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: | 7 | if [[ "${{ github.event.sender.login}}" == "rhysd" ]]; then 8 | echo "it's me" 9 | fi 10 | -------------------------------------------------------------------------------- /testdata/projects/issue173/workflows/workflow2.yaml: -------------------------------------------------------------------------------- 1 | name: 'workflow 2' 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: ./action 10 | with: 11 | goodbye: sayonara 12 | -------------------------------------------------------------------------------- /testdata/err/nested_untrusted_input.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: pull_request 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo ${{ github.event.pages[github.event.commits[github.event.issue.title].author.name].page_name }} 8 | -------------------------------------------------------------------------------- /testdata/err/object_at_runner_label.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | x: 8 | - foo: a 9 | - foo: c 10 | runs-on: ${{ matrix.x }} 11 | steps: 12 | - run: echo hi 13 | -------------------------------------------------------------------------------- /testdata/err/runner_labels_conflict_matrix.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | os: [windows-latest, macos-latest, windows] 7 | runs-on: [ubuntu-latest, '${{matrix.os}}'] 8 | steps: 9 | - run: echo ... 10 | -------------------------------------------------------------------------------- /testdata/examples/env_var_names.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:7: environment variable name "FOO=BAR" is invalid. '&', '=' and spaces should not be contained [env-var] 2 | test.yaml:7:7: environment variable name "FOO BAR" is invalid. '&', '=' and spaces should not be contained [env-var] 3 | -------------------------------------------------------------------------------- /testdata/err/issue207_work_dir_with_uses.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | foo: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | working-directory: ./foo 9 | - run: echo "$(pwd)" 10 | working-directory: ./foo 11 | -------------------------------------------------------------------------------- /testdata/examples/missing_required_keys.out: -------------------------------------------------------------------------------- 1 | test.yaml:3:3: "runs-on" section is missing in job "test" [syntax-check] 2 | test.yaml:8:9: key "VERSION_NAME" is duplicated in "matrix" section. previously defined at line:7,col:9. note that key names are case insensitive [syntax-check] 3 | -------------------------------------------------------------------------------- /testdata/ok/matrix_in_array_at_runs_on.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | host: ["macOS", "linux"] 8 | runs-on: ["self-hosted", "${{ matrix.host }}"] 9 | steps: 10 | - run: echo "hello, ${{ matrix.host }}" 11 | -------------------------------------------------------------------------------- /testdata/err/outputs_map_object.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: ./foo/bar 7 | id: foo 8 | # Check if the 'outputs' object is typed as {string => string} 9 | - run: echo ${{ steps.foo.outputs }} 10 | -------------------------------------------------------------------------------- /testdata/examples/invalid_ids_in_needs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | foo: 4 | needs: [bar, BAR] 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo 'hi' 8 | bar: 9 | needs: [unknown] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo 'hi' 13 | -------------------------------------------------------------------------------- /testdata/projects/issue173/action/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | hello: 7 | description: message 8 | required: true 9 | 10 | runs: 11 | using: 'node14' 12 | main: 'index.js' 13 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_missing_required.out: -------------------------------------------------------------------------------- 1 | workflows/test.yaml:5:11: input "required1" is required by "./workflows/reusable.yaml" reusable workflow [workflow-call] 2 | workflows/test.yaml:5:11: secret "required1" is required by "./workflows/reusable.yaml" reusable workflow [workflow-call] 3 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/workflows/ok_lower.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | upper: 5 | uses: ./reusable/upper.yaml 6 | with: 7 | my_input_1: hello 8 | my_input_2: world 9 | secrets: 10 | my_secret_1: hello 11 | my_secret_2: world 12 | -------------------------------------------------------------------------------- /cmd/actionlint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rhysd/actionlint" 7 | ) 8 | 9 | func main() { 10 | cmd := actionlint.Command{ 11 | Stdin: os.Stdin, 12 | Stdout: os.Stdout, 13 | Stderr: os.Stderr, 14 | } 15 | os.Exit(cmd.Main(os.Args)) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/examples/cron_schedule_check.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:13: invalid CRON format "0 */3 * *" in schedule event: Expected exactly 5 fields, found 4: 0 */3 * * [events] 2 | test.yaml:6:13: scheduled job runs too frequently. it runs once per 60 seconds. the shortest interval is once every 5 minutes [events] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /actionlint 2 | /.testtimestamp 3 | /.staticchecktimestamp 4 | /env.sh 5 | /.github/actionlint.yaml 6 | /.github/actionlint.yml 7 | /actionlint_fuzz-fuzz.zip 8 | /corpus 9 | /crashers 10 | /man/actionlint.1 11 | /man/actionlint.1.html 12 | /playground-dist 13 | /actionlint-workflow-ast 14 | -------------------------------------------------------------------------------- /testdata/action_metadata/input-duplicate/action.yml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | foo: 7 | description: ... 8 | FOO: 9 | description: ... 10 | 11 | runs: 12 | using: 'node14' 13 | main: 'index.js' 14 | -------------------------------------------------------------------------------- /testdata/action_metadata/output-duplicate/action.yml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | outputs: 6 | foo: 7 | description: ... 8 | FOO: 9 | description: ... 10 | 11 | runs: 12 | using: 'node14' 13 | main: 'index.js' 14 | -------------------------------------------------------------------------------- /testdata/examples/contextual_matrix_values.out: -------------------------------------------------------------------------------- 1 | /test\.yaml:19:24: property "platform" is not defined in object type {.+} \[expression\]/ 2 | /test\.yaml:21:24: property "dev" is not defined in object type {.+} \[expression\]/ 3 | test.yaml:34:24: property "os" is not defined in object type {} [expression] 4 | -------------------------------------------------------------------------------- /testdata/ok/issue-31.yaml: -------------------------------------------------------------------------------- 1 | name: Create new release 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | create_release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: ncipollo/release-action@v1 11 | with: 12 | allowUpdates: false 13 | -------------------------------------------------------------------------------- /testdata/err/issue193.out: -------------------------------------------------------------------------------- 1 | test.yaml:8:42: got unexpected character '"' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' '. do you mean string literals? only single quotes are available for string delimiter [expression] 2 | -------------------------------------------------------------------------------- /testdata/err/one_error.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 2 | -------------------------------------------------------------------------------- /testdata/err/issue170_empty_permissions.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | permissions: {} 4 | 5 | jobs: 6 | job1: 7 | permissions: {} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo '1' 11 | job2: 12 | permissions: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo '2' 16 | -------------------------------------------------------------------------------- /testdata/ok/shell_name_case_insensitive.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | jobs: 5 | test: 6 | runs-on: windows-latest 7 | steps: 8 | - run: echo hi 9 | shell: powershell 10 | - run: echo hi 11 | shell: PowerShell 12 | - run: echo hi 13 | shell: POWERSHELL 14 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_not_found/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller: 5 | uses: ./workflows/this-workflow-does-not-exist.yaml 6 | other: 7 | needs: [caller] 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo 'Unknown output ${{ needs.caller.outputs.foo }}' 11 | -------------------------------------------------------------------------------- /testdata/examples/hardcoded_credentials.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:19: "password" section in "container" section should be specified via secrets. do not put password value directly [credentials] 2 | test.yaml:17:21: "password" section in "redis" service should be specified via secrets. do not put password value directly [credentials] 3 | -------------------------------------------------------------------------------- /testdata/examples/shellcheck_integration.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck] 2 | test.yaml:14:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck] 3 | -------------------------------------------------------------------------------- /testdata/ok/allow_any_outputs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: dorny/paths-filter@v2 8 | id: filter 9 | with: 10 | filters: ... 11 | - run: npm run lint:md 12 | if: ${{ steps.filter.outputs.md == 'true' }} 13 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_inherit_secrets/workflows/reusable.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | secrets: 4 | bar: 5 | required: false 6 | piyo: 7 | required: true 8 | foo: 9 | 10 | jobs: 11 | callee: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: hello 15 | -------------------------------------------------------------------------------- /testdata/examples/popular_action_inputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:15: missing input "key" which is required by action "actions/cache@v3". all required inputs are "key", "path" [action] 2 | test.yaml:9:11: input "keys" is not defined in action "actions/cache@v3". available inputs are "key", "path", "restore-keys", "upload-chunk-size" [action] 3 | -------------------------------------------------------------------------------- /testdata/ok/issue-152.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | my_input: 5 | type: string 6 | required: true 7 | 8 | jobs: 9 | my_job: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo ${{ github.event.inputs.my_input }} 13 | - run: echo ${{ inputs.my_input }} 14 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_secrets.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | secrets: 4 | secret0: 5 | description: 'test' 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | # OK 12 | - run: echo ${{ secrets.secret0 }} 13 | # ERROR 14 | - run: echo ${{ secrets.secret1 }} 15 | -------------------------------------------------------------------------------- /testdata/examples/cron_schedule_check.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | # Cron syntax is not correct 4 | - cron: '0 */3 * *' 5 | # Interval of scheduled job is too small (job runs too frequently) 6 | - cron: '* */3 * * *' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo ... 13 | -------------------------------------------------------------------------------- /testdata/ok/workflow_dispatch_upper_case_inputs.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | logLevel: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo '${{ inputs.logLevel }}' 11 | - run: echo '${{ inputs.loglevel }}' 12 | - run: echo '${{ inputs.LOGLEVEL }}' 13 | -------------------------------------------------------------------------------- /testdata/err/github_script_untrusted_input.out: -------------------------------------------------------------------------------- 1 | test.yaml:11:162: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 2 | -------------------------------------------------------------------------------- /testdata/examples/missing_required_keys.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | # ERROR: Matrix name is duplicated. keys are case insensitive 6 | matrix: 7 | version_name: [v1, v2] 8 | VERSION_NAME: [V1, V2] 9 | # ERROR: runs-on is missing 10 | steps: 11 | - run: echo 'hello' 12 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/test.jsonl: -------------------------------------------------------------------------------- 1 | {"spec":"rhysd/action-setup-vim@v1","metadata":{"name":"Setup Vim","inputs":{"neovim":{"name":"neovim","required":false},"token":{"name":"token","required":false},"version":{"name":"version","required":false}},"outputs":{"executable":{"name":"executable"}},"skip_inputs":false,"skip_outputs":false}} 2 | -------------------------------------------------------------------------------- /testdata/err/cron_5minutes_limit.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | # It's OK. The interval can be every 5 minutes. 4 | - cron: '*/5 * * * *' 5 | # It's bad. The interval can't be less than every 5 minutes. 6 | - cron: '*/4 * * * *' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo ... 13 | -------------------------------------------------------------------------------- /testdata/examples/.github/actions/my-action-with-output/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action with output' 2 | author: 'rhysd ' 3 | description: 'my action with outputs' 4 | 5 | outputs: 6 | some_value: 7 | description: some value returned from this action 8 | 9 | runs: 10 | using: 'node14' 11 | main: 'index.js' 12 | -------------------------------------------------------------------------------- /testdata/examples/workflow_inputs_secrets_types.out: -------------------------------------------------------------------------------- 1 | test.yaml:20:23: property "uri" is not defined in object type {lucky_number: number; url: string} [expression] 2 | test.yaml:23:22: property "credentials" is not defined in object type {actions_runner_debug: string; actions_step_debug: string; credential: string; github_token: string} [expression] 3 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/skip_inputs.jsonl: -------------------------------------------------------------------------------- 1 | {"spec":"rhysd/action-setup-vim@v1","metadata":{"name":"Setup Vim","inputs":{"neovim":{"name":"neovim","required":false},"token":{"name":"token","required":false},"version":{"name":"version","required":false}},"outputs":{"executable":{"name":"executable"}},"skip_inputs":true,"skip_outputs":false}} 2 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/skip_outputs.jsonl: -------------------------------------------------------------------------------- 1 | {"spec":"rhysd/action-setup-vim@v1","metadata":{"name":"Setup Vim","inputs":{"neovim":{"name":"neovim","required":false},"token":{"name":"token","required":false},"version":{"name":"version","required":false}},"outputs":{"executable":{"name":"executable"}},"skip_inputs":false,"skip_outputs":true}} 2 | -------------------------------------------------------------------------------- /testdata/ok/issue-174-secret-description-is-optional.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/rhysd/actionlint/issues/174 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | MY_TOKEN: 7 | 8 | jobs: 9 | main: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo hello 13 | env: 14 | GH_TOKEN: ${{ secrets.MY_TOKEN }} 15 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_undefined/workflows/reusable.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | foo: 5 | type: string 6 | outputs: 7 | bar: 8 | value: 'hello' 9 | secrets: 10 | piyo: 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo 'bye' 17 | -------------------------------------------------------------------------------- /testdata/examples/job_step_ids_duplicate.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:13: step ID "STEP_ID" duplicates. previously defined at line:7,col:13. step ID must be unique within a job. note that step ID is case insensitive [id] 2 | test.yaml:12:3: key "TEST" is duplicated in "jobs" section. previously defined at line:3,col:3. note that key names are case insensitive [syntax-check] 3 | -------------------------------------------------------------------------------- /testdata/examples/popular_action_inputs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/cache@v3 8 | with: 9 | keys: | 10 | ${{ hashFiles('**/*.lock') }} 11 | ${{ hashFiles('**/*.cache') }} 12 | path: ./packages 13 | - run: make 14 | -------------------------------------------------------------------------------- /testdata/projects/local_action_case_insensitive/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: ./action 8 | with: 9 | name: rhysd 10 | message: hello 11 | id: my_action 12 | - run: echo 'User ID is ${{ steps.my_action.outputs.user_id }}' 13 | -------------------------------------------------------------------------------- /playground/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "printWidth": 120, 7 | "arrowParens": "avoid", 8 | "overrides": [ 9 | { 10 | "files": "*.css", 11 | "options": { 12 | "tabWidth": 2, 13 | "printWidth": -1 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /testdata/examples/pyflakes_integration.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:9: pyflakes reported issue in this script: 1:7: undefined name 'hello' [pyflakes] 2 | test.yaml:19:9: pyflakes reported issue in this script: 2:5: import 'sys' from line 1 shadowed by loop variable [pyflakes] 3 | test.yaml:23:9: pyflakes reported issue in this script: 1:1: 'time.sleep' imported but unused [pyflakes] 4 | -------------------------------------------------------------------------------- /testdata/ok/issue-104.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - id: test 8 | uses: actions/github-script@v7 9 | with: 10 | script: | 11 | core.setOutput('foo', 'bar'); 12 | - id: call 13 | run: | 14 | echo "Test output: ${{ steps.test.outputs.foo }}" 15 | -------------------------------------------------------------------------------- /testdata/examples/local_action_inputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:15: missing input "message" which is required by action "My action" defined at "./.github/actions/my-action". all required inputs are "message" [action] 2 | test.yaml:13:11: input "additions" is not defined in action "My action" defined at "./.github/actions/my-action". available inputs are "addition", "message", "name" [action] 3 | -------------------------------------------------------------------------------- /testdata/ok/reusable_workflow_inherit_secrets.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | jobs: 5 | pass-secret-to-action: 6 | runs-on: ubuntu-latest 7 | steps: 8 | # The CALLING_WORKFLOW_SECRET secret is passed with `secrets: inherit` 9 | - name: Use a repo or org secret from the calling workflow. 10 | uses: echo ${{ secrets.CALLING_WORKFLOW_SECRET }} 11 | -------------------------------------------------------------------------------- /testdata/ok/run_name.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#run-name 2 | # https://github.blog/changelog/2022-09-26-github-actions-dynamic-names-for-workflow-runs/ 3 | run-name: Deploy by @${{ github.actor }} 4 | 5 | on: push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo hello 12 | -------------------------------------------------------------------------------- /testdata/projects/broken_local_action/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-18.04 6 | steps: 7 | - uses: ./action/broken_input 8 | with: 9 | hello: world 10 | - uses: ./action/broken_output 11 | - uses: ./action/duplicate_input 12 | with: 13 | foo: ... 14 | - uses: ./action/duplicate_output 15 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller1: 5 | uses: ./reusable/broken.yaml 6 | caller2: 7 | uses: ./reusable/no_hook.yaml 8 | caller3: 9 | uses: ./reusable/no_on.yaml 10 | caller4: 11 | uses: ./reusable/broken_input.yaml 12 | with: 13 | empty_input: 14 | caller5: 15 | uses: ./reusable/broken_secrets.yaml 16 | -------------------------------------------------------------------------------- /testdata/examples/local_action_inputs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # missing required input "message" 7 | - uses: ./.github/actions/my-action 8 | # unexpected input "additions" 9 | - uses: ./.github/actions/my-action 10 | with: 11 | name: rhysd 12 | message: hello 13 | additions: foo, bar 14 | -------------------------------------------------------------------------------- /testdata/projects/local_action_case_insensitive/action/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | NAME: 7 | description: your name 8 | MESSAGE: 9 | description: message to this action 10 | 11 | outputs: 12 | USER_ID: 13 | description: user ID 14 | 15 | runs: 16 | using: 'node16' 17 | main: 'index.js' 18 | -------------------------------------------------------------------------------- /testdata/examples/matrix_checks.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:28: duplicate value "14" is found in matrix "node". the same value is at line:6,col:24 [matrix] 2 | test.yaml:9:19: value "13" in "exclude" does not exist in matrix "node" combinations. possible values are "10", "12", "14", "14" [matrix] 3 | test.yaml:12:13: "platform" in "exclude" section does not exist in matrix. available matrix configurations are "node", "os" [matrix] 4 | -------------------------------------------------------------------------------- /testdata/examples/matrix_checks.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | node: [10, 12, 14, 14] 7 | os: [ubuntu-latest, macos-latest] 8 | exclude: 9 | - node: 13 10 | os: ubuntu-latest 11 | - node: 10 12 | platform: ubuntu-latest 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - run: echo ... 16 | -------------------------------------------------------------------------------- /testdata/examples/unexpected_keys.out: -------------------------------------------------------------------------------- 1 | test.yaml:3:3: "steps" section is missing in job "test" [syntax-check] 2 | test.yaml:5:5: unexpected key "step" for "job" section. expected one of "concurrency", "container", "continue-on-error", "defaults", "env", "environment", "if", "name", "needs", "outputs", "permissions", "runs-on", "secrets", "services", "steps", "strategy", "timeout-minutes", "uses", "with" [syntax-check] 3 | -------------------------------------------------------------------------------- /testdata/examples/unexpected_mapping_values.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:18: expecting a single ${{...}} expression or boolean literal "true" or "false", but found plain text node [syntax-check] 2 | test.yaml:8:21: expected scalar node for integer value but found scalar node with "!!float" tag [syntax-check] 3 | test.yaml:13:26: expecting a single ${{...}} expression or float number literal, but found plain text node [syntax-check] 4 | -------------------------------------------------------------------------------- /testdata/examples/cyclic_deps_needs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | prepare: 4 | needs: [build] 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo 'prepare' 8 | install: 9 | needs: [prepare] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo 'install' 13 | build: 14 | needs: [install] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: echo 'build' 18 | -------------------------------------------------------------------------------- /testdata/ok/issue-30.yaml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | myjob: 5 | environment: 6 | name: env-name 7 | url: ${{ steps.thing.outputs.app-url }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 'Run Azure Functions Action' 11 | uses: Azure/functions-action@v1.4.0 12 | id: thing 13 | with: 14 | app-name: 'my-function-app' 15 | package: my.zip 16 | -------------------------------------------------------------------------------- /testdata/examples/unexpected_mapping_values.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | # ERROR: Boolean value "true" or "false" is expected 6 | fail-fast: off 7 | # ERROR: Integer value is expected 8 | max-parallel: 1.5 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: sleep 200 12 | # ERROR: Float value is expected 13 | timeout-minutes: two minutes 14 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow/README.md: -------------------------------------------------------------------------------- 1 | Reusable workflows are separate in [`reusable`](./reusable) otherwise errors are not deterministic. When some broken workflow is parsed first, it causes parse error `ReusableWorkflowMetadata` is created from `WorkflowCallEvent` AST node. But when `test.yaml` is parsed first, `ReusableWorkflowMetadata` instance is parsed in `reusable_workflow.go` and causes its own parse error. 2 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_input_type_check/workflows/reusable.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | str_input: 5 | type: string 6 | bool_input: 7 | type: boolean 8 | num_input: 9 | type: number 10 | broken_input: 11 | description: type is missing 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: 'bye' 18 | -------------------------------------------------------------------------------- /testdata/ok/using_reusable_workflow_call_outputs.yaml: -------------------------------------------------------------------------------- 1 | name: Call a reusable workflow and use its outputs 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | job1: 8 | uses: octo-org/example-repo/.github/workflows/called-workflow.yml@v1 9 | 10 | job2: 11 | runs-on: ubuntu-latest 12 | needs: job1 13 | steps: 14 | - run: echo ${{ needs.job1.outputs.firstword }} ${{ needs.job1.outputs.secondword }} 15 | -------------------------------------------------------------------------------- /testdata/ok/shellcheck_explicit_shell_name.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | os: [ubuntu-latest, windows-latest] 7 | runs-on: ${{ matrix.os }} 8 | steps: 9 | - name: Show file content 10 | # This causes SC1001 if `shell: pwsh` is not set explicitly 11 | run: cat xxx\yyy.txt 12 | if: ${{ matrix.os == 'windows-latest' }} 13 | shell: pwsh 14 | -------------------------------------------------------------------------------- /testdata/err/reusable_workflow_empty_secrets.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | # actionlint assumes this workflow takes no secret value. 4 | secrets: 5 | 6 | jobs: 7 | pass-secret-to-action: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Use a repo or org secret from the calling workflow. 11 | # So referring this secret causes an error 12 | uses: echo ${{ secrets.CALLING_WORKFLOW_SECRET }} 13 | -------------------------------------------------------------------------------- /testdata/examples/invalid_action_format.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # ERROR: ref is missing 7 | - uses: actions/checkout 8 | # ERROR: owner name is missing 9 | - uses: checkout@v2 10 | # ERROR: tag is empty 11 | - uses: 'docker://image:' 12 | # ERROR: local action must start with './' 13 | - uses: .github/my-actions/do-something 14 | -------------------------------------------------------------------------------- /testdata/err/invalid_event_filters.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request_review: 3 | types: submitted 4 | paths: /path/to/foo.txt 5 | paths-ignore: /path/to/bar.txt 6 | branches: main 7 | branches-ignore: test 8 | tags: v*.*.* 9 | tags-ignore: deploy/* 10 | pull_request: 11 | tags: v*.*.* 12 | tags-ignore: deploy/* 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: echo hello 19 | -------------------------------------------------------------------------------- /testdata/err/glob_more.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - '**' 5 | - 'release/v12' 6 | - '[x]' 7 | - ' ' 8 | tags: 9 | - '!*' 10 | - /v\d+/ 11 | - v[0- 12 | - v[9-0] 13 | paths: 14 | - '!' 15 | - 'foo\bar' 16 | - ' ' 17 | - ' foo' 18 | - 'foo ' 19 | - 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - run: echo hi 26 | -------------------------------------------------------------------------------- /testdata/examples/permissions.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | # ERROR: Available values for whole permissions are "write-all", "read-all" or "none" 4 | permissions: write 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | # ERROR: "checks" is correct scope name 11 | check: write 12 | # ERROR: Available values are "read", "write" or "none" 13 | issues: readable 14 | steps: 15 | - run: echo hello 16 | -------------------------------------------------------------------------------- /scripts/generate-webhook-events/testdata/no_heading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test 3 | --- 4 | 5 | Wow 6 | 7 | ### `check_run` 8 | 9 | Aaaa aaaa 10 | 11 | | Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` | 12 | | --------------------- | -------------- | ------------ | -------------| 13 | | [`check_run`](/webhooks/event-payloads/#check_run) | - `created`
- `rerequested`
- `completed` | Last commit on default branch | Default branch | 14 | -------------------------------------------------------------------------------- /fuzz/glob.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | package actionlint_fuzz 3 | 4 | import ( 5 | "github.com/rhysd/actionlint" 6 | ) 7 | 8 | func FuzzGlobGitRef(data []byte) int { 9 | errs := actionlint.ValidateRefGlob(string(data)) 10 | if len(errs) > 0 { 11 | return 0 12 | } 13 | return 1 14 | } 15 | 16 | func FuzzGlobFilePath(data []byte) int { 17 | errs := actionlint.ValidatePathGlob(string(data)) 18 | if len(errs) > 0 { 19 | return 0 20 | } 21 | return 1 22 | } 23 | -------------------------------------------------------------------------------- /testdata/examples/job_step_ids_duplicate.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo 'hello' 7 | id: step_id 8 | - run: echo 'bye' 9 | # ERROR: Duplicate step ID 10 | id: STEP_ID 11 | # ERROR: Duplicate job ID 12 | TEST: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo 'hello' 16 | # OK. Step ID uniqueness is job-local 17 | id: step_id 18 | -------------------------------------------------------------------------------- /testdata/ok/filters_for_specific_events.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: main 4 | tags: v*.*.* 5 | paths: path/to/foo 6 | pull_request: 7 | branches: main 8 | paths: path/to/foo 9 | pull_request_target: 10 | branches: main 11 | paths-ignore: path/to/foo 12 | workflow_run: 13 | workflows: foo.yaml 14 | branches: main 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: echo ... 21 | -------------------------------------------------------------------------------- /testdata/ok/issue-164.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/rhysd/actionlint/issues/164 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | include: 10 | - msg: hello macos 11 | host-labels: ["self-hosted", "macOS", "X64"] 12 | - msg: hello linux 13 | host-labels: ["self-hosted", "linux"] 14 | runs-on: ${{ matrix.host-labels }} 15 | steps: 16 | - run: echo "${{ matrix.msg }}" 17 | -------------------------------------------------------------------------------- /testdata/examples/shellcheck_integration.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo $FOO 7 | test-win: 8 | runs-on: windows-latest 9 | steps: 10 | # Shell on Windows is PowerShell by default. 11 | # shellcheck is not run in this case. 12 | - run: echo $FOO 13 | # This script is run with bash due to 'shell:' configuration 14 | - run: echo $FOO 15 | shell: bash 16 | -------------------------------------------------------------------------------- /testdata/examples/expression_syntax_error.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # " is not available for string literal delimiter 7 | - run: echo '${{ "hello" }}' 8 | # + operator does not exist 9 | - run: echo '${{ 1 + 1 }}' 10 | # Missing ')' paren 11 | - run: echo "${{ toJson(hashFiles('**/lock', '**/cache/') }}" 12 | # unexpected end of input 13 | - run: echo '${{ github.event. }}' 14 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/workflows/ok_upper.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | upper: 5 | uses: ./reusable/upper.yaml 6 | with: 7 | MY_INPUT_1: hello 8 | MY_INPUT_2: world 9 | secrets: 10 | MY_SECRET_1: hello 11 | MY_SECRET_2: world 12 | lower: 13 | uses: ./reusable/lower.yaml 14 | with: 15 | MY_INPUT_1: hello 16 | MY_INPUT_2: world 17 | secrets: 18 | MY_SECRET_1: hello 19 | MY_SECRET_2: world 20 | -------------------------------------------------------------------------------- /scripts/generate-actionlint-matcher/main.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const object = require('./object.js'); 3 | 4 | async function main(args) { 5 | const json = JSON.stringify(object, null, 2); 6 | if (args.length === 0) { 7 | console.log(json); 8 | } else { 9 | const path = args[0]; 10 | await fs.writeFile(args[0], json + '\n', 'utf8'); 11 | console.log(`Wrote to ${path}`); 12 | } 13 | } 14 | 15 | main(process.argv.slice(2)); 16 | -------------------------------------------------------------------------------- /testdata/examples/contextual_needs_object.out: -------------------------------------------------------------------------------- 1 | test.yaml:16:24: property "prepare" is not defined in object type {} [expression] 2 | test.yaml:26:24: property "foo" is not defined in object type {installed: string} [expression] 3 | test.yaml:28:24: property "some_job" is not defined in object type {install: {outputs: {installed: string}; result: string}; prepare: {outputs: {prepared: string}; result: string}} [expression] 4 | test.yaml:33:24: property "build" is not defined in object type {} [expression] 5 | -------------------------------------------------------------------------------- /testdata/projects/example_workflow_call_outputs_downstream_jobs/.github/workflows/get-build-info.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/get-build-info.yaml 2 | on: 3 | workflow_call: 4 | outputs: 5 | version: 6 | value: ${{ outputs.version }} 7 | description: version of software 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | outputs: 13 | version: ${{ steps.get_version.outputs.version }} 14 | steps: 15 | - run: ... 16 | id: get_version 17 | -------------------------------------------------------------------------------- /testdata/examples/type_checks.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:28: property access of object must be type of string but got "number" [expression] 2 | test.yaml:9:24: property "os" is not defined in object type {id: string; network: string} [expression] 3 | test.yaml:11:24: receiver of object dereference "owner" must be type of object but got "string" [expression] 4 | test.yaml:13:20: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type {string => string} [expression] 5 | -------------------------------------------------------------------------------- /testdata/examples/glob.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | # ^ is not available for branch name. This kind of mistake is usually caused by misunderstanding 5 | # that regular expression is available here 6 | - '^foo-' 7 | tags: 8 | # Invalid syntax. + cannot follow special character * 9 | - 'v*+' 10 | # Invalid character range 9-1 11 | - 'v[9-1]' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: echo ... 18 | -------------------------------------------------------------------------------- /fuzz/expr.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | package actionlint_fuzz 3 | 4 | import ( 5 | "unicode/utf8" 6 | 7 | "github.com/rhysd/actionlint" 8 | ) 9 | 10 | func FuzzExprParse(data []byte) int { 11 | if !utf8.Valid(data) { 12 | return 0 13 | } 14 | 15 | l := actionlint.NewExprLexer(string(data)) 16 | p := actionlint.NewExprParser() 17 | e, err := p.Parse(l) 18 | if err != nil { 19 | return 0 20 | } 21 | 22 | c := actionlint.NewExprSemanticsChecker(true) 23 | c.Check(e) 24 | 25 | return 1 26 | } 27 | -------------------------------------------------------------------------------- /scripts/generate-webhook-events/testdata/no_hook_name_link.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test 3 | --- 4 | 5 | ## Available events 6 | 7 | ### `check_run` 8 | 9 | oops first cell does not contain [`check_run`](/webhooks/event-payloads/#check_run)! 10 | 11 | | Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` | 12 | | --------------------- | -------------- | ------------ | -------------| 13 | | wow | - `created`
- `rerequested`
- `completed` | Last commit on default branch | Default branch | 14 | -------------------------------------------------------------------------------- /testdata/examples/reusable_workflow_outputs.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | image-version: 5 | description: "Docker image version" 6 | # ERROR: 'imagetag' does not exist (typo of 'image_tag') 7 | value: ${{ jobs.gen-image-version.outputs.imagetag }} 8 | jobs: 9 | gen-image-version: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | image_tag: "${{ steps.get_tag.outputs.tag }}" 13 | steps: 14 | - run: ./output_image_tag.sh 15 | id: get_tag 16 | -------------------------------------------------------------------------------- /testdata/ok/filters_ignore_for_specific_events.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches-ignore: test 4 | tags-ignore: deploy/* 5 | paths-ignore: path/to/foo 6 | pull_request: 7 | branches-ignore: test 8 | paths-ignore: path/to/foo 9 | pull_request_target: 10 | branches-ignore: test 11 | paths-ignore: path/to/foo 12 | workflow_run: 13 | workflows: foo.yaml 14 | branches-ignore: test 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: echo ... 21 | -------------------------------------------------------------------------------- /testdata/projects/example_inputs_secrets_in_workflow_call/.github/workflows/reusable.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/reusable.yaml 2 | on: 3 | workflow_call: 4 | inputs: 5 | name: 6 | type: string 7 | required: true 8 | id: 9 | type: number 10 | message: 11 | type: string 12 | secrets: 13 | password: 14 | required: true 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: echo '${{ outputs.required_input }}' 21 | -------------------------------------------------------------------------------- /scripts/generate-webhook-events/testdata/no_hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test 3 | --- 4 | 5 | Wow 6 | 7 | ## Manual events 8 | 9 | Foo 10 | 11 | ### `workflow_dispatch` 12 | 13 | | Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` | 14 | | ------------------ | ------------ | ------------ | ------------------| 15 | | [workflow_dispatch](/webhooks/event-payloads/#workflow_dispatch) | n/a | Last commit on the `GITHUB_REF` branch | Branch that received dispatch | 16 | 17 | ## Available events 18 | 19 | Bar 20 | -------------------------------------------------------------------------------- /testdata/action_metadata/action-yaml/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | name: 7 | description: your name 8 | default: anonymous 9 | message: 10 | description: message to this action 11 | required: true 12 | addition: 13 | description: additional information 14 | required: false 15 | 16 | outputs: 17 | user_id: 18 | description: user ID 19 | 20 | runs: 21 | using: 'node14' 22 | main: 'index.js' 23 | -------------------------------------------------------------------------------- /testdata/action_metadata/action-yml/action.yml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | name: 7 | description: your name 8 | default: anonymous 9 | message: 10 | description: message to this action 11 | required: true 12 | addition: 13 | description: additional information 14 | required: false 15 | 16 | outputs: 17 | user_id: 18 | description: user ID 19 | 20 | runs: 21 | using: 'node14' 22 | main: 'index.js' 23 | -------------------------------------------------------------------------------- /testdata/action_metadata/uppercase/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | NAME: 7 | description: your name 8 | default: anonymous 9 | MESSAGE: 10 | description: message to this action 11 | required: true 12 | ADDITION: 13 | description: additional information 14 | required: false 15 | 16 | outputs: 17 | USER_ID: 18 | description: user ID 19 | 20 | runs: 21 | using: 'node14' 22 | main: 'index.js' 23 | -------------------------------------------------------------------------------- /testdata/examples/hardcoded_credentials.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | container: 6 | image: 'example.com/owner/image' 7 | credentials: 8 | username: user 9 | # ERROR: Hardcoded password 10 | password: pass 11 | services: 12 | redis: 13 | image: redis 14 | credentials: 15 | username: user 16 | # ERROR: Hardcoded password 17 | password: pass 18 | steps: 19 | - run: echo 'hello' 20 | -------------------------------------------------------------------------------- /scripts/generate-actionlint-matcher/test/no_escape.txt: -------------------------------------------------------------------------------- 1 | ./testdata/err/one_error.yaml:6:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 2 | | 3 | 6 | - run: echo "Checking commit '${{ github.event.head_commit.message }}'" 4 | | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /testdata/err/runner_labels_conflict_matrix.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:14: label "windows-latest" conflicts with label "ubuntu-latest" defined at line:7,col:15. note: to run your job on each workers, use matrix [runner-label] 2 | test.yaml:6:30: label "macos-latest" conflicts with label "ubuntu-latest" defined at line:7,col:15. note: to run your job on each workers, use matrix [runner-label] 3 | test.yaml:6:44: label "windows" conflicts with label "ubuntu-latest" defined at line:7,col:15. note: to run your job on each workers, use matrix [runner-label] 4 | -------------------------------------------------------------------------------- /testdata/examples/.github/actions/my-action/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'My action' 2 | author: 'rhysd ' 3 | description: 'my action' 4 | 5 | inputs: 6 | name: 7 | description: your name 8 | default: anonymous 9 | message: 10 | description: message to this action 11 | required: true 12 | addition: 13 | description: additional information 14 | required: false 15 | 16 | outputs: 17 | user_id: 18 | description: user ID 19 | 20 | runs: 21 | using: 'node14' 22 | main: 'index.js' 23 | -------------------------------------------------------------------------------- /testdata/examples/shell_name_validation.out: -------------------------------------------------------------------------------- 1 | test.yaml:8:16: shell name "dash" is invalid. available names are "bash", "pwsh", "python", "sh" [shell-name] 2 | test.yaml:11:16: shell name "powershell" is invalid on macOS or Linux. available names are "bash", "pwsh", "python", "sh" [shell-name] 3 | test.yaml:17:16: shell name "fish" is invalid. available names are "bash", "pwsh", "python", "sh" [shell-name] 4 | test.yaml:27:16: shell name "sh" is invalid on Windows. available names are "bash", "cmd", "powershell", "pwsh", "python" [shell-name] 5 | -------------------------------------------------------------------------------- /testdata/err/github_script_untrusted_input.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | issues: 3 | types: [opened] 4 | 5 | jobs: 6 | comment: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/github-script@v4 10 | with: 11 | script: | 12 | github.issues.createComment({ 13 | issue_number: context.issue.number, 14 | owner: context.repo.owner, 15 | repo: context.repo.repo, 16 | body: 'Hello, ${{github.event.head_commit.author.name}}!' 17 | }) 18 | -------------------------------------------------------------------------------- /testdata/ok/operator_outside_expr.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | ver: [0, 1] 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo 'my repo' 10 | # This was not ok until https://github.com/github/docs/pull/8786 11 | if: contains(github.repository, 'rhysd') 12 | - run: echo 'my repo' 13 | # This was not ok until https://github.com/github/docs/pull/8786 14 | if: github.repository != 'rhysd/foo' && matrix.ver > 0 || !(github.token == '') 15 | -------------------------------------------------------------------------------- /testdata/projects/example_workflow_call_outputs_downstream_jobs/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | get_build_info: 5 | uses: ./.github/workflows/get-build-info.yaml 6 | downstream: 7 | needs: [get_build_info] 8 | runs-on: ubuntu-latest 9 | steps: 10 | # OK. `version` is defined in the reusable workflow 11 | - run: echo '${{ needs.get_build_info.outputs.version }}' 12 | # ERROR: `tag` is not defined in the reusable workflow 13 | - run: echo '${{ needs.get_build_info.outputs.tag }}' 14 | -------------------------------------------------------------------------------- /testdata/ok/issue-145.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | services: 5 | required: true 6 | type: string 7 | description: contents of services.json 8 | 9 | jobs: 10 | build-lint-test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | service: ${{ fromJSON(inputs.services) }} 15 | exclude: 16 | # `service` is loaded from JSON. Check should be skipped 17 | - service: 18 | npm_project: foo 19 | steps: 20 | - run: echo 1 21 | -------------------------------------------------------------------------------- /playground/lib.d.ts: -------------------------------------------------------------------------------- 1 | interface ActionlintError { 2 | kind: string; 3 | message: string; 4 | line: number; 5 | column: number; 6 | } 7 | 8 | interface Window { 9 | runActionlint?(src: string): void; 10 | getYamlSource(): string; 11 | showError(msg: string): void; 12 | onCheckCompleted(errs: ActionlintError[]): void; 13 | dismissLoading(): void; 14 | } 15 | 16 | declare class Go { 17 | importObject: Imports; 18 | run(mod: Instance): Promise; 19 | } 20 | 21 | declare const isMobile: IsMobile.isMobileResult; 22 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/skip_inputs_want.go: -------------------------------------------------------------------------------- 1 | // Code generated by actionlint/scripts/generate-popular-actions. DO NOT EDIT. 2 | 3 | package actionlint 4 | 5 | // PopularActions is data set of known popular actions. Keys are specs (owner/repo@ref) of actions 6 | // and values are their metadata. 7 | var PopularActions = map[string]*ActionMetadata{ 8 | "rhysd/action-setup-vim@v1": { 9 | Name: "Setup Vim", 10 | SkipInputs: true, 11 | Outputs: ActionMetadataOutputs{ 12 | "executable": {"executable"}, 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /testdata/ok/issue-87.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Code Base 2 | 3 | on: 4 | push: 5 | workflow_call: 6 | 7 | jobs: 8 | build: 9 | name: Lint Code Base 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Code 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Lint Code Base 19 | uses: github/super-linter@v4 20 | env: 21 | VALIDATE_ALL_CODEBASE: false 22 | DEFAULT_BRANCH: master 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/reusable/lower.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | my_input_1: 5 | required: true 6 | my_input_2: 7 | required: true 8 | outputs: 9 | my_output_1: 10 | value: ... 11 | my_output_2: 12 | value: ... 13 | secrets: 14 | my_secret_1: 15 | required: true 16 | my_secret_2: 17 | required: true 18 | 19 | jobs: 20 | test: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo '${{ inputs.my_input_1 }}' 24 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/reusable/upper.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | MY_INPUT_1: 5 | required: true 6 | MY_INPUT_2: 7 | required: true 8 | outputs: 9 | MY_OUTPUT_1: 10 | value: ... 11 | MY_OUTPUT_2: 12 | value: ... 13 | secrets: 14 | MY_SECRET_1: 15 | required: true 16 | MY_SECRET_2: 17 | required: true 18 | 19 | jobs: 20 | test: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo '${{ inputs.my_input_1 }}' 24 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: actionlint 3 | name: Lint GitHub Actions workflow files 4 | description: Runs actionlint to lint GitHub Actions workflow files 5 | language: system 6 | types: ["yaml"] 7 | files: "^.github/workflows/" 8 | entry: actionlint 9 | - id: actionlint-docker 10 | name: Lint GitHub Actions workflow file Docker 11 | description: Runs actionlint Docker image to lint GitHub Actions workflow files 12 | language: docker_image 13 | types: ["yaml"] 14 | files: "^.github/workflows/" 15 | entry: rhysd/actionlint:1.6.19 16 | -------------------------------------------------------------------------------- /testdata/examples/expand_object.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | env_string: 7 | - 'FOO=BAR' 8 | - 'FOO=PIYO' 9 | env_object: 10 | - FOO: BAR 11 | - FOO: PIYO 12 | runs-on: ubuntu-latest 13 | steps: 14 | # OK: Expanding object at 'env:' section 15 | - run: echo "$FOO" 16 | env: ${{ matrix.env_object }} 17 | # ERROR: String value cannot be expanded as object 18 | - run: echo "$FOO" 19 | env: ${{ matrix.env_string }} 20 | -------------------------------------------------------------------------------- /testdata/projects/broken_local_action.out: -------------------------------------------------------------------------------- 1 | /workflows/test\.yaml:7:15: could not parse action metadata in ".+broken_input": yaml: .+ \[action\]/ 2 | /workflows/test\.yaml:10:15: could not parse action metadata in ".+broken_output": yaml: outputs must be mapping node but scalar node was found at line:4, col:10 \[action\]/ 3 | /workflows/test\.yaml:11:15: could not parse action metadata in ".+duplicate_input": input "FOO" is duplicated \[action\]/ 4 | /workflows/test\.yaml:14:15: could not parse action metadata in ".+duplicate_output": output "FOO" is duplicated \[action\]/ 5 | -------------------------------------------------------------------------------- /scripts/generate-actionlint-matcher/test/want.json: -------------------------------------------------------------------------------- 1 | [{"message":"\"github.event.head_commit.message\" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details","filepath":"./testdata/err/one_error.yaml","line":6,"column":41,"kind":"expression","snippet":" - run: echo \"Checking commit '${{ github.event.head_commit.message }}'\"\n ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"}] 2 | -------------------------------------------------------------------------------- /testdata/examples/local_action_outputs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | # ERROR: The step is not yet run 8 | - run: echo ${{ steps.my_action.outputs.some_value }} 9 | # The action runs here and sets its outputs 10 | - uses: ./.github/actions/my-action-with-output 11 | id: my_action 12 | # OK 13 | - run: echo ${{ steps.my_action.outputs.some_value }} 14 | # ERROR: No output named 'some-value' (typo) 15 | - run: echo ${{ steps.my_action.outputs.some-value }} 16 | -------------------------------------------------------------------------------- /testdata/examples/not_persistent_matrix_values.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | foo: 7 | - 'string value' 8 | - 42 9 | - {aaa: true, bbb: null} 10 | bar: 11 | - [42] 12 | - [true] 13 | - [{aaa: true, bbb: null}] 14 | - [] 15 | runs-on: ubuntu-latest 16 | steps: 17 | # matrix.foo is any type value 18 | - run: echo ${{ matrix.foo }} 19 | # matrix.bar is array type value 20 | - run: echo ${{ matrix.bar[0] }} 21 | -------------------------------------------------------------------------------- /testdata/examples/permissions.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:14: "write" is invalid for permission for all the scopes. available values are "read-all" and "write-all" [permissions] 2 | test.yaml:11:7: unknown permission scope "check". all available permission scopes are "actions", "checks", "contents", "deployments", "discussions", "id-token", "issues", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses" [permissions] 3 | test.yaml:13:15: "readable" is invalid for permission of scope "issues". available values are "read", "write" or "none" [permissions] 4 | -------------------------------------------------------------------------------- /testdata/examples/workflow_call_definitions.out: -------------------------------------------------------------------------------- 1 | test.yaml:15:18: input of workflow_call event "port" is typed as number but its default value ":1234" cannot be parsed as a float number: strconv.ParseFloat: parsing ":1234": invalid syntax [events] 2 | test.yaml:20:15: invalid value "object" for input type of workflow_call event. it must be one of "boolean", "number", or "string" [syntax-check] 3 | test.yaml:25:18: input "path" of workflow_call event has the default value "", but it is also required. if an input is marked as required, its default value will never be used [events] 4 | -------------------------------------------------------------------------------- /testdata/reusable_workflow_metadata/ok.yaml: -------------------------------------------------------------------------------- 1 | name: minimal 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | input1: 7 | description: input 8 | type: string 9 | input2: 10 | type: boolean 11 | required: true 12 | outputs: 13 | output1: 14 | description: output 15 | value: foo 16 | secrets: 17 | secret1: 18 | description: secret 19 | secret2: 20 | required: true 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-18.04 25 | steps: 26 | - run: echo ... 27 | -------------------------------------------------------------------------------- /testdata/examples/id_naming_convention.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | # ERROR: '.' cannot be contained in ID 5 | foo-v1.2.3: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo 'job ID with version' 9 | # ERROR: ID cannot contain spaces 10 | id: echo for test 11 | # ERROR: ID cannot start with '-' 12 | -hello-world-: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo 'oops' 16 | # ERROR: ID cannot start with numbers 17 | 2d-game: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: echo 'oops' 21 | -------------------------------------------------------------------------------- /testdata/ok/workflow_call_outputs.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | image-name: 5 | description: Name of Docker image 6 | required: true 7 | type: string 8 | outputs: 9 | image-version: 10 | description: "Docker image version" 11 | value: ${{ jobs.generate-image-version.outputs.imagetag }} 12 | jobs: 13 | generate-image-version: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | imagetag: "${{ steps.hello.outputs.foo }}" 17 | steps: 18 | - run: echo hello 19 | id: hello 20 | -------------------------------------------------------------------------------- /testdata/err/invalid_id.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | -foo: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo 'must start with _ or letters' 7 | id: -foo 8 | v1.2.3: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo 'must contain only alnum or _ or -' 12 | id: v1.2.3 13 | 1-2-3: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo 'must not start with number' 17 | id: 1-2-3 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo 'must not be empty' 22 | id: "" 23 | -------------------------------------------------------------------------------- /testdata/ok/skip_inputs.yaml: -------------------------------------------------------------------------------- 1 | name: Log latest release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | logLatestRelease: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: octokit/request-action@v2.x 12 | id: get_latest_release 13 | with: 14 | route: GET /repos/{owner}/{repo}/releases/latest 15 | owner: octokit 16 | repo: request-action 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | - run: "echo latest release: ${{ steps.get_latest_release.outputs.data }}" 20 | -------------------------------------------------------------------------------- /fuzz/parse.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | package actionlint_fuzz 3 | 4 | import ( 5 | "github.com/rhysd/actionlint" 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | func canParseByGoYAML(data []byte) (ret bool) { 10 | defer func() { 11 | if err := recover(); err != nil { 12 | ret = false 13 | } 14 | }() 15 | var n yaml.Node 16 | yaml.Unmarshal(data, &n) 17 | return true 18 | } 19 | 20 | func FuzzParse(data []byte) int { 21 | if !canParseByGoYAML(data) { 22 | return 0 23 | } 24 | 25 | if _, errs := actionlint.Parse(data); len(errs) > 0 { 26 | return 0 27 | } 28 | 29 | return 1 30 | } 31 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_outputs_sema.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | output1: 5 | description: "no outputs" 6 | value: ${{ jobs.job0.outputs.some_output }} 7 | output2: 8 | description: "unknown output" 9 | value: ${{ jobs.job1.outputs.unknown_output }} 10 | jobs: 11 | job0: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: echo hi 15 | job1: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | foo: "${{ steps.hello.outputs.foo }}" 19 | steps: 20 | - run: echo hello 21 | id: hello 22 | -------------------------------------------------------------------------------- /testdata/examples/id_naming_convention.out: -------------------------------------------------------------------------------- 1 | test.yaml:5:3: invalid job ID "foo-v1.2.3". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 2 | test.yaml:10:13: invalid step ID "echo for test". step ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 3 | test.yaml:12:3: invalid job ID "-hello-world-". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 4 | test.yaml:17:3: invalid job ID "2d-game". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 5 | -------------------------------------------------------------------------------- /testdata/projects/broken_reusable_workflow.out: -------------------------------------------------------------------------------- 1 | /workflows/test\.yaml:5:11: error while parsing reusable workflow "\./reusable/broken\.yaml": yaml: .+ \[workflow-call\]/ 2 | workflows/test.yaml:7:11: error while parsing reusable workflow "./reusable/no_hook.yaml": "workflow_call" event trigger is not found in "on:" at line:1, column:5 [workflow-call] 3 | workflows/test.yaml:9:11: error while parsing reusable workflow "./reusable/no_on.yaml": "on:" is not found [workflow-call] 4 | /workflows/test\.yaml:15:11: error while parsing reusable workflow "\./reusable/broken_secrets\.yaml": yaml: .+ \[workflow-call\]/ 5 | -------------------------------------------------------------------------------- /scripts/generate-actionlint-matcher/test/escape.txt: -------------------------------------------------------------------------------- 1 | ./testdata/err/one_error.yaml:6:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 2 |  | 3 | 6 |  - run: echo "Checking commit '${{ github.event.head_commit.message }}'" 4 |  |  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 |  -------------------------------------------------------------------------------- /testdata/err/workflow_call_outputs_syntax.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:7: "value" is missing at "missing-value" output of workflow_call event [syntax-check] 2 | test.yaml:8:7: "value" is missing at "missing-all" output of workflow_call event [syntax-check] 3 | test.yaml:12:9: unexpected key "unknown-section" for "outputs at workflow_call event" section. expected one of "description", "value" [syntax-check] 4 | test.yaml:16:7: key "duplicate-key" is duplicated in "outputs" section. previously defined at line:13,col:7. note that key names are case insensitive [syntax-check] 5 | test.yaml:21:15: string should not be empty [syntax-check] 6 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/workflows/undefined.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | upper: 5 | uses: ./reusable/upper.yaml 6 | with: 7 | MY_INPUT_1: hello 8 | MY_INPUT_2: world 9 | MY_INPUT_3: undefined 10 | secrets: 11 | MY_SECRET_1: hello 12 | MY_SECRET_2: world 13 | MY_SECRET_3: undefined 14 | lower: 15 | uses: ./reusable/lower.yaml 16 | with: 17 | MY_INPUT_1: hello 18 | MY_INPUT_2: world 19 | MY_INPUT_3: undefined 20 | secrets: 21 | MY_SECRET_1: hello 22 | MY_SECRET_2: world 23 | MY_SECRET_3: undefined 24 | -------------------------------------------------------------------------------- /all_webhooks_test.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import "testing" 4 | 5 | func TestGeneratedAllWebhooks(t *testing.T) { 6 | if len(AllWebhookTypes) == 0 { 7 | t.Fatal("AllWebhookTypes is empty") 8 | } 9 | 10 | for name, types := range AllWebhookTypes { 11 | if name == "" { 12 | t.Errorf("Name is empty (types=%v)", types) 13 | continue 14 | } 15 | 16 | seen := map[string]struct{}{} 17 | for _, ty := range types { 18 | if _, ok := seen[ty]; ok { 19 | t.Errorf("type %q duplicates in webhook %q: %v", ty, name, types) 20 | } else { 21 | seen[ty] = struct{}{} 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "none", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "removeComments": true, 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitOverride": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true 17 | }, 18 | "files": [ 19 | "index.ts", 20 | "lib.d.ts", 21 | "test.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /testdata/examples/popular_action_outputs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | # ERROR: The step is not run yet at this point 8 | - run: echo ${{ steps.cache.outputs.cache-hit }} 9 | # actions/cache sets cache-hit output 10 | - uses: actions/cache@v3 11 | id: cache 12 | with: 13 | key: ${{ hashFiles('**/*.lock') }} 14 | path: ./packages 15 | # OK 16 | - run: echo ${{ steps.cache.outputs.cache-hit }} 17 | # ERROR: Typo at output name 18 | - run: echo ${{ steps.cache.outputs.cache_hit }} 19 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_missing_required/workflows/reusable.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | optional1: 5 | type: string 6 | optional2: 7 | type: string 8 | default: foo! 9 | optional3: 10 | type: string 11 | required: false 12 | required1: 13 | type: number 14 | required: true 15 | secrets: 16 | optional1: 17 | optional2: 18 | required: false 19 | required1: 20 | required: true 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - run: 'bye' 27 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/skip_outputs_want.go: -------------------------------------------------------------------------------- 1 | // Code generated by actionlint/scripts/generate-popular-actions. DO NOT EDIT. 2 | 3 | package actionlint 4 | 5 | // PopularActions is data set of known popular actions. Keys are specs (owner/repo@ref) of actions 6 | // and values are their metadata. 7 | var PopularActions = map[string]*ActionMetadata{ 8 | "rhysd/action-setup-vim@v1": { 9 | Name: "Setup Vim", 10 | Inputs: ActionMetadataInputs{ 11 | "neovim": {"neovim", false}, 12 | "token": {"token", false}, 13 | "version": {"version", false}, 14 | }, 15 | SkipOutputs: true, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /testdata/bench/small.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | unit-tests: 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-go@v3 13 | - run: go test -v -race 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-go@v3 19 | - run: go get honnef.co/go/tools/cmd/staticcheck@latest 20 | - run: | 21 | "$(go env GOPATH)/bin/staticcheck" ./... 22 | -------------------------------------------------------------------------------- /testdata/examples/webhook_checks.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # ERROR: Incorrect filter. 'branches' is correct 4 | branch: foo 5 | # ERROR: Both 'paths' and 'paths-ignore' filters cannot be used for the same event 6 | paths: path/to/foo 7 | paths-ignore: path/to/foo 8 | issues: 9 | # ERROR: Incorrect type. 'opened' is correct 10 | types: created 11 | release: 12 | # ERROR: 'tags' filter is not available for 'release' event 13 | tags: v*.*.* 14 | # ERROR: Unknown event name 15 | pullreq: 16 | 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo ... 22 | -------------------------------------------------------------------------------- /testdata/ok/issue-151-child-of-child-job.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | first: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | first: 'output from parent' 10 | steps: 11 | - run: echo 'first' 12 | 13 | second: 14 | needs: [first] 15 | runs-on: ubuntu-latest 16 | outputs: 17 | second: 'output from second' 18 | steps: 19 | - run: echo 'second' 20 | 21 | third: 22 | needs: [first, second] 23 | runs-on: ubuntu-latest 24 | steps: 25 | - run: echo '${{ toJSON(needs.second.outputs) }}' 26 | - run: echo '${{ toJSON(needs.first.outputs) }}' 27 | -------------------------------------------------------------------------------- /testdata/projects/issue173.out: -------------------------------------------------------------------------------- 1 | workflows/workflow1.yaml:9:15: missing input "hello" which is required by action "My action" defined at "./action". all required inputs are "hello" [action] 2 | workflows/workflow1.yaml:11:11: input "goodbye" is not defined in action "My action" defined at "./action". available inputs are "hello" [action] 3 | workflows/workflow2.yaml:9:15: missing input "hello" which is required by action "My action" defined at "./action". all required inputs are "hello" [action] 4 | workflows/workflow2.yaml:11:11: input "goodbye" is not defined in action "My action" defined at "./action". available inputs are "hello" [action] 5 | -------------------------------------------------------------------------------- /testdata/err/evaluated_template.out: -------------------------------------------------------------------------------- 1 | test.yaml:22:20: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type object [expression] 2 | test.yaml:22:38: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type {cache-hit: string} [expression] 3 | test.yaml:22:63: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type array [expression] 4 | test.yaml:24:20: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type null [expression] 5 | -------------------------------------------------------------------------------- /testdata/err/outputs_of_action_skipping_inputs_check.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | logLatestRelease: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: octokit/request-action@v2.x 8 | id: get_latest_release 9 | with: 10 | route: GET /repos/{owner}/{repo}/releases/latest 11 | owner: octokit 12 | repo: request-action 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | # octkit/request-action skips inputs check, but outputs are still checked 16 | - run: "echo latest release: ${{ steps.get_latest_release.outputs.this_output_does_not_exist }}" 17 | -------------------------------------------------------------------------------- /testdata/ok/no_description_workflow_call.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | username: 5 | required: true 6 | type: string 7 | secrets: 8 | token: 9 | required: true 10 | outputs: 11 | firstword: 12 | value: ${{ jobs.example_job.outputs.output1 }} 13 | 14 | jobs: 15 | example_job: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | output1: ${{ steps.step1.outputs.firstword }} 19 | steps: 20 | - id: step1 21 | run: echo "::set-output name=firstword::hello, ${{ inputs.username }}" 22 | env: 23 | TOKEN: ${{ secrets.token }} 24 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_input_type_check/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller1: 5 | uses: ./workflows/reusable.yaml 6 | with: 7 | # Note: any value can be converted into bool 8 | str_input: null 9 | num_input: false 10 | bool_input: 'foo!' 11 | broken_input: null 12 | caller2: 13 | uses: ./workflows/reusable.yaml 14 | with: 15 | str_input: ${{ true }} 16 | num_input: ${{ 'foo' }} 17 | broken_input: 42 18 | caller3: 19 | uses: ./workflows/reusable.yaml 20 | with: 21 | str_input: 22 | num_input: 23 | broken_input: 'hello' 24 | -------------------------------------------------------------------------------- /testdata/examples/workflow_call_jobs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | job1: 4 | uses: owner/repo/path/to/workflow.yml@v1 5 | # ERROR: 'runs-on' is not available on calling reusable workflow 6 | runs-on: ubuntu-latest 7 | job2: 8 | # ERROR: Local file path with ref is not available 9 | uses: ./.github/workflows/ci.yml@main 10 | job3: 11 | # ERROR: 'with' is only available on calling reusable workflow 12 | with: 13 | foo: bar 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo hello 17 | job4: 18 | # ERROR: This workflow does not exist 19 | uses: ./.github/workflows/not-existing.yml 20 | -------------------------------------------------------------------------------- /testdata/ok/workflow_call_with_strategy.yaml: -------------------------------------------------------------------------------- 1 | name: Reusable workflow with matrix strategy 2 | 3 | # This was enabled by https://github.blog/changelog/2022-08-22-github-actions-improvements-to-reusable-workflows-2/ 4 | # 5 | # Issue: #197 6 | # Doc: https://docs.github.com/en/actions/using-workflows/reusing-workflows#example-matrix-strategy-with-a-reusable-workflow 7 | 8 | on: 9 | push: 10 | 11 | jobs: 12 | ReuseableMatrixJobForDeployment: 13 | strategy: 14 | matrix: 15 | target: [dev, stage, prod] 16 | uses: octocat/octo-repo/.github/workflows/deployment.yml@main 17 | with: 18 | target: ${{ matrix.target }} 19 | -------------------------------------------------------------------------------- /testdata/examples/type_checks.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # ERROR: `env` is object. Index access to object is invalid 7 | - run: echo '${{ env[0] }}' 8 | # ERROR: Properties in objects are strongly typed. Missing property can be caught 9 | - run: echo '${{ job.container.os }}' 10 | # ERROR: `github.repository` is string. Trying to access .owner property is invalid 11 | - run: echo '${{ github.repository.owner }}' 12 | # ERROR: Objects, arrays and null should not be evaluated at ${{ }} since the outputs are useless 13 | - run: echo '${{ env }}' 14 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok/workflows/reusable_all_optional.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | str: 5 | type: string 6 | default: hi 7 | num: 8 | type: number 9 | default: 42 10 | bool: 11 | type: boolean 12 | default: true 13 | outputs: 14 | output1: 15 | value: ${{ jobs.run.outputs.message }} 16 | secrets: 17 | foo: 18 | 19 | jobs: 20 | run: 21 | runs-on: ubuntu-latest 22 | outputs: 23 | message: '${{ inputs.str }}, ${{ inputs.num }}, ${{ inputs.bool }}, ${{ secrets.foo }}' 24 | steps: 25 | - run: echo hello 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOLANG_VER=latest 2 | ARG ALPINE_VER=latest 3 | 4 | FROM golang:${GOLANG_VER} as builder 5 | WORKDIR /go/src/app 6 | COPY go.* *.go ./ 7 | COPY cmd cmd/ 8 | ENV CGO_ENABLED 0 9 | ARG ACTIONLINT_VER= 10 | RUN go build -v -ldflags "-s -w -X github.com/rhysd/actionlint.version=${ACTIONLINT_VER}" ./cmd/actionlint 11 | 12 | FROM koalaman/shellcheck-alpine:stable as shellcheck 13 | 14 | FROM alpine:${ALPINE_VER} 15 | COPY --from=builder /go/src/app/actionlint /usr/local/bin/ 16 | COPY --from=shellcheck /bin/shellcheck /usr/local/bin/shellcheck 17 | RUN apk add --no-cache py3-pyflakes 18 | USER guest 19 | ENTRYPOINT ["/usr/local/bin/actionlint"] 20 | -------------------------------------------------------------------------------- /testdata/err/issue155_env_in_job_level_if.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | # Issue #155: `env` should not be available in `jobs..if` 4 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability 5 | 6 | jobs: 7 | test1: 8 | runs-on: ubuntu-latest 9 | if: ${{ env.FOO == 'aaa' }} 10 | steps: 11 | - run: echo 'hello' 12 | test2: 13 | runs-on: ubuntu-latest 14 | if: env.FOO == 'aaa' 15 | steps: 16 | - run: echo 'hello' 17 | test3: 18 | uses: org/repo/workflow.yml@v1 19 | if: ${{ env.FOO == 'aaa' }} 20 | test4: 21 | uses: org/repo/workflow.yml@v1 22 | if: env.FOO == 'aaa' 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rhysd/actionlint 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/fatih/color v1.13.0 7 | github.com/google/go-cmp v0.5.6 8 | github.com/mattn/go-colorable v0.1.13 9 | github.com/mattn/go-runewidth v0.0.13 10 | github.com/robfig/cron v1.2.0 11 | github.com/yuin/goldmark v1.4.12 12 | golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde 13 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/mattn/go-isatty v0.0.16 // indirect 19 | github.com/rivo/uniseg v0.2.0 // indirect 20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/download.yaml: -------------------------------------------------------------------------------- 1 | name: Download script 2 | on: 3 | push: 4 | paths: 5 | - scripts/download-actionlint.bash 6 | - scripts/test-download-actionlint.bash 7 | pull_request: 8 | paths: 9 | - scripts/download-actionlint.bash 10 | - scripts/test-download-actionlint.bash 11 | workflow_dispatch: 12 | 13 | jobs: 14 | download: 15 | name: Test download script 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - run: ./scripts/test-download-actionlint.bash 23 | shell: bash 24 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/want.go: -------------------------------------------------------------------------------- 1 | // Code generated by actionlint/scripts/generate-popular-actions. DO NOT EDIT. 2 | 3 | package actionlint 4 | 5 | // PopularActions is data set of known popular actions. Keys are specs (owner/repo@ref) of actions 6 | // and values are their metadata. 7 | var PopularActions = map[string]*ActionMetadata{ 8 | "rhysd/action-setup-vim@v1": { 9 | Name: "Setup Vim", 10 | Inputs: ActionMetadataInputs{ 11 | "neovim": {"neovim", false}, 12 | "token": {"token", false}, 13 | "version": {"version", false}, 14 | }, 15 | Outputs: ActionMetadataOutputs{ 16 | "executable": {"executable"}, 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /testdata/examples/invalid_action_format.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:15: specifying action "actions/checkout" in invalid format because ref is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action] 2 | test.yaml:9:15: specifying action "checkout@v2" in invalid format because owner is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action] 3 | test.yaml:11:15: tag of Docker action should not be empty: "docker://image" [action] 4 | test.yaml:13:15: specifying action ".github/my-actions/do-something" in invalid format because ref is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action] 5 | -------------------------------------------------------------------------------- /testdata/examples/pyflakes_integration.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | linux: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # Yay! No error 7 | - run: print('${{ runner.os }}') 8 | shell: python 9 | # ERROR: Undefined variable 10 | - run: print(hello) 11 | shell: python 12 | linux2: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | # Run script with Python by default 17 | shell: python 18 | steps: 19 | - run: | 20 | import sys 21 | for sys in ['system1', 'system2']: 22 | print(sys) 23 | - run: | 24 | from time import sleep 25 | print(100) 26 | -------------------------------------------------------------------------------- /testdata/examples/workflow_inputs_secrets_types.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | url: 5 | description: 'your URL' 6 | type: string 7 | lucky_number: 8 | description: 'your lucky number' 9 | type: number 10 | secrets: 11 | credential: 12 | description: 'your credential' 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-20.04 17 | steps: 18 | - name: Send data 19 | # ERROR: uri is typo of url 20 | run: curl ${{ inputs.uri }} -d ${{ inputs.lucky_number }} 21 | env: 22 | # ERROR: credentials is typo of credential 23 | TOKEN: ${{ secrets.credentials }} 24 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok/workflows/reusable_all_required.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | str: 5 | type: string 6 | required: true 7 | num: 8 | type: number 9 | required: true 10 | bool: 11 | type: boolean 12 | required: true 13 | outputs: 14 | output1: 15 | value: ${{ jobs.run.outputs.message }} 16 | secrets: 17 | foo: 18 | required: true 19 | 20 | jobs: 21 | run: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | message: '${{ inputs.str }}, ${{ inputs.num }}, ${{ inputs.bool }}, ${{ secrets.foo }}' 25 | steps: 26 | - run: echo hello 27 | -------------------------------------------------------------------------------- /testdata/err/env_context_banned.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:15: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 2 | test.yaml:14:19: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 3 | test.yaml:17:24: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 4 | test.yaml:18:21: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 5 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_inputs.yaml: -------------------------------------------------------------------------------- 1 | # Note: Added at #154 2 | on: 3 | workflow_call: 4 | inputs: 5 | input_not_ok: 6 | description: test 7 | type: string 8 | required: true # set this input is required 9 | default: a # but default value is provided 10 | input_ok_1: 11 | description: test 12 | type: string 13 | default: a 14 | input_ok_2: 15 | description: test 16 | type: string 17 | required: true 18 | input_ok_3: 19 | description: test 20 | type: string 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - run: echo hello 27 | -------------------------------------------------------------------------------- /testdata/err/exclusive_webhook_filters.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | paths: path/to/foo 4 | paths-ignore: path/to/bar 5 | branches-ignore: bar 6 | branches: foo 7 | tags: v*.*.* 8 | tags-ignore: dev 9 | pull_request: 10 | paths-ignore: path/to/bar 11 | paths: path/to/foo 12 | branches: foo 13 | branches-ignore: bar 14 | pull_request_target: 15 | paths: path/to/foo 16 | paths-ignore: path/to/bar 17 | branches-ignore: bar 18 | branches: foo 19 | workflow_run: 20 | workflows: foo.yaml 21 | branches: foo 22 | branches-ignore: bar 23 | 24 | jobs: 25 | test: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - run: echo hello 29 | -------------------------------------------------------------------------------- /testdata/ok/workflow_call_job.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | call1: 4 | uses: org/repo/workflow.yml@v1 5 | call2: 6 | uses: org/repo/workflow.yml@v1 7 | with: 8 | foo: bar 9 | call3: 10 | uses: org/repo/workflow.yml@v1 11 | secrets: 12 | foo: bar 13 | call4: 14 | uses: org/repo/workflow.yml@v1 15 | with: 16 | foo: bar 17 | secrets: 18 | foo: bar 19 | call5: 20 | name: Test 21 | uses: org/repo/workflow.yml@v1 22 | with: 23 | foo: bar 24 | secrets: 25 | foo: bar 26 | needs: ['call1'] 27 | permissions: read-all 28 | call6: 29 | # Edge case. Give up checking format. 30 | uses: ${{ runner.name }} 31 | -------------------------------------------------------------------------------- /testdata/err/issue155_env_in_job_level_if.out: -------------------------------------------------------------------------------- 1 | test.yaml:9:13: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 2 | test.yaml:14:9: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 3 | test.yaml:19:13: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 4 | test.yaml:22:9: undefined variable "env". available variables are "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 5 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_undefined/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller: 5 | uses: ./workflows/reusable.yaml 6 | with: 7 | aaa: this is not existing 8 | foo: this is existing 9 | secrets: 10 | bbb: this is not existing 11 | piyo: this is existing 12 | other: 13 | needs: [caller] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo '${{ needs.caller.outputs.bar }} is existing' 17 | - run: echo '${{ needs.caller.outputs.ccc }} is not existing' 18 | empty: 19 | uses: ./workflows/empty_reusable.yaml 20 | with: 21 | input1: this is not existing 22 | secrets: 23 | secret1: this is not existing 24 | -------------------------------------------------------------------------------- /testdata/projects/recursive_workflow_call/workflows/recursive.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | input1: 5 | type: string 6 | input2: 7 | type: number 8 | required: true 9 | outputs: 10 | output1: 11 | value: '...' 12 | secrets: 13 | secret1: 14 | secret2: 15 | required: true 16 | 17 | jobs: 18 | caller: 19 | uses: ./workflows/recursive.yaml 20 | with: 21 | input1: hello 22 | input2: 42 23 | secrets: 24 | secret1: aaa 25 | secret2: bbb 26 | other: 27 | needs: [caller] 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: echo '${{ needs.caller.outputs.output1 }}' 31 | -------------------------------------------------------------------------------- /testdata/ok/untrusted_inputs.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | # Direct access to untrusted inputs 9 | - run: | 10 | echo "ISSUE: $ISSUE, COMMITS: $COMMIT, REF: $REF" 11 | env: 12 | ISSUE: ${{ github.event.issue.body }} 13 | COMMITS: ${{ toJSON(github.event.commits.*.message) }} 14 | REF: ${{ github.head_ref }} 15 | # Indirect access to untrusted inputs via object filter 16 | - run: | 17 | echo "BODIES: $BODIES, EMAILS: $EMAILS" 18 | env: 19 | BODIES: ${{ toJSON(github.event.*.body) }} 20 | EMAILS: ${{ toJSON(github.event.*.*.email) }} 21 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_undefined.out: -------------------------------------------------------------------------------- 1 | workflows/test.yaml:7:7: input "aaa" is not defined in "./workflows/reusable.yaml" reusable workflow. defined input is "foo" [workflow-call] 2 | workflows/test.yaml:10:7: secret "bbb" is not defined in "./workflows/reusable.yaml" reusable workflow. defined secret is "piyo" [workflow-call] 3 | workflows/test.yaml:17:24: property "ccc" is not defined in object type {bar: string} [expression] 4 | workflows/test.yaml:21:7: input "input1" is not defined in "./workflows/empty_reusable.yaml" reusable workflow. no input is defined [workflow-call] 5 | workflows/test.yaml:23:7: secret "secret1" is not defined in "./workflows/empty_reusable.yaml" reusable workflow. no secret is defined [workflow-call] 6 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/README.md: -------------------------------------------------------------------------------- 1 | generate-popular-actions 2 | ======================== 3 | 4 | This is a script for generating [`popular_actions.go`](../../popular_actions.go). 5 | 6 | It does: 7 | 8 | - Fetchs metadata of popular actions 9 | - from https://github.com 10 | - from JSONL file in local 11 | - Generates the fetched data set of metadata 12 | - as Go source file 13 | - as JSONL file 14 | 15 | ## Usage 16 | 17 | Generate Go source: 18 | 19 | ```sh 20 | go run ./scripts/generate-popular-actions ./popular_actions.go 21 | ``` 22 | 23 | Detect new releases on GitHub: 24 | 25 | ```sh 26 | go run ./scripts/generate-popular-actions -d 27 | ``` 28 | 29 | Please see output of `-help` flag for more details. 30 | -------------------------------------------------------------------------------- /scripts/generate-webhook-events/testdata/ok.go: -------------------------------------------------------------------------------- 1 | // Code generated by actionlint/scripts/generate-webhook-events. DO NOT EDIT. 2 | 3 | package actionlint 4 | 5 | // AllWebhookTypes is a table of all webhooks with their types. This variable was generated by 6 | // script at ./scripts/generate-webhook-events based on 7 | // https://github.com/github/docs/blob/main/content/actions/using-workflows/events-that-trigger-workflows.md 8 | var AllWebhookTypes = map[string][]string{ 9 | "check_run": {"created", "rerequested", "completed"}, 10 | "discussion": {"opened", "edited", "deleted", "transferred", "pinned", "unpinned", "labeled", "unlabeled", "locked", "unlocked", "category_changed", "answered", "unanswered"}, 11 | "create": {}, 12 | } 13 | -------------------------------------------------------------------------------- /testdata/examples/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branch: main 4 | tags: 5 | - 'v\d+' 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | os: [macos-latest, linux-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - run: echo "Checking commit '${{ github.event.head_commit.message }}'" 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node_version: 16.x 18 | - uses: actions/cache@v3 19 | with: 20 | path: ~/.npm 21 | key: ${{ matrix.platform }}-node-${{ hashFiles('**/package-lock.json') }} 22 | if: ${{ github.repository.permissions.admin == true }} 23 | - run: npm install && npm test 24 | -------------------------------------------------------------------------------- /testdata/ok/issue-66.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | jobs: 6 | job1: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo 'job1' 10 | job2: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - run: echo 'job2' 14 | job3: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: echo 'job3' 18 | notify: 19 | if: always() 20 | needs: [job1, job2, job3] 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: ScribeMD/slack-templates@0 24 | with: 25 | bot-token: ${{ secrets.SLACK_TEMPLATES_BOT_TOKEN }} 26 | channel-id: ${{ secrets.SLACK_TEMPLATES_CHANNEL_ID }} 27 | template: result 28 | results: "${{ join(needs.*.result, ' ') }}" 29 | -------------------------------------------------------------------------------- /testdata/examples/runner_label_check.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | runner: 7 | # OK 8 | - macos-latest 9 | # ERROR: Unknown runner 10 | - linux-latest 11 | # OK: Preset labels for self-hosted runner 12 | - [self-hosted, linux, x64] 13 | # OK: Single preset label for self-hosted runner 14 | - arm64 15 | # ERROR: Unknown label "gpu". Custom label must be defined in actionlint.yaml config file 16 | - gpu 17 | runs-on: ${{ matrix.runner }} 18 | steps: 19 | - run: echo ... 20 | 21 | test2: 22 | # ERROR: Too old macOS worker 23 | runs-on: macos-10.13 24 | steps: 25 | - run: echo ... 26 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func ExampleCommand() { 11 | var output bytes.Buffer 12 | 13 | // Create command instance populating stdin/stdout/stderr 14 | cmd := Command{ 15 | Stdin: os.Stdin, 16 | Stdout: &output, 17 | Stderr: &output, 18 | } 19 | 20 | // Run the command end-to-end. Note that given args should contain program name 21 | workflow := filepath.Join(".github", "workflows", "release.yaml") 22 | status := cmd.Main([]string{"actionlint", "-shellcheck=", "-pyflakes=", workflow}) 23 | 24 | fmt.Println("Exited with status", status) 25 | // Output: Exited with status 0 26 | 27 | if status != 0 { 28 | panic("actionlint command failed: " + output.String()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /playground/Makefile: -------------------------------------------------------------------------------- 1 | LIBSRCS := $(filter-out ../%_test.go, $(wildcard ../*.go)) ../go.mod ../go.sum 2 | 3 | serve: build 4 | npm run serve 5 | 6 | build: node_modules lib main.wasm index.js test.js 7 | 8 | node_modules: 9 | npm install 10 | 11 | index.js test.js: index.ts test.ts lib.d.ts tsconfig.json 12 | npm run build 13 | 14 | lib: node_modules index.js 15 | bash ./post-install.bash 16 | 17 | install: node_modules lib 18 | 19 | main.wasm: main.go $(LIBSRCS) 20 | GOOS=js GOARCH=wasm go build -o main.wasm 21 | 22 | .testtimestamp: node_modules lib test.js main.wasm 23 | npm test 24 | touch .testtimestamp 25 | 26 | test: .testtimestamp 27 | 28 | clean: 29 | rm -f ./main.wasm ./index.js ./index.js.map .testtimestamp 30 | rm -rf ./lib 31 | 32 | .PHONY: build install serve test clean 33 | -------------------------------------------------------------------------------- /testdata/err/evaluated_template.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | # OK: Expanding object value 6 | matrix: ${{ fromJSON(env.MATRIX_VALUES) }} 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/cache@v1 10 | id: cache 11 | with: 12 | key: foo 13 | path: bar 14 | # OK 15 | - run: echo "${{ 'hello' }} ${{ true }} ${{ 42 }}" 16 | # OK: Any type 17 | - run: echo "${{ env.FOO }}" 18 | # OK: Expanding object value 19 | - run: echo "$FOO" 20 | env: ${{ matrix.env }} 21 | # ERROR: loose object, strict object, and array 22 | - run: echo "${{github.event}} ${{steps.cache.outputs}} ${{github.event.commits.*}}" 23 | # ERROR: null 24 | - run: echo "${{null}}" 25 | -------------------------------------------------------------------------------- /testdata/examples/workflow_call_jobs.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:5: when a reusable workflow is called with "uses", "runs-on" is not available. only following keys are allowed: "name", "uses", "with", "secrets", "needs", "if", and "permissions" in job "job1" [syntax-check] 2 | test.yaml:9:11: reusable workflow call "./.github/workflows/ci.yml@main" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call] 3 | test.yaml:12:5: "with" is only available for a reusable workflow call with "uses" but "uses" is not found in job "job3" [syntax-check] 4 | /test\.yaml:19:11: could not read reusable workflow file for "\./\.github/workflows/not-existing\.yml": .+ \[workflow-call\]/ 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Documents 2 | ========= 3 | 4 | - [Checks](checks.md): Full list of all checks done by actionlint with example inputs, outputs, and playground links. 5 | - [Installation](install.md): Installation instructions. Prebuilt binaries, Homebrew package, a Docker image, building from 6 | source, a download script (for CI) are available. 7 | - [Usage](usage.md): How to use `actionlint` command locally or on GitHub Actions, the online playground, an official Docker 8 | image, and integrations with reviewdog, Problem Matchers, super-linter, pre-commit. 9 | - [Configuration](config.md): How to configure actionlint behavior. Currently only labels of self-hosted runners can be 10 | configured. 11 | - [Go API](api.md): How to use actionlint as Go library. 12 | - [References](reference.md): Links to resources. 13 | -------------------------------------------------------------------------------- /scripts/yaml-to-playground-url.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This script inputs YAML workflow code from stdin and outputs a playground URL 4 | // for the workflow to stdout. 5 | // 6 | // Usage: 7 | // pbpaste | node ./scripts/yaml-to-playground-url.js 8 | // node ./scripts/yaml-to-playground-url.js < test.yaml 9 | 10 | const fs = require('fs'); 11 | const pako = require('../playground/node_modules/pako'); 12 | 13 | const re = /^\s*#/; 14 | const stdin = fs.readFileSync(process.stdin.fd, 'utf8').trim(); 15 | const lines = stdin.split('\n').filter(l => !re.test(l)); // remove comment lines 16 | const src = lines.join('\n'); 17 | const compressed = pako.deflate(new TextEncoder().encode(src)); 18 | const b64 = Buffer.from(compressed).toString('base64'); 19 | console.log(`https://rhysd.github.io/actionlint#${b64}`); 20 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_outputs_syntax.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | missing-description: 5 | value: ${{ jobs.test.outputs.x }} 6 | missing-value: 7 | description: "description" 8 | missing-all: 9 | unknown-section: 10 | description: "description" 11 | value: ${{ jobs.test.outputs.x }} 12 | unknown-section: 13 | duplicate-key: 14 | description: "description" 15 | value: ${{ jobs.test.outputs.x }} 16 | duplicate-key: 17 | description: "description" 18 | value: ${{ jobs.test.output }} 19 | empty-value: 20 | description: "description" 21 | value: 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | outputs: 26 | x: hi 27 | steps: 28 | - run: echo 29 | -------------------------------------------------------------------------------- /testdata/examples/expression_syntax_error.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:24: got unexpected character '"' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' '. do you mean string literals? only single quotes are available for string delimiter [expression] 2 | test.yaml:9:26: got unexpected character '+' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' ' [expression] 3 | test.yaml:11:65: unexpected end of input while parsing arguments of function call. expecting ",", ")" [expression] 4 | test.yaml:13:38: unexpected end of input while parsing object property dereference like 'a.b' or array element dereference like 'a.*'. expecting "IDENT", "*" [expression] 5 | -------------------------------------------------------------------------------- /expr.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import "fmt" 4 | 5 | // ExprError is an error type caused by lexing/parsing expression syntax. For more details, see 6 | // https://docs.github.com/en/actions/learn-github-actions/expressions 7 | type ExprError struct { 8 | // Message is an error message 9 | Message string 10 | // Offset is byte offset position which caused the error 11 | Offset int 12 | // Offset is line number position which caused the error. Note that this value is 1-based. 13 | Line int 14 | // Column is column number position which caused the error. Note that this value is 1-based. 15 | Column int 16 | } 17 | 18 | func (e *ExprError) Error() string { 19 | return fmt.Sprintf("%d:%d:%d: %s", e.Line, e.Column, e.Offset, e.Message) 20 | } 21 | 22 | func (e *ExprError) String() string { 23 | return e.Error() 24 | } 25 | -------------------------------------------------------------------------------- /testdata/projects/README.md: -------------------------------------------------------------------------------- 1 | This directory contains project directories tested by `TestLinterLintProject` in `linter_test.go`. 2 | 3 | Each directory represents one project. And `.out` file describes all errors when linting the project (one error per 4 | line). Error messages in the file must be sorted by file paths and error positions. An empty file means no error. 5 | 6 | Each project must contain `workflows` directory and it must contain at least one workflow file. 7 | 8 | Working directory is set to `projects/` when running the tests. File paths in `.out` files should be relative to the 9 | project directory. 10 | 11 | ``` 12 | ├── some_project 13 | │ ├── action 14 | │ │ └── action1.yaml 15 | │ └── workflows 16 | │ ├── workflow1.yaml 17 | │ └── workflow2.yaml 18 | └── some_project.out 19 | ``` 20 | -------------------------------------------------------------------------------- /testdata/err/issue151_child_of_child_job.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | first: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | first: 'output from parent' 10 | steps: 11 | - run: echo 'first' 12 | 13 | second: 14 | needs: [first] 15 | runs-on: ubuntu-latest 16 | outputs: 17 | second: 'output from second' 18 | steps: 19 | - run: echo 'second' 20 | 21 | third: 22 | needs: [second] 23 | runs-on: ubuntu-latest 24 | steps: 25 | - run: echo '${{ toJSON(needs.second.outputs) }}' 26 | - run: echo '${{ toJSON(needs.first.outputs) }}' 27 | 28 | third-ok: 29 | needs: [first, second] 30 | runs-on: ubuntu-latest 31 | steps: 32 | - run: echo '${{ toJSON(needs.second.outputs) }}' 33 | - run: echo '${{ toJSON(needs.first.outputs) }}' 34 | -------------------------------------------------------------------------------- /testdata/examples/contextual_steps_outputs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | outputs: 6 | # Step outputs can be used in job outputs since this section is evaluated after all steps were run 7 | foo: '${{ steps.get_value.outputs.name }}' 8 | steps: 9 | # Access undefined step outputs 10 | - run: echo '${{ steps.get_value.outputs.name }}' 11 | # Outputs are set here 12 | - run: echo '::set-output name=foo::value' 13 | id: get_value 14 | # OK 15 | - run: echo '${{ steps.get_value.outputs.name }}' 16 | # OK 17 | - run: echo '${{ steps.get_value.conclusion }}' 18 | other: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Access undefined step outputs. Step objects are job-local 22 | - run: echo '${{ steps.get_value.outputs.name }}' 23 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_job.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | # steps is only for normal job, uses is only for call job 4 | call1: 5 | uses: org/repo/workflow.yml@v1 6 | steps: 7 | - run: echo 8 | # with requires uses 9 | call2: 10 | with: 11 | foo: bar 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: echo 15 | # secrets requires uses 16 | call3: 17 | secrets: 18 | aaa: bbb 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo 22 | # uses is empty 23 | call4: 24 | uses: 25 | # relative path 26 | call5: 27 | uses: "./foo/bar/workflow.yml@main" 28 | # absolute path 29 | call6: 30 | uses: "/foo/bar/workflow.yml@main" 31 | # missing repo name 32 | call7: 33 | uses: "foo/workflow.yml@main" 34 | # missing ref 35 | call8: 36 | uses: "foo/bar/workflow.yml" 37 | -------------------------------------------------------------------------------- /testdata/ok/workflow_call_outputs_sema.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | uses-output1: 5 | description: "test" 6 | value: ${{ jobs.job1.outputs.output1 }} 7 | uses-output2: 8 | description: "test" 9 | value: ${{ jobs.job1.outputs.output2 }} 10 | uses-job2: 11 | description: "test" 12 | value: ${{ jobs.job2.outputs.output1 }} 13 | jobs: 14 | job1: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | output1: "${{ steps.hello.outputs.foo }}" 18 | output2: "${{ steps.bye.outputs.foo }}" 19 | steps: 20 | - run: echo hello 21 | id: hello 22 | - run: echo bye 23 | id: bye 24 | job2: 25 | runs-on: ubuntu-latest 26 | outputs: 27 | output1: "${{ steps.hello.outputs.foo }}" 28 | steps: 29 | - run: echo hello 30 | id: hello 31 | -------------------------------------------------------------------------------- /testdata/err/invalid_id.out: -------------------------------------------------------------------------------- 1 | test.yaml:3:3: invalid job ID "-foo". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 2 | test.yaml:7:13: invalid step ID "-foo". step ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 3 | test.yaml:8:3: invalid job ID "v1.2.3". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 4 | test.yaml:12:13: invalid step ID "v1.2.3". step ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 5 | test.yaml:13:3: invalid job ID "1-2-3". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 6 | test.yaml:17:13: invalid step ID "1-2-3". step ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id] 7 | test.yaml:22:13: string should not be empty [syntax-check] 8 | -------------------------------------------------------------------------------- /testdata/err/nested_untrusted_input.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:23: "github.event.pages.*.page_name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 2 | test.yaml:7:42: "github.event.commits.*.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 3 | test.yaml:7:63: "github.event.issue.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 4 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_ok/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | caller1: 5 | uses: ./workflows/reusable_all_required.yaml 6 | with: 7 | str: hi 8 | num: 13 9 | bool: true 10 | secrets: 11 | foo: bar 12 | caller2: 13 | uses: ./workflows/reusable_all_optional.yaml 14 | with: 15 | str: hi 16 | num: 13 17 | bool: true 18 | secrets: 19 | foo: bar 20 | caller3: 21 | uses: ./workflows/reusable_all_optional.yaml 22 | caller4: 23 | uses: ./workflows/empty1.yaml 24 | caller5: 25 | uses: ./workflows/empty2.yaml 26 | caller6: 27 | uses: ./workflows/empty3.yaml 28 | pass-through-placeholder: 29 | uses: ./workflows/reusable_all_required.yaml 30 | with: 31 | str: ${{ 'hi' }} 32 | num: ${{ 13 }} 33 | bool: ${{ true }} 34 | secrets: 35 | foo: ${{ 'bar' }} 36 | -------------------------------------------------------------------------------- /testdata/format/test.md: -------------------------------------------------------------------------------- 1 | ### Error at line 3, col 5 of `testdata/format/test.yaml` 2 | 3 | unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" 4 | 5 | ``` 6 | branch: main 7 | ^~~~~~~ 8 | ``` 9 | 10 | ### Error at line 6, col 14 of `testdata/format/test.yaml` 11 | 12 | label "linux-latest" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "windows-2016", "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-18.04", "macos-latest", "macos-12", "macos-12.0", "macos-11", "macos-11.0", "macos-10.15", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file 13 | 14 | ``` 15 | runs-on: linux-latest 16 | ^~~~~~~~~~~~ 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /playground/post-install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | rm -rf ./lib 6 | mkdir -p ./lib/css/fonts 7 | mkdir -p ./lib/js 8 | 9 | cp node_modules/codemirror/lib/codemirror.css ./lib/css/ 10 | cp node_modules/codemirror/theme/material-darker.css ./lib/css/ 11 | cp node_modules/bulma/css/bulma.min.css ./lib/css/ 12 | cp node_modules/bulmaswatch/darkly/bulmaswatch.min.css ./lib/css/ 13 | cp node_modules/devicon/devicon.min.css ./lib/css/ 14 | 15 | cp node_modules/devicon/fonts/* ./lib/css/fonts/ 16 | 17 | cp node_modules/codemirror/lib/codemirror.js ./lib/js/ 18 | cp node_modules/codemirror/addon/selection/active-line.js ./lib/js/ 19 | cp node_modules/codemirror/mode/yaml/yaml.js ./lib/js/ 20 | cp node_modules/ismobilejs/dist/isMobile.min.js ./lib/js/ 21 | cp node_modules/pako/dist/pako.min.js ./lib/js/ 22 | 23 | cat "$(go env GOROOT)/misc/wasm/wasm_exec.js" >> ./lib/js/wasm_exec.js 24 | -------------------------------------------------------------------------------- /testdata/examples/workflow_dispatch_input_types.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:15: input type of workflow_dispatch event must be one of "string", "boolean", "choice", "environment" but got "number" [syntax-check] 2 | test.yaml:8:7: input type of "kind" is "choice" but "options" is not set [events] 3 | test.yaml:16:18: default value "Chobi" of "name" input is not included in its options "\"Tama\", \"Mike\"" [events] 4 | test.yaml:22:18: type of "verbose" input is "boolean". its default value "yes" must be "true" or "false" [events] 5 | test.yaml:29:24: property "massage" is not defined in object type {id: any; kind: string; message: string; name: string; verbose: bool} [expression] 6 | test.yaml:31:28: property access of object must be type of string but got "bool" [expression] 7 | test.yaml:33:24: property "massage" is not defined in object type {id: string; kind: string; message: string; name: string; verbose: string} [expression] 8 | -------------------------------------------------------------------------------- /testdata/examples/shell_name_validation.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | linux: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo 'hello' 7 | # ERROR: Unavailable shell 8 | shell: dash 9 | - run: echo 'hello' 10 | # ERROR: 'powershell' is only available on Windows 11 | shell: powershell 12 | mac: 13 | runs-on: macos-latest 14 | defaults: 15 | run: 16 | # ERROR: default config is also checked. fish is not supported 17 | shell: fish 18 | steps: 19 | - run: echo 'hello' 20 | # OK: Custom shell 21 | shell: 'perl {0}' 22 | windows: 23 | runs-on: windows-latest 24 | steps: 25 | - run: echo 'hello' 26 | # ERROR: 'sh' is only available on Windows 27 | shell: sh 28 | - run: echo 'hello' 29 | # OK: 'powershell' is only available on Windows 30 | shell: powershell 31 | -------------------------------------------------------------------------------- /testdata/projects/example_inputs_secrets_in_workflow_call/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | # Check required/undefined inputs and secrets 5 | missing-required: 6 | uses: ./.github/workflows/reusable.yaml 7 | with: 8 | # ERROR: Undefined input 9 | user: rhysd 10 | # ERROR: Required input "name" is missing 11 | secrets: 12 | # ERROR: Undefined secret 13 | credentials: my-token 14 | # ERROR: Required secret "password" is missing 15 | 16 | # Check types of inputs defined in reusable workflow 17 | type-checks: 18 | uses: ./.github/workflows/reusable.yaml 19 | with: 20 | name: rhysd 21 | # ERROR: Cannot assign bool value to number input 22 | id: true 23 | # ERROR: Cannot assign null to string input. If you want to pass string "null", use ${{ 'null' }} 24 | message: null 25 | secrets: 26 | password: p@ssw0rd 27 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_input_type_check.out: -------------------------------------------------------------------------------- 1 | workflows/reusable.yaml:10:7: "type" is missing at "broken_input" input of workflow_call event [syntax-check] 2 | workflows/test.yaml:8:18: input "str_input" is typed as string by reusable workflow "./workflows/reusable.yaml". null value cannot be assigned [expression] 3 | workflows/test.yaml:9:18: input "num_input" is typed as number by reusable workflow "./workflows/reusable.yaml". bool value cannot be assigned [expression] 4 | workflows/test.yaml:15:18: input "str_input" is typed as string by reusable workflow "./workflows/reusable.yaml". bool value cannot be assigned [expression] 5 | workflows/test.yaml:16:18: input "num_input" is typed as number by reusable workflow "./workflows/reusable.yaml". string value cannot be assigned [expression] 6 | workflows/test.yaml:22:17: input "num_input" is typed as number by reusable workflow "./workflows/reusable.yaml". string value cannot be assigned [expression] 7 | -------------------------------------------------------------------------------- /testdata/examples/webhook_checks.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" [syntax-check] 2 | test.yaml:7:5: both "paths" and "paths-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns [events] 3 | test.yaml:10:12: invalid activity type "created" for "issues" Webhook event. available types are "assigned", "closed", "deleted", "demilestoned", "edited", "labeled", "locked", "milestoned", "opened", "pinned", "reopened", "transferred", "unassigned", "unlabeled", "unlocked", "unpinned" [events] 4 | test.yaml:13:5: "tags" filter is not available for release event. it is only for push event [events] 5 | test.yaml:15:3: unknown Webhook event "pullreq". see https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events for list of all Webhook event names [events] 6 | -------------------------------------------------------------------------------- /testdata/format/test.json: -------------------------------------------------------------------------------- 1 | [{"message":"unexpected key \"branch\" for \"push\" section. expected one of \"branches\", \"branches-ignore\", \"paths\", \"paths-ignore\", \"tags\", \"tags-ignore\", \"types\", \"workflows\"","filepath":"testdata/format/test.yaml","line":3,"column":5,"kind":"syntax-check","snippet":" branch: main\n ^~~~~~~"},{"message":"label \"linux-latest\" is unknown. available labels are \"windows-latest\", \"windows-2022\", \"windows-2019\", \"windows-2016\", \"ubuntu-latest\", \"ubuntu-22.04\", \"ubuntu-20.04\", \"ubuntu-18.04\", \"macos-latest\", \"macos-12\", \"macos-12.0\", \"macos-11\", \"macos-11.0\", \"macos-10.15\", \"self-hosted\", \"x64\", \"arm\", \"arm64\", \"linux\", \"macos\", \"windows\". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file","filepath":"testdata/format/test.yaml","line":6,"column":14,"kind":"runner-label","snippet":" runs-on: linux-latest\n ^~~~~~~~~~~~"}] 2 | -------------------------------------------------------------------------------- /testdata/format/test.jsonl: -------------------------------------------------------------------------------- 1 | {"message":"unexpected key \"branch\" for \"push\" section. expected one of \"branches\", \"branches-ignore\", \"paths\", \"paths-ignore\", \"tags\", \"tags-ignore\", \"types\", \"workflows\"","filepath":"testdata/format/test.yaml","line":3,"column":5,"kind":"syntax-check","snippet":" branch: main\n ^~~~~~~"} 2 | {"message":"label \"linux-latest\" is unknown. available labels are \"windows-latest\", \"windows-2022\", \"windows-2019\", \"windows-2016\", \"ubuntu-latest\", \"ubuntu-22.04\", \"ubuntu-20.04\", \"ubuntu-18.04\", \"macos-latest\", \"macos-12\", \"macos-12.0\", \"macos-11\", \"macos-11.0\", \"macos-10.15\", \"self-hosted\", \"x64\", \"arm\", \"arm64\", \"linux\", \"macos\", \"windows\". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file","filepath":"testdata/format/test.yaml","line":6,"column":14,"kind":"runner-label","snippet":" runs-on: linux-latest\n ^~~~~~~~~~~~"} 3 | -------------------------------------------------------------------------------- /testdata/projects/example_inputs_secrets_in_workflow_call.out: -------------------------------------------------------------------------------- 1 | workflows/test.yaml:6:11: input "name" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call] 2 | workflows/test.yaml:6:11: secret "password" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call] 3 | workflows/test.yaml:9:7: input "user" is not defined in "./.github/workflows/reusable.yaml" reusable workflow. defined inputs are "id", "message", "name" [workflow-call] 4 | workflows/test.yaml:13:7: secret "credentials" is not defined in "./.github/workflows/reusable.yaml" reusable workflow. defined secret is "password" [workflow-call] 5 | workflows/test.yaml:22:11: input "id" is typed as number by reusable workflow "./.github/workflows/reusable.yaml". bool value cannot be assigned [expression] 6 | workflows/test.yaml:24:16: input "message" is typed as string by reusable workflow "./.github/workflows/reusable.yaml". null value cannot be assigned [expression] 7 | -------------------------------------------------------------------------------- /popular_actions_test.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestPopularActionsDataset(t *testing.T) { 9 | if len(PopularActions) == 0 { 10 | t.Fatal("popular actions data set is empty") 11 | } 12 | 13 | for n, meta := range PopularActions { 14 | if meta == nil { 15 | t.Fatalf("metadata for %s is nil", n) 16 | } 17 | if meta.Name == "" { 18 | t.Fatalf("action name for %s is empty", n) 19 | } 20 | for id, i := range meta.Inputs { 21 | if id != strings.ToLower(id) { 22 | t.Errorf("input ID %q is not in lower case at %q", id, n) 23 | } 24 | if i.Name == "" { 25 | t.Errorf("input name is not empty at ID %q at %q", id, n) 26 | } 27 | } 28 | for id, o := range meta.Outputs { 29 | if id != strings.ToLower(id) { 30 | t.Errorf("output ID %q is not in lower case at %q", id, n) 31 | } 32 | if o.Name == "" { 33 | t.Errorf("output name is not empty at ID %q at %q", id, n) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /testdata/examples/contexts_and_buitin_funcs.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | # Access undefined context 7 | - run: echo '${{ unknown_context }}' 8 | # Access undefined property of context 9 | - run: echo '${{ github.events }}' 10 | # Calling undefined function (start's'With is correct) 11 | - run: echo "${{ startWith('hello, world', 'lo,') }}" 12 | # Wrong number of arguments 13 | - run: echo "${{ startsWith('hello, world') }}" 14 | # Wrong type of parameter 15 | - run: echo "${{ startsWith('hello, world', github.event) }}" 16 | # Function overloads can be handled properly. contains() has string version and array version 17 | - run: echo "${{ contains('hello, world', 'lo,') }}" 18 | - run: echo "${{ contains(github.event.labels.*.name, 'enhancement') }}" 19 | # format() has a special check for formating string 20 | - run: echo "${{ format('{0}{1}', 1, 2, 3) }}" 21 | -------------------------------------------------------------------------------- /testdata/examples/workflow_call_definitions.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | scheme: 5 | description: Scheme of URL 6 | # OK: Type is string 7 | default: https 8 | type: string 9 | host: 10 | default: example.com 11 | type: string 12 | port: 13 | description: Port of URL 14 | # ERROR: Type is number but default value is string 15 | default: ':1234' 16 | type: number 17 | query: 18 | description: Query of URL 19 | # ERROR: Type must be one of number, string, boolean 20 | type: object 21 | path: 22 | description: Path of URL 23 | required: true 24 | # ERROR: Default value is never used since this input is required 25 | default: '' 26 | type: string 27 | jobs: 28 | do: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - run: echo "${{ inputs.scheme }}://${{ inputs.host }}:${{ inputs.port }}${{ inputs.path }}" 32 | -------------------------------------------------------------------------------- /testdata/examples/contexts_and_buitin_funcs.out: -------------------------------------------------------------------------------- 1 | test.yaml:7:24: undefined variable "unknown_context". available variables are "env", "github", "inputs", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy" [expression] 2 | /test\.yaml:9:24: property "events" is not defined in object type {.+} \[expression\]/ 3 | test.yaml:11:24: undefined function "startWith". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson" [expression] 4 | test.yaml:13:24: number of arguments is wrong. function "startsWith(string, string) -> bool" takes 2 parameters but 1 arguments are given [expression] 5 | test.yaml:15:51: 2nd argument of function call is not assignable. "object" cannot be assigned to "string". called function type is "startsWith(string, string) -> bool" [expression] 6 | test.yaml:20:24: format string "{0}{1}" does not contain placeholder {2}. remove argument which is unused in the format string [expression] 7 | -------------------------------------------------------------------------------- /testdata/err/workflow_dispatch_type_check_inputs.out: -------------------------------------------------------------------------------- 1 | test.yaml:21:24: property "select" is not defined in object type {boolean: bool; choice: string; environment: string; string: string} [expression] 2 | test.yaml:22:24: property "select" is not defined in object type {boolean: string; choice: string; environment: string; string: string} [expression] 3 | test.yaml:24:31: property access of object must be type of string but got "bool" [expression] 4 | test.yaml:28:39: index access of array must be type of number but got "string" [expression] 5 | test.yaml:29:39: index access of array must be type of number but got "string" [expression] 6 | test.yaml:30:39: index access of array must be type of number but got "string" [expression] 7 | test.yaml:31:39: index access of array must be type of number but got "string" [expression] 8 | test.yaml:32:39: index access of array must be type of number but got "string" [expression] 9 | test.yaml:33:39: index access of array must be type of number but got "string" [expression] 10 | -------------------------------------------------------------------------------- /testdata/examples/workflow_dispatch_input_types.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | # Unknown input type 5 | id: 6 | type: number 7 | # ERROR: No options for 'choice' input type 8 | kind: 9 | type: choice 10 | name: 11 | type: choice 12 | options: 13 | - Tama 14 | - Mike 15 | # ERROR: Default value is not in options 16 | default: Chobi 17 | message: 18 | type: string 19 | verbose: 20 | type: boolean 21 | # ERROR: Boolean value must be 'true' or 'false' 22 | default: yes 23 | 24 | jobs: 25 | test: 26 | runs-on: ubuntu-latest 27 | steps: 28 | # ERROR: Undefined input 29 | - run: echo "${{ inputs.massage }}" 30 | # ERROR: Bool value is not available for object key 31 | - run: echo "${{ env[inputs.verbose] }}" 32 | # ERROR: `github.event.inputs` is also not defined 33 | - run: echo "${{ github.event.inputs.massage }}" 34 | -------------------------------------------------------------------------------- /testdata/examples/untrusted_input.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: pull_request 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Print pull request title 9 | # ERROR: Using the potentially untrusted input can cause script injection 10 | run: echo '${{ github.event.pull_request.title }}' 11 | - uses: actions/stale@v4 12 | with: 13 | repo-token: ${{ secrets.TOKEN }} 14 | # This is OK because action input is not evaluated by shell 15 | stale-pr-message: ${{ github.event.pull_request.title }} was closed 16 | - uses: actions/github-script@v4 17 | with: 18 | # ERROR: Using the potentially untrusted input can cause script injection 19 | script: console.log('${{ github.event.head_commit.author.name }}') 20 | - name: Get comments 21 | # ERROR: Accessing untrusted inputs via `.*` object filter; bodies of comment, review, and review_comment 22 | run: echo '${{ toJSON(github.event.*.body) }}' 23 | -------------------------------------------------------------------------------- /testdata/ok/workflow_call_event.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | input0: 5 | description: 'hello' 6 | type: string 7 | input1: 8 | description: 'with default and required' 9 | default: 'hello' 10 | required: false 11 | type: string 12 | input2: 13 | description: 'with default' 14 | default: 23 15 | type: number 16 | input3: 17 | description: 'with required' 18 | required: true 19 | type: boolean 20 | secrets: 21 | secret0: 22 | description: '🙊' 23 | secret1: 24 | description: 'with required' 25 | required: true 26 | 27 | jobs: 28 | test: 29 | runs-on: ubuntu-20.04 30 | steps: 31 | - run: | 32 | echo ${{ inputs.input0 }} 33 | echo ${{ inputs.input1 }} 34 | echo ${{ inputs.input2 }} 35 | echo ${{ inputs.input3 }} 36 | echo ${{ secrets.secret0 }} 37 | echo ${{ secrets.secret1 }} 38 | -------------------------------------------------------------------------------- /scripts/generate-popular-actions/testdata/fetched.go: -------------------------------------------------------------------------------- 1 | // Code generated by actionlint/scripts/generate-popular-actions. DO NOT EDIT. 2 | 3 | package actionlint 4 | 5 | // PopularActions is data set of known popular actions. Keys are specs (owner/repo@ref) of actions 6 | // and values are their metadata. 7 | var PopularActions = map[string]*ActionMetadata{ 8 | "rhysd/action-setup-vim@v1.2.7": { 9 | Name: "Setup Vim", 10 | Inputs: ActionMetadataInputs{ 11 | "neovim": {"neovim", false}, 12 | "token": {"token", false}, 13 | "version": {"version", false}, 14 | }, 15 | Outputs: ActionMetadataOutputs{ 16 | "executable": {"executable"}, 17 | }, 18 | }, 19 | "rhysd/changelog-from-release/action@v2.2.2": { 20 | Name: "Run changelog-from-release", 21 | Inputs: ActionMetadataInputs{ 22 | "commit": {"commit", false}, 23 | "file": {"file", true}, 24 | "github_token": {"github_token", true}, 25 | "push": {"push", false}, 26 | "version": {"version", false}, 27 | }, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /testdata/examples/glob.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:10: character '^' is invalid for branch and tag names. ref name cannot contain spaces, ~, ^, :, [, ?, *. see `man git-check-ref-format` for more details. note that regular expression is unavailable. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob] 2 | test.yaml:9:12: invalid glob pattern. unexpected character '+' while checking special character + (one or more). the preceding character must not be special character. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob] 3 | test.yaml:11:14: invalid glob pattern. unexpected character '1' while checking character range in []. start of range '9' (57) is larger than end of range '1' (49). note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob] 4 | -------------------------------------------------------------------------------- /testdata/err/env_context_banned.yaml: -------------------------------------------------------------------------------- 1 | # issue #158 2 | on: push 3 | 4 | env: 5 | # 'env:' at toplevel cannot refer 'env' context 6 | ERROR1: ${{ env.PATH }} 7 | 8 | jobs: 9 | my_job: 10 | runs-on: ubuntu-latest 11 | env: 12 | FOO: aaa 13 | # 'env:' at job level cannot refer 'env' context 14 | ERROR2: ${{ env.PATH }} 15 | steps: 16 | # 'uses:' and 'id:' cause errors 17 | - uses: test/${{ env.FOO }}@main 18 | id: foo-${{ env.FOO }} 19 | # 'env:' at step level can refer 'env' context 20 | - run: echo "$BAR" 21 | env: 22 | BAR: ${{ env.FOO }} 23 | # This still should be OK 24 | - uses: test/my-action@main 25 | env: 26 | OS: ${{ runner.os }} 27 | id: foo-${{ runner.name }} 28 | container-job: 29 | runs-on: ubuntu-latest 30 | container: 31 | image: node:14.16 32 | # 'env:' at 'container:' can refer 'env' context 33 | env: 34 | MYPATH: ${{ env.PATH }} 35 | steps: 36 | - run: echo hello 37 | -------------------------------------------------------------------------------- /scripts/generate-actionlint-matcher/README.md: -------------------------------------------------------------------------------- 1 | generate-actionlint-matcher 2 | =========================== 3 | 4 | This script generates [`actionlint-matcher.json`](../../.github/actionlint-matcher.json). 5 | 6 | ## Usage 7 | 8 | ```sh 9 | make .github/actionlint-matcher.json 10 | ``` 11 | 12 | or directly run the script 13 | 14 | ```sh 15 | node ./scripts/generate-actionlint-matcher/main.js .github/actionlint-matcher.json 16 | ``` 17 | 18 | ## Test 19 | 20 | ```sh 21 | node ./scripts/generate-actionlint-matcher/test.js 22 | ``` 23 | 24 | The test uses test data at `./scripts/generate-actionlint-matcher/test/*.txt`. They should be updated when actionlint changes 25 | the default error message format. To update them: 26 | 27 | ```sh 28 | make ./scripts/generate-actionlint-matcher/test/escape.txt 29 | make ./scripts/generate-actionlint-matcher/test/no_escape.txt 30 | make ./scripts/generate-actionlint-matcher/test/want.json 31 | ``` 32 | 33 | or expand glob by your shell: 34 | 35 | ```sh 36 | make ./scripts/generate-actionlint-matcher/test/* 37 | ``` 38 | -------------------------------------------------------------------------------- /playground/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 8px; 3 | width: 100%; 4 | } 5 | 6 | #header-bar { 7 | padding: 1rem; 8 | background-color: transparent; 9 | } 10 | 11 | #permalink-btn { 12 | margin-right: 1rem; 13 | } 14 | 15 | #logo { 16 | color: inherit; 17 | } 18 | 19 | main { 20 | width: 100%; 21 | display: flex; 22 | flex-direction: row; 23 | } 24 | 25 | @media (max-width: 1200px) { 26 | main { 27 | flex-direction: column; 28 | } 29 | } 30 | 31 | .split-pane { 32 | box-sizing: border-box; 33 | padding: 8px; 34 | width: 50%; 35 | display: flex; 36 | flex-direction: column; 37 | } 38 | 39 | @media (max-width: 1200px) { 40 | .split-pane { 41 | width: 100%; 42 | } 43 | } 44 | 45 | #error-msg, 46 | #success-msg { 47 | display: none; 48 | } 49 | 50 | .CodeMirror { 51 | height: auto; 52 | border: solid 1px #555; 53 | border-radius: 4px; 54 | } 55 | 56 | .error-marker { 57 | width: 1em; 58 | } 59 | 60 | #lint-result { 61 | border-radius: 4px; 62 | cursor: pointer; 63 | } 64 | 65 | footer { 66 | margin-top: 32px; 67 | } 68 | -------------------------------------------------------------------------------- /scripts/generate-webhook-events/README.md: -------------------------------------------------------------------------------- 1 | generate-webhook-events 2 | ======================= 3 | 4 | This is a script for generating [`all_webhooks.go`](../../all_webhooks.go). 5 | 6 | It does: 7 | 8 | 1. Fetch [the official markdown document](https://raw.githubusercontent.com/github/docs/main/content/actions/using-workflows/events-that-trigger-workflows.md) 9 | 2. Parse the markdown file and find Webhook names and their types from tables 10 | 3. Generate mappings from Webhook names to their types as Go map variable 11 | 12 | ## Usage 13 | 14 | ``` 15 | generate-webhook-events [[srcfile] dstfile] 16 | ``` 17 | 18 | Generate `all_webhooks.go` file: 19 | 20 | ```sh 21 | go run ./scripts/generate-webhook-events ./all_webhooks.go 22 | ``` 23 | 24 | When the markdown file is in local: 25 | 26 | ```sh 27 | go run ./scripts/generate-webhook-events ./events-that-trigger-workflows.md ./all_webhooks.go 28 | ``` 29 | 30 | For debugging, specifying `-` to `dstfile` outputs the generated source to stdout: 31 | 32 | ```sh 33 | go run ./scripts/generate-webhook-events - 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /testdata/err/upper_case_duplicate_keys.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | foo: 5 | type: string 6 | FOO: 7 | type: number 8 | secrets: 9 | foo: 10 | FOO: 11 | outputs: 12 | foo: 13 | value: ... 14 | FOO: 15 | value: ... 16 | 17 | env: 18 | foo: ... 19 | FOO: ... 20 | 21 | jobs: 22 | test: 23 | strategy: 24 | matrix: 25 | version_name: [v1, v2] 26 | VERSION_NAME: [V1, V2] 27 | runs-on: ubuntu-latest 28 | RUNS-ON: UBUNTU-LATEST 29 | container: foo:latest 30 | services: 31 | redis: 32 | image: redis 33 | REDIS: 34 | image: redis2 35 | steps: 36 | - run: echo 'hello' 37 | env: 38 | FOO: ... 39 | foo: ... 40 | - uses: foo/bar@main 41 | with: 42 | foo: ... 43 | FOO: ... 44 | call: 45 | uses: owner/repo@main 46 | with: 47 | foo_input: ... 48 | FOO_input: ... 49 | secrets: 50 | foo_secret: ... 51 | FOO_secret: ... 52 | -------------------------------------------------------------------------------- /scripts/generate-actionlint-matcher/object.js: -------------------------------------------------------------------------------- 1 | const ESCAPE = '(?:\\x1b\\[\\d+m)'; // Matching to ANSI color escape sequence 2 | const FILEPATH = '.+?'; 3 | const LINE = '\\d+'; 4 | const COL = '\\d+'; 5 | const MESSAGE = '.+?'; 6 | const KIND = '.+?'; 7 | 8 | let regexp = '^E?(F)E*:E*(L)E*:E*(C)E*: E*(M)E* \\[(K)\\]$'; 9 | regexp = regexp.replace(/E/g, ESCAPE); // replaceAll is not available in node v14 10 | regexp = regexp.replace('F', FILEPATH); 11 | regexp = regexp.replace('L', LINE); 12 | regexp = regexp.replace('C', COL); 13 | regexp = regexp.replace('M', MESSAGE); 14 | regexp = regexp.replace('K', KIND); 15 | 16 | const object = { 17 | problemMatcher: [ 18 | { 19 | owner: 'actionlint', 20 | pattern: [ 21 | { 22 | regexp, 23 | file: 1, 24 | line: 2, 25 | column: 3, 26 | message: 4, 27 | code: 5, 28 | }, 29 | ], 30 | }, 31 | ], 32 | }; 33 | 34 | module.exports = object; 35 | -------------------------------------------------------------------------------- /testdata/examples/untrusted_input.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:24: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 2 | test.yaml:19:36: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 3 | test.yaml:22:31: object filter extracts potentially untrusted properties "github.event.comment.body", "github.event.discussion.body", "github.event.issue.body", "github.event.pull_request.body", "github.event.review.body", "github.event.review_comment.body". avoid using the value directly in inline scripts. instead, pass the value through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 4 | -------------------------------------------------------------------------------- /testdata/err/invalid_event_filters.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:5: "paths" filter is not available for pull_request_review event. it is only for push, pull_request, pull_request_target events [events] 2 | test.yaml:5:5: "paths-ignore" filter is not available for pull_request_review event. it is only for push, pull_request, pull_request_target events [events] 3 | test.yaml:6:5: "branches" filter is not available for pull_request_review event. it is only for push, pull_request, pull_request_target, workflow_run events [events] 4 | test.yaml:7:5: "branches-ignore" filter is not available for pull_request_review event. it is only for push, pull_request, pull_request_target, workflow_run events [events] 5 | test.yaml:8:5: "tags" filter is not available for pull_request_review event. it is only for push event [events] 6 | test.yaml:9:5: "tags-ignore" filter is not available for pull_request_review event. it is only for push event [events] 7 | test.yaml:11:5: "tags" filter is not available for pull_request event. it is only for push event [events] 8 | test.yaml:12:5: "tags-ignore" filter is not available for pull_request event. it is only for push event [events] 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | the MIT License 2 | 3 | Copyright (c) 2021 rhysd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 17 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 20 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /testdata/examples/contextual_needs_object.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | install: 4 | outputs: 5 | installed: '...' 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo 'install something' 9 | prepare: 10 | outputs: 11 | prepared: '...' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: echo 'parepare something' 15 | # ERROR: Outputs in other job is not accessible 16 | - run: echo '${{ needs.prepare.outputs.prepared }}' 17 | build: 18 | needs: [install, prepare] 19 | outputs: 20 | built: '...' 21 | runs-on: ubuntu-latest 22 | steps: 23 | # OK: Accessing job results 24 | - run: echo 'build something with ${{ needs.install.outputs.installed }} and ${{ needs.prepare.outputs.prepared }}' 25 | # ERROR: Accessing undefined output causes an error 26 | - run: echo '${{ needs.install.outputs.foo }}' 27 | # ERROR: Accessing undefined job ID 28 | - run: echo '${{ needs.some_job }}' 29 | other: 30 | runs-on: ubuntu-latest 31 | steps: 32 | # ERROR: Cannot access outputs across jobs 33 | - run: echo '${{ needs.build.outputs.built }}' 34 | -------------------------------------------------------------------------------- /testdata/err/exclusive_webhook_filters.out: -------------------------------------------------------------------------------- 1 | test.yaml:4:5: both "paths" and "paths-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns [events] 2 | test.yaml:6:5: both "branches" and "branches-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns [events] 3 | test.yaml:8:5: both "tags" and "tags-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns [events] 4 | test.yaml:11:5: both "paths" and "paths-ignore" filters cannot be used for the same event "pull_request". note: use '!' to negate patterns [events] 5 | test.yaml:13:5: both "branches" and "branches-ignore" filters cannot be used for the same event "pull_request". note: use '!' to negate patterns [events] 6 | test.yaml:16:5: both "paths" and "paths-ignore" filters cannot be used for the same event "pull_request_target". note: use '!' to negate patterns [events] 7 | test.yaml:18:5: both "branches" and "branches-ignore" filters cannot be used for the same event "pull_request_target". note: use '!' to negate patterns [events] 8 | test.yaml:22:5: both "branches" and "branches-ignore" filters cannot be used for the same event "workflow_run". note: use '!' to negate patterns [events] 9 | -------------------------------------------------------------------------------- /fuzz/check.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | package actionlint_fuzz 3 | 4 | import "github.com/rhysd/actionlint" 5 | 6 | func parseWorkflowPanicFree(data []byte) *actionlint.Workflow { 7 | // Avoid Parse() panicking. It panics when go-yaml panics 8 | defer func() { recover() }() 9 | w, _ := actionlint.Parse(data) 10 | return w 11 | } 12 | 13 | func FuzzCheck(data []byte) int { 14 | w := parseWorkflowPanicFree(data) 15 | if w == nil { 16 | return 0 17 | } 18 | 19 | ac := actionlint.NewLocalActionsCache(nil, nil) 20 | wc := actionlint.NewLocalReusableWorkflowCache(nil, "", nil) 21 | 22 | rules := []actionlint.Rule{ 23 | actionlint.NewRuleMatrix(), 24 | actionlint.NewRuleCredentials(), 25 | actionlint.NewRuleShellName(), 26 | actionlint.NewRuleRunnerLabel([]string{}), 27 | actionlint.NewRuleEvents(), 28 | actionlint.NewRuleGlob(), 29 | actionlint.NewRuleJobNeeds(), 30 | actionlint.NewRuleAction(ac), 31 | actionlint.NewRuleEnvVar(), 32 | actionlint.NewRuleID(), 33 | actionlint.NewRuleExpression(ac, wc), 34 | } 35 | 36 | v := actionlint.NewVisitor() 37 | for _, rule := range rules { 38 | v.AddPass(rule) 39 | } 40 | 41 | if err := v.Visit(w); err != nil { 42 | return 0 43 | } 44 | 45 | return 1 46 | } 47 | -------------------------------------------------------------------------------- /playground/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "mocha": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "plugins": [ 13 | "@typescript-eslint" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": 12, 18 | "project": "./tsconfig.json" 19 | }, 20 | "globals": { 21 | "CodeMirror": "readonly", 22 | "isMobile": "readonly", 23 | "Go": "readonly" 24 | }, 25 | "rules": { 26 | "indent": [ 27 | "error", 28 | 4 29 | ], 30 | "linebreak-style": [ 31 | "error", 32 | "unix" 33 | ], 34 | "quotes": [ 35 | "error", 36 | "single" 37 | ], 38 | "semi": [ 39 | "error", 40 | "always" 41 | ], 42 | "eqeqeq": [ 43 | "error", 44 | "always" 45 | ], 46 | "no-constant-condition": [ 47 | "error", 48 | { "checkLoops": false } 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /testdata/examples/contextual_matrix_values.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | os: [ubuntu-latest, windows-latest] 7 | node: [14, 15] 8 | package: 9 | - name: 'foo' 10 | optional: true 11 | - name: 'bar' 12 | optional: false 13 | include: 14 | - node: 15 15 | npm: 7.5.4 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | # Access undefined matrix value 19 | - run: echo '${{ matrix.platform }}' 20 | # Matrix value is strongly typed. Below line causes an error since matrix.package is {name: string, optional: bool} 21 | - run: echo '${{ matrix.package.dev }}' 22 | # OK 23 | - run: | 24 | echo 'os: ${{ matrix.os }}' 25 | echo 'node version: ${{ matrix.node }}' 26 | echo 'package: ${{ matrix.package.name }} (optional=${{ matrix.package.optional }})' 27 | # Additional matrix values in 'include:' are supported 28 | - run: echo 'npm version is specified' 29 | if: ${{ contains(matrix.npm, '7.5') }} 30 | test2: 31 | runs-on: ubuntu-latest 32 | steps: 33 | # Matrix values in other job is not accessible 34 | - run: echo '${{ matrix.os }}' 35 | -------------------------------------------------------------------------------- /rule_credentials.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // RuleCredentials is a rule to check credentials in workflows 9 | type RuleCredentials struct { 10 | RuleBase 11 | } 12 | 13 | // NewRuleCredentials creates new RuleCredentials instance 14 | func NewRuleCredentials() *RuleCredentials { 15 | return &RuleCredentials{ 16 | RuleBase: RuleBase{name: "credentials"}, 17 | } 18 | } 19 | 20 | // VisitJobPre is callback when visiting Job node before visiting its children. 21 | func (rule *RuleCredentials) VisitJobPre(n *Job) error { 22 | if n.Container != nil { 23 | rule.checkContainer("\"container\" section", n.Container) 24 | } 25 | for _, s := range n.Services { 26 | rule.checkContainer(fmt.Sprintf("%q service", s.Name.Value), s.Container) 27 | } 28 | return nil 29 | } 30 | 31 | func (rule *RuleCredentials) checkContainer(where string, n *Container) { 32 | if n.Credentials == nil || n.Credentials.Password == nil { 33 | return 34 | } 35 | 36 | p := n.Credentials.Password 37 | s := strings.TrimSpace(p.Value) 38 | if !strings.HasPrefix(s, "${{") || !strings.HasSuffix(s, "}}") { 39 | rule.errorf(p.Pos, "\"password\" section in %s should be specified via secrets. do not put password value directly", where) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | This document describes how to configure [actionlint](..) behavior. 5 | 6 | Note that configuration file is optional. The author tries to keep configuration file as minimal as possible not to 7 | bother users to configure behavior of actionlint. Running actionlint without configuration file would work fine in most 8 | cases. 9 | 10 | ## Configuration file 11 | 12 | Configuration file `actionlint.yaml` or `actionlint.yml` can be put in `.github` directory. 13 | 14 | You don't need to write the first configuration file by your hand. `actionlint` command can generate a default configuration 15 | with `-init-config` flag. 16 | 17 | ```sh 18 | actionlint -init-config 19 | vim .github/actionlint.yaml 20 | ``` 21 | 22 | Currently only one item can be configured. 23 | 24 | ```yaml 25 | self-hosted-runner: 26 | # Labels of self-hosted runner in array of string 27 | labels: 28 | - linux.2xlarge 29 | - windows-latest-xl 30 | - linux-multi-gpu 31 | ``` 32 | 33 | - `self-hosted-runner`: Configuration for your self-hosted runner environment 34 | - `labels`: Label names added to your self-hosted runners as list of string 35 | 36 | --- 37 | 38 | [Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Go API](api.md) | [References](reference.md) 39 | -------------------------------------------------------------------------------- /testdata/err/workflow_dispatch_type_check_inputs.yaml: -------------------------------------------------------------------------------- 1 | name: Test for type check of workflow_dispatch inputs 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | string: 6 | type: string 7 | choice: 8 | type: choice 9 | options: 10 | - aaa 11 | boolean: 12 | type: boolean 13 | environment: 14 | type: environment 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | # Unkown input name 21 | - run: echo "${{ inputs.select }}" 22 | - run: echo "${{ github.event.inputs.select }}" 23 | # Bool value is not available for object key 24 | - run: echo "${{ github[inputs.boolean] }}" 25 | # ... but github.event.inputs.* is available since its properties are all strings 26 | - run: echo "${{ github[github.event.inputs.boolean] }}" 27 | # String value cannot be index value of array 28 | - run: echo "${{ github.event.*[github.event.inputs.choice] }}" 29 | - run: echo "${{ github.event.*[github.event.inputs.string] }}" 30 | - run: echo "${{ github.event.*[github.event.inputs.environment] }}" 31 | - run: echo "${{ github.event.*[inputs.choice] }}" 32 | - run: echo "${{ github.event.*[inputs.string] }}" 33 | - run: echo "${{ github.event.*[inputs.environment] }}" 34 | -------------------------------------------------------------------------------- /rule_shellcheck_test.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | ) 7 | 8 | func TestRuleShellcheckSanitizeExpressionsInScript(t *testing.T) { 9 | testCases := []struct { 10 | input string 11 | want string 12 | }{ 13 | { 14 | "", 15 | "", 16 | }, 17 | { 18 | "foo", 19 | "foo", 20 | }, 21 | { 22 | "${{}}", 23 | "_____", 24 | }, 25 | { 26 | "${{ matrix.foo }}", 27 | "_________________", 28 | }, 29 | { 30 | "aaa ${{ matrix.foo }} bbb", 31 | "aaa _________________ bbb", 32 | }, 33 | { 34 | "${{}}${{}}", 35 | "__________", 36 | }, 37 | { 38 | "p${{a}}q${{b}}r", 39 | "p______q______r", 40 | }, 41 | { 42 | "${{", 43 | "${{", 44 | }, 45 | { 46 | "}}", 47 | "}}", 48 | }, 49 | { 50 | "aaa${{foo", 51 | "aaa${{foo", 52 | }, 53 | { 54 | "a${{b}}${{c", 55 | "a______${{c", 56 | }, 57 | { 58 | "a${{b}}c}}d", 59 | "a______c}}d", 60 | }, 61 | { 62 | "a}}b${{c}}d", 63 | "a}}b______d", 64 | }, 65 | } 66 | 67 | for i, tc := range testCases { 68 | t.Run(strconv.Itoa(i), func(t *testing.T) { 69 | have := sanitizeExpressionsInScript(tc.input) 70 | if tc.want != have { 71 | t.Fatalf("sanitized result is unexpected.\nwant: %q\nhave: %q", tc.want, have) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/matcher.yaml: -------------------------------------------------------------------------------- 1 | name: Problem Matchers 2 | on: 3 | push: 4 | paths: 5 | - 'scripts/generate-actionlint-matcher/*.js' 6 | - 'testdata/examples/*.out' 7 | - 'testdata/err/*.out' 8 | branches: 9 | - main 10 | tags-ignore: 11 | - '*' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | matcher-test: 16 | name: Test generate-actionlint-matcher 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-go@v3 21 | with: 22 | go-version: '1.19' 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: "lts/*" 26 | - name: Update test data 27 | run: make ./scripts/generate-actionlint-matcher/test/* SKIP_GO_GENERATE=true 28 | - name: Test actionlint-matcher.json 29 | run: node ./scripts/generate-actionlint-matcher/test.js 30 | - name: Ensure .github/actionlint-matcher.json is up-to-date 31 | run: | 32 | make .github/actionlint-matcher.json 33 | if git diff --quiet; then 34 | echo 'OK' 35 | else 36 | echo 'ERROR! .github/actionlint-matcher.json is outdated. Update it by "make .github/actionlint-matcher.json"' >&2 37 | set -x 38 | git diff 39 | exit 1 40 | fi 41 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | Playground for actionlint 2 | ========================= 3 | 4 | This is a development directory for [actionlint playground](https://rhysd.github.io/actionlint/). 5 | 6 | The playground is built with HTML/CSS/TypeScript/Wasm. All dependencies are defined in `package.json` and managed by `npm`. 7 | Tasks for development are defined in [`Makefile`](./Makefile). 8 | 9 | ## Tasks 10 | 11 | ```sh 12 | # Install dependencies, build main.wasm, start serving the app at localhost:1234 using Python 13 | make 14 | 15 | # Install dependencies, build main.wasm 16 | make build 17 | 18 | # Install dependencies 19 | make install 20 | 21 | # Run tests 22 | make test 23 | 24 | # Clean all built files and dependencies 25 | make clean 26 | ``` 27 | 28 | ## Lint 29 | 30 | Sources are linted with [eslint](https://eslint.org/) with [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint), 31 | [prettier](https://prettier.io/) and [stylelint](https://stylelint.io/). 32 | 33 | `lint` npm script applies all the liters: 34 | 35 | ```sh 36 | npm run lint 37 | ``` 38 | 39 | ## Deployment 40 | 41 | Deployment is automated by [`deploy.bash`](./deploy.bash). See [CONTRIBUTING.md](../CONTRIBUTING.md) for more details. 42 | To optimize `main.wasm`, `wasm-opt` command is required. Install [Binaryen](https://github.com/WebAssembly/binaryen) in 43 | advance. 44 | -------------------------------------------------------------------------------- /testdata/err/workflow_dispatch_input_types.yaml: -------------------------------------------------------------------------------- 1 | name: Test for workflow_dispatch input types 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | # no options for chice 6 | choice_input_no_options: 7 | type: choice 8 | choice_input_options_is_empty: 9 | type: choice 10 | options: [] 11 | choice_input_empty_option: 12 | type: choice 13 | options: [''] 14 | choice_default_is_not_in_options: 15 | type: choice 16 | options: 17 | - foo 18 | default: bar 19 | choice_duplicate_options: 20 | type: choice 21 | options: 22 | - foo 23 | - foo 24 | boolean_invalid_default: 25 | type: boolean 26 | default: foo 27 | boolean_with_options: 28 | type: boolean 29 | options: 30 | - aaa 31 | - bbb 32 | string_with_options: 33 | type: string 34 | options: 35 | - aaa 36 | - bbb 37 | environment_with_options: 38 | type: environment 39 | options: 40 | - aaa 41 | - bbb 42 | no_type_with_options: 43 | options: 44 | - aaa 45 | - bbb 46 | input_unknown_type: 47 | type: unknown 48 | 49 | jobs: 50 | test: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - run: echo hi 54 | -------------------------------------------------------------------------------- /testdata/examples/runner_label_check.out: -------------------------------------------------------------------------------- 1 | test.yaml:10:13: label "linux-latest" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "windows-2016", "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-18.04", "macos-latest", "macos-12", "macos-12.0", "macos-11", "macos-11.0", "macos-10.15", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label] 2 | test.yaml:16:13: label "gpu" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "windows-2016", "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-18.04", "macos-latest", "macos-12", "macos-12.0", "macos-11", "macos-11.0", "macos-10.15", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label] 3 | test.yaml:23:14: label "macos-10.13" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "windows-2016", "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-18.04", "macos-latest", "macos-12", "macos-12.0", "macos-11", "macos-11.0", "macos-10.15", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label] 4 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case/workflows/output.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | upper: 5 | uses: ./reusable/upper.yaml 6 | with: 7 | my_input_1: hello 8 | my_input_2: world 9 | secrets: 10 | my_secret_1: hello 11 | my_secret_2: world 12 | lower: 13 | uses: ./reusable/lower.yaml 14 | with: 15 | my_input_1: hello 16 | my_input_2: world 17 | secrets: 18 | my_secret_1: hello 19 | my_secret_2: world 20 | downstream: 21 | needs: [upper, lower] 22 | runs-on: ubuntu-latest 23 | steps: 24 | - run: echo 'OK ${{ needs.upper.outputs.my_output_1 }}' 25 | - run: echo 'OK ${{ needs.upper.outputs.MY_OUTPUT_1 }}' 26 | - run: echo 'OK ${{ needs.lower.outputs.my_output_1 }}' 27 | - run: echo 'OK ${{ needs.lower.outputs.MY_OUTPUT_1 }}' 28 | - run: echo 'OK ${{ needs.upper.outputs.my_output_2 }}' 29 | - run: echo 'OK ${{ needs.upper.outputs.MY_OUTPUT_2 }}' 30 | - run: echo 'OK ${{ needs.lower.outputs.my_output_2 }}' 31 | - run: echo 'OK ${{ needs.lower.outputs.MY_OUTPUT_2 }}' 32 | - run: echo 'ERROR ${{ needs.upper.outputs.my_output_3 }}' 33 | - run: echo 'ERROR ${{ needs.upper.outputs.MY_OUTPUT_3 }}' 34 | - run: echo 'ERROR ${{ needs.lower.outputs.my_output_3 }}' 35 | - run: echo 'ERROR ${{ needs.lower.outputs.MY_OUTPUT_3 }}' 36 | -------------------------------------------------------------------------------- /testdata/err/workflow_dispatch_input_types.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:7: input type of "choice_input_no_options" is "choice" but "options" is not set [events] 2 | test.yaml:8:7: input type of "choice_input_options_is_empty" is "choice" but "options" is not set [events] 3 | test.yaml:10:18: "options" section should not be empty [syntax-check] 4 | test.yaml:13:19: string should not be empty [syntax-check] 5 | test.yaml:18:18: default value "bar" of "choice_default_is_not_in_options" input is not included in its options "\"foo\"" [events] 6 | test.yaml:23:13: option "foo" is duplicated in options of "choice_duplicate_options" input [events] 7 | test.yaml:26:18: type of "boolean_invalid_default" input is "boolean". its default value "foo" must be "true" or "false" [events] 8 | test.yaml:27:7: "options" can not be set to "boolean_with_options" input because its input type is not "choice" [events] 9 | test.yaml:32:7: "options" can not be set to "string_with_options" input because its input type is not "choice" [events] 10 | test.yaml:37:7: "options" can not be set to "environment_with_options" input because its input type is not "choice" [events] 11 | test.yaml:42:7: "options" can not be set to "no_type_with_options" input because its input type is not "choice" [events] 12 | test.yaml:47:15: input type of workflow_dispatch event must be one of "string", "boolean", "choice", "environment" but got "unknown" [syntax-check] 13 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Config is configuration of actionlint. This struct instance is parsed from "actionlint.yaml" 12 | // file usually put in ".github" directory. 13 | type Config struct { 14 | // SelfHostedRunner is configuration for self-hosted runner. 15 | SelfHostedRunner struct { 16 | // Labels is label names for self-hosted runner. 17 | Labels []string `yaml:"labels"` 18 | } `yaml:"self-hosted-runner"` 19 | } 20 | 21 | func parseConfig(b []byte, path string) (*Config, error) { 22 | var c Config 23 | if err := yaml.Unmarshal(b, &c); err != nil { 24 | msg := strings.ReplaceAll(err.Error(), "\n", " ") 25 | return nil, fmt.Errorf("could not parse config file %q: %s", path, msg) 26 | } 27 | return &c, nil 28 | } 29 | 30 | func readConfigFile(path string) (*Config, error) { 31 | b, err := os.ReadFile(path) 32 | if err != nil { 33 | return nil, fmt.Errorf("could not read config file %q: %w", path, err) 34 | } 35 | return parseConfig(b, path) 36 | } 37 | 38 | func writeDefaultConfigFile(path string) error { 39 | b := []byte(`self-hosted-runner: 40 | # Labels of self-hosted runner in array of string 41 | labels: [] 42 | `) 43 | if err := os.WriteFile(path, b, 0644); err != nil { 44 | return fmt.Errorf("could not write default configuration file at %q: %w", path, err) 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /rule_env_var.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import "strings" 4 | 5 | // RuleEnvVar is a rule checker to check environment variables setup. 6 | type RuleEnvVar struct { 7 | RuleBase 8 | } 9 | 10 | // NewRuleEnvVar creates new RuleEnvVar instance. 11 | func NewRuleEnvVar() *RuleEnvVar { 12 | return &RuleEnvVar{ 13 | RuleBase: RuleBase{name: "env-var"}, 14 | } 15 | } 16 | 17 | // VisitStep is callback when visiting Step node. 18 | func (rule *RuleEnvVar) VisitStep(n *Step) error { 19 | rule.checkEnv(n.Env) 20 | return nil 21 | } 22 | 23 | // VisitJobPre is callback when visiting Job node before visiting its children. 24 | func (rule *RuleEnvVar) VisitJobPre(n *Job) error { 25 | rule.checkEnv(n.Env) 26 | if n.Container != nil { 27 | rule.checkEnv(n.Container.Env) 28 | } 29 | for _, s := range n.Services { 30 | rule.checkEnv(s.Container.Env) 31 | } 32 | return nil 33 | } 34 | 35 | // VisitWorkflowPre is callback when visiting Workflow node before visiting its children. 36 | func (rule *RuleEnvVar) VisitWorkflowPre(n *Workflow) error { 37 | rule.checkEnv(n.Env) 38 | return nil 39 | } 40 | 41 | func (rule *RuleEnvVar) checkEnv(env *Env) { 42 | if env == nil { 43 | return 44 | } 45 | for _, v := range env.Vars { 46 | if strings.ContainsAny(v.Name.Value, "&= ") { 47 | rule.errorf( 48 | v.Name.Pos, 49 | "environment variable name %q is invalid. '&', '=' and spaces should not be contained", 50 | v.Name.Value, 51 | ) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | This document describes links to resources. 5 | 6 | - Repository: https://github.com/rhysd/actionlint 7 | - Playground: https://rhysd.github.io/actionlint/ 8 | - GitHub Actions official documentations 9 | - Workflow syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 10 | - Expression syntax: https://docs.github.com/en/actions/learn-github-actions/expressions 11 | - Built-in functions: https://docs.github.com/en/actions/learn-github-actions/expressions#functions 12 | - Webhook events: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events 13 | - Self-hosted runner: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners 14 | - Security: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions 15 | - CRON syntax: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07 16 | - shellcheck: https://github.com/koalaman/shellcheck 17 | - pyflakes: https://github.com/PyCQA/pyflakes 18 | - Japanese blog posts 19 | - GitHub Actions のワークフローをチェックする actionlint をつくった: https://rhysd.hatenablog.com/entry/2021/07/11/214313 20 | - actionlint v1.4 → v1.6 で実装した新機能の紹介: https://rhysd.hatenablog.com/entry/2021/08/11/221044 21 | 22 | --- 23 | 24 | [Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md) 25 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_event.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | # input without fields 5 | input0: 6 | # input without type 7 | input1: 8 | description: 'hi' 9 | # input without description 10 | input2: 11 | type: string 12 | # input with invalid type 13 | input3: 14 | description: 'hi' 15 | type: unknown 16 | # non-boolean type value for 'required' 17 | input4: 18 | description: 'hi' 19 | required: yes 20 | # unknown field 21 | input5: 22 | description: 'hi' 23 | type: number 24 | unknown: hello 25 | # duplicate input name 26 | input0: 27 | description: 'hi' 28 | type: number 29 | # type mismatch string for number 30 | input6: 31 | description: 'hi' 32 | default: foooo 33 | type: number 34 | # type mismatch (number for boolean) 35 | input7: 36 | description: 'hi' 37 | default: 123 38 | type: boolean 39 | secrets: 40 | # no field 41 | secret0: 42 | # unknown field 43 | secret1: 44 | description: '' 45 | unknown: bye 46 | # duplicate secret name 47 | secret1: 48 | description: '' 49 | # unknown key for workflow_call event 50 | unknown: 51 | 52 | jobs: 53 | test: 54 | runs-on: ubuntu-20.04 55 | steps: 56 | - run: echo ${{ inputs.unknown_input }} 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | # Every Saturday 6:41 in JST 10 | - cron: '41 21 * * 5' 11 | workflow_dispatch: 12 | 13 | # This environment is necessary to avoid the following issue 14 | # https://github.com/github/codeql/issues/6321 15 | env: 16 | CODEQL_EXTRACTOR_GO_BUILD_TRACING: 'on' 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: ['go', 'javascript'] 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: github/codeql-action/init@v2 33 | with: 34 | config-file: ./.github/codeql/codeql-config.yaml 35 | languages: ${{ matrix.language }} 36 | - uses: github/codeql-action/autobuild@v2 37 | if: ${{ matrix.language != 'go' }} 38 | - uses: actions/setup-go@v3 39 | with: 40 | go-version: '1.19' 41 | if: ${{ matrix.language == 'go' }} 42 | - name: Build Go sources 43 | run: | 44 | set -x 45 | go build -v ./cmd/actionlint 46 | GOOS=js GOARCH=wasm go build -v -o ./playground/main.wasm ./playground 47 | if: ${{ matrix.language == 'go' }} 48 | - uses: github/codeql-action/analyze@v2 49 | -------------------------------------------------------------------------------- /playground/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "syscall/js" 6 | 7 | "github.com/rhysd/actionlint" 8 | ) 9 | 10 | var ( 11 | window = js.Global().Get("window") 12 | ) 13 | 14 | func fail(err error, when string) { 15 | window.Call("showError", err.Error()+" on "+when) 16 | } 17 | 18 | func encodeErrorAsMap(err *actionlint.Error) map[string]interface{} { 19 | obj := make(map[string]interface{}, 4) 20 | obj["message"] = err.Message 21 | obj["line"] = err.Line 22 | obj["column"] = err.Column 23 | obj["kind"] = err.Kind 24 | return obj 25 | } 26 | 27 | func lint(source string) interface{} { 28 | opts := actionlint.LinterOptions{} 29 | linter, err := actionlint.NewLinter(io.Discard, &opts) 30 | if err != nil { 31 | fail(err, "creating linter instance") 32 | return nil 33 | } 34 | 35 | errs, err := linter.Lint("test.yaml", []byte(source), nil) 36 | if err != nil { 37 | fail(err, "applying lint rules") 38 | return nil 39 | } 40 | 41 | ret := make([]interface{}, 0, len(errs)) 42 | for _, err := range errs { 43 | ret = append(ret, encodeErrorAsMap(err)) 44 | } 45 | 46 | window.Call("onCheckCompleted", js.ValueOf(ret)) 47 | 48 | return nil 49 | } 50 | 51 | func runActionlint(_this js.Value, args []js.Value) interface{} { 52 | source := args[0].String() 53 | return lint(source) 54 | } 55 | 56 | func main() { 57 | window.Set("runActionlint", js.FuncOf(runActionlint)) 58 | window.Call("dismissLoading") 59 | lint(window.Call("getYamlSource").String()) // Show the first result 60 | select {} 61 | } 62 | -------------------------------------------------------------------------------- /testdata/projects/workflow_call_upper_case.out: -------------------------------------------------------------------------------- 1 | workflows/missing.yaml:5:11: input "MY_INPUT_2" is required by "./reusable/upper.yaml" reusable workflow [workflow-call] 2 | workflows/missing.yaml:5:11: secret "MY_SECRET_2" is required by "./reusable/upper.yaml" reusable workflow [workflow-call] 3 | workflows/output.yaml:32:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] 4 | workflows/output.yaml:33:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] 5 | workflows/output.yaml:34:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] 6 | workflows/output.yaml:35:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] 7 | workflows/undefined.yaml:9:7: input "MY_INPUT_3" is not defined in "./reusable/upper.yaml" reusable workflow. defined inputs are "MY_INPUT_1", "MY_INPUT_2" [workflow-call] 8 | workflows/undefined.yaml:13:7: secret "MY_SECRET_3" is not defined in "./reusable/upper.yaml" reusable workflow. defined secrets are "MY_SECRET_1", "MY_SECRET_2" [workflow-call] 9 | workflows/undefined.yaml:19:7: input "MY_INPUT_3" is not defined in "./reusable/lower.yaml" reusable workflow. defined inputs are "my_input_1", "my_input_2" [workflow-call] 10 | workflows/undefined.yaml:23:7: secret "MY_SECRET_3" is not defined in "./reusable/lower.yaml" reusable workflow. defined secrets are "my_secret_1", "my_secret_2" [workflow-call] 11 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actionlint-playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "", 6 | "main": "main.js", 7 | "scripts": { 8 | "lint:stylelint": "stylelint style.css", 9 | "lint:prettier": "prettier --check \"*.ts\"", 10 | "lint:eslint": "eslint --max-warnings 0 \"*.ts\"", 11 | "lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:stylelint", 12 | "prettier": "prettier --write \"*.ts\"", 13 | "build": "tsc -p .", 14 | "watch": "tsc -p . --watch", 15 | "serve": "http-server . -p 1234", 16 | "test": "mocha ./test.js" 17 | }, 18 | "author": "rhysd (https://rhysd.github.io/)", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@types/jsdom": "^20.0.0", 22 | "@types/mocha": "^9.1.1", 23 | "bulma": "^0.9.4", 24 | "bulmaswatch": "^0.8.1", 25 | "codemirror": "^5.65.8", 26 | "devicon": "^2.15.1", 27 | "ismobilejs": "^1.1.1", 28 | "jsdom": "^20.0.0", 29 | "mocha": "^10.0.0", 30 | "pako": "^2.0.4" 31 | }, 32 | "devDependencies": { 33 | "@peculiar/webcrypto": "^1.4.0", 34 | "@types/codemirror": "^5.60.5", 35 | "@types/node": "^18.7.13", 36 | "@types/pako": "^2.0.0", 37 | "@typescript-eslint/eslint-plugin": "^5.35.1", 38 | "@typescript-eslint/parser": "^5.35.1", 39 | "eslint": "^8.22.0", 40 | "http-server": "^14.1.1", 41 | "prettier": "^2.7.1", 42 | "stylelint": "^14.11.0", 43 | "stylelint-config-standard": "^28.0.0", 44 | "typescript": "^4.7.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /testdata/ok/workflow_dispatch_input_types.yaml: -------------------------------------------------------------------------------- 1 | name: Test for workflow_dispatch input types 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | name: 6 | type: choice 7 | description: Name of event 8 | options: 9 | - workflow_dispatch 10 | - workflow_call 11 | - webhook 12 | type: 13 | type: choice 14 | description: Type of input 15 | options: 16 | - choice 17 | - string 18 | - boolean 19 | - environment 20 | default: string 21 | submitter: 22 | type: string 23 | message: 24 | type: string 25 | default: hello 26 | verbose: 27 | type: boolean 28 | default: false 29 | dry-run: 30 | type: boolean 31 | default: true 32 | notification: 33 | type: boolean 34 | environment: 35 | type: environment 36 | 37 | jobs: 38 | test: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - run: echo '${{ github.event.inputs.name }}' 42 | - run: echo '${{ github.event.inputs.type }}' 43 | - run: echo '${{ github.event.inputs.submitter }}' 44 | - run: echo '${{ github.event.inputs.message }}' 45 | - run: echo '${{ github.event.inputs.verbose }}' 46 | - run: echo '${{ github.event.inputs.dry-run }}' 47 | - run: echo '${{ github.event.inputs.notification }}' 48 | - run: echo '${{ github.event.inputs.environment }}' 49 | - run: echo "${{ contains('hello, world!', github.event.inputs.name) }}" 50 | if: ${{ github.event.inputs.verbose }} 51 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_job.out: -------------------------------------------------------------------------------- 1 | test.yaml:6:5: when a reusable workflow is called with "uses", "steps" is not available. only following keys are allowed: "name", "uses", "with", "secrets", "needs", "if", and "permissions" in job "call1" [syntax-check] 2 | test.yaml:10:5: "with" is only available for a reusable workflow call with "uses" but "uses" is not found in job "call2" [syntax-check] 3 | test.yaml:17:5: "secrets" is only available for a reusable workflow call with "uses" but "uses" is not found in job "call3" [syntax-check] 4 | test.yaml:24:10: string should not be empty [syntax-check] 5 | test.yaml:27:11: reusable workflow call "./foo/bar/workflow.yml@main" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call] 6 | test.yaml:30:11: reusable workflow call "/foo/bar/workflow.yml@main" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call] 7 | test.yaml:33:11: reusable workflow call "foo/workflow.yml@main" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call] 8 | test.yaml:36:11: reusable workflow call "foo/bar/workflow.yml" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call] 9 | -------------------------------------------------------------------------------- /testdata/err/workflow_call_event.out: -------------------------------------------------------------------------------- 1 | test.yaml:5:7: "type" is missing at "input0" input of workflow_call event [syntax-check] 2 | test.yaml:7:7: "type" is missing at "input1" input of workflow_call event [syntax-check] 3 | test.yaml:15:15: invalid value "unknown" for input type of workflow_call event. it must be one of "boolean", "number", or "string" [syntax-check] 4 | test.yaml:17:7: "type" is missing at "input4" input of workflow_call event [syntax-check] 5 | test.yaml:19:19: expecting a single ${{...}} expression or boolean literal "true" or "false", but found plain text node [syntax-check] 6 | test.yaml:24:9: unexpected key "unknown" for "inputs at workflow_call event" section. expected one of "default", "description", "required", "type" [syntax-check] 7 | test.yaml:26:7: key "input0" is duplicated in "inputs" section. previously defined at line:5,col:7. note that key names are case insensitive [syntax-check] 8 | test.yaml:32:18: input of workflow_call event "input6" is typed as number but its default value "foooo" cannot be parsed as a float number: strconv.ParseFloat: parsing "foooo": invalid syntax [events] 9 | test.yaml:37:18: input of workflow_call event "input7" is typed as boolean. its default value must be true or false but got "123" [events] 10 | test.yaml:45:9: unexpected key "unknown" for "secrets" section. expected one of "description", "required" [syntax-check] 11 | test.yaml:47:7: key "secret1" is duplicated in "secrets" section. previously defined at line:43,col:7. note that key names are case insensitive [syntax-check] 12 | test.yaml:50:5: unexpected key "unknown" for "workflow_call" section. expected one of "inputs", "secrets" [syntax-check] 13 | /test\.yaml:56:23: property "unknown_input" is not defined in object type {.+} \[expression\]/ 14 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | release: 2 | draft: false 3 | prerelease: true 4 | 5 | before: 6 | hooks: 7 | - go mod download 8 | 9 | builds: 10 | - <<: &build_defaults 11 | main: ./cmd/actionlint 12 | ldflags: -s -w -X github.com/rhysd/actionlint.version={{.Version}} -X "github.com/rhysd/actionlint.installedFrom=installed by downloading from release page" 13 | env: 14 | - CGO_ENABLED=0 15 | id: macos 16 | goos: [darwin] 17 | goarch: [amd64, arm64] 18 | 19 | - <<: *build_defaults 20 | id: linux 21 | goos: [linux] 22 | goarch: [386, arm, amd64, arm64] 23 | 24 | - <<: *build_defaults 25 | id: windows 26 | goos: [windows] 27 | goarch: [386, amd64, arm64] 28 | 29 | - <<: *build_defaults 30 | id: freebsd 31 | goos: [freebsd] 32 | goarch: [386, amd64] 33 | 34 | archives: 35 | - <<: &archives_defaults 36 | files: 37 | - README.md 38 | - LICENSE.txt 39 | - docs 40 | - man/actionlint.1 41 | id: nix 42 | builds: [macos, linux, freebsd] 43 | format: tar.gz 44 | - <<: *archives_defaults 45 | id: windows 46 | builds: [windows] 47 | format: zip 48 | 49 | brews: 50 | - name: actionlint 51 | tap: 52 | owner: rhysd 53 | name: actionlint 54 | folder: HomebrewFormula 55 | commit_author: 56 | name: 'github-actions[bot]' 57 | email: '41898282+github-actions[bot]@users.noreply.github.com' 58 | homepage: https://github.com/rhysd/actionlint#readme 59 | description: Static checker for GitHub Actions workflow files 60 | license: MIT 61 | install: | 62 | bin.install "actionlint" 63 | man1.install "man/actionlint.1" 64 | test: | 65 | system "#{bin}/actionlint -version" 66 | 67 | changelog: 68 | skip: true 69 | -------------------------------------------------------------------------------- /quotes.go: -------------------------------------------------------------------------------- 1 | package actionlint 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type quotesBuilder struct { 10 | builder strings.Builder 11 | buf []byte 12 | comma bool 13 | } 14 | 15 | func (b *quotesBuilder) append(s string) { 16 | if b.comma { 17 | b.builder.WriteString(", ") 18 | } else { 19 | b.comma = true 20 | } 21 | b.buf = strconv.AppendQuote(b.buf[:0], s) 22 | b.builder.Write(b.buf) 23 | } 24 | 25 | func (b *quotesBuilder) appendRune(r rune) { 26 | if b.comma { 27 | b.builder.WriteString(", ") 28 | } else { 29 | b.comma = true 30 | } 31 | b.buf = strconv.AppendQuoteRune(b.buf[:0], r) 32 | b.builder.Write(b.buf) 33 | } 34 | 35 | func (b *quotesBuilder) build() string { 36 | return b.builder.String() 37 | } 38 | 39 | func sortedQuotes(ss []string) string { 40 | l := len(ss) 41 | if l == 0 { 42 | return "" 43 | } 44 | sort.Strings(ss) 45 | n, max := 0, 0 46 | for _, s := range ss { 47 | m := len(s) + 2 // 2 for delims 48 | n += m 49 | if m > max { 50 | max = m 51 | } 52 | } 53 | n += (l - 1) * 2 // comma 54 | b := quotesBuilder{} 55 | b.buf = make([]byte, 0, max) 56 | b.builder.Grow(n) 57 | for _, s := range ss { 58 | b.append(s) 59 | } 60 | return b.build() 61 | } 62 | 63 | func quotesAll(sss ...[]string) string { 64 | n, max := 0, 0 65 | for _, ss := range sss { 66 | for _, s := range ss { 67 | m := len(s) + 2 // 2 for delims 68 | n += m 69 | if m > max { 70 | max = m 71 | } 72 | } 73 | n += (len(ss) - 1) * 2 // comma 74 | } 75 | b := quotesBuilder{} 76 | b.buf = make([]byte, 0, max) 77 | n += (len(sss) - 1) * 2 // comma 78 | if n > 0 { 79 | b.builder.Grow(n) 80 | } 81 | for _, ss := range sss { 82 | for _, s := range ss { 83 | b.append(s) 84 | } 85 | } 86 | return b.build() 87 | } 88 | -------------------------------------------------------------------------------- /testdata/examples/main.out: -------------------------------------------------------------------------------- 1 | test.yaml:3:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" [syntax-check] 2 | test.yaml:5:11: character '\' is invalid for branch and tag names. only special characters [, ?, +, *, \ ! can be escaped with \. see `man git-check-ref-format` for more details. note that regular expression is unavailable. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob] 3 | test.yaml:10:28: label "linux-latest" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "windows-2016", "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-18.04", "macos-latest", "macos-12", "macos-12.0", "macos-11", "macos-11.0", "macos-10.15", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label] 4 | test.yaml:13:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] 5 | test.yaml:17:11: input "node_version" is not defined in action "actions/setup-node@v3". available inputs are "always-auth", "architecture", "cache", "cache-dependency-path", "check-latest", "node-version", "node-version-file", "registry-url", "scope", "token" [action] 6 | test.yaml:21:20: property "platform" is not defined in object type {os: string} [expression] 7 | test.yaml:22:17: receiver of object dereference "permissions" must be type of object but got "string" [expression] 8 | --------------------------------------------------------------------------------