Merge pull request 'feat(action): parse action.yml like GitHub, ParseUses and ParseImage' (#8) from feat/action-manifest into main
380 files · +17565 -34
M README.md +1 -1
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # ermite-actions | |
| 2 | 2 | ||
| 3 | - | The GitHub Actions workflow language in Go: expressions `${{ }}`, template tokens and the workflow schema, the workflow model, matrices, job graphs and trigger filters. [ermite](https://git.bnema.dev/bnema/ermite) plans runs with it and [ermite-runner](https://git.bnema.dev/bnema/ermite-runner) executes jobs with it. | |
| 3 | + | The GitHub Actions workflow language in Go: expressions `${{ }}`, template tokens and the workflow schema, the workflow model, matrices, job graphs and trigger filters, and `action.yml` manifests (`action`) with the `uses:` parser. [ermite](https://git.bnema.dev/bnema/ermite) plans runs with it and [ermite-runner](https://git.bnema.dev/bnema/ermite-runner) executes jobs with it. | |
| 4 | 4 | ||
| 5 | 5 | The goal is GitHub's exact behaviour. Semantics and test cases come from GitHub's MIT sources ([actions/languageservices](https://github.com/actions/languageservices), [actions/runner](https://github.com/actions/runner)); see [THIRD_PARTY.md](THIRD_PARTY.md). | |
| 6 | 6 | ||
M THIRD_PARTY.md +4 -2
| @@ -1,12 +1,12 @@ | |||
| 1 | 1 | # Third-party material | |
| 2 | 2 | ||
| 3 | - | Test fixtures and schemas under `testdata/`, `workflow/testdata/` and `template/schema/` are copied or ported from the projects below. Each copied file names its source in its first lines or in a `SOURCE` file next to it. | |
| 3 | + | Test fixtures and schemas under `testdata/`, `workflow/testdata/`, `template/schema/`, `action/testdata/` and `action/schema/` are copied or ported from the projects below. Each copied file names its source in its first lines or in a `SOURCE` file next to it. | |
| 4 | 4 | ||
| 5 | 5 | ## actions/languageservices | |
| 6 | 6 | ||
| 7 | 7 | https://github.com/actions/languageservices | |
| 8 | 8 | ||
| 9 | - | Used: `expr/testdata/languageservices/*.json` (the cross-language expression fixtures, commit 4043eda158e16579cc5fb1b0b07a4bce2a76f0b5) and the `@actions/expressions` package that the other `expr` oracle files were generated from. | |
| 9 | + | Used: `expr/testdata/languageservices/*.json` (the cross-language expression fixtures, commit 4043eda158e16579cc5fb1b0b07a4bce2a76f0b5) and the `@actions/expressions` package that the other `expr` oracle files were generated from; `action/schema/action-v1.0.min.json` (the action manifest schema, from `@actions/workflow-parser` 0.3.61, same content as `workflow-parser/src/action-v1.0.json` of that commit), the manifests inlined in `workflow-parser/src/actions/action-parser.test.ts` and `languageservice/src/validate-action.test.ts` (`action/testdata/cases/ls_*.yml`), and `action/testdata/strict.json`, generated with the npm package. | |
| 10 | 10 | ||
| 11 | 11 | ``` | |
| 12 | 12 | MIT License | |
| @@ -35,3 +35,5 @@ SOFTWARE. | |||
| 35 | 35 | ## actions/runner | |
| 36 | 36 | ||
| 37 | 37 | https://github.com/actions/runner — MIT License, Copyright (c) GitHub, Inc. Same terms as above. | |
| 38 | + | ||
| 39 | + | Used: `action/testdata/cases/runner_*.yml` (`src/Test/TestData`, commit 5f0b3ddcc6ee9fe9441e00d55e85b94ea4aad798), `action/gen/runner/Driver.cs` (derived from `Runner.Worker/ActionManifestManagerLegacy.cs`), and `action/testdata/runner.json` and `uses.json`, which are the output of the runner's own loader built from that commit. | |
A action/PORTING.md +235 -0
| @@ -0,0 +1,235 @@ | |||
| 1 | + | # Porting notes for package action | |
| 2 | + | ||
| 3 | + | `Parse` reads an `action.yml` the way **the C# runner** does when it loads an | |
| 4 | + | action (actions/runner at `5f0b3ddcc6ee9fe9441e00d55e85b94ea4aad798`), with the | |
| 5 | + | vendored schema of `@actions/workflow-parser` 0.3.61 (MIT, GitHub, | |
| 6 | + | actions/languageservices). `Lint` reproduces what that npm package reports. | |
| 7 | + | Nothing here was recorded on github.com: phase A of the CI-2 plan has not | |
| 8 | + | produced recordings of action manifests yet. What is checked is the runner's | |
| 9 | + | own code, run by `gen/runner`, and the npm package's own code, run by | |
| 10 | + | `gen/strict.mjs`. | |
| 11 | + | ||
| 12 | + | ## Which loader the runner runs | |
| 13 | + | ||
| 14 | + | `ActionManifestManagerWrapper.Load` (the only caller) runs | |
| 15 | + | `ActionManifestManagerLegacy` and returns its result. The newer | |
| 16 | + | `ActionManifestManager` (on `Sdk/WorkflowParser`, the code the npm package is | |
| 17 | + | a port of) runs next to it only when the feature flag | |
| 18 | + | `actions_runner_compare_workflow_parser` or the environment variable | |
| 19 | + | `ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER` is set, and its result is thrown away | |
| 20 | + | (`EvaluateAndCompare`). So `Parse` follows the **legacy** loader and the legacy | |
| 21 | + | converter `PipelineTemplateConverter.ConvertToSteps`. The two loaders agree on | |
| 22 | + | most files and differ on a few (below); the new one is not reproduced. | |
| 23 | + | ||
| 24 | + | ## Checks | |
| 25 | + | ||
| 26 | + | - `testdata/runner.json`: the answer of the runner's loader on every file of | |
| 27 | + | `testdata/cases` (338 files). `gen/runner/Driver.cs` is | |
| 28 | + | `ActionManifestManagerLegacy.Load/ConvertRuns/ConvertInputs` with what needs | |
| 29 | + | an `IExecutionContext` removed (tracing, the file table, expression values: | |
| 30 | + | none of it changes loading), compiled into a copy of `src/Sdk` with | |
| 31 | + | `gen/runner/build.sh`, which refuses a clone at another commit or with | |
| 32 | + | changes. `TestAgainstRunner` compares the error list (text and order) and the | |
| 33 | + | model (inputs and defaults, deprecations, every field of `runs`, every step's | |
| 34 | + | id, condition, name, env, with, shell, working directory, reference, and the | |
| 35 | + | outputs). The only thing not compared word for word is YamlDotNet's wording of | |
| 36 | + | YAML syntax errors (`While parsing a node, ...`), where yaml.v3's wording is | |
| 37 | + | used; only the shape is compared. | |
| 38 | + | - `testdata/uses.json`: the same driver on 68 `uses:` values; `ParseUses` is | |
| 39 | + | compared on error text and on the reference the runner builds | |
| 40 | + | (`TestParseUsesAgainstRunner`). | |
| 41 | + | - `testdata/strict.json`: `parseAction` + `convertActionTemplate` of the npm | |
| 42 | + | package on every case; `Lint` is compared on the error list | |
| 43 | + | (`TestLintAgainstReference`). Eleven files are excused, each with its reason | |
| 44 | + | (`knownLintDifferences`). | |
| 45 | + | - `FuzzParse` (no panic, an action iff no error, at most ten errors, every step | |
| 46 | + | has an id and a condition) and `FuzzParseUses`. | |
| 47 | + | ||
| 48 | + | Regenerate with `gen/regen.sh` (`RUNNER_CLONE=<clone>` for the runner part); the | |
| 49 | + | diff must be empty. The cases are: the 24 files of the runner's | |
| 50 | + | `src/Test/TestData` that are action manifests (`runner_*`), the 96 manifests | |
| 51 | + | inlined in the language service tests (`ls_*`: `action-parser.test.ts`, | |
| 52 | + | `validate-action.test.ts`), and 218 written for this package. The | |
| 53 | + | language service's own expectations (`toBeGreaterThan(0)`) are not ported: the | |
| 54 | + | cases are replayed through the reference instead, which gives the messages. | |
| 55 | + | ||
| 56 | + | ## Model | |
| 57 | + | ||
| 58 | + | `Action` has `Name`, `Description`, `Author`, ordered `Inputs` (with | |
| 59 | + | `Description`, `Required`, `Default` as a token, `HasDefault`, | |
| 60 | + | `DeprecationMessage`), ordered `Outputs` (with `Value`), and `Runs`: | |
| 61 | + | `Kind` (`RunsNode`, `RunsDocker`, `RunsComposite`, `RunsPlugin`), `Using` | |
| 62 | + | (lower case), node `Main/Pre/Post`, docker `Image/Entrypoint/PreEntrypoint/ | |
| 63 | + | PostEntrypoint/Args/Env`, `PreIf/PostIf`, composite `Steps` (`[]*workflow.Step`), | |
| 64 | + | `Plugin`. Conditions default to `always()` for node and docker actions | |
| 65 | + | (`ActionManifestManagerLegacy.cs`, `preIfToken?.Value ?? "always()"`); they are | |
| 66 | + | kept as written, without `success() &&` (the schema's own description of | |
| 67 | + | `pre-if` says the same). A composite action's conditions are `always()`. | |
| 68 | + | ||
| 69 | + | Steps are `workflow.Step`, with two additions to that type: | |
| 70 | + | `EnvExpression` and `WithExpression` hold `env: ${{ fromJSON(...) }}` and | |
| 71 | + | `with: ${{ ... }}`, which the runner accepts on a step of an action and the | |
| 72 | + | workflow converter does not. `Step.If` is the runner's: `success()` when empty, | |
| 73 | + | `success() && (cond)` when the condition has no status function; the contexts | |
| 74 | + | of `if` are the schema's `step-if` plus `vars` and `needs`, which the runner's | |
| 75 | + | converter adds (`s_stepNamedValues`) and the schema lacks. | |
| 76 | + | ||
| 77 | + | ## What the runner does that the other readers do not | |
| 78 | + | ||
| 79 | + | All verified by `TestAgainstRunner`. | |
| 80 | + | ||
| 81 | + | - **Schema.** The runner reads `action-root` (all keys but `name`, | |
| 82 | + | `description`, `inputs`, `outputs`, `runs` are accepted, nothing is | |
| 83 | + | required); the language service reads `action-root-strict`. `using` is any | |
| 84 | + | non-empty string for the schema and compared with `OrdinalIgnoreCase` by the | |
| 85 | + | loader: `Node20` and `DOCKER` run on GitHub, the strict schema refuses them. | |
| 86 | + | `branding` is not read at all: `branding: {icon: nope}` loads. | |
| 87 | + | - **Errors are exceptions.** After the positioned errors, the exception that | |
| 88 | + | ended the load is printed as `Exception.ToString()` (class name, `.NET` | |
| 89 | + | message format, then a stack trace that is not reproduced): `using: node99` | |
| 90 | + | is `System.ArgumentOutOfRangeException: Specified argument was out of the | |
| 91 | + | range of valid values. (Parameter ''using: node99' is not supported, use | |
| 92 | + | 'docker', 'node12', 'node16', 'node20' or 'node24' instead.')`. `Error.Exception` | |
| 93 | + | holds the class; `Error.Error()` formats it. At most ten errors are kept; | |
| 94 | + | `Failed to load <path>` is the runner's final message (`LoadFailed`). | |
| 95 | + | - **Missing runs, using, main, image, steps** each have their text | |
| 96 | + | (`Top level 'runs:' section is required for ...`, `Missing 'using' value. ...`, | |
| 97 | + | `You are using a JavaScript Action but there is not an entry JavaScript file | |
| 98 | + | provided in ...`, `... Container Action but an image is not provided in ...`, | |
| 99 | + | `... composite action but there are no steps provided in ...`), after the | |
| 100 | + | schema's own errors when it has some (`There's not enough info to determine | |
| 101 | + | what you meant. Add one of these properties: ...`). | |
| 102 | + | - **YAML.** Anchors and aliases are refused (`Anchors are not currently | |
| 103 | + | supported. Remove the anchor 'x'`); an explicit tag other than `!!bool`, | |
| 104 | + | `!!float`, `!!int`, `!!null` is `Unexpected tag '...'` (the runner's string tag | |
| 105 | + | is `tag:yaml.org,2002:string`, which no document spells, so `!!str` fails); | |
| 106 | + | a tagged quoted scalar, an out-of-range tagged number and a plain `0x`/`0o` | |
| 107 | + | integer over 32 bits (`Int32.TryParse`) have their messages; a second document | |
| 108 | + | is `Expected stream end parse event`, an empty file `Expected document start | |
| 109 | + | parse event`, a collection used as a key `Expected mapping end`. Such an error | |
| 110 | + | stops the read where the runner meets it, so errors found earlier in the | |
| 111 | + | document stay (`template.Context.RunnerYAML`). | |
| 112 | + | - **Keys, integers, case.** Mapping keys are resolved like values: `~` is null | |
| 113 | + | (`Unexpected value ''`), `0x10` is the key `16`, `1.0` is `1`, `TRUE` is `true`, | |
| 114 | + | so `0x10` and `16` are duplicates. `0x` and `0o` integers, plain or `!!int`, | |
| 115 | + | are int32 with wraparound (`0xFFFFFFFF` is `-1`, `0x80000000` is | |
| 116 | + | `-2147483648`; over 32 bits is an error, and `!!float` refuses them). Duplicate | |
| 117 | + | keys, input names and step ids compare with .NET's OrdinalIgnoreCase | |
| 118 | + | (`expr.OrdinalFold`): `é` and `É` are the same, `s` and `ſ` are not. | |
| 119 | + | - **Depth.** `TemplateMemory(maxDepth: 100)`: `Maximum object depth exceeded` | |
| 120 | + | from the 100th nested collection, the root mapping being the first. Skipped | |
| 121 | + | values (unknown keys) count (`template.Context.MaxDepth`). | |
| 122 | + | - **Numbers as strings.** A number read where a string is wanted shows as .NET's | |
| 123 | + | `G15` (`1E+21`, `0.3` for `0.30000000000000004`, `-0`), not as JavaScript's | |
| 124 | + | (`template.DotNetNumberString`). | |
| 125 | + | - **Steps** (legacy `ConvertToSteps`): the first token of the wrong type ends the | |
| 126 | + | conversion (`Unexpected type 'StringToken' encountered while reading 'steps'. | |
| 127 | + | ...`) instead of being recorded and skipped; an unknown key in a step is | |
| 128 | + | `Error while reading 'steps item key'. Unexpected value 'k'`; the id check | |
| 129 | + | (`ReferenceNameBuilder`) accepts ids that start with `__`, lets an id of 100 | |
| 130 | + | characters or more through, compares ids with OrdinalIgnoreCase, and words its | |
| 131 | + | message `... IDs must start with a letter or '_' and must be less than 100 | |
| 132 | + | characters.` (the language service has `and and`); generated ids are | |
| 133 | + | `__run`, `__owner_repo`, `__<image>`, `__self`, `__selfRepository`, made unique | |
| 134 | + | with `_2`, `_3`, ... A step whose `uses` is malformed is recorded as an error | |
| 135 | + | and **kept**, which makes the load fail later with | |
| 136 | + | `System.NullReferenceException` if it has no id (the runner dereferences its | |
| 137 | + | reference); a `$/x@ref` step is dropped instead. | |
| 138 | + | - **`uses`.** `Expected format {org}/{repo}[/path]@ref. Actual 'x'` and | |
| 139 | + | `Self-repository references do not support an '@ref' suffix. Actual '$/x@v1'`. | |
| 140 | + | Segments are split on `/` and `\` and empty ones dropped, so `/a/b@c` and | |
| 141 | + | `a//b@c` are `a/b@c`; `docker://` is case-sensitive (`Docker://x` is an error) | |
| 142 | + | although the image of a docker *action* is not (`ParseImage`). | |
| 143 | + | ||
| 144 | + | ## Where the sources disagree | |
| 145 | + | ||
| 146 | + | 1. **Runner (legacy) / runner (new) / language service**, on the same file: | |
| 147 | + | - Step id text: legacy `... '_' and must be ...`; new loader adds | |
| 148 | + | `IDs starting with '__' are reserved.` and refuses them; language service | |
| 149 | + | has `and and` and the reserved check too. `Parse` follows legacy (what runs). | |
| 150 | + | - `$/` steps: legacy generates `__selfRepository`; new generates the same; | |
| 151 | + | language service `__self` (`createActionStepId`). | |
| 152 | + | - Conditions of `runs.pre-if`, `post-if`: the runner reads them as plain | |
| 153 | + | strings and never parses them when loading (an unknown context fails at run | |
| 154 | + | time); the language service parses them with the contexts of `runs-if` | |
| 155 | + | (`Unrecognized named-value: 'foo'`). `pre-if: ${{ always() }}` makes the | |
| 156 | + | runner fail (`Unrecognized function: 'always'`, the schema type there is | |
| 157 | + | `non-empty-string`, which allows no context), while the language service | |
| 158 | + | accepts the schema and flags the markers in its own pass. | |
| 159 | + | 2. **Schema vs runner's `action_yaml.json`.** Equal in meaning for every | |
| 160 | + | definition `Parse` reads (`action-root` and below); the vendored file adds | |
| 161 | + | `-strict` variants, `branding`, `using`, `runs-if` and descriptions, orders | |
| 162 | + | the `runs` alternatives differently and writes `{"type": x}` where the | |
| 163 | + | runner writes `x`. Checked by diffing the two after removing descriptions. | |
| 164 | + | 3. **Schema vs plan/task text.** The task lists the composite step keys as | |
| 165 | + | `uses, run, shell, with, env, name, id, if, working-directory, | |
| 166 | + | continue-on-error`. The schema: a `run` step takes `name, id, if, run, shell, | |
| 167 | + | env, continue-on-error, working-directory`; a `uses` step takes `name, id, if, | |
| 168 | + | uses, with, env, continue-on-error`. `shell` and `working-directory` on a | |
| 169 | + | `uses` step and `with` on a `run` step are refused; so is `timeout-minutes`, | |
| 170 | + | although the runner's converter would read it. | |
| 171 | + | 4. **`required` inputs: the plan vs the runner.** The CI-2 plan says "required | |
| 172 | + | inputs warned like GitHub". The runner has no such warning for an action: | |
| 173 | + | `ActionRunner.cs` warns `Unexpected input(s) 'x', valid inputs are ['a', 'b']` | |
| 174 | + | (only for actions that have a manifest, `entryPoint` and `args` being valid | |
| 175 | + | for docker actions) and `Input 'x' has been deprecated with message: ...`, | |
| 176 | + | and `inputs.<id>.required` is not read by the loader nor checked later (a | |
| 177 | + | `grep` of `Runner.Worker`, `Runner.Common`, `Sdk/WorkflowParser` and | |
| 178 | + | `Sdk/DTPipelines` finds `required` checks only for reusable workflow inputs | |
| 179 | + | and secrets, `WorkflowTemplateConverter.cs:2166,2223`). `Missing required | |
| 180 | + | input` is the language service's editor message | |
| 181 | + | (`validate-action-reference.ts`: `Missing required input \`x\`` / `Missing | |
| 182 | + | required inputs: \`x\`, \`y\``), shown when no `with:` gives the input and it | |
| 183 | + | has no default. `Input.Required` is parsed so that an editor can do the same; | |
| 184 | + | the executor should not warn. | |
| 185 | + | 5. **Language service TypeScript vs its own schema.** `convertRuns` falls back to | |
| 186 | + | an empty composite when `using` or `main` is missing; the model here follows | |
| 187 | + | the runner (an error instead). | |
| 188 | + | 6. **`uses` messages.** Runner `{org}/{repo}[/path]@ref`; language service | |
| 189 | + | `{owner}/{repo}[/path]@{ref}`, `'uses' value in action cannot be blank`, and | |
| 190 | + | it rejects `/repo@v4` where the runner accepts `/a/b@c`. `ParseUses` is the | |
| 191 | + | runner's. | |
| 192 | + | ||
| 193 | + | ## Differences from the runner | |
| 194 | + | ||
| 195 | + | - **Stack traces** are not reproduced. | |
| 196 | + | - **YAML syntax errors** have yaml.v3's wording (`file.yml: did not find expected | |
| 197 | + | key at line 5`); YamlDotNet's is `file.yml: (Line: 6, Col: 34, Idx: 122) - | |
| 198 | + | (Line: 6, Col: 44, Idx: 132): While parsing a block mapping, ...`. | |
| 199 | + | - **Limits that the runner does not state**: a manifest over `template.MaxFileBytes` | |
| 200 | + | (1 MiB) and a document that expands to more than `template.MaxNodes` nodes are | |
| 201 | + | refused before reading. The runner reads the file with | |
| 202 | + | `TemplateMemory(maxBytes: 10 MiB, maxEvents: 1 000 000)`; those two are not | |
| 203 | + | reproduced. | |
| 204 | + | - **Not ported**: what happens after loading (`ActionManager`: choosing between | |
| 205 | + | `action.yml`, `action.yaml` and `Dockerfile`, `Can't find 'action.yml', | |
| 206 | + | 'action.yaml' or 'Dockerfile'`, the `Dockerfile` image check `ParseImage` | |
| 207 | + | reproduces, input defaults evaluated at run time, `Input '<n>' has been | |
| 208 | + | deprecated with message: ...`, composite depth). `ParseImage` and | |
| 209 | + | `Input.DeprecationMessage` carry what is needed. | |
| 210 | + | - **Expression errors** come from package `expr`, which follows the runner's | |
| 211 | + | expression engine (`... Located at position N within expression: ...`). `Lint` | |
| 212 | + | strips nothing: the language service's expression messages lack that suffix, so | |
| 213 | + | `TestLintAgainstReference` strips it from ours before comparing. | |
| 214 | + | - **`Lint` ordering of schema errors** for files with several (`Required property | |
| 215 | + | is missing` after `Unexpected value`) is the reference's and is replayed. | |
| 216 | + | - A mapping key that is a collection: the reference reads `undefined` | |
| 217 | + | (`Unexpected value 'undefined'`); `Lint` reports `Unexpected value ''` | |
| 218 | + | (`knownLintDifferences`). `Parse` is the runner's (`Expected mapping end`). | |
| 219 | + | - `Lint` keeps keys as strings, so it differs from the reference on keys that | |
| 220 | + | are not strings: it misses a null key under a loose-key mapping (`inputs`, | |
| 221 | + | `env`), which the reference and the runner reject, and the reference fails on | |
| 222 | + | other non-string keys with `raw.indexOf is not a function`. Fixing the null | |
| 223 | + | key in `Lint` would change how `template` reads workflow files, so it is | |
| 224 | + | left alone. | |
| 225 | + | ||
| 226 | + | ## To verify against recordings | |
| 227 | + | ||
| 228 | + | - [ ] What github.com prints for a manifest that fails to load (the runner's | |
| 229 | + | messages are the ones above; whether the hosted service wraps or reorders | |
| 230 | + | them is unknown). | |
| 231 | + | - [ ] That the hosted runners run the legacy loader (the flag | |
| 232 | + | `actions_runner_compare_workflow_parser` may be set there; its result is | |
| 233 | + | still discarded in this commit). | |
| 234 | + | - [ ] `using: node12`/`node16` after the migration to node20 (`HandlerFactory`), | |
| 235 | + | which belongs to the executor. | |
A action/action_test.go +310 -0
| @@ -0,0 +1,310 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "sort" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "bnema.dev/ermite-actions/template" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func mustParse(t *testing.T, src string) *Action { | |
| 14 | + | t.Helper() | |
| 15 | + | a, errs := Parse("action.yml", []byte(src)) | |
| 16 | + | if len(errs) > 0 { | |
| 17 | + | t.Fatalf("errors: %q", errTexts(errs)) | |
| 18 | + | } | |
| 19 | + | return a | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func TestModel(t *testing.T) { | |
| 23 | + | a := mustParse(t, ` | |
| 24 | + | name: Say | |
| 25 | + | description: Says things | |
| 26 | + | author: me | |
| 27 | + | inputs: | |
| 28 | + | Who: | |
| 29 | + | description: the one | |
| 30 | + | required: true | |
| 31 | + | default: world | |
| 32 | + | old: | |
| 33 | + | deprecationMessage: use who | |
| 34 | + | expr: | |
| 35 | + | default: ${{ github.ref }} | |
| 36 | + | outputs: | |
| 37 | + | out: | |
| 38 | + | description: the out | |
| 39 | + | value: ${{ steps.a.outputs.r }} | |
| 40 | + | branding: | |
| 41 | + | icon: award | |
| 42 | + | color: blue | |
| 43 | + | runs: | |
| 44 | + | using: Node20 | |
| 45 | + | main: dist/index.js | |
| 46 | + | post: dist/post.js | |
| 47 | + | `) | |
| 48 | + | if a.Name != "Say" || a.Description != "Says things" || a.Author != "me" { | |
| 49 | + | t.Errorf("header: %+v", a) | |
| 50 | + | } | |
| 51 | + | if len(a.Inputs) != 3 || a.Inputs[0].Name != "Who" || !a.Inputs[0].Required || a.Inputs[0].Default.String() != "world" || a.Inputs[0].Description != "the one" { | |
| 52 | + | t.Errorf("inputs: %+v", a.Inputs) | |
| 53 | + | } | |
| 54 | + | if in := a.Input("who"); in == nil || in.Name != "Who" { | |
| 55 | + | t.Errorf("Input(who) = %v", in) | |
| 56 | + | } | |
| 57 | + | if in := a.Input("nope"); in != nil { | |
| 58 | + | t.Errorf("Input(nope) = %v", in) | |
| 59 | + | } | |
| 60 | + | if !a.Inputs[1].Deprecated() || a.Inputs[1].DeprecationMessage != "use who" || a.Inputs[1].HasDefault || a.Inputs[1].Default.String() != "" { | |
| 61 | + | t.Errorf("deprecated input: %+v", a.Inputs[1]) | |
| 62 | + | } | |
| 63 | + | if got := a.Inputs[2].Default.String(); got != "${{ github.ref }}" || !a.Inputs[2].HasDefault { | |
| 64 | + | t.Errorf("expression default: %q", got) | |
| 65 | + | } | |
| 66 | + | if len(a.Outputs) != 1 || a.Outputs[0].Name != "out" || a.Outputs[0].Value.String() != "${{ steps.a.outputs.r }}" || a.Outputs[0].Description != "the out" { | |
| 67 | + | t.Errorf("outputs: %+v", a.Outputs) | |
| 68 | + | } | |
| 69 | + | r := a.Runs | |
| 70 | + | if r.Kind != RunsNode || r.Using != "node20" || r.Main != "dist/index.js" || r.Post != "dist/post.js" || r.Pre != "" { | |
| 71 | + | t.Errorf("runs: %+v", r) | |
| 72 | + | } | |
| 73 | + | // The runner defaults both conditions to always(), pre or not. | |
| 74 | + | if r.PreIf != "always()" || r.PostIf != "always()" || r.HasPre() || !r.HasPost() { | |
| 75 | + | t.Errorf("conditions: %+v", r) | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestRunsConditions(t *testing.T) { | |
| 80 | + | a := mustParse(t, "runs:\n using: docker\n image: Dockerfile\n pre-entrypoint: p.sh\n pre-if: runner.os == 'Linux'\n post-entrypoint: q.sh\n post-if: failure()\n entrypoint: e.sh\n args: [a, '${{ inputs.x }}']\n env: {K: V}\n") | |
| 81 | + | r := a.Runs | |
| 82 | + | // Conditions are kept as written: the runner does not add success(). | |
| 83 | + | if r.PreIf != "runner.os == 'Linux'" || r.PostIf != "failure()" || !r.HasPre() || !r.HasPost() { | |
| 84 | + | t.Errorf("%+v", r) | |
| 85 | + | } | |
| 86 | + | if r.Image != "Dockerfile" || r.Entrypoint != "e.sh" || r.PreEntrypoint != "p.sh" || r.PostEntrypoint != "q.sh" { | |
| 87 | + | t.Errorf("%+v", r) | |
| 88 | + | } | |
| 89 | + | if r.Args == nil || len(r.Args.Items) != 2 || r.Args.Items[1].String() != "${{ inputs.x }}" || r.Env == nil || r.Env.Pairs[0].Value.String() != "V" { | |
| 90 | + | t.Errorf("args/env: %+v %+v", r.Args, r.Env) | |
| 91 | + | } | |
| 92 | + | c := mustParse(t, "runs:\n using: composite\n steps: []\n") | |
| 93 | + | if c.Runs.PreIf != "always()" || c.Runs.PostIf != "always()" || c.Runs.HasPre() || c.Runs.HasPost() { | |
| 94 | + | t.Errorf("composite: %+v", c.Runs) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func TestCompositeSteps(t *testing.T) { | |
| 99 | + | a := mustParse(t, ` | |
| 100 | + | runs: | |
| 101 | + | using: composite | |
| 102 | + | steps: | |
| 103 | + | - run: echo | |
| 104 | + | shell: bash | |
| 105 | + | - uses: actions/checkout@v4 | |
| 106 | + | with: | |
| 107 | + | fetch-depth: 0 | |
| 108 | + | - id: named | |
| 109 | + | if: always() | |
| 110 | + | run: echo | |
| 111 | + | shell: sh | |
| 112 | + | working-directory: sub | |
| 113 | + | env: | |
| 114 | + | A: ${{ inputs.a }} | |
| 115 | + | continue-on-error: true | |
| 116 | + | name: n | |
| 117 | + | - uses: docker://alpine:3 | |
| 118 | + | - uses: ./x | |
| 119 | + | - uses: actions/checkout@v4 | |
| 120 | + | `) | |
| 121 | + | var ids []string | |
| 122 | + | for _, s := range a.Runs.Steps { | |
| 123 | + | ids = append(ids, s.ID) | |
| 124 | + | } | |
| 125 | + | if got, want := strings.Join(ids, ","), "__run,__actions_checkout,named,__alpine_3,__self,__actions_checkout_2"; got != want { | |
| 126 | + | t.Errorf("ids %s, want %s", got, want) | |
| 127 | + | } | |
| 128 | + | s := a.Runs.Steps[2] | |
| 129 | + | if !s.Explicit || s.If.Expression != "always()" || s.Shell.String() != "bash" && s.Shell.String() != "sh" || s.WorkingDirectory.String() != "sub" || s.Name.String() != "n" || s.ContinueOnError.String() != "true" { | |
| 130 | + | t.Errorf("step: %+v", s) | |
| 131 | + | } | |
| 132 | + | if got := a.Runs.Steps[0].If.Expression; got != "success()" { | |
| 133 | + | t.Errorf("default if %q", got) | |
| 134 | + | } | |
| 135 | + | if got := a.Runs.Steps[1].With.Pairs[0].Value.String(); got != "0" { | |
| 136 | + | t.Errorf("with %q", got) | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func TestStepIfIsWrappedLikeTheRunner(t *testing.T) { | |
| 141 | + | a := mustParse(t, "runs:\n using: composite\n steps:\n - run: x\n shell: sh\n if: github.ref == 'main'\n - run: y\n shell: sh\n if: ${{ failure() }}\n - run: z\n shell: sh\n if: vars.A == needs.b.result\n") | |
| 142 | + | want := []string{"success() && (github.ref == 'main')", "failure()", "success() && (vars.A == needs.b.result)"} | |
| 143 | + | for i, s := range a.Runs.Steps { | |
| 144 | + | if s.If.Expression != want[i] { | |
| 145 | + | t.Errorf("step %d: %q, want %q", i, s.If.Expression, want[i]) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func TestErrorsAreTheRunners(t *testing.T) { | |
| 151 | + | tests := []struct { | |
| 152 | + | name, src string | |
| 153 | + | want []string | |
| 154 | + | }{ | |
| 155 | + | {"unsupported using", "runs:\n using: node99\n main: x\n", []string{ | |
| 156 | + | "System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter ''using: node99' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.')"}}, | |
| 157 | + | {"missing using", "runs:\n main: x\n", []string{ | |
| 158 | + | "System.NotSupportedException: Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'."}}, | |
| 159 | + | {"no main", "runs:\n using: node20\n", []string{ | |
| 160 | + | "action.yml (Line: 2, Col: 3): There's not enough info to determine what you meant. Add one of these properties: args, entrypoint, env, image, main, post, post-entrypoint, pre, pre-entrypoint, steps", | |
| 161 | + | "System.ArgumentNullException: Value cannot be null. (Parameter 'You are using a JavaScript Action but there is not an entry JavaScript file provided in action.yml.')"}}, | |
| 162 | + | {"no image", "runs:\n using: docker\n", []string{ | |
| 163 | + | "action.yml (Line: 2, Col: 3): There's not enough info to determine what you meant. Add one of these properties: args, entrypoint, env, image, main, post, post-entrypoint, pre, pre-entrypoint, steps", | |
| 164 | + | "System.ArgumentNullException: Value cannot be null. (Parameter 'You are using a Container Action but an image is not provided in action.yml.')"}}, | |
| 165 | + | {"no steps", "runs:\n using: composite\n", []string{ | |
| 166 | + | "action.yml (Line: 2, Col: 3): There's not enough info to determine what you meant. Add one of these properties: args, entrypoint, env, image, main, post, post-entrypoint, pre, pre-entrypoint, steps", | |
| 167 | + | "System.ArgumentNullException: Value cannot be null. (Parameter 'You are using a composite action but there are no steps provided in action.yml.')"}}, | |
| 168 | + | {"no runs", "name: a\n", []string{"Top level 'runs:' section is required for action.yml"}}, | |
| 169 | + | {"shell required", "runs:\n using: composite\n steps:\n - run: x\n", []string{ | |
| 170 | + | "action.yml (Line: 4, Col: 7): Required property is missing: shell"}}, | |
| 171 | + | } | |
| 172 | + | for _, tc := range tests { | |
| 173 | + | t.Run(tc.name, func(t *testing.T) { | |
| 174 | + | a, errs := Parse("action.yml", []byte(tc.src)) | |
| 175 | + | if a != nil { | |
| 176 | + | t.Fatal("action returned") | |
| 177 | + | } | |
| 178 | + | got := errTexts(errs) | |
| 179 | + | if tc.name == "shell required" { | |
| 180 | + | // The runner follows the schema error with the exception that | |
| 181 | + | // stops the load. | |
| 182 | + | if len(got) != 2 || got[0] != tc.want[0] || !strings.HasPrefix(got[1], "GitHub.DistributedTask.ObjectTemplating.TemplateValidationException: The template is not valid. action.yml (Line: 4, Col: 7): Required property is missing: shell") { | |
| 183 | + | t.Errorf("got %q", got) | |
| 184 | + | } | |
| 185 | + | return | |
| 186 | + | } | |
| 187 | + | if strings.Join(got, "\n") != strings.Join(tc.want, "\n") { | |
| 188 | + | t.Errorf("got %q\nwant %q", got, tc.want) | |
| 189 | + | } | |
| 190 | + | }) | |
| 191 | + | } | |
| 192 | + | if got := LoadFailed("action.yml"); got != "Failed to load action.yml" { | |
| 193 | + | t.Errorf("LoadFailed: %q", got) | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | func TestLimits(t *testing.T) { | |
| 198 | + | // Ten errors at most, as the runner keeps. | |
| 199 | + | var b strings.Builder | |
| 200 | + | b.WriteString("runs:\n using: node20\n main: m\n") | |
| 201 | + | for i := 0; i < 25; i++ { | |
| 202 | + | b.WriteString(" k" + string(rune('a'+i)) + ": 1\n") | |
| 203 | + | } | |
| 204 | + | _, errs := Parse("action.yml", []byte(b.String())) | |
| 205 | + | if len(errs) != 10 { | |
| 206 | + | t.Errorf("%d errors, want 10", len(errs)) | |
| 207 | + | } | |
| 208 | + | // The runner's TemplateMemory allows 100 nested collections, the root | |
| 209 | + | // mapping being the first: 99 brackets pass, 100 do not (depth_seq_99 and | |
| 210 | + | // depth_seq_100 in the oracle). | |
| 211 | + | deep := func(n int) string { | |
| 212 | + | return "x: " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\nruns:\n using: node20\n main: m\n" | |
| 213 | + | } | |
| 214 | + | if _, errs := Parse("action.yml", []byte(deep(99))); len(errs) != 0 { | |
| 215 | + | t.Errorf("99 levels: %q", errTexts(errs)) | |
| 216 | + | } | |
| 217 | + | _, errs = Parse("action.yml", []byte(deep(100))) | |
| 218 | + | if len(errs) != 2 || errs[0].Error() != "action.yml: Maximum object depth exceeded" { | |
| 219 | + | t.Errorf("100 levels: %q", errTexts(errs)) | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func TestLintIsTheLanguageService(t *testing.T) { | |
| 224 | + | // The runner runs this file; the language service refuses it. | |
| 225 | + | src := "icon: x.svg\nruns:\n using: node20\n main: m.js\n" | |
| 226 | + | if _, errs := Parse("action.yml", []byte(src)); len(errs) != 0 { | |
| 227 | + | t.Errorf("Parse: %q", errTexts(errs)) | |
| 228 | + | } | |
| 229 | + | got := errTexts(Lint("action.yml", []byte(src))) | |
| 230 | + | want := []string{ | |
| 231 | + | "action.yml (Line: 1, Col: 1): Unexpected value 'icon'", | |
| 232 | + | "action.yml (Line: 1, Col: 1): Required property is missing: name", | |
| 233 | + | "action.yml (Line: 1, Col: 1): Required property is missing: description", | |
| 234 | + | } | |
| 235 | + | // Order: the reference reports the unexpected key first, the missing | |
| 236 | + | // properties after the mapping. | |
| 237 | + | if strings.Join(got, "\n") != strings.Join(want, "\n") { | |
| 238 | + | t.Errorf("Lint:\n got %q\nwant %q", got, want) | |
| 239 | + | } | |
| 240 | + | ok := "name: a\ndescription: d\nruns:\n using: node20\n main: m.js\n" | |
| 241 | + | if errs := Lint("action.yml", []byte(ok)); len(errs) != 0 { | |
| 242 | + | t.Errorf("Lint of a valid file: %q", errTexts(errs)) | |
| 243 | + | } | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | func contexts(t *testing.T, tok interface { | |
| 247 | + | Definition() *template.DefinitionInfo | |
| 248 | + | }) string { | |
| 249 | + | t.Helper() | |
| 250 | + | d := tok.Definition() | |
| 251 | + | if d == nil { | |
| 252 | + | t.Fatal("token without definition") | |
| 253 | + | } | |
| 254 | + | names := append([]string(nil), d.NamedContexts()...) | |
| 255 | + | for _, f := range d.Functions() { | |
| 256 | + | names = append(names, f.Name+"()") | |
| 257 | + | } | |
| 258 | + | sort.Strings(names) | |
| 259 | + | return strings.Join(names, ",") | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // The contexts an expression may use come from the schema, key by key; the | |
| 263 | + | // tokens carry them so that a caller can evaluate with the right environment. | |
| 264 | + | func TestContextsPerKey(t *testing.T) { | |
| 265 | + | a := mustParse(t, ` | |
| 266 | + | inputs: | |
| 267 | + | d: | |
| 268 | + | default: ${{ github.ref }} | |
| 269 | + | outputs: | |
| 270 | + | o: | |
| 271 | + | value: ${{ steps.a.outputs.x }} | |
| 272 | + | runs: | |
| 273 | + | using: composite | |
| 274 | + | steps: | |
| 275 | + | - run: echo ${{ inputs.d }} | |
| 276 | + | shell: sh | |
| 277 | + | env: | |
| 278 | + | A: ${{ matrix.a }} | |
| 279 | + | - uses: a/b@c | |
| 280 | + | with: | |
| 281 | + | k: ${{ steps.a.outputs.x }} | |
| 282 | + | `) | |
| 283 | + | const stepCtx = "env,github,hashFiles(),inputs,job,matrix,runner,steps,strategy" | |
| 284 | + | if got, want := contexts(t, a.Inputs[0].Default), "github,hashFiles(),job,matrix,runner,strategy"; got != want { | |
| 285 | + | t.Errorf("input default: %s, want %s", got, want) | |
| 286 | + | } | |
| 287 | + | if got, want := contexts(t, a.Outputs[0].Value), "env,github,inputs,job,matrix,runner,steps,strategy"; got != want { | |
| 288 | + | t.Errorf("output value: %s, want %s", got, want) | |
| 289 | + | } | |
| 290 | + | if got := contexts(t, a.Runs.Steps[0].Run); got != stepCtx { | |
| 291 | + | t.Errorf("run: %s, want %s", got, stepCtx) | |
| 292 | + | } | |
| 293 | + | if got := contexts(t, a.Runs.Steps[0].Env.Pairs[0].Value); got != stepCtx { | |
| 294 | + | t.Errorf("env: %s", got) | |
| 295 | + | } | |
| 296 | + | if got := contexts(t, a.Runs.Steps[1].With.Pairs[0].Value); got != stepCtx { | |
| 297 | + | t.Errorf("with: %s", got) | |
| 298 | + | } | |
| 299 | + | // "if" takes the status functions, and the runner adds vars and needs. | |
| 300 | + | if got, want := contexts(t, a.Runs.Steps[0].If), "always(),cancelled(),env,failure(),github,hashFiles(),inputs,job,matrix,needs,runner,steps,strategy,success(),vars"; got != want { | |
| 301 | + | t.Errorf("if: %s, want %s", got, want) | |
| 302 | + | } | |
| 303 | + | b := mustParse(t, "runs:\n using: docker\n image: Dockerfile\n args: ['${{ inputs.x }}']\n env:\n K: ${{ inputs.y }}\n") | |
| 304 | + | if got := contexts(t, b.Runs.Args.Items[0]); got != "inputs" { | |
| 305 | + | t.Errorf("args: %s", got) | |
| 306 | + | } | |
| 307 | + | if got := contexts(t, b.Runs.Env.Pairs[0].Value); got != "inputs" { | |
| 308 | + | t.Errorf("env: %s", got) | |
| 309 | + | } | |
| 310 | + | } | |
A action/doc.go +33 -0
| @@ -0,0 +1,33 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | // Package action reads GitHub Actions action.yml manifests the way GitHub | |
| 4 | + | // does: the model it returns is what the runner's ActionManifestManager gets | |
| 5 | + | // out of the same file, with the same errors. | |
| 6 | + | // | |
| 7 | + | // [Parse] follows the C# runner (actions/runner, Runner.Worker/ | |
| 8 | + | // ActionManifestManagerLegacy.cs, which is what ActionManifestManagerWrapper | |
| 9 | + | // runs; the newer ActionManifestManager only runs next to it, behind a | |
| 10 | + | // comparison flag, and its result is thrown away). The runner reads the file | |
| 11 | + | // with its own schema, action_yaml.json, which is the "action-root" definition | |
| 12 | + | // of the schema that this package vendors, action-v1.0.json of the npm package | |
| 13 | + | // @actions/workflow-parser 0.3.61 (MIT, GitHub, actions/languageservices): that | |
| 14 | + | // definition accepts unknown top-level keys (icon, color, branding...), needs | |
| 15 | + | // no name and no description, and checks the keys under "runs" and the | |
| 16 | + | // contexts that expressions may use. [ParseStrict] reads the file against the | |
| 17 | + | // same schema's "action-root-strict" definition instead, which is the language | |
| 18 | + | // service's linter: name, description and runs are required, branding and | |
| 19 | + | // "using" are checked and unknown top-level keys are refused. GitHub accepts | |
| 20 | + | // files that the strict form refuses; use it to lint, not to run. | |
| 21 | + | // | |
| 22 | + | // The steps of a composite action are [workflow.Step] values, converted the way | |
| 23 | + | // the runner converts them (which is not the way package workflow converts the | |
| 24 | + | // steps of a workflow file). Expressions stay unevaluated [template.Token] | |
| 25 | + | // values; package template evaluates them. | |
| 26 | + | // | |
| 27 | + | // Errors read like the runner's: positioned messages ("action.yml (Line: 3, | |
| 28 | + | // Col: 5): Unexpected value 'x'") and, after them, the exceptions that the | |
| 29 | + | // runner reports with their .NET class name ("System.ArgumentNullException: | |
| 30 | + | // ..."). The runner prints exception stack traces after them; those are not | |
| 31 | + | // reproduced. PORTING.md lists what differs from the runner and from the | |
| 32 | + | // language service, and what was checked against which source. | |
| 33 | + | package action | |
A action/fuzz_test.go +70 -0
| @@ -0,0 +1,70 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "testing" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func FuzzParse(f *testing.F) { | |
| 12 | + | files, _ := filepath.Glob("testdata/cases/*.yml") | |
| 13 | + | for _, name := range files { | |
| 14 | + | if data, err := os.ReadFile(name); err == nil { | |
| 15 | + | f.Add(data) | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | f.Add([]byte("name: a\ndescription: d\nruns:\n using: node20\n main: m.js\n")) | |
| 19 | + | f.Add([]byte("")) | |
| 20 | + | f.Add([]byte("\x00\xff")) | |
| 21 | + | f.Add([]byte("runs: &a\n using: *a\n")) | |
| 22 | + | f.Add([]byte("runs:\n using: composite\n steps:\n - uses: $/x@\n")) | |
| 23 | + | f.Fuzz(func(t *testing.T, data []byte) { | |
| 24 | + | a, errs := Parse("action.yml", data) | |
| 25 | + | if (a == nil) == (len(errs) == 0) { | |
| 26 | + | t.Fatalf("action nil = %v with %d errors", a == nil, len(errs)) | |
| 27 | + | } | |
| 28 | + | if len(errs) > maxErrors { | |
| 29 | + | t.Fatalf("%d errors, the limit is %d", len(errs), maxErrors) | |
| 30 | + | } | |
| 31 | + | for _, e := range errs { | |
| 32 | + | _ = e.Error() | |
| 33 | + | } | |
| 34 | + | if a != nil { | |
| 35 | + | if a.Runs == nil { | |
| 36 | + | t.Fatal("no runs") | |
| 37 | + | } | |
| 38 | + | for _, s := range a.Runs.Steps { | |
| 39 | + | if s.ID == "" || s.If == nil { | |
| 40 | + | t.Fatalf("step without id or condition: %+v", s) | |
| 41 | + | } | |
| 42 | + | if s.Uses != nil { | |
| 43 | + | _, _ = ParseUses(s.Uses.Value) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | _ = a.Runs.HasPre() | |
| 47 | + | _ = a.Runs.HasPost() | |
| 48 | + | if a.Runs.Kind == RunsDocker { | |
| 49 | + | _, _ = ParseImage(a.Runs.Image) | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | _ = Lint("action.yml", data) | |
| 53 | + | }) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func FuzzParseUses(f *testing.F) { | |
| 57 | + | for _, s := range []string{"actions/checkout@v4", "./x", "docker://a", "$/a", "a@b@c", "", "//", `\@`, "a/b@"} { | |
| 58 | + | f.Add(s) | |
| 59 | + | } | |
| 60 | + | f.Fuzz(func(t *testing.T, s string) { | |
| 61 | + | u, err := ParseUses(s) | |
| 62 | + | if (err == nil) != (u.Kind != 0) { | |
| 63 | + | t.Fatalf("%q: kind %d with error %v", s, u.Kind, err) | |
| 64 | + | } | |
| 65 | + | if u.Kind == UsesRepository && (u.Owner == "" || u.Repo == "" || u.Ref == "") { | |
| 66 | + | t.Fatalf("%q: %+v", s, u) | |
| 67 | + | } | |
| 68 | + | _, _ = ParseImage(s) | |
| 69 | + | }) | |
| 70 | + | } | |
A action/gen/regen.sh +18 -0
| @@ -0,0 +1,18 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # SPDX-License-Identifier: MIT | |
| 3 | + | # | |
| 4 | + | # Regenerates the two files that are the answer of a reference implementation: | |
| 5 | + | # action/testdata/strict.json (@actions/workflow-parser, from gen/package-lock.json) | |
| 6 | + | # action/testdata/runner.json (the C# runner; needs RUNNER_CLONE, see gen/runner/build.sh) | |
| 7 | + | # Run from anywhere; `git diff` afterwards must be empty unless the cases changed. | |
| 8 | + | set -eu | |
| 9 | + | root=$(cd "$(dirname "$0")/../.." && pwd) | |
| 10 | + | cd "$root/gen" | |
| 11 | + | npm ci --ignore-scripts --no-audit --no-fund | |
| 12 | + | cd "$root" | |
| 13 | + | node --import ./gen/register.mjs action/gen/strict.mjs > action/testdata/strict.json | |
| 14 | + | if [ -n "${RUNNER_CLONE:-}" ]; then | |
| 15 | + | action/gen/runner/build.sh | |
| 16 | + | else | |
| 17 | + | echo "RUNNER_CLONE not set: runner.json left as it is" >&2 | |
| 18 | + | fi | |
A action/gen/runner/Driver.cs +422 -0
| @@ -0,0 +1,422 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | // | |
| 3 | + | // Driver that runs the C# runner's action.yml loader on a directory of files. | |
| 4 | + | // It is compiled into a copy of actions/runner src/Sdk (see build.sh), so that | |
| 5 | + | // it can use the internal types the runner uses. | |
| 6 | + | // | |
| 7 | + | // Load, ConvertRuns and ConvertInputs below are | |
| 8 | + | // Runner.Worker/ActionManifestManagerLegacy.cs at the commit in | |
| 9 | + | // runner-commit.txt, with what needs an IExecutionContext removed (tracing, | |
| 10 | + | // the file table, expression values: none of it influences loading) and the | |
| 11 | + | // result written to JSON instead of ActionDefinitionData. Nothing else was | |
| 12 | + | // changed: the flow of exceptions and errors is the runner's, because it | |
| 13 | + | // decides which messages a user sees. | |
| 14 | + | // | |
| 15 | + | // Why the legacy loader: ActionManifestManagerWrapper.Load runs | |
| 16 | + | // ActionManifestManagerLegacy and returns its result; the newer | |
| 17 | + | // ActionManifestManager only runs next to it when the feature flag | |
| 18 | + | // actions_runner_compare_workflow_parser or the environment variable | |
| 19 | + | // ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER is set, and its result is discarded. | |
| 20 | + | ||
| 21 | + | using System; | |
| 22 | + | using System.Collections.Generic; | |
| 23 | + | using System.IO; | |
| 24 | + | using System.Linq; | |
| 25 | + | using GitHub.DistributedTask.ObjectTemplating; | |
| 26 | + | using GitHub.DistributedTask.ObjectTemplating.Schema; | |
| 27 | + | using GitHub.DistributedTask.ObjectTemplating.Tokens; | |
| 28 | + | using GitHub.DistributedTask.Pipelines; | |
| 29 | + | using GitHub.DistributedTask.Pipelines.ObjectTemplating; | |
| 30 | + | using Newtonsoft.Json; | |
| 31 | + | using Newtonsoft.Json.Linq; | |
| 32 | + | ||
| 33 | + | namespace ActionOracle | |
| 34 | + | { | |
| 35 | + | public static class Program | |
| 36 | + | { | |
| 37 | + | public static int Main(string[] args) | |
| 38 | + | { | |
| 39 | + | if (args.Length != 2 && args.Length != 3) | |
| 40 | + | { | |
| 41 | + | Console.Error.WriteLine("usage: Sdk.dll <action_yaml.json> <dir with *.yml> [<file with one uses value per line>]"); | |
| 42 | + | return 2; | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | var schemaJson = File.ReadAllText(args[0]); | |
| 46 | + | var schema = TemplateSchema.Load(new JsonObjectReader(null, schemaJson)); | |
| 47 | + | if (args.Length == 3) | |
| 48 | + | { | |
| 49 | + | // One-step composite action per "uses" value (with an id, so that | |
| 50 | + | // a value of the wrong form is reported and nothing else fails): | |
| 51 | + | // the answer is the error list and the reference the runner made. | |
| 52 | + | var uses = new JObject(); | |
| 53 | + | foreach (var line in File.ReadAllLines(args[2])) | |
| 54 | + | { | |
| 55 | + | var yaml = "name: a\ndescription: d\nruns:\n using: composite\n steps:\n - uses: " + JsonConvert.ToString(line) + "\n id: x\n"; | |
| 56 | + | uses[line] = Load(schema, "u.yml", yaml); | |
| 57 | + | } | |
| 58 | + | Console.Out.Write(uses.ToString(Formatting.Indented)); | |
| 59 | + | Console.Out.WriteLine(); | |
| 60 | + | return 0; | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | var result = new JObject(); | |
| 64 | + | foreach (var file in Directory.GetFiles(args[1], "*.yml").OrderBy(x => x, StringComparer.Ordinal)) | |
| 65 | + | { | |
| 66 | + | var name = Path.GetFileName(file); | |
| 67 | + | result[name] = Load(schema, name, File.ReadAllText(file)); | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | Console.Out.Write(result.ToString(Formatting.Indented)); | |
| 71 | + | Console.Out.WriteLine(); | |
| 72 | + | return 0; | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | static JObject Load(TemplateSchema schema, string fileRelativePath, string fileContent) | |
| 76 | + | { | |
| 77 | + | var templateContext = new TemplateContext | |
| 78 | + | { | |
| 79 | + | CancellationToken = System.Threading.CancellationToken.None, | |
| 80 | + | Errors = new TemplateValidationErrors(10, int.MaxValue), | |
| 81 | + | Memory = new TemplateMemory(maxDepth: 100, maxEvents: 1000000, maxBytes: 10 * 1024 * 1024), | |
| 82 | + | Schema = schema, | |
| 83 | + | TraceWriter = new EmptyTraceWriter(), | |
| 84 | + | }; | |
| 85 | + | ||
| 86 | + | var summary = new JObject(); | |
| 87 | + | JObject execution = null; | |
| 88 | + | var inputs = new JArray(); | |
| 89 | + | var deprecated = new JObject(); | |
| 90 | + | ||
| 91 | + | try | |
| 92 | + | { | |
| 93 | + | var token = default(TemplateToken); | |
| 94 | + | var fileId = templateContext.GetFileId(fileRelativePath); | |
| 95 | + | using (var stringReader = new StringReader(fileContent)) | |
| 96 | + | { | |
| 97 | + | var yamlObjectReader = new YamlObjectReader(fileId, stringReader); | |
| 98 | + | token = TemplateReader.Read(templateContext, "action-root", yamlObjectReader, fileId, out _); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | var actionMapping = token.AssertMapping("action manifest root"); | |
| 102 | + | var actionOutputs = default(MappingToken); | |
| 103 | + | var actionRunValueToken = default(TemplateToken); | |
| 104 | + | ||
| 105 | + | foreach (var actionPair in actionMapping) | |
| 106 | + | { | |
| 107 | + | var propertyName = actionPair.Key.AssertString($"action.yml property key"); | |
| 108 | + | ||
| 109 | + | switch (propertyName.Value) | |
| 110 | + | { | |
| 111 | + | case "name": | |
| 112 | + | summary["name"] = actionPair.Value.AssertString("name").Value; | |
| 113 | + | break; | |
| 114 | + | case "outputs": | |
| 115 | + | actionOutputs = actionPair.Value.AssertMapping("outputs"); | |
| 116 | + | break; | |
| 117 | + | case "description": | |
| 118 | + | summary["description"] = actionPair.Value.AssertString("description").Value; | |
| 119 | + | break; | |
| 120 | + | case "inputs": | |
| 121 | + | ConvertInputs(actionPair.Value, inputs, deprecated); | |
| 122 | + | break; | |
| 123 | + | case "runs": | |
| 124 | + | actionRunValueToken = actionPair.Value; | |
| 125 | + | break; | |
| 126 | + | default: | |
| 127 | + | break; | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | if (actionRunValueToken != null) | |
| 132 | + | { | |
| 133 | + | execution = ConvertRuns(templateContext, actionRunValueToken, fileRelativePath, actionOutputs); | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | catch (Exception ex) | |
| 137 | + | { | |
| 138 | + | templateContext.Errors.Add(ex); | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | var errors = new JArray(); | |
| 142 | + | foreach (var error in templateContext.Errors) | |
| 143 | + | { | |
| 144 | + | // ex.ToString() ends with the stack trace, which names temporary | |
| 145 | + | // paths and line numbers of this driver: keep the message only. | |
| 146 | + | var message = error.Message; | |
| 147 | + | var at = message.IndexOf("\n at ", StringComparison.Ordinal); | |
| 148 | + | errors.Add(at >= 0 ? message.Substring(0, at) : message); | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | if (errors.Count > 0) | |
| 152 | + | { | |
| 153 | + | summary["errors"] = errors; | |
| 154 | + | summary["failed"] = $"Failed to load {fileRelativePath}"; | |
| 155 | + | return summary; | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | if (execution == null) | |
| 159 | + | { | |
| 160 | + | summary["errors"] = new JArray($"Top level 'runs:' section is required for {fileRelativePath}"); | |
| 161 | + | return summary; | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | summary["inputs"] = inputs; | |
| 165 | + | if (deprecated.Count > 0) | |
| 166 | + | { | |
| 167 | + | summary["deprecated"] = deprecated; | |
| 168 | + | } | |
| 169 | + | summary["execution"] = execution; | |
| 170 | + | return summary; | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | static JObject ConvertRuns( | |
| 174 | + | TemplateContext templateContext, | |
| 175 | + | TemplateToken inputsToken, | |
| 176 | + | String fileRelativePath, | |
| 177 | + | MappingToken outputs = null) | |
| 178 | + | { | |
| 179 | + | var runsMapping = inputsToken.AssertMapping("runs"); | |
| 180 | + | var usingToken = default(StringToken); | |
| 181 | + | var imageToken = default(StringToken); | |
| 182 | + | var argsToken = default(SequenceToken); | |
| 183 | + | var entrypointToken = default(StringToken); | |
| 184 | + | var envToken = default(MappingToken); | |
| 185 | + | var mainToken = default(StringToken); | |
| 186 | + | var pluginToken = default(StringToken); | |
| 187 | + | var preToken = default(StringToken); | |
| 188 | + | var preEntrypointToken = default(StringToken); | |
| 189 | + | var preIfToken = default(StringToken); | |
| 190 | + | var postToken = default(StringToken); | |
| 191 | + | var postEntrypointToken = default(StringToken); | |
| 192 | + | var postIfToken = default(StringToken); | |
| 193 | + | var steps = default(List<Step>); | |
| 194 | + | ||
| 195 | + | foreach (var run in runsMapping) | |
| 196 | + | { | |
| 197 | + | var runsKey = run.Key.AssertString("runs key").Value; | |
| 198 | + | switch (runsKey) | |
| 199 | + | { | |
| 200 | + | case "using": | |
| 201 | + | usingToken = run.Value.AssertString("using"); | |
| 202 | + | break; | |
| 203 | + | case "image": | |
| 204 | + | imageToken = run.Value.AssertString("image"); | |
| 205 | + | break; | |
| 206 | + | case "args": | |
| 207 | + | argsToken = run.Value.AssertSequence("args"); | |
| 208 | + | break; | |
| 209 | + | case "entrypoint": | |
| 210 | + | entrypointToken = run.Value.AssertString("entrypoint"); | |
| 211 | + | break; | |
| 212 | + | case "env": | |
| 213 | + | envToken = run.Value.AssertMapping("env"); | |
| 214 | + | break; | |
| 215 | + | case "main": | |
| 216 | + | mainToken = run.Value.AssertString("main"); | |
| 217 | + | break; | |
| 218 | + | case "plugin": | |
| 219 | + | pluginToken = run.Value.AssertString("plugin"); | |
| 220 | + | break; | |
| 221 | + | case "post": | |
| 222 | + | postToken = run.Value.AssertString("post"); | |
| 223 | + | break; | |
| 224 | + | case "post-entrypoint": | |
| 225 | + | postEntrypointToken = run.Value.AssertString("post-entrypoint"); | |
| 226 | + | break; | |
| 227 | + | case "post-if": | |
| 228 | + | postIfToken = run.Value.AssertString("post-if"); | |
| 229 | + | break; | |
| 230 | + | case "pre": | |
| 231 | + | preToken = run.Value.AssertString("pre"); | |
| 232 | + | break; | |
| 233 | + | case "pre-entrypoint": | |
| 234 | + | preEntrypointToken = run.Value.AssertString("pre-entrypoint"); | |
| 235 | + | break; | |
| 236 | + | case "pre-if": | |
| 237 | + | preIfToken = run.Value.AssertString("pre-if"); | |
| 238 | + | break; | |
| 239 | + | case "steps": | |
| 240 | + | var stepsToken = run.Value.AssertSequence("steps"); | |
| 241 | + | steps = PipelineTemplateConverter.ConvertToSteps(templateContext, stepsToken); | |
| 242 | + | templateContext.Errors.Check(); | |
| 243 | + | break; | |
| 244 | + | default: | |
| 245 | + | break; | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | if (usingToken != null) | |
| 250 | + | { | |
| 251 | + | if (string.Equals(usingToken.Value, "docker", StringComparison.OrdinalIgnoreCase)) | |
| 252 | + | { | |
| 253 | + | if (string.IsNullOrEmpty(imageToken?.Value)) | |
| 254 | + | { | |
| 255 | + | throw new ArgumentNullException($"You are using a Container Action but an image is not provided in {fileRelativePath}."); | |
| 256 | + | } | |
| 257 | + | else | |
| 258 | + | { | |
| 259 | + | return new JObject | |
| 260 | + | { | |
| 261 | + | ["type"] = "docker", | |
| 262 | + | ["using"] = usingToken.Value, | |
| 263 | + | ["image"] = imageToken.Value, | |
| 264 | + | ["args"] = argsToken == null ? null : new JArray(argsToken.Select(x => x.ToString())), | |
| 265 | + | ["entrypoint"] = entrypointToken?.Value, | |
| 266 | + | ["env"] = envToken == null ? null : Pairs(envToken), | |
| 267 | + | ["pre"] = preEntrypointToken?.Value, | |
| 268 | + | ["preIf"] = preIfToken?.Value ?? "always()", | |
| 269 | + | ["post"] = postEntrypointToken?.Value, | |
| 270 | + | ["postIf"] = postIfToken?.Value ?? "always()", | |
| 271 | + | }; | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) || | |
| 275 | + | string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) || | |
| 276 | + | string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) || | |
| 277 | + | string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase)) | |
| 278 | + | { | |
| 279 | + | if (string.IsNullOrEmpty(mainToken?.Value)) | |
| 280 | + | { | |
| 281 | + | throw new ArgumentNullException($"You are using a JavaScript Action but there is not an entry JavaScript file provided in {fileRelativePath}."); | |
| 282 | + | } | |
| 283 | + | else | |
| 284 | + | { | |
| 285 | + | return new JObject | |
| 286 | + | { | |
| 287 | + | ["type"] = "node", | |
| 288 | + | ["using"] = usingToken.Value, | |
| 289 | + | ["main"] = mainToken.Value, | |
| 290 | + | ["pre"] = preToken?.Value, | |
| 291 | + | ["preIf"] = preIfToken?.Value ?? "always()", | |
| 292 | + | ["post"] = postToken?.Value, | |
| 293 | + | ["postIf"] = postIfToken?.Value ?? "always()", | |
| 294 | + | }; | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | else if (string.Equals(usingToken.Value, "composite", StringComparison.OrdinalIgnoreCase)) | |
| 298 | + | { | |
| 299 | + | if (steps == null) | |
| 300 | + | { | |
| 301 | + | throw new ArgumentNullException($"You are using a composite action but there are no steps provided in {fileRelativePath}."); | |
| 302 | + | } | |
| 303 | + | else | |
| 304 | + | { | |
| 305 | + | return new JObject | |
| 306 | + | { | |
| 307 | + | ["type"] = "composite", | |
| 308 | + | ["using"] = usingToken.Value, | |
| 309 | + | ["steps"] = new JArray(steps.Cast<ActionStep>().Select(Summarize)), | |
| 310 | + | ["outputs"] = outputs == null ? null : new JArray(outputs.Select(x => new JArray(x.Key.ToString(), OutputValue(x.Value)))), | |
| 311 | + | }; | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | else | |
| 315 | + | { | |
| 316 | + | throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead."); | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | else if (pluginToken != null) | |
| 320 | + | { | |
| 321 | + | return new JObject { ["type"] = "plugin", ["plugin"] = pluginToken.Value }; | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'."); | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | static string OutputValue(TemplateToken value) | |
| 328 | + | { | |
| 329 | + | if (value is MappingToken m) | |
| 330 | + | { | |
| 331 | + | foreach (var p in m) | |
| 332 | + | { | |
| 333 | + | if (p.Key.ToString() == "value") return p.Value.ToString(); | |
| 334 | + | } | |
| 335 | + | } | |
| 336 | + | return null; | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | static JObject Summarize(ActionStep a) | |
| 340 | + | { | |
| 341 | + | var o = new JObject { ["id"] = a.ContextName, ["if"] = a.Condition }; | |
| 342 | + | o["name"] = a.DisplayNameToken?.ToString(); | |
| 343 | + | o["continueOnError"] = a.ContinueOnError?.ToString(); | |
| 344 | + | o["timeout"] = a.TimeoutInMinutes?.ToString(); | |
| 345 | + | if (a.Environment is MappingToken) | |
| 346 | + | { | |
| 347 | + | o["env"] = Pairs(a.Environment); | |
| 348 | + | } | |
| 349 | + | else if (a.Environment != null) | |
| 350 | + | { | |
| 351 | + | o["envExpr"] = a.Environment.ToString(); | |
| 352 | + | } | |
| 353 | + | if (a.Reference is ScriptReference) | |
| 354 | + | { | |
| 355 | + | o["kind"] = "run"; | |
| 356 | + | var inputs = (MappingToken)a.Inputs; | |
| 357 | + | foreach (var p in inputs) | |
| 358 | + | { | |
| 359 | + | o[p.Key.ToString()] = p.Value.ToString(); | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | else | |
| 363 | + | { | |
| 364 | + | o["kind"] = "uses"; | |
| 365 | + | if (a.Inputs is MappingToken) | |
| 366 | + | { | |
| 367 | + | o["with"] = Pairs(a.Inputs); | |
| 368 | + | } | |
| 369 | + | else if (a.Inputs != null) | |
| 370 | + | { | |
| 371 | + | o["withExpr"] = a.Inputs.ToString(); | |
| 372 | + | } | |
| 373 | + | if (a.Reference is ContainerRegistryReference c) | |
| 374 | + | { | |
| 375 | + | o["reference"] = new JObject { ["type"] = "docker", ["image"] = c.Image }; | |
| 376 | + | } | |
| 377 | + | else if (a.Reference is RepositoryPathReference r) | |
| 378 | + | { | |
| 379 | + | o["reference"] = new JObject { ["type"] = r.RepositoryType, ["name"] = r.Name, ["ref"] = r.Ref, ["path"] = r.Path }; | |
| 380 | + | } | |
| 381 | + | } | |
| 382 | + | return o; | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | static JArray Pairs(TemplateToken t) | |
| 386 | + | { | |
| 387 | + | return t is MappingToken m ? new JArray(m.Select(x => new JArray(x.Key.ToString(), x.Value.ToString()))) : new JArray(t.ToString()); | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | static void ConvertInputs(TemplateToken inputsToken, JArray inputs, JObject deprecated) | |
| 391 | + | { | |
| 392 | + | var inputsMapping = inputsToken.AssertMapping("inputs"); | |
| 393 | + | foreach (var input in inputsMapping) | |
| 394 | + | { | |
| 395 | + | bool hasDefault = false; | |
| 396 | + | var inputName = input.Key.AssertString("input name"); | |
| 397 | + | var inputMetadata = input.Value.AssertMapping("input metadata"); | |
| 398 | + | var entry = new JObject { ["name"] = inputName.Value }; | |
| 399 | + | foreach (var metadata in inputMetadata) | |
| 400 | + | { | |
| 401 | + | var metadataName = metadata.Key.AssertString("input metadata").Value; | |
| 402 | + | if (string.Equals(metadataName, "default", StringComparison.OrdinalIgnoreCase)) | |
| 403 | + | { | |
| 404 | + | hasDefault = true; | |
| 405 | + | entry["default"] = metadata.Value.ToString(); | |
| 406 | + | } | |
| 407 | + | else if (string.Equals(metadataName, "deprecationMessage", StringComparison.OrdinalIgnoreCase)) | |
| 408 | + | { | |
| 409 | + | var message = metadata.Value.AssertString("input deprecationMessage"); | |
| 410 | + | deprecated.Add(inputName.Value, message.Value); | |
| 411 | + | } | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | if (!hasDefault) | |
| 415 | + | { | |
| 416 | + | entry["default"] = string.Empty; | |
| 417 | + | } | |
| 418 | + | inputs.Add(entry); | |
| 419 | + | } | |
| 420 | + | } | |
| 421 | + | } | |
| 422 | + | } | |
A action/gen/runner/README.md +18 -0
| @@ -0,0 +1,18 @@ | |||
| 1 | + | # The runner's action.yml loader, as an oracle | |
| 2 | + | ||
| 3 | + | `Driver.cs` is `ActionManifestManagerLegacy.Load`, `ConvertRuns` and | |
| 4 | + | `ConvertInputs` of actions/runner (the loader the runner runs; see | |
| 5 | + | `../../PORTING.md`) with what needs an `IExecutionContext` removed, writing its | |
| 6 | + | result as JSON. `build.sh` copies `src/Sdk` of a clone at the commit in | |
| 7 | + | `runner-commit.txt` to a temporary directory, adds the driver and builds it. | |
| 8 | + | ||
| 9 | + | RUNNER_CLONE=~/dev/clones/runner action/gen/runner/build.sh | |
| 10 | + | ||
| 11 | + | It writes `testdata/runner.json` (the files of `testdata/cases`) and | |
| 12 | + | `testdata/uses.json` (each line of `testdata/uses.txt` as the `uses` of a | |
| 13 | + | one-step composite action). | |
| 14 | + | ||
| 15 | + | Needs the .NET SDK (any version that builds `net8.0`; the output was made with | |
| 16 | + | SDK 10 and the 9.0 runtime through `DOTNET_ROLL_FORWARD=Major`), network access | |
| 17 | + | for NuGet, and a clone that is clean in `src/Sdk`. The driver prints the message | |
| 18 | + | of each recorded error without its stack trace. | |
A action/gen/runner/build.sh +36 -0
| @@ -0,0 +1,36 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # SPDX-License-Identifier: MIT | |
| 3 | + | # | |
| 4 | + | # Builds the runner-oracle for package action and runs it on the files in | |
| 5 | + | # ../../testdata/cases, writing ../../testdata/runner.json, and on each line of | |
| 6 | + | # ../../testdata/uses.txt as the "uses" of a one-step composite action, writing | |
| 7 | + | # ../../testdata/uses.json. | |
| 8 | + | # | |
| 9 | + | # RUNNER_CLONE=~/dev/clones/runner action/gen/runner/build.sh | |
| 10 | + | # | |
| 11 | + | # Requirements: a clone of https://github.com/actions/runner at the commit in | |
| 12 | + | # runner-commit.txt (checked), the .NET SDK, network access for NuGet | |
| 13 | + | # (restoring the runner's packages), and a .NET runtime of version 8 or newer | |
| 14 | + | # (the Sdk project targets net8.0; DOTNET_ROLL_FORWARD=Major runs it on a newer | |
| 15 | + | # runtime). The clone is copied to a temporary directory, never modified. | |
| 16 | + | set -eu | |
| 17 | + | here=$(cd "$(dirname "$0")" && pwd) | |
| 18 | + | : "${RUNNER_CLONE:?set RUNNER_CLONE to a clone of actions/runner}" | |
| 19 | + | want=$(cat "$here/runner-commit.txt") | |
| 20 | + | have=$(git -C "$RUNNER_CLONE" rev-parse HEAD) | |
| 21 | + | [ "$have" = "$want" ] || { echo "runner clone is at $have, want $want" >&2; exit 1; } | |
| 22 | + | git -C "$RUNNER_CLONE" diff --quiet -- src/Sdk src/Runner.Worker/action_yaml.json || { echo "runner clone is modified" >&2; exit 1; } | |
| 23 | + | work=$(mktemp -d) | |
| 24 | + | trap 'rm -rf "$work"' EXIT | |
| 25 | + | cp -r "$RUNNER_CLONE/src/Sdk" "$work/Sdk" | |
| 26 | + | cp "$RUNNER_CLONE/src/Directory.Build.props" "$RUNNER_CLONE/src/NuGet.Config" "$work/" 2>/dev/null || true | |
| 27 | + | cp "$here/Driver.cs" "$work/Sdk/Driver.cs" | |
| 28 | + | (cd "$work/Sdk" && dotnet build Sdk.csproj -c Release -p:OutputType=Exe -p:RuntimeIdentifiers= -v:q -nologo >&2) | |
| 29 | + | out="$here/../../testdata/runner.json" | |
| 30 | + | DOTNET_ROLL_FORWARD=Major dotnet "$work/Sdk/bin/Release/net8.0/Sdk.dll" \ | |
| 31 | + | "$RUNNER_CLONE/src/Runner.Worker/action_yaml.json" "$here/../../testdata/cases" > "$out" | |
| 32 | + | echo "wrote $out" >&2 | |
| 33 | + | out="$here/../../testdata/uses.json" | |
| 34 | + | DOTNET_ROLL_FORWARD=Major dotnet "$work/Sdk/bin/Release/net8.0/Sdk.dll" \ | |
| 35 | + | "$RUNNER_CLONE/src/Runner.Worker/action_yaml.json" "$here/../../testdata/cases" "$here/../../testdata/uses.txt" > "$out" | |
| 36 | + | echo "wrote $out" >&2 | |
A action/gen/runner/runner-commit.txt +1 -0
| @@ -0,0 +1,1 @@ | |||
| 1 | + | 5f0b3ddcc6ee9fe9441e00d55e85b94ea4aad798 | |
A action/gen/strict.mjs +41 -0
| @@ -0,0 +1,41 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | // | |
| 3 | + | // Generates testdata/strict.json: for each file in testdata/cases/*.yml, the | |
| 4 | + | // errors that @actions/workflow-parser 0.3.61 (MIT, GitHub, | |
| 5 | + | // actions/languageservices) reports when it parses the file with parseAction | |
| 6 | + | // (schema "action-root-strict") and converts it with convertActionTemplate | |
| 7 | + | // (which validates the pre-if and post-if conditions and the step conditions). | |
| 8 | + | // strict_test.go replays the answers against ParseStrict. | |
| 9 | + | // | |
| 10 | + | // action/gen/regen.sh | |
| 11 | + | ||
| 12 | + | import { pathToFileURL } from "node:url"; | |
| 13 | + | import path from "node:path"; | |
| 14 | + | import fs from "node:fs"; | |
| 15 | + | ||
| 16 | + | const dist = path.resolve(process.argv[2] ?? new URL("../../gen/node_modules/@actions/workflow-parser/dist", import.meta.url).pathname); | |
| 17 | + | const imp = p => import(pathToFileURL(path.join(dist, p)).href); | |
| 18 | + | const { parseAction } = await imp("actions/action-parser.js"); | |
| 19 | + | const { convertActionTemplate } = await imp("actions/action-template.js"); | |
| 20 | + | const { NoOperationTraceWriter } = await imp("templates/trace-writer.js"); | |
| 21 | + | ||
| 22 | + | const dir = new URL("../testdata/cases/", import.meta.url); | |
| 23 | + | const out = {}; | |
| 24 | + | for (const f of fs.readdirSync(dir).sort()) { | |
| 25 | + | if (!f.endsWith(".yml")) continue; | |
| 26 | + | const content = fs.readFileSync(new URL(f, dir), "utf8"); | |
| 27 | + | const r = parseAction({ name: f, content }, new NoOperationTraceWriter()); | |
| 28 | + | const res = { errors: [] }; | |
| 29 | + | if (r.value) { | |
| 30 | + | // Default error policy: the conversion only runs on a document without | |
| 31 | + | // schema errors, and adds its own (pre-if, post-if, step conditions). | |
| 32 | + | const t = convertActionTemplate(r.context, r.value); | |
| 33 | + | res.name = t.name; res.description = t.description; res.author = t.author; | |
| 34 | + | res.inputs = (t.inputs || []).map(i => ({ id: i.id, required: i.required, deprecationMessage: i.deprecationMessage, default: i.default === undefined ? undefined : String(i.default) })); | |
| 35 | + | res.outputs = (t.outputs || []).map(o => ({ id: o.id, value: o.value === undefined ? undefined : String(o.value) })); | |
| 36 | + | res.runs = { using: t.runs?.using, main: t.runs?.main, image: t.runs?.image, preIf: t.runs?.preIf, postIf: t.runs?.postIf, steps: (t.runs?.steps || []).length }; | |
| 37 | + | } | |
| 38 | + | res.errors = r.context.errors.getErrors().map(e => e.message); | |
| 39 | + | out[f] = res; | |
| 40 | + | } | |
| 41 | + | console.log(JSON.stringify(out, null, 1)); | |
A action/lint.go +106 -0
| @@ -0,0 +1,106 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bnema.dev/ermite-actions/expr" | |
| 7 | + | "bnema.dev/ermite-actions/template" | |
| 8 | + | "bnema.dev/ermite-actions/workflow" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // strictRootType is the language service's definition for an editor. | |
| 12 | + | const strictRootType = "action-root-strict" | |
| 13 | + | ||
| 14 | + | // Lint reports what the language service (@actions/workflow-parser | |
| 15 | + | // parseAction and convertActionTemplate, the code behind an editor's | |
| 16 | + | // squiggles) finds wrong in an action.yml, and returns no model. | |
| 17 | + | // | |
| 18 | + | // It reads the file against "action-root-strict": name, description and runs | |
| 19 | + | // are required, "using" must be exactly one of docker, node12, node16, node20, | |
| 20 | + | // node24 or composite, "branding" is checked, and unknown top-level keys such | |
| 21 | + | // as "icon" and "color" and a "plugin" action are refused. GitHub's runner | |
| 22 | + | // accepts several of those; [Parse] says what GitHub does, Lint what a linter | |
| 23 | + | // should flag. The conversion adds the errors of the conditions: pre-if and | |
| 24 | + | // post-if, and the "if" of composite steps, are parsed with the contexts the | |
| 25 | + | // schema grants (a step condition may not use vars or needs here, though the | |
| 26 | + | // runner lets it). | |
| 27 | + | // | |
| 28 | + | // The messages are the language service's: positioned, the expression errors | |
| 29 | + | // without the runner's ". Located at position" suffix being the one | |
| 30 | + | // difference of ours (package expr follows the runner). YAML syntax errors | |
| 31 | + | // have yaml.v3's wording. | |
| 32 | + | func Lint(path string, data []byte) []Error { | |
| 33 | + | sch, err := Schema() | |
| 34 | + | if err != nil { | |
| 35 | + | return []Error{{Raw: "internal error: " + err.Error()}} | |
| 36 | + | } | |
| 37 | + | ctx := template.NewContext(sch) | |
| 38 | + | root := template.ReadYAML(ctx, strictRootType, path, data) | |
| 39 | + | if !ctx.HasErrors() && root != nil { | |
| 40 | + | lintConditions(ctx, sch, root) | |
| 41 | + | } | |
| 42 | + | return collect(ctx) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // lintConditions is validateRunsIfCondition for pre-if and post-if and | |
| 46 | + | // convertToIfCondition for the "if" of every step of "runs.steps". | |
| 47 | + | func lintConditions(ctx *template.Context, sch *template.Schema, root template.Token) { | |
| 48 | + | m, ok := root.(*template.Mapping) | |
| 49 | + | if !ok { | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | runs, ok := m.Find("runs").(*template.Mapping) | |
| 53 | + | if !ok { | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | runsIf := sch.Info("runs-if") | |
| 57 | + | stepIf := sch.Info("step-if") | |
| 58 | + | for _, p := range runs.Pairs { | |
| 59 | + | switch p.Key.String() { | |
| 60 | + | case "pre-if", "post-if": | |
| 61 | + | if s, ok := p.Value.(*template.String); ok { | |
| 62 | + | if _, err := expr.Parse(s.Value, runsIf.NamedContexts(), runsIf.Functions()); err != nil { | |
| 63 | + | ctx.ErrorAt(s, err.Error()) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | case "steps": | |
| 67 | + | seq, ok := p.Value.(*template.Sequence) | |
| 68 | + | if !ok { | |
| 69 | + | continue | |
| 70 | + | } | |
| 71 | + | for _, it := range seq.Items { | |
| 72 | + | if sm, ok := it.(*template.Mapping); ok { | |
| 73 | + | lintStepIf(ctx, stepIf, sm.Find("if")) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func lintStepIf(ctx *template.Context, def *template.DefinitionInfo, tok template.Token) { | |
| 81 | + | var condition string | |
| 82 | + | switch s := tok.(type) { | |
| 83 | + | case nil: | |
| 84 | + | return | |
| 85 | + | case *template.BasicExpression: | |
| 86 | + | condition = s.Expression | |
| 87 | + | case *template.String: | |
| 88 | + | condition = s.Value | |
| 89 | + | default: | |
| 90 | + | return | |
| 91 | + | } | |
| 92 | + | if isNullOrWhiteSpace(condition) { | |
| 93 | + | return | |
| 94 | + | } | |
| 95 | + | // Condition text is trimmed for a string; an expression is used as it is. | |
| 96 | + | if _, isString := tok.(*template.String); isString { | |
| 97 | + | condition = trimSpaceJS(condition) | |
| 98 | + | } | |
| 99 | + | final := condition | |
| 100 | + | if node, err := expr.Parse(condition, def.NamedContexts(), def.Functions()); err == nil && !workflow.HasStatusFunction(node) { | |
| 101 | + | final = "success() && (" + condition + ")" | |
| 102 | + | } | |
| 103 | + | if _, err := expr.Parse(final, def.NamedContexts(), def.Functions()); err != nil { | |
| 104 | + | ctx.ErrorAt(tok, err.Error()) | |
| 105 | + | } | |
| 106 | + | } | |
A action/lint_test.go +120 -0
| @@ -0,0 +1,120 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/json" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | "regexp" | |
| 10 | + | "strings" | |
| 11 | + | "testing" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // testdata/strict.json is what @actions/workflow-parser 0.3.61 answers on the | |
| 15 | + | // files of testdata/cases with parseAction and convertActionTemplate (see | |
| 16 | + | // gen/strict.mjs). TestLintAgainstReference replays it against Lint. | |
| 17 | + | ||
| 18 | + | type strictAnswer struct { | |
| 19 | + | Errors []string `json:"errors"` | |
| 20 | + | Name string `json:"name"` | |
| 21 | + | Descr string `json:"description"` | |
| 22 | + | Author string `json:"author"` | |
| 23 | + | Inputs []struct { | |
| 24 | + | ID string `json:"id"` | |
| 25 | + | Required bool `json:"required"` | |
| 26 | + | DeprecationMessage string `json:"deprecationMessage"` | |
| 27 | + | Default string `json:"default"` | |
| 28 | + | } `json:"inputs"` | |
| 29 | + | Outputs []struct { | |
| 30 | + | ID string `json:"id"` | |
| 31 | + | Value string `json:"value"` | |
| 32 | + | } `json:"outputs"` | |
| 33 | + | Runs struct { | |
| 34 | + | Using string `json:"using"` | |
| 35 | + | Main string `json:"main"` | |
| 36 | + | Image string `json:"image"` | |
| 37 | + | PreIf string `json:"preIf"` | |
| 38 | + | PostIf string `json:"postIf"` | |
| 39 | + | Steps int `json:"steps"` | |
| 40 | + | } `json:"runs"` | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | var ( | |
| 44 | + | // The reference words expression parse errors like the JavaScript | |
| 45 | + | // expression library: no position suffix. package expr follows the C# | |
| 46 | + | // runner, which adds ". Located at position N within expression: ...". | |
| 47 | + | exprSuffix = regexp.MustCompile(`\. Located at position \d+ within expression: .*$`) | |
| 48 | + | // yaml.v3 and the JavaScript yaml library word syntax errors differently. | |
| 49 | + | jsYAMLError = regexp.MustCompile(` at line \d+, column \d+:\n`) | |
| 50 | + | ) | |
| 51 | + | ||
| 52 | + | // knownLintDifferences are the files on which the reference's answer is not | |
| 53 | + | // reproduced, with the reason. | |
| 54 | + | var knownLintDifferences = map[string]string{ | |
| 55 | + | "yaml_key_collection.yml": "a mapping key that is a collection: the reference reads it as undefined (\"Unexpected value 'undefined'\"), package template reports \"Unexpected value ''\" (twice); see template/PORTING.md", | |
| 56 | + | "fold_inputs_sharp.yml": "duplicate keys: the reference compares with JavaScript's toUpperCase (\u00df is SS), template with strings.ToUpper; Lint is not the runner's OrdinalIgnoreCase, Parse is", | |
| 57 | + | "int32_key.yml": jsKey, | |
| 58 | + | "key_bool_dup.yml": jsKey, | |
| 59 | + | "key_float_dup.yml": jsKey, | |
| 60 | + | "key_hex_dup.yml": jsKey, | |
| 61 | + | "key_hex_input.yml": jsKey, | |
| 62 | + | "key_null_env.yml": nullKey, | |
| 63 | + | "key_null_inputs.yml": nullKey, | |
| 64 | + | "key_null_top.yml": jsKey, | |
| 65 | + | "key_number_runs.yml": jsKey, | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // nullKey: Lint misses a null key under a loose-key mapping; the reference and | |
| 69 | + | // the runner reject it. Fixing it in template would change how workflow files | |
| 70 | + | // are read, so it is left as it is. | |
| 71 | + | const nullKey = "Lint misses a null key under a loose-key mapping; the reference and the runner reject it" | |
| 72 | + | ||
| 73 | + | // jsKey: the reference resolves a plain key to a number, boolean or null (and | |
| 74 | + | // then crashes on it, "raw.indexOf is not a function", or words it | |
| 75 | + | // differently); template keeps keys of the workflow-style readers as strings. | |
| 76 | + | // The runner's reader resolves keys like values, which Parse reproduces. | |
| 77 | + | const jsKey = "the reference resolves non-string keys and fails on them with its own wording; Lint keeps keys as strings" | |
| 78 | + | ||
| 79 | + | func TestLintAgainstReference(t *testing.T) { | |
| 80 | + | data, err := os.ReadFile("testdata/strict.json") | |
| 81 | + | if err != nil { | |
| 82 | + | t.Fatal(err) | |
| 83 | + | } | |
| 84 | + | var want map[string]strictAnswer | |
| 85 | + | if err := json.Unmarshal(data, &want); err != nil { | |
| 86 | + | t.Fatal(err) | |
| 87 | + | } | |
| 88 | + | files, _ := filepath.Glob("testdata/cases/*.yml") | |
| 89 | + | for _, f := range files { | |
| 90 | + | name := filepath.Base(f) | |
| 91 | + | w, ok := want[name] | |
| 92 | + | if !ok { | |
| 93 | + | t.Errorf("%s: missing from strict.json", name) | |
| 94 | + | continue | |
| 95 | + | } | |
| 96 | + | t.Run(strings.TrimSuffix(name, ".yml"), func(t *testing.T) { | |
| 97 | + | src, err := os.ReadFile(f) | |
| 98 | + | if err != nil { | |
| 99 | + | t.Fatal(err) | |
| 100 | + | } | |
| 101 | + | errs := Lint(name, src) | |
| 102 | + | if len(w.Errors) == 1 && jsYAMLError.MatchString(w.Errors[0]) { | |
| 103 | + | if len(errs) != 1 || !strings.HasPrefix(errs[0].Error(), name+": ") { | |
| 104 | + | t.Errorf("syntax error: got %q, reference %q", errTexts(errs), w.Errors) | |
| 105 | + | } | |
| 106 | + | return | |
| 107 | + | } | |
| 108 | + | var got []string | |
| 109 | + | for _, e := range errs { | |
| 110 | + | got = append(got, exprSuffix.ReplaceAllString(e.Error(), "")) | |
| 111 | + | } | |
| 112 | + | if note, known := knownLintDifferences[name]; known { | |
| 113 | + | t.Skip(note) | |
| 114 | + | } | |
| 115 | + | if strings.Join(got, "\n") != strings.Join(w.Errors, "\n") { | |
| 116 | + | t.Fatalf("errors differ\n got: %q\nwant: %q", got, w.Errors) | |
| 117 | + | } | |
| 118 | + | }) | |
| 119 | + | } | |
| 120 | + | } | |
A action/load.go +309 -0
| @@ -0,0 +1,309 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | _ "embed" | |
| 7 | + | "strings" | |
| 8 | + | "sync" | |
| 9 | + | ||
| 10 | + | "bnema.dev/ermite-actions/template" | |
| 11 | + | "bnema.dev/ermite-actions/workflow" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | //go:embed schema/action-v1.0.min.json | |
| 15 | + | var schemaJSON []byte | |
| 16 | + | ||
| 17 | + | var ( | |
| 18 | + | schemaOnce sync.Once | |
| 19 | + | schema *template.Schema | |
| 20 | + | schemaErr error | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | // Schema returns the vendored action schema (action-v1.0). It is read once and | |
| 24 | + | // shared; it must not be modified. | |
| 25 | + | func Schema() (*template.Schema, error) { | |
| 26 | + | schemaOnce.Do(func() { schema, schemaErr = template.ParseSchema(schemaJSON) }) | |
| 27 | + | return schema, schemaErr | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // rootType is the definition the runner reads an action.yml against: its | |
| 31 | + | // action_yaml.json has "action-root", whose definitions the vendored schema | |
| 32 | + | // keeps under the same names. ("action-root-strict" is the language service's | |
| 33 | + | // linter, which also wants name and description and refuses unknown keys that | |
| 34 | + | // GitHub accepts.) | |
| 35 | + | const rootType = "action-root" | |
| 36 | + | ||
| 37 | + | // The limits of the runner's TemplateContext for an action manifest | |
| 38 | + | // (ActionManifestManagerLegacy.CreateTemplateContext). | |
| 39 | + | const ( | |
| 40 | + | maxDepth = 100 | |
| 41 | + | maxErrors = 10 | |
| 42 | + | ) | |
| 43 | + | ||
| 44 | + | // LoadFailed returns the line the runner ends with when [Parse] returned | |
| 45 | + | // errors: it reports each error, then fails the step with this text. | |
| 46 | + | func LoadFailed(file string) string { return "Failed to load " + file } | |
| 47 | + | ||
| 48 | + | // Parse reads, validates and converts an action.yml (or action.yaml) the way | |
| 49 | + | // GitHub's runner does. path is only used in messages, where the runner puts | |
| 50 | + | // the manifest's path inside the actions directory ("actions/checkout/v4/ | |
| 51 | + | // action.yml"). | |
| 52 | + | // | |
| 53 | + | // The errors are the runner's, in its order: first the ones the template | |
| 54 | + | // reader and the converters record with a position, then the one exception | |
| 55 | + | // that stopped the load, if any, worded like the runner prints it | |
| 56 | + | // ("System.ArgumentNullException: Value cannot be null. (Parameter 'You are | |
| 57 | + | // using a JavaScript Action but there is not an entry JavaScript file provided | |
| 58 | + | // in action.yml.')", stack trace left out). At most ten errors are kept. When | |
| 59 | + | // the list is not empty the action is nil, and the runner fails with | |
| 60 | + | // [LoadFailed]. A manifest without "runs" gives the one error "Top level | |
| 61 | + | // 'runs:' section is required for <path>". | |
| 62 | + | func Parse(path string, data []byte) (*Action, []Error) { | |
| 63 | + | sch, err := Schema() | |
| 64 | + | if err != nil { | |
| 65 | + | return nil, []Error{{Raw: "internal error: " + err.Error()}} | |
| 66 | + | } | |
| 67 | + | ctx := template.NewContext(sch) | |
| 68 | + | ctx.MaxDepth, ctx.ErrorLimit, ctx.RunnerYAML = maxDepth, maxErrors, true | |
| 69 | + | l := &loader{ctx: ctx, schema: sch, file: path} | |
| 70 | + | a := l.load(data) | |
| 71 | + | if ctx.HasErrors() { | |
| 72 | + | return nil, collect(ctx) | |
| 73 | + | } | |
| 74 | + | if a == nil || a.Runs == nil { | |
| 75 | + | return nil, []Error{{Raw: "Top level 'runs:' section is required for " + path}} | |
| 76 | + | } | |
| 77 | + | return a, nil | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func collect(ctx *template.Context) []Error { | |
| 81 | + | var out []Error | |
| 82 | + | for _, e := range ctx.Errors() { | |
| 83 | + | out = append(out, *e) | |
| 84 | + | } | |
| 85 | + | return out | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | type loader struct { | |
| 89 | + | ctx *template.Context | |
| 90 | + | schema *template.Schema | |
| 91 | + | file string | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // load is ActionManifestManagerLegacy.Load: everything that is thrown is | |
| 95 | + | // recorded in the context, like the runner's catch. | |
| 96 | + | func (l *loader) load(data []byte) (a *Action) { | |
| 97 | + | defer l.catch() | |
| 98 | + | return l.loadRoot(template.ReadYAML(l.ctx, rootType, l.file, data)) | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func (l *loader) catch() { | |
| 102 | + | if r := recover(); r != nil { | |
| 103 | + | t, ok := r.(thrown) | |
| 104 | + | if !ok { | |
| 105 | + | panic(r) | |
| 106 | + | } | |
| 107 | + | l.ctx.ErrorException(t.class, t.msg) | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (l *loader) loadRoot(root template.Token) (a *Action) { | |
| 112 | + | a = &Action{File: l.file} | |
| 113 | + | m := assertMapping(root, "action manifest root") | |
| 114 | + | var outputs *template.Mapping | |
| 115 | + | var runs template.Token | |
| 116 | + | for _, p := range m.Pairs { | |
| 117 | + | key := assertString(p.Key, "action.yml property key") | |
| 118 | + | switch key.Value { | |
| 119 | + | case "name": | |
| 120 | + | a.Name = assertString(p.Value, "name").Value | |
| 121 | + | case "outputs": | |
| 122 | + | outputs = assertMapping(p.Value, "outputs") | |
| 123 | + | case "description": | |
| 124 | + | a.Description = assertString(p.Value, "description").Value | |
| 125 | + | case "inputs": | |
| 126 | + | l.convertInputs(p.Value, a) | |
| 127 | + | case "runs": | |
| 128 | + | runs = p.Value | |
| 129 | + | case "author": | |
| 130 | + | if s, ok := p.Value.(*template.String); ok { | |
| 131 | + | a.Author = s.Value | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | if outputs != nil { | |
| 136 | + | a.Outputs = convertOutputs(outputs) | |
| 137 | + | } | |
| 138 | + | if runs != nil { | |
| 139 | + | a.Runs = l.convertRuns(runs, m) | |
| 140 | + | } | |
| 141 | + | return a | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func (l *loader) check() { | |
| 145 | + | errs := l.ctx.Errors() | |
| 146 | + | if len(errs) == 0 { | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | msgs := make([]string, len(errs)) | |
| 150 | + | for i, e := range errs { | |
| 151 | + | msgs[i] = e.Error() | |
| 152 | + | } | |
| 153 | + | throwf(exTemplate, "The template is not valid. %s", strings.Join(msgs, ",")) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (l *loader) convertInputs(t template.Token, a *Action) { | |
| 157 | + | for _, p := range assertMapping(t, "inputs").Pairs { | |
| 158 | + | name := assertString(p.Key, "input name") | |
| 159 | + | in := Input{Name: name.Value, Pos: name.Pos()} | |
| 160 | + | for _, md := range assertMapping(p.Value, "input metadata").Pairs { | |
| 161 | + | mdName := assertString(md.Key, "input metadata").Value | |
| 162 | + | switch { | |
| 163 | + | case asciiEqualFold(mdName, "default"): | |
| 164 | + | in.Default, in.HasDefault = md.Value, true | |
| 165 | + | case asciiEqualFold(mdName, "deprecationMessage"): | |
| 166 | + | // The runner's Dictionary.Add would throw on a repeated input | |
| 167 | + | // name, but the template reader has already refused one | |
| 168 | + | // (names compare case-insensitively there). | |
| 169 | + | in.DeprecationMessage = assertString(md.Value, "input deprecationMessage").Value | |
| 170 | + | case mdName == "description": | |
| 171 | + | if s, ok := md.Value.(*template.String); ok { | |
| 172 | + | in.Description = s.Value | |
| 173 | + | } | |
| 174 | + | case mdName == "required": | |
| 175 | + | switch v := md.Value.(type) { | |
| 176 | + | case *template.Boolean: | |
| 177 | + | in.Required = v.Value | |
| 178 | + | case *template.String: | |
| 179 | + | in.Required = v.Value == "true" | |
| 180 | + | } | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | if !in.HasDefault { | |
| 184 | + | in.Default = template.NewString(template.Origin{}, "") | |
| 185 | + | } | |
| 186 | + | a.Inputs = append(a.Inputs, in) | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | func convertOutputs(m *template.Mapping) []Output { | |
| 191 | + | var out []Output | |
| 192 | + | for _, p := range m.Pairs { | |
| 193 | + | o := Output{Name: p.Key.String(), Pos: p.Key.Pos()} | |
| 194 | + | if om, ok := p.Value.(*template.Mapping); ok { | |
| 195 | + | for _, kv := range om.Pairs { | |
| 196 | + | switch kv.Key.String() { | |
| 197 | + | case "description": | |
| 198 | + | if s, ok := kv.Value.(*template.String); ok { | |
| 199 | + | o.Description = s.Value | |
| 200 | + | } | |
| 201 | + | case "value": | |
| 202 | + | if template.IsScalar(kv.Value) { | |
| 203 | + | o.Value = kv.Value | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | out = append(out, o) | |
| 209 | + | } | |
| 210 | + | return out | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // convertRuns is ConvertRuns. | |
| 214 | + | func (l *loader) convertRuns(t template.Token, _ *template.Mapping) *Runs { | |
| 215 | + | rm := assertMapping(t, "runs") | |
| 216 | + | var ( | |
| 217 | + | usingTok, imageTok, entrypointTok, mainTok, pluginTok *template.String | |
| 218 | + | preTok, preEntrypointTok, preIfTok *template.String | |
| 219 | + | postTok, postEntrypointTok, postIfTok *template.String | |
| 220 | + | argsTok *template.Sequence | |
| 221 | + | envTok *template.Mapping | |
| 222 | + | steps []*workflow.Step | |
| 223 | + | haveSteps bool | |
| 224 | + | ) | |
| 225 | + | for _, p := range rm.Pairs { | |
| 226 | + | key := assertString(p.Key, "runs key").Value | |
| 227 | + | v := p.Value | |
| 228 | + | switch key { | |
| 229 | + | case "using": | |
| 230 | + | usingTok = assertString(v, "using") | |
| 231 | + | case "image": | |
| 232 | + | imageTok = assertString(v, "image") | |
| 233 | + | case "args": | |
| 234 | + | argsTok = assertSequence(v, "args") | |
| 235 | + | case "entrypoint": | |
| 236 | + | entrypointTok = assertString(v, "entrypoint") | |
| 237 | + | case "env": | |
| 238 | + | envTok = assertMapping(v, "env") | |
| 239 | + | case "main": | |
| 240 | + | mainTok = assertString(v, "main") | |
| 241 | + | case "plugin": | |
| 242 | + | pluginTok = assertString(v, "plugin") | |
| 243 | + | case "post": | |
| 244 | + | postTok = assertString(v, "post") | |
| 245 | + | case "post-entrypoint": | |
| 246 | + | postEntrypointTok = assertString(v, "post-entrypoint") | |
| 247 | + | case "post-if": | |
| 248 | + | postIfTok = assertString(v, "post-if") | |
| 249 | + | case "pre": | |
| 250 | + | preTok = assertString(v, "pre") | |
| 251 | + | case "pre-entrypoint": | |
| 252 | + | preEntrypointTok = assertString(v, "pre-entrypoint") | |
| 253 | + | case "pre-if": | |
| 254 | + | preIfTok = assertString(v, "pre-if") | |
| 255 | + | case "steps": | |
| 256 | + | steps, haveSteps = l.convertSteps(assertSequence(v, "steps")), true | |
| 257 | + | l.check() | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | str := func(s *template.String) string { | |
| 261 | + | if s == nil { | |
| 262 | + | return "" | |
| 263 | + | } | |
| 264 | + | return s.Value | |
| 265 | + | } | |
| 266 | + | orAlways := func(s *template.String) string { | |
| 267 | + | if s == nil { | |
| 268 | + | return "always()" | |
| 269 | + | } | |
| 270 | + | return s.Value | |
| 271 | + | } | |
| 272 | + | r := &Runs{Pos: rm.Pos()} | |
| 273 | + | if usingTok != nil { | |
| 274 | + | using := usingTok.Value | |
| 275 | + | switch { | |
| 276 | + | case asciiEqualFold(using, "docker"): | |
| 277 | + | if str(imageTok) == "" { | |
| 278 | + | throwf(exArgumentNull, "You are using a Container Action but an image is not provided in %s.", l.file) | |
| 279 | + | } | |
| 280 | + | r.Kind, r.Using = RunsDocker, "docker" | |
| 281 | + | r.Image, r.Entrypoint = imageTok.Value, str(entrypointTok) | |
| 282 | + | r.Args, r.Env = argsTok, envTok | |
| 283 | + | r.PreEntrypoint, r.PostEntrypoint = str(preEntrypointTok), str(postEntrypointTok) | |
| 284 | + | r.PreIf, r.PostIf = orAlways(preIfTok), orAlways(postIfTok) | |
| 285 | + | case asciiEqualFold(using, "node12"), asciiEqualFold(using, "node16"), asciiEqualFold(using, "node20"), asciiEqualFold(using, "node24"): | |
| 286 | + | if str(mainTok) == "" { | |
| 287 | + | throwf(exArgumentNull, "You are using a JavaScript Action but there is not an entry JavaScript file provided in %s.", l.file) | |
| 288 | + | } | |
| 289 | + | r.Kind, r.Using = RunsNode, strings.ToLower(using) | |
| 290 | + | r.Main, r.Pre, r.Post = mainTok.Value, str(preTok), str(postTok) | |
| 291 | + | r.PreIf, r.PostIf = orAlways(preIfTok), orAlways(postIfTok) | |
| 292 | + | case asciiEqualFold(using, "composite"): | |
| 293 | + | if !haveSteps { | |
| 294 | + | throwf(exArgumentNull, "You are using a composite action but there are no steps provided in %s.", l.file) | |
| 295 | + | } | |
| 296 | + | r.Kind, r.Using, r.Steps = RunsComposite, "composite", steps | |
| 297 | + | r.PreIf, r.PostIf = "always()", "always()" | |
| 298 | + | default: | |
| 299 | + | throwf(exArgumentRng, "'using: %s' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.", using) | |
| 300 | + | } | |
| 301 | + | return r | |
| 302 | + | } | |
| 303 | + | if pluginTok != nil { | |
| 304 | + | r.Kind, r.Using, r.Plugin = RunsPlugin, "plugin", pluginTok.Value | |
| 305 | + | return r | |
| 306 | + | } | |
| 307 | + | throwf(exNotSupported, "Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.") | |
| 308 | + | return nil | |
| 309 | + | } | |
A action/model.go +162 -0
| @@ -0,0 +1,162 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bnema.dev/ermite-actions/template" | |
| 7 | + | "bnema.dev/ermite-actions/workflow" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Error is a problem found in an action manifest. See the package comment for | |
| 11 | + | // the form of its message. | |
| 12 | + | type Error = template.Error | |
| 13 | + | ||
| 14 | + | // Action is a parsed action.yml. | |
| 15 | + | type Action struct { | |
| 16 | + | // File is the name given to Parse. | |
| 17 | + | File string | |
| 18 | + | // Name, Description and Author are the keys of the same names; "" when | |
| 19 | + | // absent, or not a string. The runner does not read Author. | |
| 20 | + | Name, Description, Author string | |
| 21 | + | // Inputs are the declared inputs in file order. | |
| 22 | + | Inputs []Input | |
| 23 | + | // Outputs are the declared outputs in file order. Only a composite | |
| 24 | + | // action's outputs have a Value that is used. | |
| 25 | + | Outputs []Output | |
| 26 | + | // Runs says how the action runs. It is never nil in a returned Action. | |
| 27 | + | Runs *Runs | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // Input returns the input called name, compared like the runner compares | |
| 31 | + | // input names (OrdinalIgnoreCase), or nil. | |
| 32 | + | func (a *Action) Input(name string) *Input { | |
| 33 | + | for i := range a.Inputs { | |
| 34 | + | if ordinalEqual(a.Inputs[i].Name, name) { | |
| 35 | + | return &a.Inputs[i] | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | return nil | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Input is an entry of "inputs". | |
| 42 | + | type Input struct { | |
| 43 | + | // Name is the key, as written. | |
| 44 | + | Name string | |
| 45 | + | // Pos is where the key is. | |
| 46 | + | Pos template.Pos | |
| 47 | + | // Description is the "description" key, or "". | |
| 48 | + | Description string | |
| 49 | + | // Required is "required: true" (a boolean, or the string "true"). The | |
| 50 | + | // runner does not check it. | |
| 51 | + | Required bool | |
| 52 | + | // Default is the default value: a string, or an expression | |
| 53 | + | // (${{ github.ref }}) that the runner evaluates when the step does not | |
| 54 | + | // give the input; the context it may use is the one of | |
| 55 | + | // "input-default-context" in the schema (github, strategy, matrix, job, | |
| 56 | + | // runner and hashFiles). It is never nil: an input without a default has | |
| 57 | + | // the empty string, as in the runner. HasDefault tells them apart. | |
| 58 | + | Default template.Token | |
| 59 | + | HasDefault bool | |
| 60 | + | // DeprecationMessage is "deprecationMessage", or "". The runner warns | |
| 61 | + | // "Input '<name>' has been deprecated with message: <message>" when a | |
| 62 | + | // step gives the input. | |
| 63 | + | DeprecationMessage string | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Deprecated reports whether the input has a deprecation message. | |
| 67 | + | func (in *Input) Deprecated() bool { return in.DeprecationMessage != "" } | |
| 68 | + | ||
| 69 | + | // Output is an entry of "outputs". | |
| 70 | + | type Output struct { | |
| 71 | + | Name string | |
| 72 | + | Pos template.Pos | |
| 73 | + | Description string | |
| 74 | + | // Value is the "value" token of a composite action's output: a string or | |
| 75 | + | // an expression over github, strategy, matrix, steps, inputs, job, runner | |
| 76 | + | // and env. nil when the output has none. | |
| 77 | + | Value template.Token | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // RunsKind is how an action runs. | |
| 81 | + | type RunsKind int | |
| 82 | + | ||
| 83 | + | // The kinds of action. | |
| 84 | + | const ( | |
| 85 | + | // RunsNode is a JavaScript action: using node12, node16, node20 or node24. | |
| 86 | + | RunsNode RunsKind = iota + 1 | |
| 87 | + | // RunsDocker is a container action: using docker. | |
| 88 | + | RunsDocker | |
| 89 | + | // RunsComposite is a composite action: using composite. | |
| 90 | + | RunsComposite | |
| 91 | + | // RunsPlugin is "runs: {plugin: name}", an internal runner plugin. The | |
| 92 | + | // manifest loads, but only the runner's own plugins can run. | |
| 93 | + | RunsPlugin | |
| 94 | + | ) | |
| 95 | + | ||
| 96 | + | // Runs is the "runs" key. Which fields are set depends on Kind. | |
| 97 | + | type Runs struct { | |
| 98 | + | Kind RunsKind | |
| 99 | + | // Using is the runtime: "node12", "node16", "node20", "node24", "docker", | |
| 100 | + | // "composite", or "plugin". The runner compares "using" without regard to | |
| 101 | + | // case; Using is lower case. | |
| 102 | + | Using string | |
| 103 | + | // Pos is where "runs" is. | |
| 104 | + | Pos template.Pos | |
| 105 | + | ||
| 106 | + | // Node actions. | |
| 107 | + | // | |
| 108 | + | // Main is the script to run; Pre and Post are scripts that run before the | |
| 109 | + | // job's first step and after its last, or "". PreIf and PostIf are their | |
| 110 | + | // conditions: expressions written without ${{ }}, evaluated as they are | |
| 111 | + | // (the runner does not add "success() &&"), "always()" when absent. | |
| 112 | + | // Docker actions have the same four conditions for their entrypoints. | |
| 113 | + | Main, Pre, Post string | |
| 114 | + | PreIf, PostIf string | |
| 115 | + | ||
| 116 | + | // Docker actions. | |
| 117 | + | // | |
| 118 | + | // Image is a path to a Dockerfile ("Dockerfile", "images/Dockerfile") or | |
| 119 | + | // "docker://image[:tag]"; see [ParseImage]. Entrypoint overrides the | |
| 120 | + | // image's ENTRYPOINT. Args is a sequence of strings and Env a mapping of | |
| 121 | + | // strings, both of which may hold ${{ inputs.x }} expressions (and no other | |
| 122 | + | // context); nil when absent. PreEntrypoint and PostEntrypoint are the | |
| 123 | + | // entrypoints that run before and after, or "". | |
| 124 | + | Image, Entrypoint, PreEntrypoint, PostEntrypoint string | |
| 125 | + | Args *template.Sequence | |
| 126 | + | Env *template.Mapping | |
| 127 | + | ||
| 128 | + | // Composite actions. | |
| 129 | + | // | |
| 130 | + | // Steps are the steps in file order. A run step has a shell; the contexts | |
| 131 | + | // of each key are those of the schema (github, inputs, strategy, matrix, | |
| 132 | + | // steps, job, runner, env), and "if" also takes vars and needs. A step | |
| 133 | + | // has no timeout-minutes and no working-directory on a uses step. | |
| 134 | + | Steps []*workflow.Step | |
| 135 | + | ||
| 136 | + | // Plugin actions. | |
| 137 | + | Plugin string | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // HasPre reports whether the action has something to run before the job's | |
| 141 | + | // steps: a node "pre" or a docker "pre-entrypoint". | |
| 142 | + | func (r *Runs) HasPre() bool { | |
| 143 | + | switch r.Kind { | |
| 144 | + | case RunsNode: | |
| 145 | + | return r.Pre != "" | |
| 146 | + | case RunsDocker: | |
| 147 | + | return r.PreEntrypoint != "" | |
| 148 | + | } | |
| 149 | + | return false | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // HasPost reports whether the action has something to run after the job's | |
| 153 | + | // steps: a node "post" or a docker "post-entrypoint". | |
| 154 | + | func (r *Runs) HasPost() bool { | |
| 155 | + | switch r.Kind { | |
| 156 | + | case RunsNode: | |
| 157 | + | return r.Post != "" | |
| 158 | + | case RunsDocker: | |
| 159 | + | return r.PostEntrypoint != "" | |
| 160 | + | } | |
| 161 | + | return false | |
| 162 | + | } | |
A action/oracle_test.go +323 -0
| @@ -0,0 +1,323 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/json" | |
| 7 | + | "fmt" | |
| 8 | + | "os" | |
| 9 | + | "path/filepath" | |
| 10 | + | "regexp" | |
| 11 | + | "strings" | |
| 12 | + | "testing" | |
| 13 | + | ||
| 14 | + | "bnema.dev/ermite-actions/template" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // testdata/runner.json is what the C# runner's own loader answers on the files | |
| 18 | + | // of testdata/cases (see gen/runner): the errors it reports and a summary of | |
| 19 | + | // what it loaded. TestAgainstRunner replays it. | |
| 20 | + | ||
| 21 | + | type runnerAnswer struct { | |
| 22 | + | Name string `json:"name"` | |
| 23 | + | Description string `json:"description"` | |
| 24 | + | Errors []string `json:"errors"` | |
| 25 | + | Inputs []struct { | |
| 26 | + | Name string `json:"name"` | |
| 27 | + | Default *string `json:"default"` | |
| 28 | + | } `json:"inputs"` | |
| 29 | + | Deprecated map[string]string `json:"deprecated"` | |
| 30 | + | Execution *struct { | |
| 31 | + | Type string `json:"type"` | |
| 32 | + | Using string `json:"using"` | |
| 33 | + | Image string `json:"image"` | |
| 34 | + | Args []string `json:"args"` | |
| 35 | + | Entrypoint *string `json:"entrypoint"` | |
| 36 | + | Env [][2]string `json:"env"` | |
| 37 | + | Main string `json:"main"` | |
| 38 | + | Pre *string `json:"pre"` | |
| 39 | + | PreIf string `json:"preIf"` | |
| 40 | + | Post *string `json:"post"` | |
| 41 | + | PostIf string `json:"postIf"` | |
| 42 | + | Plugin string `json:"plugin"` | |
| 43 | + | Steps []struct { | |
| 44 | + | ID string `json:"id"` | |
| 45 | + | If string `json:"if"` | |
| 46 | + | Kind string `json:"kind"` | |
| 47 | + | Name *string `json:"name"` | |
| 48 | + | ContinueOnError *string `json:"continueOnError"` | |
| 49 | + | Timeout *string `json:"timeout"` | |
| 50 | + | Env [][2]string `json:"env"` | |
| 51 | + | EnvExpr *string `json:"envExpr"` | |
| 52 | + | WithExpr *string `json:"withExpr"` | |
| 53 | + | Script *string `json:"script"` | |
| 54 | + | Shell *string `json:"shell"` | |
| 55 | + | WorkingDir *string `json:"workingDirectory"` | |
| 56 | + | With [][2]string `json:"with"` | |
| 57 | + | Reference *struct { | |
| 58 | + | Type string `json:"type"` | |
| 59 | + | Image string `json:"image"` | |
| 60 | + | Name *string `json:"name"` | |
| 61 | + | Ref *string `json:"ref"` | |
| 62 | + | Path *string `json:"path"` | |
| 63 | + | } `json:"reference"` | |
| 64 | + | } `json:"steps"` | |
| 65 | + | Outputs [][2]*string `json:"outputs"` | |
| 66 | + | } `json:"execution"` | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func loadRunner(t *testing.T) map[string]runnerAnswer { | |
| 70 | + | t.Helper() | |
| 71 | + | data, err := os.ReadFile("testdata/runner.json") | |
| 72 | + | if err != nil { | |
| 73 | + | t.Fatal(err) | |
| 74 | + | } | |
| 75 | + | var m map[string]runnerAnswer | |
| 76 | + | if err := json.Unmarshal(data, &m); err != nil { | |
| 77 | + | t.Fatal(err) | |
| 78 | + | } | |
| 79 | + | return m | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func errTexts(errs []Error) []string { | |
| 83 | + | out := make([]string, len(errs)) | |
| 84 | + | for i := range errs { | |
| 85 | + | out[i] = errs[i].Error() | |
| 86 | + | } | |
| 87 | + | return out | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func orEmpty(s *string) string { | |
| 91 | + | if s == nil { | |
| 92 | + | return "" | |
| 93 | + | } | |
| 94 | + | return *s | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func pairsOf(m *template.Mapping) [][2]string { | |
| 98 | + | if m == nil { | |
| 99 | + | return nil | |
| 100 | + | } | |
| 101 | + | var out [][2]string | |
| 102 | + | for _, p := range m.Pairs { | |
| 103 | + | out = append(out, [2]string{p.Key.String(), p.Value.String()}) | |
| 104 | + | } | |
| 105 | + | return out | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func TestAgainstRunner(t *testing.T) { | |
| 109 | + | want := loadRunner(t) | |
| 110 | + | files, err := filepath.Glob("testdata/cases/*.yml") | |
| 111 | + | if err != nil || len(files) == 0 { | |
| 112 | + | t.Fatalf("no cases: %v", err) | |
| 113 | + | } | |
| 114 | + | if len(files) != len(want) { | |
| 115 | + | t.Errorf("%d case files, %d answers: run gen/runner/build.sh", len(files), len(want)) | |
| 116 | + | } | |
| 117 | + | for _, f := range files { | |
| 118 | + | name := filepath.Base(f) | |
| 119 | + | w, ok := want[name] | |
| 120 | + | if !ok { | |
| 121 | + | t.Errorf("%s: no answer in runner.json", name) | |
| 122 | + | continue | |
| 123 | + | } | |
| 124 | + | t.Run(strings.TrimSuffix(name, ".yml"), func(t *testing.T) { | |
| 125 | + | data, err := os.ReadFile(f) | |
| 126 | + | if err != nil { | |
| 127 | + | t.Fatal(err) | |
| 128 | + | } | |
| 129 | + | a, errs := Parse(name, data) | |
| 130 | + | got := errTexts(errs) | |
| 131 | + | if mismatch := compareErrors(got, w.Errors); mismatch != "" { | |
| 132 | + | t.Fatalf("errors differ: %s\n got: %q\nwant: %q", mismatch, got, w.Errors) | |
| 133 | + | } | |
| 134 | + | if len(w.Errors) > 0 { | |
| 135 | + | if a != nil { | |
| 136 | + | t.Error("action returned with errors") | |
| 137 | + | } | |
| 138 | + | return | |
| 139 | + | } | |
| 140 | + | if a == nil { | |
| 141 | + | t.Fatal("no action and no errors") | |
| 142 | + | } | |
| 143 | + | compareAction(t, a, w) | |
| 144 | + | }) | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // compareErrors compares messages with the runner's, which are recorded | |
| 149 | + | // without their stack traces. A YAML syntax error has YamlDotNet's wording in | |
| 150 | + | // the runner and yaml.v3's here: only the shape is compared. | |
| 151 | + | // yamlDotNetError matches the runner's YAML syntax errors: "file.yml: (Line: 3, | |
| 152 | + | // Col: 1, Idx: 16) - (Line: 3, Col: 1, Idx: 16): While parsing a node, ...". | |
| 153 | + | var yamlDotNetError = regexp.MustCompile(`^[^ ]+: \(Line: \d+, Col: \d+, Idx: \d+\) - \(Line: \d+, Col: \d+, Idx: \d+\): `) | |
| 154 | + | ||
| 155 | + | func compareErrors(got, want []string) string { | |
| 156 | + | if len(want) >= 1 && yamlDotNetError.MatchString(want[0]) { | |
| 157 | + | if len(got) == 0 || !strings.HasPrefix(got[0], strings.SplitN(want[0], ":", 2)[0]+": ") { | |
| 158 | + | return "syntax error not reported on the file" | |
| 159 | + | } | |
| 160 | + | // The runner follows a syntax error with the exception from the null | |
| 161 | + | // root; compare the rest. | |
| 162 | + | if len(got) != len(want) { | |
| 163 | + | return fmt.Sprintf("%d errors, want %d", len(got), len(want)) | |
| 164 | + | } | |
| 165 | + | for i := 1; i < len(got); i++ { | |
| 166 | + | if got[i] != want[i] { | |
| 167 | + | return fmt.Sprintf("error %d", i) | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | return "" | |
| 171 | + | } | |
| 172 | + | if len(got) != len(want) { | |
| 173 | + | return fmt.Sprintf("%d errors, want %d", len(got), len(want)) | |
| 174 | + | } | |
| 175 | + | for i := range got { | |
| 176 | + | if got[i] != want[i] { | |
| 177 | + | return fmt.Sprintf("error %d", i) | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | return "" | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func compareAction(t *testing.T, a *Action, w runnerAnswer) { | |
| 184 | + | t.Helper() | |
| 185 | + | if a.Name != w.Name || a.Description != w.Description { | |
| 186 | + | t.Errorf("name/description: got %q/%q, want %q/%q", a.Name, a.Description, w.Name, w.Description) | |
| 187 | + | } | |
| 188 | + | if len(a.Inputs) != len(w.Inputs) { | |
| 189 | + | t.Fatalf("%d inputs, want %d", len(a.Inputs), len(w.Inputs)) | |
| 190 | + | } | |
| 191 | + | for i, in := range w.Inputs { | |
| 192 | + | g := a.Inputs[i] | |
| 193 | + | if g.Name != in.Name || g.Default.String() != orEmpty(in.Default) { | |
| 194 | + | t.Errorf("input %d: got %q default %q, want %q default %q", i, g.Name, g.Default.String(), in.Name, orEmpty(in.Default)) | |
| 195 | + | } | |
| 196 | + | if dep := w.Deprecated[in.Name]; g.DeprecationMessage != dep { | |
| 197 | + | t.Errorf("input %q: deprecation %q, want %q", in.Name, g.DeprecationMessage, dep) | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | e := w.Execution | |
| 201 | + | r := a.Runs | |
| 202 | + | if e == nil || r == nil { | |
| 203 | + | t.Fatalf("execution: %v / %v", e, r) | |
| 204 | + | } | |
| 205 | + | kinds := map[string]RunsKind{"node": RunsNode, "docker": RunsDocker, "composite": RunsComposite, "plugin": RunsPlugin} | |
| 206 | + | if r.Kind != kinds[e.Type] { | |
| 207 | + | t.Fatalf("kind %v, want %s", r.Kind, e.Type) | |
| 208 | + | } | |
| 209 | + | switch r.Kind { | |
| 210 | + | case RunsNode: | |
| 211 | + | if r.Main != e.Main || r.Pre != orEmpty(e.Pre) || r.Post != orEmpty(e.Post) || r.PreIf != e.PreIf || r.PostIf != e.PostIf || !strings.EqualFold(r.Using, e.Using) { | |
| 212 | + | t.Errorf("node: got %+v, want %+v", r, e) | |
| 213 | + | } | |
| 214 | + | case RunsDocker: | |
| 215 | + | var args []string | |
| 216 | + | if r.Args != nil { | |
| 217 | + | for _, it := range r.Args.Items { | |
| 218 | + | args = append(args, it.String()) | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | if r.Image != e.Image || r.Entrypoint != orEmpty(e.Entrypoint) || r.PreEntrypoint != orEmpty(e.Pre) || r.PostEntrypoint != orEmpty(e.Post) || r.PreIf != e.PreIf || r.PostIf != e.PostIf { | |
| 222 | + | t.Errorf("docker: got %+v, want %+v", r, e) | |
| 223 | + | } | |
| 224 | + | if fmt.Sprint(args) != fmt.Sprint(e.Args) { | |
| 225 | + | t.Errorf("args: got %q, want %q", args, e.Args) | |
| 226 | + | } | |
| 227 | + | if fmt.Sprint(pairsOf(r.Env)) != fmt.Sprint(e.Env) { | |
| 228 | + | t.Errorf("env: got %q, want %q", pairsOf(r.Env), e.Env) | |
| 229 | + | } | |
| 230 | + | case RunsPlugin: | |
| 231 | + | if r.Plugin != e.Plugin { | |
| 232 | + | t.Errorf("plugin: %q, want %q", r.Plugin, e.Plugin) | |
| 233 | + | } | |
| 234 | + | case RunsComposite: | |
| 235 | + | if len(r.Steps) != len(e.Steps) { | |
| 236 | + | t.Fatalf("%d steps, want %d", len(r.Steps), len(e.Steps)) | |
| 237 | + | } | |
| 238 | + | for i, ws := range e.Steps { | |
| 239 | + | s := r.Steps[i] | |
| 240 | + | if s.ID != ws.ID || s.If.String() != "${{ "+ws.If+" }}" && s.If.Expression != ws.If { | |
| 241 | + | t.Errorf("step %d: id %q if %q, want id %q if %q", i, s.ID, s.If.Expression, ws.ID, ws.If) | |
| 242 | + | } | |
| 243 | + | if tokStr(s.Name) != orEmpty(ws.Name) || tokStr(s.ContinueOnError) != orEmpty(ws.ContinueOnError) { | |
| 244 | + | t.Errorf("step %d: name/continue-on-error %q/%q, want %q/%q", i, tokStr(s.Name), tokStr(s.ContinueOnError), orEmpty(ws.Name), orEmpty(ws.ContinueOnError)) | |
| 245 | + | } | |
| 246 | + | if tokStr(s.EnvExpression) != orEmpty(ws.EnvExpr) || tokStr(s.WithExpression) != orEmpty(ws.WithExpr) { | |
| 247 | + | t.Errorf("step %d env/with expression: got %q/%q, want %q/%q", i, tokStr(s.EnvExpression), tokStr(s.WithExpression), orEmpty(ws.EnvExpr), orEmpty(ws.WithExpr)) | |
| 248 | + | } | |
| 249 | + | if fmt.Sprint(pairsOf(s.Env)) != fmt.Sprint(nilIfEmpty(ws.Env)) { | |
| 250 | + | t.Errorf("step %d env: got %q, want %q", i, pairsOf(s.Env), ws.Env) | |
| 251 | + | } | |
| 252 | + | switch ws.Kind { | |
| 253 | + | case "run": | |
| 254 | + | if s.Run == nil || tokStr(s.Run) != orEmpty(ws.Script) || tokStr(s.Shell) != orEmpty(ws.Shell) || tokStr(s.WorkingDirectory) != orEmpty(ws.WorkingDir) { | |
| 255 | + | t.Errorf("step %d run: got %q/%q/%q, want %+v", i, tokStr(s.Run), tokStr(s.Shell), tokStr(s.WorkingDirectory), ws) | |
| 256 | + | } | |
| 257 | + | case "uses": | |
| 258 | + | if s.Uses == nil || fmt.Sprint(pairsOf(s.With)) != fmt.Sprint(nilIfEmpty(ws.With)) { | |
| 259 | + | t.Errorf("step %d uses: got %v with %q, want %+v", i, s.Uses, pairsOf(s.With), ws) | |
| 260 | + | } | |
| 261 | + | u, err := ParseUses(s.Uses.Value) | |
| 262 | + | if err != nil { | |
| 263 | + | t.Fatalf("step %d: %v", i, err) | |
| 264 | + | } | |
| 265 | + | checkReference(t, i, u, ws.Reference) | |
| 266 | + | } | |
| 267 | + | } | |
| 268 | + | if len(a.Outputs) != len(e.Outputs) { | |
| 269 | + | t.Fatalf("%d outputs, want %d", len(a.Outputs), len(e.Outputs)) | |
| 270 | + | } | |
| 271 | + | for i, o := range e.Outputs { | |
| 272 | + | if a.Outputs[i].Name != *o[0] || tokStr(a.Outputs[i].Value) != orEmpty(o[1]) { | |
| 273 | + | t.Errorf("output %d: got %q=%q, want %v", i, a.Outputs[i].Name, tokStr(a.Outputs[i].Value), o) | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | } | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | func nilIfEmpty(p [][2]string) [][2]string { | |
| 280 | + | if len(p) == 0 { | |
| 281 | + | return nil | |
| 282 | + | } | |
| 283 | + | return p | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | func tokStr(t template.Token) string { | |
| 287 | + | if t == nil { | |
| 288 | + | return "" | |
| 289 | + | } | |
| 290 | + | return t.String() | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | func checkReference(t *testing.T, i int, u Uses, ref *struct { | |
| 294 | + | Type string `json:"type"` | |
| 295 | + | Image string `json:"image"` | |
| 296 | + | Name *string `json:"name"` | |
| 297 | + | Ref *string `json:"ref"` | |
| 298 | + | Path *string `json:"path"` | |
| 299 | + | }) { | |
| 300 | + | t.Helper() | |
| 301 | + | if ref == nil { | |
| 302 | + | t.Errorf("step %d: no reference in the answer", i) | |
| 303 | + | return | |
| 304 | + | } | |
| 305 | + | switch u.Kind { | |
| 306 | + | case UsesDocker: | |
| 307 | + | if ref.Type != "docker" || ref.Image != u.Image { | |
| 308 | + | t.Errorf("step %d: %+v, want %+v", i, u, ref) | |
| 309 | + | } | |
| 310 | + | case UsesLocal: | |
| 311 | + | if ref.Type != "self" || orEmpty(ref.Path) != u.Path { | |
| 312 | + | t.Errorf("step %d: %+v, want %+v", i, u, ref) | |
| 313 | + | } | |
| 314 | + | case UsesSelfRepository: | |
| 315 | + | if ref.Type != "selfRepository" || orEmpty(ref.Path) != u.Path { | |
| 316 | + | t.Errorf("step %d: %+v, want %+v", i, u, ref) | |
| 317 | + | } | |
| 318 | + | case UsesRepository: | |
| 319 | + | if ref.Type != "GitHub" || orEmpty(ref.Name) != u.Name || orEmpty(ref.Ref) != u.Ref || orEmpty(ref.Path) != u.Path { | |
| 320 | + | t.Errorf("step %d: %+v, want %+v", i, u, ref) | |
| 321 | + | } | |
| 322 | + | } | |
| 323 | + | } | |
A action/refnames.go +113 -0
| @@ -0,0 +1,113 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "fmt" | |
| 7 | + | "unicode/utf16" | |
| 8 | + | ||
| 9 | + | "bnema.dev/ermite-actions/expr" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // refNames is a port of the runner's ReferenceNameBuilder | |
| 13 | + | // (Sdk/DTPipelines/Pipelines/ObjectTemplating/ReferenceNameBuilder.cs), which | |
| 14 | + | // validates the ids written in a composite action and makes the ones the file | |
| 15 | + | // leaves out. It is not package workflow's idBuilder: the runner's accepts ids | |
| 16 | + | // that start with "__", lets an id of 100 characters or more through, counts | |
| 17 | + | // UTF-16 code units, and words its error without the reference parser's | |
| 18 | + | // doubled "and". | |
| 19 | + | type refNames struct { | |
| 20 | + | name []uint16 | |
| 21 | + | distinct map[string]bool // OrdinalIgnoreCase folds | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | const maxNodeNameLength = 100 | |
| 25 | + | ||
| 26 | + | func newRefNames() *refNames { return &refNames{distinct: map[string]bool{}} } | |
| 27 | + | ||
| 28 | + | // add records name and reports whether it was new (HashSet.Add with | |
| 29 | + | // OrdinalIgnoreCase). A name with non-ASCII characters can get here when it has | |
| 30 | + | // 100 characters or more, and "é...é" then collides with "É...É". | |
| 31 | + | func (b *refNames) add(name string) bool { | |
| 32 | + | k := expr.OrdinalFold(name) | |
| 33 | + | if b.distinct[k] { | |
| 34 | + | return false | |
| 35 | + | } | |
| 36 | + | b.distinct[k] = true | |
| 37 | + | return true | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // appendSegment is AppendSegment. | |
| 41 | + | func (b *refNames) appendSegment(value string) { | |
| 42 | + | if value == "" { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | units := utf16.Encode([]rune(value)) | |
| 46 | + | if len(b.name) == 0 { | |
| 47 | + | first := units[0] | |
| 48 | + | legal := first >= 'a' && first <= 'z' || first >= 'A' && first <= 'Z' || first == '_' | |
| 49 | + | if !legal && (first >= '0' && first <= '9' || first == '-') { | |
| 50 | + | b.name = append(b.name, '_') | |
| 51 | + | } | |
| 52 | + | } else { | |
| 53 | + | b.name = append(b.name, '_') | |
| 54 | + | } | |
| 55 | + | for _, c := range units { | |
| 56 | + | switch { | |
| 57 | + | case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '_', c == '-': | |
| 58 | + | b.name = append(b.name, c) | |
| 59 | + | default: | |
| 60 | + | b.name = append(b.name, '_') | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // build is Build: the name made from the segments ("job" when there are none) | |
| 66 | + | // made unique with a numeric suffix. | |
| 67 | + | func (b *refNames) build() string { | |
| 68 | + | original := "job" | |
| 69 | + | if len(b.name) > 0 { | |
| 70 | + | original = string(utf16.Decode(b.name)) | |
| 71 | + | } | |
| 72 | + | for attempt := 1; ; attempt++ { | |
| 73 | + | suffix := "" | |
| 74 | + | if attempt > 1 { | |
| 75 | + | if attempt >= 1000 { | |
| 76 | + | throwf(exInvalidOp, "Unable to create a unique name") | |
| 77 | + | } | |
| 78 | + | suffix = fmt.Sprintf("_%d", attempt) | |
| 79 | + | } | |
| 80 | + | // original holds only ASCII: appendSegment replaced everything else. | |
| 81 | + | candidate := original[:min(len(original), maxNodeNameLength-len(suffix))] + suffix | |
| 82 | + | if b.add(candidate) { | |
| 83 | + | b.name = b.name[:0] | |
| 84 | + | return candidate | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // tryAddKnownName is TryAddKnownName: it returns the error text, or "". Note | |
| 90 | + | // the runner's condition: an invalid name is only refused when it is shorter | |
| 91 | + | // than 100 characters. | |
| 92 | + | func (b *refNames) tryAddKnownName(value string) string { | |
| 93 | + | if !validName(value) && utf16Len(value) < maxNodeNameLength { | |
| 94 | + | return fmt.Sprintf("The identifier '%s' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and must be less than %d characters.", value, maxNodeNameLength) | |
| 95 | + | } | |
| 96 | + | if !b.add(value) { | |
| 97 | + | return fmt.Sprintf("The identifier '%s' may not be used more than once within the same scope.", value) | |
| 98 | + | } | |
| 99 | + | return "" | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // validName is NameValidation.IsValid(name, allowHyphens: true). | |
| 103 | + | func validName(name string) bool { | |
| 104 | + | for i, c := range utf16.Encode([]rune(name)) { | |
| 105 | + | switch { | |
| 106 | + | case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c == '_': | |
| 107 | + | case i > 0 && (c >= '0' && c <= '9' || c == '-'): | |
| 108 | + | default: | |
| 109 | + | return false | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | return true | |
| 113 | + | } | |
A action/schema/SOURCE +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | action-v1.0.min.json is dist/action-v1.0.min.json of the npm package | |
| 2 | + | @actions/workflow-parser 0.3.61 (https://github.com/actions/languageservices, | |
| 3 | + | MIT, Copyright GitHub, Inc.), copied unmodified. It is the same JSON as | |
| 4 | + | workflow-parser/src/action-v1.0.json at commit | |
| 5 | + | 4043eda158e16579cc5fb1b0b07a4bce2a76f0b5 (same content, different spacing). | |
| 6 | + | Package action reads its "action-root" definition. See ../../THIRD_PARTY.md. | |
A action/schema/action-v1.0.min.json +1 -0
| @@ -0,0 +1,1 @@ | |||
| 1 | + | { "definitions": { "action-root": { "description": "Action file", "mapping": { "properties": { "name": "string", "description": "string", "inputs": "inputs", "outputs": "outputs", "runs": "runs" }, "loose-key-type": "non-empty-string", "loose-value-type": "any" } }, "action-root-strict": { "description": "GitHub Action manifest file (action.yml/action.yaml) that defines an action's metadata, inputs, outputs, and execution configuration.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions)", "mapping": { "properties": { "name": { "type": "non-empty-string", "required": true, "description": "The name of your action. GitHub displays the name in the Actions tab to help visually identify actions in each job.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#name)" }, "description": { "type": "string", "required": true, "description": "A short description of the action. GitHub displays this description in the Actions Marketplace.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#description)" }, "author": { "type": "string", "description": "The name of the action's author.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#author)" }, "inputs": "inputs-strict", "outputs": "outputs", "runs": { "type": "runs-strict", "required": true }, "branding": "branding" } } }, "inputs": { "mapping": { "loose-key-type": "non-empty-string", "loose-value-type": "input" } }, "inputs-strict": { "description": "Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Inputs ids with uppercase letters are converted to lowercase during runtime.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#inputs)", "mapping": { "loose-key-type": "non-empty-string", "loose-value-type": "input-strict" } }, "input": { "mapping": { "properties": { "default": "input-default-context" }, "loose-key-type": "non-empty-string", "loose-value-type": "any" } }, "input-strict": { "description": "An input parameter for this action.", "mapping": { "properties": { "description": { "type": "string", "description": "A string description of the input parameter.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_iddescription)" }, "required": { "type": "boolean", "description": "A boolean to indicate whether the action requires the input parameter. Set to true when the parameter is required.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_idrequired)" }, "default": { "type": "input-default-context", "description": "A string representing the default value. The default value is used when an input parameter isn't specified in a workflow file.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_iddefault)" }, "deprecationMessage": { "type": "string", "description": "A string shown to users using the deprecated input, warning them that the input is deprecated and mentioning any alternatives.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_iddeprecationmessage)" } }, "loose-key-type": "non-empty-string", "loose-value-type": "any" } }, "input-default-context": { "description": "A string representing the default value. The default value is used when an input parameter isn't specified in a workflow file.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_iddefault)", "context": ["github", "strategy", "matrix", "job", "runner", "hashFiles(1,255)"], "string": {} }, "outputs": { "description": "Output parameters allow you to declare data that an action sets. Actions that run later in a workflow can use the output data set in previously run actions.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-composite-actions)", "mapping": { "loose-key-type": "non-empty-string", "loose-value-type": "output-definition" } }, "output-definition": { "description": "An output parameter for this action.", "mapping": { "properties": { "description": { "type": "string", "description": "A string description of the output parameter.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_iddescription)" }, "value": { "type": "output-value", "description": "The value that the output parameter will be mapped to. You can set this to a string or an expression with context.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_idvalue)" } } } }, "output-value": { "description": "The value that the output parameter will be mapped to. You can set this to a string or an expression with context.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_idvalue)", "context": ["github", "strategy", "matrix", "steps", "inputs", "job", "runner", "env"], "string": {} }, "runs-if": { "description": "Condition to control when this action's pre or post script runs.", "context": ["runner", "github", "job", "strategy", "matrix", "env", "inputs", "always(0,0)", "success(0,0)", "failure(0,0)", "cancelled(0,0)", "hashFiles(1,255)"], "string": {} }, "runs": { "one-of": ["container-runs", "node-runs", "composite-runs", "plugin-runs"] }, "runs-strict": { "description": "Specifies whether this is a JavaScript action, a composite action, or a Docker container action and how the action is executed.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runs)", "one-of": ["container-runs-strict", "node-runs-strict", "composite-runs-strict"] }, "plugin-runs": { "mapping": { "properties": { "plugin": "non-empty-string" } } }, "container-runs": { "mapping": { "properties": { "using": "non-empty-string", "image": "non-empty-string", "entrypoint": "non-empty-string", "args": "container-runs-args", "env": "container-runs-env", "pre-entrypoint": "non-empty-string", "pre-if": "non-empty-string", "post-entrypoint": "non-empty-string", "post-if": "non-empty-string" } } }, "container-runs-args": { "description": "An array of strings that define the inputs for a Docker container. Inputs can include hardcoded strings. GitHub passes the args to the container's ENTRYPOINT when the container starts up.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsargs)", "sequence": { "item-type": "container-runs-context" } }, "container-runs-env": { "description": "Specifies a key/value map of environment variables to set in the container environment.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsenv)", "context": ["inputs"], "mapping": { "loose-key-type": "non-empty-string", "loose-value-type": "string" } }, "container-runs-context": { "context": ["inputs"], "string": {} }, "node-runs": { "mapping": { "properties": { "using": "non-empty-string", "main": "non-empty-string", "pre": "non-empty-string", "pre-if": "non-empty-string", "post": "non-empty-string", "post-if": "non-empty-string" } } }, "composite-runs": { "mapping": { "properties": { "using": "non-empty-string", "steps": "composite-steps" } } }, "container-runs-strict": { "description": "Configuration for Docker container actions.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runs)", "mapping": { "properties": { "using": { "type": "using", "required": true, "description": "The runtime used to execute the action. Must be docker for Docker container actions.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsusing)" }, "image": { "type": "non-empty-string", "required": true, "description": "The Docker image to use as the container to run the action. The value can be the Docker base image name, a local Dockerfile in your repository, or a public image in Docker Hub or another registry.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsimage)" }, "entrypoint": { "type": "non-empty-string", "description": "Overrides the Docker ENTRYPOINT in the Dockerfile, or sets it if one wasn't already specified.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsentrypoint)" }, "args": "container-runs-args", "env": "container-runs-env", "pre-entrypoint": { "type": "non-empty-string", "description": "Allows you to run a script before the entrypoint action begins.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspre-entrypoint)" }, "pre-if": { "type": "runs-if", "description": "Allows you to define conditions for the pre: action execution. The pre: action will only run if the conditions in pre-if are met. If not set, then pre-if defaults to always().\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspre-if)" }, "post-entrypoint": { "type": "non-empty-string", "description": "Allows you to run a cleanup script once the runs.entrypoint action has completed.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspost-entrypoint)" }, "post-if": { "type": "runs-if", "description": "Allows you to define conditions for the post: action execution. The post: action will only run if the conditions in post-if are met. If not set, then post-if defaults to always().\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspost-if)" } } } }, "node-runs-strict": { "description": "Configuration for JavaScript actions executed with Node.js.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runs)", "mapping": { "properties": { "using": { "type": "using", "required": true, "description": "The runtime used to execute the action. Use node20 or node24 for JavaScript actions.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsusing)" }, "main": { "type": "non-empty-string", "required": true, "description": "The file that contains your action code. The runtime specified in using executes this file.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsmain)" }, "pre": { "type": "non-empty-string", "description": "Allows you to run a script at the start of a job, before the main: action begins. You can use pre: to run prerequisite setup scripts.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspre)" }, "pre-if": { "type": "runs-if", "description": "Allows you to define conditions for the pre: action execution. The pre: action will only run if the conditions in pre-if are met. If not set, then pre-if defaults to always().\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspre-if)" }, "post": { "type": "non-empty-string", "description": "Allows you to run a script at the end of a job, once the main: action has completed. You can use post: to run cleanup scripts.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspost)" }, "post-if": { "type": "runs-if", "description": "Allows you to define conditions for the post: action execution. The post: action will only run if the conditions in post-if are met. If not set, then post-if defaults to always().\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runspost-if)" } } } }, "composite-runs-strict": { "description": "Configuration for composite actions that run multiple steps.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runs)", "mapping": { "properties": { "using": { "type": "using", "required": true, "description": "The runtime used to execute the action. Must be composite for composite actions.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsusing)" }, "steps": { "type": "composite-steps", "required": true } } } }, "composite-steps": { "description": "The steps that you plan to run in this action. These can be either run steps or uses steps.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runssteps)", "sequence": { "item-type": "composite-step" } }, "composite-step": { "description": "A step within a composite action.", "one-of": ["run-step", "uses-step"] }, "run-step": { "description": "Runs a command-line program using the operating system's shell.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsrun)", "mapping": { "properties": { "name": { "type": "string-steps-context", "description": "A name for your step to display on GitHub.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsname)" }, "id": { "type": "non-empty-string", "description": "A unique identifier for the step. You can use the id to reference the step in contexts.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsid)" }, "if": { "type": "step-if", "description": "You can use the if conditional to prevent a step from running unless a condition is met. You can use any supported context and expression to create a conditional.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsif)" }, "run": { "type": "string-steps-context", "required": true, "description": "The command you want to run. This can be inline or a script in your action repository.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsrun)" }, "shell": { "type": "string-steps-context", "required": true, "description": "The shell where you want to run the command. Any shell supported by the runner can be used. Required if run is set.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsshell)" }, "env": "step-env", "continue-on-error": { "type": "boolean-steps-context", "description": "Prevents the action from failing when a step fails. Set to true to allow the action to pass when this step fails.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepscontinue-on-error)" }, "working-directory": { "type": "string-steps-context", "description": "Specifies the working directory where the command is run.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsworking-directory)" } } } }, "uses-step": { "description": "Runs another action as part of a step in your action.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsuses)", "mapping": { "properties": { "name": { "type": "string-steps-context", "description": "A name for your step to display on GitHub.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsname)" }, "id": { "type": "non-empty-string", "description": "A unique identifier for the step. You can use the id to reference the step in contexts.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsid)" }, "if": { "type": "step-if", "description": "You can use the if conditional to prevent a step from running unless a condition is met. You can use any supported context and expression to create a conditional.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsif)" }, "uses": { "type": "non-empty-string", "required": true, "description": "Selects an action to run as part of a step in your action. An action is a reusable unit of code.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsuses)" }, "with": "step-with", "env": "step-env", "continue-on-error": { "type": "boolean-steps-context", "description": "Prevents the action from failing when a step fails. Set to true to allow the action to pass when this step fails.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepscontinue-on-error)" } } } }, "string-steps-context": { "context": ["github", "inputs", "strategy", "matrix", "steps", "job", "runner", "env", "hashFiles(1,255)"], "string": {} }, "boolean-steps-context": { "context": ["github", "inputs", "strategy", "matrix", "steps", "job", "runner", "env", "hashFiles(1,255)"], "boolean": {} }, "step-env": { "description": "Sets variables for steps to use in the runner environment. You can also set variables for the entire action.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsenv)", "context": ["github", "inputs", "strategy", "matrix", "steps", "job", "runner", "env", "hashFiles(1,255)"], "mapping": { "loose-key-type": "non-empty-string", "loose-value-type": "string" } }, "step-if": { "description": "You can use the if conditional to prevent a step from running unless a condition is met. You can use any supported context and expression to create a conditional.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsif)", "context": ["github", "inputs", "strategy", "matrix", "steps", "job", "runner", "env", "always(0,0)", "failure(0,0)", "cancelled(0,0)", "success(0,0)", "hashFiles(1,255)"], "string": {} }, "step-with": { "description": "A map of the input parameters defined by the action. Each input parameter is a key/value pair.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#runsstepswith)", "context": ["github", "inputs", "strategy", "matrix", "steps", "job", "runner", "env", "hashFiles(1,255)"], "mapping": { "loose-key-type": "non-empty-string", "loose-value-type": "string" } }, "branding": { "description": "You can use a color and Feather icon to create a badge to personalize and distinguish your action in GitHub Marketplace.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#branding)", "mapping": { "properties": { "icon": { "type": "branding-icon", "description": "The name of the v4.28.0 Feather icon to use.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#brandingicon)" }, "color": { "type": "branding-color", "description": "The background color of the badge.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#brandingcolor)" } } } }, "branding-icon": { "description": "The name of the v4.28.0 Feather icon to use. Brand icons are omitted as well as: coffee, columns, divide-circle, divide-square, divide, frown, hexagon, key, meh, mouse-pointer, smile, tool, x-octagon.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#brandingicon)", "allowed-values": ["activity", "airplay", "alert-circle", "alert-octagon", "alert-triangle", "align-center", "align-justify", "align-left", "align-right", "anchor", "aperture", "archive", "arrow-down-circle", "arrow-down-left", "arrow-down-right", "arrow-down", "arrow-left-circle", "arrow-left", "arrow-right-circle", "arrow-right", "arrow-up-circle", "arrow-up-left", "arrow-up-right", "arrow-up", "at-sign", "award", "bar-chart-2", "bar-chart", "battery-charging", "battery", "bell-off", "bell", "bluetooth", "bold", "book-open", "book", "bookmark", "box", "briefcase", "calendar", "camera-off", "camera", "cast", "check-circle", "check-square", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "chevrons-down", "chevrons-left", "chevrons-right", "chevrons-up", "circle", "clipboard", "clock", "cloud-drizzle", "cloud-lightning", "cloud-off", "cloud-rain", "cloud-snow", "cloud", "code", "command", "compass", "copy", "corner-down-left", "corner-down-right", "corner-left-down", "corner-left-up", "corner-right-down", "corner-right-up", "corner-up-left", "corner-up-right", "cpu", "credit-card", "crop", "crosshair", "database", "delete", "disc", "dollar-sign", "download-cloud", "download", "droplet", "edit-2", "edit-3", "edit", "external-link", "eye-off", "eye", "fast-forward", "feather", "file-minus", "file-plus", "file-text", "file", "film", "filter", "flag", "folder-minus", "folder-plus", "folder", "gift", "git-branch", "git-commit", "git-merge", "git-pull-request", "globe", "grid", "hard-drive", "hash", "headphones", "heart", "help-circle", "home", "image", "inbox", "info", "italic", "layers", "layout", "life-buoy", "link-2", "link", "list", "loader", "lock", "log-in", "log-out", "mail", "map-pin", "map", "maximize-2", "maximize", "menu", "message-circle", "message-square", "mic-off", "mic", "minimize-2", "minimize", "minus-circle", "minus-square", "minus", "monitor", "moon", "more-horizontal", "more-vertical", "move", "music", "navigation-2", "navigation", "octagon", "package", "paperclip", "pause-circle", "pause", "percent", "phone-call", "phone-forwarded", "phone-incoming", "phone-missed", "phone-off", "phone-outgoing", "phone", "pie-chart", "play-circle", "play", "plus-circle", "plus-square", "plus", "pocket", "power", "printer", "radio", "refresh-ccw", "refresh-cw", "repeat", "rewind", "rotate-ccw", "rotate-cw", "rss", "save", "scissors", "search", "send", "server", "settings", "share-2", "share", "shield-off", "shield", "shopping-bag", "shopping-cart", "shuffle", "sidebar", "skip-back", "skip-forward", "slash", "sliders", "smartphone", "speaker", "square", "star", "stop-circle", "sun", "sunrise", "sunset", "tablet", "tag", "target", "terminal", "thermometer", "thumbs-down", "thumbs-up", "toggle-left", "toggle-right", "trash-2", "trash", "trending-down", "trending-up", "triangle", "truck", "tv", "type", "umbrella", "underline", "unlock", "upload-cloud", "upload", "user-check", "user-minus", "user-plus", "user-x", "user", "users", "video-off", "video", "voicemail", "volume-1", "volume-2", "volume-x", "volume", "watch", "wifi-off", "wifi", "wind", "x-circle", "x-square", "x", "zap-off", "zap", "zoom-in", "zoom-out"] }, "branding-color": { "description": "The background color of the badge.\n\n[Documentation](https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#brandingcolor)", "allowed-values": ["white", "yellow", "blue", "green", "orange", "red", "purple", "gray-dark"] }, "using": { "description": "The runtime used to execute the action.", "allowed-values": ["docker", "node12", "node16", "node20", "node24", "composite"] }, "non-empty-string": { "string": { "require-non-empty": true } } } } | |
A action/steps.go +205 -0
| @@ -0,0 +1,205 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package action | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "bnema.dev/ermite-actions/expr" | |
| 9 | + | "bnema.dev/ermite-actions/template" | |
| 10 | + | "bnema.dev/ermite-actions/workflow" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // convertSteps is the runner's PipelineTemplateConverter.ConvertToSteps | |
| 14 | + | // (Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs). | |
| 15 | + | // It is not the conversion of package workflow, which reads the steps of a | |
| 16 | + | // workflow file: this one throws at the first token of the wrong type, keeps | |
| 17 | + | // a step whose "uses" has the wrong form (and then fails on it later if it has | |
| 18 | + | // no id), accepts ids that start with "__", and names a "$/" action | |
| 19 | + | // "selfRepository". See the runner for each of those. | |
| 20 | + | func (l *loader) convertSteps(items *template.Sequence) []*workflow.Step { | |
| 21 | + | names := newRefNames() | |
| 22 | + | var result []*workflow.Step | |
| 23 | + | var refs []*stepRef | |
| 24 | + | for _, it := range items.Items { | |
| 25 | + | s, ref := l.convertStep(it, names) | |
| 26 | + | if s != nil { | |
| 27 | + | result = append(result, s) | |
| 28 | + | refs = append(refs, ref) | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | for i, s := range result { | |
| 32 | + | if s.ID != "" { | |
| 33 | + | continue | |
| 34 | + | } | |
| 35 | + | ref := refs[i] | |
| 36 | + | if ref == nil { | |
| 37 | + | // The runner dereferences the Reference of a step whose "uses" had | |
| 38 | + | // the wrong form. | |
| 39 | + | throwf(exNullRef, "Object reference not set to an instance of an object.") | |
| 40 | + | } | |
| 41 | + | name := ref.name | |
| 42 | + | if name == "" { | |
| 43 | + | name = "run" | |
| 44 | + | } | |
| 45 | + | names.appendSegment("__" + name) | |
| 46 | + | s.ID = names.build() | |
| 47 | + | } | |
| 48 | + | return result | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // stepRef is what the runner makes of a step's action reference that decides | |
| 52 | + | // the generated id: the image of a docker:// action, "owner/repo", "self" for | |
| 53 | + | // a local action, "selfRepository" for a $/ action, "" for a run step. | |
| 54 | + | type stepRef struct{ name string } | |
| 55 | + | ||
| 56 | + | func (l *loader) convertStep(t template.Token, names *refNames) (*workflow.Step, *stepRef) { | |
| 57 | + | m := assertMapping(t, "steps item") | |
| 58 | + | step := &workflow.Step{Pos: t.Pos()} | |
| 59 | + | var run template.Token | |
| 60 | + | var ifToken template.Token | |
| 61 | + | var uses *template.String | |
| 62 | + | for _, p := range m.Pairs { | |
| 63 | + | key := assertString(p.Key, "steps item key") | |
| 64 | + | v := p.Value | |
| 65 | + | switch key.Value { | |
| 66 | + | case "continue-on-error": | |
| 67 | + | if !template.IsExpression(v) { | |
| 68 | + | assertBoolean(v, "step continue-on-error") | |
| 69 | + | } | |
| 70 | + | step.ContinueOnError = assertScalar(v, "steps continue-on-error") | |
| 71 | + | case "env": | |
| 72 | + | if !template.IsExpression(v) { | |
| 73 | + | for _, kv := range assertMapping(v, "environment").Pairs { | |
| 74 | + | if template.IsExpression(kv.Key) { | |
| 75 | + | continue | |
| 76 | + | } | |
| 77 | + | assertString(kv.Key, "environment key") | |
| 78 | + | if template.IsExpression(kv.Value) { | |
| 79 | + | continue | |
| 80 | + | } | |
| 81 | + | assertString(kv.Value, "environment value") | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | if em, ok := v.(*template.Mapping); ok { | |
| 85 | + | step.Env = em | |
| 86 | + | } else { | |
| 87 | + | step.EnvExpression = v | |
| 88 | + | } | |
| 89 | + | case "id": | |
| 90 | + | id := assertString(v, "steps item id") | |
| 91 | + | if id.Value != "" { | |
| 92 | + | if msg := names.tryAddKnownName(id.Value); msg != "" { | |
| 93 | + | l.ctx.ErrorAt(id, msg) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | step.ID, step.Explicit = id.Value, id.Value != "" | |
| 97 | + | case "if": | |
| 98 | + | ifToken = assertScalar(v, "steps item if") | |
| 99 | + | case "name": | |
| 100 | + | step.Name = assertScalar(v, "steps item name") | |
| 101 | + | case "run": | |
| 102 | + | run = assertScalar(v, "steps item run") | |
| 103 | + | case "shell": | |
| 104 | + | step.Shell = assertScalar(v, "steps item shell") | |
| 105 | + | case "timeout-minutes": | |
| 106 | + | if !template.IsExpression(v) { | |
| 107 | + | if _, ok := v.(*template.Number); !ok { | |
| 108 | + | assertFail(v, "step timeout-minutes", "NumberToken") | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | step.TimeoutMinutes = assertScalar(v, "steps item timeout-minutes") | |
| 112 | + | case "uses": | |
| 113 | + | uses = assertString(v, "steps item uses") | |
| 114 | + | case "with": | |
| 115 | + | if !template.IsExpression(v) { | |
| 116 | + | for _, kv := range assertMapping(v, "inputs").Pairs { | |
| 117 | + | if template.IsExpression(kv.Key) { | |
| 118 | + | continue | |
| 119 | + | } | |
| 120 | + | assertString(kv.Key, "inputs key") | |
| 121 | + | if template.IsExpression(kv.Value) { | |
| 122 | + | continue | |
| 123 | + | } | |
| 124 | + | assertString(kv.Value, "inputs value") | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | if wm, ok := v.(*template.Mapping); ok { | |
| 128 | + | step.With = wm | |
| 129 | + | } else { | |
| 130 | + | step.WithExpression = v | |
| 131 | + | } | |
| 132 | + | case "working-directory": | |
| 133 | + | step.WorkingDirectory = assertScalar(v, "steps item working-directory") | |
| 134 | + | case "clean", "fetch-depth", "lfs", "path", "submodules": | |
| 135 | + | assertScalar(v, "steps item "+key.Value) | |
| 136 | + | default: | |
| 137 | + | throwf(exArgument, "Error while reading 'steps item key'. Unexpected value '%s'", key.Value) | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | step.If = l.convertIf(ifToken) | |
| 141 | + | ||
| 142 | + | if run != nil { | |
| 143 | + | step.Run = run | |
| 144 | + | return step, &stepRef{} | |
| 145 | + | } | |
| 146 | + | if uses == nil { | |
| 147 | + | assertFail(nil, "steps item uses", "StringToken") | |
| 148 | + | } | |
| 149 | + | step.Uses = uses | |
| 150 | + | u, err := ParseUses(uses.Value) | |
| 151 | + | if err != nil { | |
| 152 | + | l.ctx.ErrorAt(uses, err.Error()) | |
| 153 | + | if strings.HasPrefix(uses.Value, "$/") { | |
| 154 | + | // A $/ value with a ref drops the step. | |
| 155 | + | return nil, nil | |
| 156 | + | } | |
| 157 | + | return step, nil | |
| 158 | + | } | |
| 159 | + | ref := &stepRef{name: "self"} | |
| 160 | + | switch u.Kind { | |
| 161 | + | case UsesDocker: | |
| 162 | + | ref.name = u.Image | |
| 163 | + | case UsesRepository: | |
| 164 | + | ref.name = u.Name | |
| 165 | + | case UsesSelfRepository: | |
| 166 | + | ref.name = "selfRepository" | |
| 167 | + | } | |
| 168 | + | return step, ref | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // stepIfContexts are the contexts the runner's converter grants the "if" of a | |
| 172 | + | // step on top of the ones the schema's step-if gives (which lacks vars and | |
| 173 | + | // needs): s_stepNamedValues. | |
| 174 | + | var stepIfContexts = []string{"vars", "needs"} | |
| 175 | + | ||
| 176 | + | // convertIf is ConvertToIfCondition for a step: a condition without a status | |
| 177 | + | // function is wrapped as "success() && (cond)", an empty one is "success()". | |
| 178 | + | func (l *loader) convertIf(tok template.Token) *template.BasicExpression { | |
| 179 | + | def := l.schema.Info("step-if").WithContexts(stepIfContexts...) | |
| 180 | + | var condition string | |
| 181 | + | switch s := tok.(type) { | |
| 182 | + | case nil: | |
| 183 | + | case *template.BasicExpression: | |
| 184 | + | condition = s.Expression | |
| 185 | + | default: | |
| 186 | + | condition = assertString(tok, "step if").Value | |
| 187 | + | } | |
| 188 | + | final := "success()" | |
| 189 | + | if !isNullOrWhiteSpace(condition) { | |
| 190 | + | node, err := expr.Parse(condition, def.NamedContexts(), def.Functions()) | |
| 191 | + | if err != nil { | |
| 192 | + | l.ctx.ErrorAt(tok, err.Error()) | |
| 193 | + | return nil | |
| 194 | + | } | |
| 195 | + | final = condition | |
| 196 | + | if node != nil && !workflow.HasStatusFunction(node) { | |
| 197 | + | final = "success() && (" + condition + ")" | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | var o template.Origin | |
| 201 | + | if tok != nil { | |
| 202 | + | o = template.Origin{File: tok.File(), Pos: tok.Pos()} | |
| 203 | + | } | |
| 204 | + | return template.NewExpression(o, final, def) | |
| 205 | + | } | |
A action/testdata/SOURCE +22 -0
| @@ -0,0 +1,22 @@ | |||
| 1 | + | cases/runner_*.yml are copied from actions/runner (MIT, Copyright (c) GitHub, Inc., | |
| 2 | + | https://github.com/actions/runner), src/Test/TestData, at commit | |
| 3 | + | 5f0b3ddcc6ee9fe9441e00d55e85b94ea4aad798 (the self_repository_* files, the | |
| 4 | + | docker and node action manifests, the conditional composite action). | |
| 5 | + | ||
| 6 | + | cases/ls_parser_*.yml and cases/ls_validate_*.yml are the manifests written | |
| 7 | + | inline in the tests of actions/languageservices (MIT, Copyright GitHub, Inc.), | |
| 8 | + | commit 4043eda158e16579cc5fb1b0b07a4bce2a76f0b5: | |
| 9 | + | workflow-parser/src/actions/action-parser.test.ts and | |
| 10 | + | languageservice/src/validate-action.test.ts. Each file name is the test title. | |
| 11 | + | ||
| 12 | + | The other files of cases/ are written for this repository. uses.txt too. | |
| 13 | + | ||
| 14 | + | runner.json and uses.json are the output of the C# runner's own action.yml | |
| 15 | + | loader (actions/runner commit above) on the files of cases/ and uses.txt, run | |
| 16 | + | by ../gen/runner (Driver.cs is derived from Runner.Worker/ | |
| 17 | + | ActionManifestManagerLegacy.cs, MIT, Copyright (c) GitHub, Inc.). | |
| 18 | + | ||
| 19 | + | strict.json is the output of @actions/workflow-parser 0.3.61 (MIT, Copyright | |
| 20 | + | GitHub, Inc.) on the files of cases/, generated by ../gen/strict.mjs. | |
| 21 | + | ||
| 22 | + | See ../../THIRD_PARTY.md. Regenerate with ../gen/regen.sh. | |
A action/testdata/cases/branding_bad_icon.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | branding: | |
| 4 | + | icon: not-an-icon | |
| 5 | + | color: pink | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/composite_all_keys.yml +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | who: | |
| 5 | + | default: world | |
| 6 | + | outputs: | |
| 7 | + | out: | |
| 8 | + | description: the out | |
| 9 | + | value: ${{ steps.a.outputs.r }} | |
| 10 | + | runs: | |
| 11 | + | using: composite | |
| 12 | + | steps: | |
| 13 | + | - id: a | |
| 14 | + | name: Say ${{ inputs.who }} | |
| 15 | + | if: always() && inputs.who != '' | |
| 16 | + | run: echo "r=1" >> $GITHUB_OUTPUT | |
| 17 | + | shell: bash | |
| 18 | + | working-directory: ${{ github.workspace }} | |
| 19 | + | env: | |
| 20 | + | X: ${{ inputs.who }} | |
| 21 | + | continue-on-error: true | |
| 22 | + | - uses: actions/checkout@v4 | |
| 23 | + | with: | |
| 24 | + | fetch-depth: 0 | |
| 25 | + | ref: ${{ github.sha }} | |
| 26 | + | env: | |
| 27 | + | E: 1 | |
| 28 | + | continue-on-error: ${{ inputs.who == 'x' }} | |
| 29 | + | if: ${{ failure() }} | |
A action/testdata/cases/composite_basic.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo hi | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_continue_expr.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | continue-on-error: ${{ inputs.x == 'y' }} | |
| 9 | + | env: ${{ fromJSON(inputs.env) }} | |
| 10 | + | - uses: a/b@c | |
| 11 | + | with: ${{ fromJSON(inputs.with) }} | |
A action/testdata/cases/composite_continue_str.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | continue-on-error: 'yes' | |
A action/testdata/cases/composite_continue_true_str.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | continue-on-error: 'true' | |
A action/testdata/cases/composite_env_expr_mapping.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | env: ${{ fromJSON('{}') }} | |
A action/testdata/cases/composite_env_scalar.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | env: x | |
A action/testdata/cases/composite_expr_bad_function.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ nope(1) }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_empty.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_hashfiles.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ hashFiles('**/go.sum') }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_insert.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: ${{ insert }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_needs_in_run.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ needs.a.result }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_secrets.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ secrets.X }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_status_in_run.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ success() }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_unclosed.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ github.ref | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_expr_vars_in_run.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ vars.A }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_id_dup.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | id: a | |
| 9 | + | - run: echo | |
| 10 | + | shell: bash | |
| 11 | + | id: A | |
A action/testdata/cases/composite_id_empty.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | id: '' | |
A action/testdata/cases/composite_id_hyphen_first.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | id: -a | |
A action/testdata/cases/composite_id_invalid.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | id: 1a | |
A action/testdata/cases/composite_id_long.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | id: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | |
A action/testdata/cases/composite_id_reserved.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | id: __run | |
A action/testdata/cases/composite_if_bad.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: foo == 1 | |
A action/testdata/cases/composite_if_bool.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: false | |
A action/testdata/cases/composite_if_empty.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: '' | |
A action/testdata/cases/composite_if_literal_text.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: foo ${{ github.ref }} | |
A action/testdata/cases/composite_if_marker_vars.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: ${{ vars.x == 'y' }} | |
A action/testdata/cases/composite_if_needs.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: needs.a.result == 'success' | |
A action/testdata/cases/composite_if_syntax.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: 1 == | |
A action/testdata/cases/composite_if_vars.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | if: vars.x == 'y' | |
A action/testdata/cases/composite_name_only.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - name: x | |
A action/testdata/cases/composite_no_steps.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
A action/testdata/cases/composite_outputs_bad_context.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | outputs: | |
| 4 | + | a: | |
| 5 | + | value: ${{ secrets.x }} | |
| 6 | + | runs: | |
| 7 | + | using: composite | |
| 8 | + | steps: [] | |
A action/testdata/cases/composite_outputs_needs.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | outputs: | |
| 4 | + | a: | |
| 5 | + | value: ${{ needs.x }} | |
| 6 | + | runs: | |
| 7 | + | using: composite | |
| 8 | + | steps: [] | |
A action/testdata/cases/composite_outputs_value.yml +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | outputs: | |
| 4 | + | a: | |
| 5 | + | value: ${{ steps.x.outputs.y }} | |
| 6 | + | b: | |
| 7 | + | description: only desc | |
| 8 | + | c: | |
| 9 | + | value: plain | |
| 10 | + | runs: | |
| 11 | + | using: composite | |
| 12 | + | steps: | |
| 13 | + | - run: echo | |
| 14 | + | shell: bash | |
| 15 | + | id: x | |
A action/testdata/cases/composite_run_and_uses.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | uses: a/b@c | |
A action/testdata/cases/composite_run_no_shell.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
A action/testdata/cases/composite_shell_only.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - shell: bash | |
A action/testdata/cases/composite_step_background.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | background: true | |
A action/testdata/cases/composite_step_ids_generated.yml +16 -0
| @@ -0,0 +1,16 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: a | |
| 7 | + | shell: bash | |
| 8 | + | - run: b | |
| 9 | + | shell: bash | |
| 10 | + | - uses: actions/setup-go@v5 | |
| 11 | + | - uses: docker://ghcr.io/a/b:1 | |
| 12 | + | - uses: ./x | |
| 13 | + | - uses: actions/setup-go@v5 | |
| 14 | + | - run: c | |
| 15 | + | shell: bash | |
| 16 | + | id: __run_2 | |
A action/testdata/cases/composite_step_not_mapping.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - echo | |
A action/testdata/cases/composite_step_null.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - | |
A action/testdata/cases/composite_step_shell_on_uses.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: a/b@c | |
| 7 | + | shell: bash | |
A action/testdata/cases/composite_step_unknown_key.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | timeout-minutes: 5 | |
A action/testdata/cases/composite_step_with_on_run.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo | |
| 7 | + | shell: bash | |
| 8 | + | with: | |
| 9 | + | a: b | |
A action/testdata/cases/composite_steps_empty.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: [] | |
A action/testdata/cases/composite_steps_scalar.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: x | |
A action/testdata/cases/composite_upper.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: Composite | |
| 5 | + | steps: [] | |
A action/testdata/cases/composite_uses_bad_emptyref.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@ | |
| 7 | + | id: x | |
A action/testdata/cases/composite_uses_bad_noat.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout | |
| 7 | + | id: x | |
A action/testdata/cases/composite_uses_bad_noat_noid.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout | |
A action/testdata/cases/composite_uses_bad_norepo.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: checkout@v4 | |
| 7 | + | id: x | |
A action/testdata/cases/composite_uses_bad_two_at.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: a/b@c@d | |
| 7 | + | id: x | |
A action/testdata/cases/composite_uses_empty.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: '' | |
A action/testdata/cases/composite_uses_forms.yml +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@v4 | |
| 7 | + | - uses: actions/aws/ec2@main | |
| 8 | + | - uses: ./local/action | |
| 9 | + | - uses: docker://alpine:3 | |
| 10 | + | - uses: '.\\win\\action' | |
| 11 | + | - uses: $/self | |
| 12 | + | - uses: owner/repo/a/b/c@v1.2 | |
| 13 | + | - uses: actions/checkout@v4 | |
| 14 | + | - run: echo | |
| 15 | + | shell: sh | |
| 16 | + | - run: echo | |
| 17 | + | shell: sh | |
A action/testdata/cases/composite_uses_self_ref.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: $/foo@v1 | |
A action/testdata/cases/composite_uses_self_ref_id.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: $/foo@v1 | |
| 7 | + | id: x | |
A action/testdata/cases/composite_with_nonstring.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: a/b@c | |
| 7 | + | with: | |
| 8 | + | n: 1 | |
| 9 | + | b: true | |
| 10 | + | z: | |
| 11 | + | l: [1] | |
A action/testdata/cases/composite_with_scalar.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: a/b@c | |
| 7 | + | with: x | |
A action/testdata/cases/depth_exceeded.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | x: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] | |
| 4 | + | runs: | |
| 5 | + | using: node20 | |
| 6 | + | main: m.js | |
A action/testdata/cases/depth_exceeded_in_skipped.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
| 6 | + | weird: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] | |
A action/testdata/cases/depth_ok.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | x: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] | |
| 4 | + | runs: | |
| 5 | + | using: node20 | |
| 6 | + | main: m.js | |
A action/testdata/cases/depth_seq_100.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | x: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] | |
| 2 | + | runs: | |
| 3 | + | using: node20 | |
| 4 | + | main: m.js | |
A action/testdata/cases/depth_seq_98.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | x: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] | |
| 2 | + | runs: | |
| 3 | + | using: node20 | |
| 4 | + | main: m.js | |
A action/testdata/cases/depth_seq_99.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | x: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] | |
| 2 | + | runs: | |
| 3 | + | using: node20 | |
| 4 | + | main: m.js | |
A action/testdata/cases/description_expr.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: ${{ x }} | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
A action/testdata/cases/docker_args_context_github.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | args: | |
| 7 | + | - ${{ github.ref }} | |
A action/testdata/cases/docker_args_scalar.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | args: abc | |
A action/testdata/cases/docker_dockerfile.yml +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | args: | |
| 7 | + | - a | |
| 8 | + | - ${{ inputs.x }} | |
| 9 | + | entrypoint: main.sh | |
| 10 | + | env: | |
| 11 | + | A: b | |
| 12 | + | C: ${{ inputs.x }} | |
| 13 | + | pre-entrypoint: pre.sh | |
| 14 | + | pre-if: success() | |
| 15 | + | post-entrypoint: post.sh | |
| 16 | + | post-if: failure() | |
| 17 | + | inputs: | |
| 18 | + | x: | |
| 19 | + | default: 'y' | |
A action/testdata/cases/docker_env_context_github.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | env: | |
| 7 | + | A: ${{ github.ref }} | |
A action/testdata/cases/docker_env_list.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | env: | |
| 7 | + | - A | |
A action/testdata/cases/docker_env_number.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | env: | |
| 7 | + | A: 1 | |
| 8 | + | B: true | |
A action/testdata/cases/docker_image.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: docker://alpine:3.19 | |
A action/testdata/cases/docker_no_image.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | entrypoint: x | |
A action/testdata/cases/docker_upper.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: DOCKER | |
| 5 | + | image: Dockerfile | |
A action/testdata/cases/docker_with_main.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | main: x.js | |
A action/testdata/cases/emoji_uses.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: 😀/b@v1 | |
A action/testdata/cases/empty_file.yml +0 -0
no textual change.
A action/testdata/cases/fold_inputs_accent.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | é: | |
| 5 | + | default: x | |
| 6 | + | É: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/fold_inputs_dotless.yml +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | ı: | |
| 5 | + | default: x | |
| 6 | + | I: | |
| 7 | + | default: y | |
| 8 | + | i: | |
| 9 | + | default: z | |
| 10 | + | runs: | |
| 11 | + | using: node20 | |
| 12 | + | main: m.js | |
A action/testdata/cases/fold_inputs_greek.yml +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | σ: | |
| 5 | + | default: x | |
| 6 | + | ς: | |
| 7 | + | default: y | |
| 8 | + | Σ: | |
| 9 | + | default: z | |
| 10 | + | runs: | |
| 11 | + | using: node20 | |
| 12 | + | main: m.js | |
A action/testdata/cases/fold_inputs_kelvin.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | k: | |
| 5 | + | default: x | |
| 6 | + | K: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/fold_inputs_s.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | s: | |
| 5 | + | default: x | |
| 6 | + | ſ: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/fold_inputs_sharp.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | ß: | |
| 5 | + | default: x | |
| 6 | + | SS: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/fold_runs_keys.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
| 6 | + | é: 1 | |
| 7 | + | É: 2 | |
A action/testdata/cases/fold_step_ids_ascii_dup.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: x | |
| 7 | + | shell: sh | |
| 8 | + | id: abc | |
| 9 | + | - run: x | |
| 10 | + | shell: sh | |
| 11 | + | id: ABC | |
A action/testdata/cases/fold_step_ids_long.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: x | |
| 7 | + | shell: sh | |
| 8 | + | id: éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé | |
| 9 | + | - run: x | |
| 10 | + | shell: sh | |
| 11 | + | id: ÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉ | |
A action/testdata/cases/fold_step_ids_long_s.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: x | |
| 7 | + | shell: sh | |
| 8 | + | id: ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss | |
| 9 | + | - run: x | |
| 10 | + | shell: sh | |
| 11 | + | id: ſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſſ | |
A action/testdata/cases/fold_top_keys.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | NAME: B | |
| 3 | + | é: 1 | |
| 4 | + | É: 2 | |
| 5 | + | description: D | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/fold_using_dotless.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: DOCKER | |
| 5 | + | image: Dockerfile | |
A action/testdata/cases/fold_using_kelvin.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: Komposite | |
| 5 | + | steps: [] | |
A action/testdata/cases/inputs_default_bad_context.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: ${{ inputs.b }} | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/inputs_default_hashfiles.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: ${{ hashFiles('x') }} | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/inputs_default_list.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: [1] | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/inputs_default_secrets.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: ${{ secrets.b }} | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/inputs_deprecation_nonstring.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | deprecationMessage: [x] | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/inputs_dup_deprecated_case.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | deprecationMessage: x | |
| 6 | + | DeprecationMessage: y | |
| 7 | + | runs: | |
| 8 | + | using: node20 | |
| 9 | + | main: m.js | |
A action/testdata/cases/inputs_dup_names_case.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1 | |
| 6 | + | A: | |
| 7 | + | default: 2 | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/inputs_empty_key.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | '': | |
| 5 | + | default: x | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/inputs_list.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | - a | |
| 5 | + | runs: | |
| 6 | + | using: node20 | |
| 7 | + | main: m.js | |
A action/testdata/cases/inputs_scalar_value.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: x | |
| 5 | + | runs: | |
| 6 | + | using: node20 | |
| 7 | + | main: m.js | |
A action/testdata/cases/inputs_variants.yml +31 -0
| @@ -0,0 +1,31 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | plain: | |
| 5 | + | with-default: | |
| 6 | + | default: x | |
| 7 | + | expr-default: | |
| 8 | + | default: ${{ github.ref }} | |
| 9 | + | required-bool: | |
| 10 | + | required: true | |
| 11 | + | description: desc | |
| 12 | + | required-str: | |
| 13 | + | required: 'true' | |
| 14 | + | deprecated: | |
| 15 | + | deprecationMessage: use other | |
| 16 | + | default: '' | |
| 17 | + | Cap: | |
| 18 | + | default: y | |
| 19 | + | upper-key: | |
| 20 | + | DEFAULT: z | |
| 21 | + | number-default: | |
| 22 | + | default: 5 | |
| 23 | + | bool-default: | |
| 24 | + | default: false | |
| 25 | + | null-default: | |
| 26 | + | default: | |
| 27 | + | unknown: | |
| 28 | + | anything: [1, 2] | |
| 29 | + | runs: | |
| 30 | + | using: node20 | |
| 31 | + | main: m.js | |
A action/testdata/cases/inputs_variants_ok.yml +31 -0
| @@ -0,0 +1,31 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | with-default: | |
| 5 | + | default: x | |
| 6 | + | expr-default: | |
| 7 | + | default: ${{ github.ref }} | |
| 8 | + | required-bool: | |
| 9 | + | required: true | |
| 10 | + | description: desc | |
| 11 | + | required-str: | |
| 12 | + | required: 'true' | |
| 13 | + | deprecated: | |
| 14 | + | deprecationMessage: use other | |
| 15 | + | default: '' | |
| 16 | + | Cap: | |
| 17 | + | default: y | |
| 18 | + | upper-key: | |
| 19 | + | DEFAULT: z | |
| 20 | + | number-default: | |
| 21 | + | default: 5 | |
| 22 | + | bool-default: | |
| 23 | + | default: false | |
| 24 | + | null-default: | |
| 25 | + | default: | |
| 26 | + | unknown: | |
| 27 | + | anything: [1, 2] | |
| 28 | + | empty-mapping: {} | |
| 29 | + | runs: | |
| 30 | + | using: node20 | |
| 31 | + | main: m.js | |
A action/testdata/cases/int32_00.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0x7FFFFFFF | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_01.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0x80000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_02.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0xFFFFFFFF | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_03.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0x100000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_04.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0o17777777777 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_05.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0o20000000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_06.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0o37777777777 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_07.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0o40000000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_08.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!int 0xFFFFFFFF | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_09.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!int 0x80000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_10.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!int 0x100000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_11.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!int 0o20000000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_12.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!int 0o40000000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_13.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!int 12 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_14.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: !!float 0x10 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_15.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0x0 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_16.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0xDEADBEEF | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/int32_key.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | 0xFFFFFFFF: | |
| 5 | + | default: x | |
| 6 | + | 0x80000000: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/key_bool_dup.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | TRUE: | |
| 5 | + | default: x | |
| 6 | + | true: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/key_float_dup.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | 1.0: | |
| 5 | + | default: x | |
| 6 | + | '1': | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/key_hex_dup.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | 0x10: | |
| 5 | + | default: x | |
| 6 | + | 16: | |
| 7 | + | default: y | |
| 8 | + | runs: | |
| 9 | + | using: node20 | |
| 10 | + | main: m.js | |
A action/testdata/cases/key_hex_input.yml +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | 0x10: | |
| 5 | + | default: x | |
| 6 | + | 1.0: | |
| 7 | + | default: y | |
| 8 | + | TRUE: | |
| 9 | + | default: z | |
| 10 | + | runs: | |
| 11 | + | using: node20 | |
| 12 | + | main: m.js | |
A action/testdata/cases/key_null_env.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | env: | |
| 7 | + | ~: v | |
A action/testdata/cases/key_null_inputs.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | ~: | |
| 5 | + | default: x | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/key_null_top.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | ~: 1 | |
| 2 | + | name: A | |
| 3 | + | description: D | |
| 4 | + | runs: | |
| 5 | + | using: node20 | |
| 6 | + | main: m.js | |
A action/testdata/cases/key_number_runs.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
| 6 | + | 1e3: x | |
A action/testdata/cases/ls_parser_accepts_valid_context_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: Node Action | |
| 2 | + | description: A node action | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: dist/index.js | |
| 6 | + | pre: dist/setup.js | |
| 7 | + | pre-if: runner.os == 'Linux'⏎̸ | |
A action/testdata/cases/ls_parser_converts_a_composite_action.yml +22 -0
| @@ -0,0 +1,22 @@ | |||
| 1 | + | name: Composite Action | |
| 2 | + | description: A composite action | |
| 3 | + | author: Test Author | |
| 4 | + | inputs: | |
| 5 | + | name: | |
| 6 | + | description: The name | |
| 7 | + | required: true | |
| 8 | + | default: World | |
| 9 | + | outputs: | |
| 10 | + | result: | |
| 11 | + | description: The result | |
| 12 | + | value: ${{ steps.main.outputs.result }} | |
| 13 | + | runs: | |
| 14 | + | using: composite | |
| 15 | + | steps: | |
| 16 | + | - id: main | |
| 17 | + | name: Main step | |
| 18 | + | run: echo Hello ${{ inputs.name }} | |
| 19 | + | shell: bash | |
| 20 | + | branding: | |
| 21 | + | icon: star | |
| 22 | + | color: green⏎̸ | |
A action/testdata/cases/ls_parser_converts_a_docker_action.yml +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | name: Docker Action | |
| 2 | + | description: A docker action | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | entrypoint: /entrypoint.sh | |
| 7 | + | args: | |
| 8 | + | - --name | |
| 9 | + | - ${{ inputs.name }} | |
| 10 | + | env: | |
| 11 | + | DEBUG: "true"⏎̸ | |
A action/testdata/cases/ls_parser_converts_a_node_action.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: Node Action | |
| 2 | + | description: A node action | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: dist/index.js | |
| 6 | + | pre: dist/setup.js | |
| 7 | + | pre-if: runner.os == 'Linux' | |
| 8 | + | post: dist/cleanup.js | |
| 9 | + | post-if: always()⏎̸ | |
A action/testdata/cases/ls_parser_converts_uses_steps_in_composite_action.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: Composite with Uses | |
| 2 | + | description: Composite action with uses steps | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@v4 | |
| 7 | + | with: | |
| 8 | + | fetch-depth: 0⏎̸ | |
A action/testdata/cases/ls_parser_parses_a_docker_action.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: Docker Action | |
| 2 | + | description: A Docker action | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | args: | |
| 7 | + | - ${{ inputs.name }} | |
| 8 | + | env: | |
| 9 | + | DEBUG: "true"⏎̸ | |
A action/testdata/cases/ls_parser_parses_a_javascript_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: JS Action | |
| 2 | + | description: A JavaScript action | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: dist/index.js | |
| 6 | + | pre: dist/setup.js | |
| 7 | + | post: dist/cleanup.js⏎̸ | |
A action/testdata/cases/ls_parser_parses_a_minimal_action_yml.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: A simple action | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo Hello | |
| 7 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_parses_action_with_branding.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: Branded Action | |
| 2 | + | description: Action with branding | |
| 3 | + | branding: | |
| 4 | + | icon: award | |
| 5 | + | color: blue | |
| 6 | + | runs: | |
| 7 | + | using: composite | |
| 8 | + | steps: | |
| 9 | + | - run: echo Hello | |
| 10 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_parses_action_with_inputs_and_outputs.yml +20 -0
| @@ -0,0 +1,20 @@ | |||
| 1 | + | name: Action with I/O | |
| 2 | + | description: Action with inputs and outputs | |
| 3 | + | inputs: | |
| 4 | + | name: | |
| 5 | + | description: The name to greet | |
| 6 | + | required: true | |
| 7 | + | default: World | |
| 8 | + | verbose: | |
| 9 | + | description: Enable verbose mode | |
| 10 | + | required: false | |
| 11 | + | outputs: | |
| 12 | + | greeting: | |
| 13 | + | description: The greeting message | |
| 14 | + | value: ${{ steps.greet.outputs.message }} | |
| 15 | + | runs: | |
| 16 | + | using: composite | |
| 17 | + | steps: | |
| 18 | + | - id: greet | |
| 19 | + | run: echo "::set-output name=message::Hello ${{ inputs.name }}" | |
| 20 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_reports_error_for_invalid_context_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: Node Action | |
| 2 | + | description: A node action | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: dist/index.js | |
| 6 | + | pre: dist/setup.js | |
| 7 | + | pre-if: foo == bar⏎̸ | |
A action/testdata/cases/ls_parser_reports_error_for_invalid_yaml.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: Invalid Action | |
| 2 | + | description: Action with bad YAML | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - name: 'Hello ${{ fromJSON('test') }}' | |
| 7 | + | run: echo test | |
| 8 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_validates_branding_color_values.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: Bad Color | |
| 2 | + | description: Action with invalid branding color | |
| 3 | + | branding: | |
| 4 | + | icon: award | |
| 5 | + | color: pink | |
| 6 | + | runs: | |
| 7 | + | using: composite | |
| 8 | + | steps: | |
| 9 | + | - run: echo Hello | |
| 10 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_validates_branding_icon_values.yml +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | name: Bad Icon | |
| 2 | + | description: Action with invalid branding icon | |
| 3 | + | branding: | |
| 4 | + | icon: invalid-icon-name | |
| 5 | + | color: blue | |
| 6 | + | runs: | |
| 7 | + | using: composite | |
| 8 | + | steps: | |
| 9 | + | - run: echo Hello | |
| 10 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_validates_required_fields.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | runs: | |
| 2 | + | using: composite | |
| 3 | + | steps: | |
| 4 | + | - run: echo Hello | |
| 5 | + | shell: bash⏎̸ | |
A action/testdata/cases/ls_parser_validates_shell_is_required_for_run_steps.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: Missing Shell | |
| 2 | + | description: Action without shell in run step | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo Hello⏎̸ | |
A action/testdata/cases/ls_validate_allows_expression_with_cancelled_in_post_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: cancelled() | |
A action/testdata/cases/ls_validate_allows_expression_with_failure_in_post_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: failure() | |
A action/testdata/cases/ls_validate_allows_format_with_only_replacement_tokens.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format with only tokens | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: ${{ format('{0}', github.event_name) }} | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_full_sha.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Full SHA | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 | |
A action/testdata/cases/ls_validate_allows_hashfiles_function_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: hashFiles in if | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: hashFiles('**/package-lock.json') != '' | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_hashfiles_function_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: hashFiles in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: hashFiles('**/package-lock.json') != '' | |
A action/testdata/cases/ls_validate_allows_if_without_expression_markers_auto_wrapped.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: If without markers | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: github.event_name == 'push' | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_status_functions_in_post_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Status functions in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: always() || failure() | |
A action/testdata/cases/ls_validate_allows_success_failure_always_cancelled_functions_in_composi.yml +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Status functions in if | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: success() && !cancelled() | |
| 7 | + | run: echo success | |
| 8 | + | shell: bash | |
| 9 | + | - if: failure() | |
| 10 | + | run: echo failure | |
| 11 | + | shell: bash | |
| 12 | + | - if: always() | |
| 13 | + | run: echo always | |
| 14 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_success_function.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Success function | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: success() | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_valid_contexts_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid context in if | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: github.event_name == 'push' | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_valid_contexts_in_post_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid context in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: runner.os == 'Linux' | |
A action/testdata/cases/ls_validate_allows_valid_contexts_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid context in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: runner.os == 'Linux' | |
A action/testdata/cases/ls_validate_allows_valid_expression_in_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid if expression | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: ${{ github.event_name == 'push' }} | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_valid_expression_in_post_if_for_node_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: always() | |
A action/testdata/cases/ls_validate_allows_valid_expression_in_pre_if_for_node_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: success() | |
A action/testdata/cases/ls_validate_allows_valid_format_call_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid format | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: format('{0} {1}', 'a', 'b') == 'a b' | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_allows_valid_format_call_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Valid format in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: format('{0}', runner.os) == 'Linux' | |
A action/testdata/cases/ls_validate_errors_on_explicit_expression_syntax_in_post_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Explicit expression in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: ${{ always() }} | |
A action/testdata/cases/ls_validate_errors_on_explicit_expression_syntax_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Explicit expression in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: ${{ runner.os == 'Windows' }} | |
A action/testdata/cases/ls_validate_errors_on_explicit_expression_with_literal_text_in_post_if_f.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Literal text in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: event == ${{ github.event_name }} | |
A action/testdata/cases/ls_validate_errors_on_explicit_expression_with_literal_text_in_post_if_f_2.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Literal text in post-if | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | post-entrypoint: /cleanup.sh | |
| 7 | + | post-if: event == ${{ github.event_name }} | |
A action/testdata/cases/ls_validate_errors_on_explicit_expression_with_literal_text_in_pre_if_fo.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Literal text in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: push == ${{ github.event_name }} | |
A action/testdata/cases/ls_validate_errors_on_explicit_expression_with_literal_text_in_pre_if_fo_2.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Literal text in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | pre-entrypoint: /setup.sh | |
| 7 | + | pre-if: push == ${{ github.event_name }} | |
A action/testdata/cases/ls_validate_errors_on_format_with_literal_text_in_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format with literal text | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: ${{ format('event is {0}', github.event_name) }} | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_errors_on_format_with_too_few_arguments_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format mismatch | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: format('{0} {1}', 'only-one') | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_errors_on_format_with_too_few_arguments_in_input_default.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format mismatch in input default | |
| 3 | + | inputs: | |
| 4 | + | greeting: | |
| 5 | + | description: Greeting message | |
| 6 | + | default: ${{ format('{0} {1}', 'hello') }} | |
| 7 | + | runs: | |
| 8 | + | using: node20 | |
| 9 | + | main: index.js | |
A action/testdata/cases/ls_validate_errors_on_format_with_too_few_arguments_in_post_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format mismatch in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: format('{0} {1} {2}', 'a', 'b') | |
A action/testdata/cases/ls_validate_errors_on_format_with_too_few_arguments_in_pre_if.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format mismatch in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: format('{0} {1}', 'only-one') | |
A action/testdata/cases/ls_validate_errors_on_format_with_too_few_arguments_in_run_expression.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Format mismatch in run | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo ${{ format('{0} {1}', 'only-one') }} | |
| 7 | + | shell: bash | |
A action/testdata/cases/ls_validate_errors_on_invalid_format.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid format | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: invalid-format | |
A action/testdata/cases/ls_validate_errors_on_invalid_format_string_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid format | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: format('{', 'arg') | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_errors_on_missing_ref.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Missing version | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout | |
A action/testdata/cases/ls_validate_errors_on_reusable_workflow_in_step_uses.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Wrong workflow reference | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: owner/repo/.github/workflows/build.yml@main | |
A action/testdata/cases/ls_validate_errors_on_unknown_function_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown function in if | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: unknownFunc() | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_errors_on_unknown_function_in_post_if_for_docker_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown function in post-if | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | post-entrypoint: /cleanup.sh | |
| 7 | + | post-if: unknownFunc() | |
A action/testdata/cases/ls_validate_errors_on_unknown_function_in_post_if_for_node_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown function in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: unknownFunc() | |
A action/testdata/cases/ls_validate_errors_on_unknown_function_in_pre_if_for_docker_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown function in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | pre-entrypoint: /setup.sh | |
| 7 | + | pre-if: unknownFunc() | |
A action/testdata/cases/ls_validate_errors_on_unknown_function_in_pre_if_for_node_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown function in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: unknownFunc() | |
A action/testdata/cases/ls_validate_errors_when_literal_text_mixed_with_embedded_expression.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Literal text in if | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: push == ${{ github.event_name }} | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_lets_schema_handle_invalid_using_value.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - bad using value | |
| 3 | + | runs: | |
| 4 | + | using: not-supported | |
| 5 | + | main: index.js | |
A action/testdata/cases/ls_validate_lets_schema_handle_missing_using.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - no using | |
| 3 | + | runs: | |
| 4 | + | main: index.js | |
A action/testdata/cases/ls_validate_passes_validation_for_valid_composite_action_uses_steps.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Composite Action | |
| 2 | + | description: A composite action with uses steps | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@v4 | |
| 7 | + | with: | |
| 8 | + | repository: owner/repo | |
A action/testdata/cases/ls_validate_reports_error_for_composite_action_missing_steps.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - composite without steps | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
A action/testdata/cases/ls_validate_reports_error_for_composite_action_with_main.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - composite with main | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo "hello" | |
| 7 | + | shell: bash | |
| 8 | + | main: index.js | |
A action/testdata/cases/ls_validate_reports_error_for_composite_step_missing_shell.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Missing shell | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo "Hi" | |
A action/testdata/cases/ls_validate_reports_error_for_docker_action_missing_image.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - docker without image | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
A action/testdata/cases/ls_validate_reports_error_for_docker_action_with_entrypoint_but_missing_.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - docker without image | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | entrypoint: /entrypoint.sh | |
A action/testdata/cases/ls_validate_reports_error_for_docker_action_with_main.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - docker with main | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | main: index.js | |
A action/testdata/cases/ls_validate_reports_error_for_docker_action_with_steps.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - docker with steps | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | steps: | |
| 7 | + | - run: echo "hello" | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_reports_error_for_invalid_branding_color.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Bad color | |
| 3 | + | branding: | |
| 4 | + | icon: activity | |
| 5 | + | color: pink | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: index.js | |
A action/testdata/cases/ls_validate_reports_error_for_invalid_branding_icon.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Bad icon | |
| 3 | + | branding: | |
| 4 | + | icon: not-a-real-icon | |
| 5 | + | color: blue | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: index.js | |
A action/testdata/cases/ls_validate_reports_error_for_invalid_yaml_syntax.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Bad YAML | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: | | |
| 7 | + | echo "Bad indentation" | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_reports_error_for_missing_required_description.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | runs: | |
| 3 | + | using: composite | |
| 4 | + | steps: | |
| 5 | + | - run: echo "Hi" | |
| 6 | + | shell: bash | |
A action/testdata/cases/ls_validate_reports_error_for_missing_required_name.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | description: An action without a name | |
| 2 | + | runs: | |
| 3 | + | using: composite | |
| 4 | + | steps: | |
| 5 | + | - run: echo "Hi" | |
| 6 | + | shell: bash | |
A action/testdata/cases/ls_validate_reports_error_for_missing_runs.yml +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: An action without runs | |
A action/testdata/cases/ls_validate_reports_error_for_missing_using_in_runs.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Missing using | |
| 3 | + | runs: | |
| 4 | + | main: index.js | |
A action/testdata/cases/ls_validate_reports_error_for_node20_action_missing_main.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - node20 without main | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | pre: setup.js | |
A action/testdata/cases/ls_validate_reports_error_for_node20_action_with_steps.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - node20 with steps | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | steps: | |
| 7 | + | - run: echo "hello" | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_reports_error_for_node24_action_missing_main.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - node24 without main | |
| 3 | + | runs: | |
| 4 | + | using: node24 | |
| 5 | + | pre: setup.js | |
A action/testdata/cases/ls_validate_reports_error_for_node24_action_with_only_using_no_narrowing.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Invalid - node24 without main | |
| 3 | + | runs: | |
| 4 | + | using: node24 | |
A action/testdata/cases/ls_validate_reports_unresolved_action_in_composite_action_uses_steps.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Composite Action | |
| 2 | + | description: A composite action with uses steps | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/nonexistent@v1 | |
A action/testdata/cases/ls_validate_validates_a_composite_action_with_a_self_repository_step.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Does something | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: $/actions/nested | |
A action/testdata/cases/ls_validate_validates_a_docker_action.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: A Docker action | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
A action/testdata/cases/ls_validate_validates_a_minimal_composite_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Does something | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: echo "Hello" | |
| 7 | + | shell: bash | |
A action/testdata/cases/ls_validate_validates_a_node20_action.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: A JavaScript action | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
A action/testdata/cases/ls_validate_validates_action_inputs_in_composite_action_uses_steps.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Composite Action | |
| 2 | + | description: A composite action with uses steps | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@v4 | |
| 7 | + | with: | |
| 8 | + | invalid-input: value | |
A action/testdata/cases/ls_validate_validates_an_action_with_branding.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Branded action | |
| 3 | + | branding: | |
| 4 | + | icon: activity | |
| 5 | + | color: blue | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: index.js | |
A action/testdata/cases/ls_validate_validates_an_action_with_inputs_and_outputs.yml +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Action with I/O | |
| 3 | + | inputs: | |
| 4 | + | name: | |
| 5 | + | description: The name to greet | |
| 6 | + | required: true | |
| 7 | + | greeting: | |
| 8 | + | description: The greeting | |
| 9 | + | default: Hello | |
| 10 | + | outputs: | |
| 11 | + | result: | |
| 12 | + | description: The greeting result | |
| 13 | + | runs: | |
| 14 | + | using: composite | |
| 15 | + | steps: | |
| 16 | + | - run: echo "$\{{ inputs.greeting }} $\{{ inputs.name }}" | |
| 17 | + | shell: bash | |
A action/testdata/cases/ls_validate_validates_docker_uses_format.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Uses docker image | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: docker://alpine:3.14 | |
A action/testdata/cases/ls_validate_validates_if_in_uses_step.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: If in uses step | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: push == ${{ github.event_name }} | |
| 7 | + | uses: actions/checkout@v4 | |
A action/testdata/cases/ls_validate_validates_local_uses_format.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Uses local action | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: ./local-action | |
A action/testdata/cases/ls_validate_validates_required_inputs_in_composite_action_uses_steps.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Composite Action | |
| 2 | + | description: A composite action with uses steps | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/some-action@v1 | |
A action/testdata/cases/ls_validate_validates_valid_uses_format_with_version.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Uses another action | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@v4 | |
A action/testdata/cases/ls_validate_warns_on_short_sha.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Short SHA | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - uses: actions/checkout@a1b2c3d | |
A action/testdata/cases/ls_validate_warns_on_unknown_context_in_composite_step_if.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown context in if | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - if: foo == bar | |
| 7 | + | run: echo hi | |
| 8 | + | shell: bash | |
A action/testdata/cases/ls_validate_warns_on_unknown_context_in_post_if_for_docker_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown context in post-if | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | post-entrypoint: /cleanup.sh | |
| 7 | + | post-if: foo == bar | |
A action/testdata/cases/ls_validate_warns_on_unknown_context_in_post_if_for_node_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown context in post-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | post: cleanup.js | |
| 7 | + | post-if: foo == bar | |
A action/testdata/cases/ls_validate_warns_on_unknown_context_in_pre_if_for_docker_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown context in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: docker | |
| 5 | + | image: Dockerfile | |
| 6 | + | pre-entrypoint: /setup.sh | |
| 7 | + | pre-if: foo == bar | |
A action/testdata/cases/ls_validate_warns_on_unknown_context_in_pre_if_for_node_action.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: My Action | |
| 2 | + | description: Unknown context in pre-if | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: foo == bar | |
A action/testdata/cases/many_errors.yml +20 -0
| @@ -0,0 +1,20 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
| 6 | + | k0: 1 | |
| 7 | + | k1: 1 | |
| 8 | + | k2: 1 | |
| 9 | + | k3: 1 | |
| 10 | + | k4: 1 | |
| 11 | + | k5: 1 | |
| 12 | + | k6: 1 | |
| 13 | + | k7: 1 | |
| 14 | + | k8: 1 | |
| 15 | + | k9: 1 | |
| 16 | + | k10: 1 | |
| 17 | + | k11: 1 | |
| 18 | + | k12: 1 | |
| 19 | + | k13: 1 | |
| 20 | + | k14: 1 | |
A action/testdata/cases/many_errors_composite.yml +41 -0
| @@ -0,0 +1,41 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: x | |
| 7 | + | shell: bash | |
| 8 | + | k0: 1 | |
| 9 | + | - run: x | |
| 10 | + | shell: bash | |
| 11 | + | k1: 1 | |
| 12 | + | - run: x | |
| 13 | + | shell: bash | |
| 14 | + | k2: 1 | |
| 15 | + | - run: x | |
| 16 | + | shell: bash | |
| 17 | + | k3: 1 | |
| 18 | + | - run: x | |
| 19 | + | shell: bash | |
| 20 | + | k4: 1 | |
| 21 | + | - run: x | |
| 22 | + | shell: bash | |
| 23 | + | k5: 1 | |
| 24 | + | - run: x | |
| 25 | + | shell: bash | |
| 26 | + | k6: 1 | |
| 27 | + | - run: x | |
| 28 | + | shell: bash | |
| 29 | + | k7: 1 | |
| 30 | + | - run: x | |
| 31 | + | shell: bash | |
| 32 | + | k8: 1 | |
| 33 | + | - run: x | |
| 34 | + | shell: bash | |
| 35 | + | k9: 1 | |
| 36 | + | - run: x | |
| 37 | + | shell: bash | |
| 38 | + | k10: 1 | |
| 39 | + | - run: x | |
| 40 | + | shell: bash | |
| 41 | + | k11: 1 | |
A action/testdata/cases/name_empty.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: '' | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
A action/testdata/cases/name_expr.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: ${{ github.ref }} | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
A action/testdata/cases/name_list.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: [a] | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
A action/testdata/cases/name_number.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: 123 | |
| 2 | + | description: true | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: m.js | |
A action/testdata/cases/nested_steps_if_status.yml +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: composite | |
| 5 | + | steps: | |
| 6 | + | - run: x | |
| 7 | + | shell: bash | |
| 8 | + | if: contains(format('{0}', success()), 'x') | |
| 9 | + | - run: y | |
| 10 | + | shell: bash | |
| 11 | + | if: failure() || cancelled() | |
| 12 | + | - run: z | |
| 13 | + | shell: bash | |
| 14 | + | if: github.event_name == 'push' | |
A action/testdata/cases/no_description.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | name: A | |
| 2 | + | runs: | |
| 3 | + | using: node20 | |
| 4 | + | main: m.js | |
A action/testdata/cases/no_name.yml +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | description: D | |
| 2 | + | runs: | |
| 3 | + | using: node20 | |
| 4 | + | main: m.js | |
A action/testdata/cases/node12.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node12 | |
| 5 | + | main: index.js | |
| 6 | + | pre: setup.js | |
| 7 | + | pre-if: runner.os == 'Linux' | |
| 8 | + | post: cleanup.js | |
| 9 | + | post-if: failure() | |
A action/testdata/cases/node16.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node16 | |
| 5 | + | main: index.js | |
A action/testdata/cases/node20_min.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: dist/index.js | |
A action/testdata/cases/node24.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node24 | |
| 5 | + | main: index.js | |
| 6 | + | post: post.js | |
A action/testdata/cases/node_empty_main.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: '' | |
A action/testdata/cases/node_pre_if_bad_context.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: p.js | |
| 7 | + | pre-if: foo == bar | |
A action/testdata/cases/node_pre_if_expr_marker.yml +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | pre: p.js | |
| 7 | + | pre-if: ${{ always() }} | |
A action/testdata/cases/node_upper.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: Node20 | |
| 5 | + | main: index.js | |
A action/testdata/cases/node_with_steps.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | main: index.js | |
| 6 | + | steps: | |
| 7 | + | - run: echo | |
| 8 | + | shell: bash | |
A action/testdata/cases/node_without_main.yml +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | runs: | |
| 4 | + | using: node20 | |
| 5 | + | pre: a.js | |
A action/testdata/cases/num_00.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1.0 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_01.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1e3 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_02.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0x1F | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_03.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0o17 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_04.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 017 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_05.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1e21 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_06.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 123456789012345678 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_07.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 123456789012345678901234 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_08.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0.1 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_09.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0.30000000000000004 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_10.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: .5 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_11.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1_000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_12.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: +5 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_13.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: -0 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_14.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: -5 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_15.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1.5e-7 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_16.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1E5 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_17.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0xFFFFFFFFF | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_18.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0o7777777777777 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_19.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 1e999 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_20.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: .inf | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_21.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: -.inf | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_22.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: .nan | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_23.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: ~ | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_24.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: null | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_25.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: True | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_26.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: yes | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_27.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 0.000001 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/num_28.yml +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | inputs: | |
| 4 | + | a: | |
| 5 | + | default: 100000000000000000000 | |
| 6 | + | runs: | |
| 7 | + | using: node20 | |
| 8 | + | main: m.js | |
A action/testdata/cases/only_comment.yml +1 -0
| @@ -0,0 +1,1 @@ | |||
| 1 | + | # nothing | |
A action/testdata/cases/only_runs.yml +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | runs: | |
| 2 | + | using: node20 | |
| 3 | + | main: m.js | |
A action/testdata/cases/outputs_scalar.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | outputs: x | |
| 4 | + | runs: | |
| 5 | + | using: node20 | |
| 6 | + | main: m.js | |
A action/testdata/cases/outputs_unknown_key.yml +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | name: A | |
| 2 | + | description: D | |
| 3 | + | outputs: | |
| 4 | + | a: | |
| 5 | + | value: x | |
| 6 | + | other: y | |
| 7 | + | runs: | |
| 8 | + | using: node20 | |
| 9 | + | main: m.js | |
A action/testdata/cases/outputs_value_scalar.yml +7 -0 collapsed to keep the page light
A action/testdata/cases/plugin.yml +4 -0 collapsed to keep the page light
A action/testdata/cases/plugin_with_using.yml +6 -0 collapsed to keep the page light
A action/testdata/cases/root_list.yml +2 -0 collapsed to keep the page light
A action/testdata/cases/root_null.yml +1 -0 collapsed to keep the page light
A action/testdata/cases/root_scalar.yml +1 -0 collapsed to keep the page light
A action/testdata/cases/runner_composite_action_without_using_token.yml +9 -0 collapsed to keep the page light
A action/testdata/cases/runner_conditional_composite_action.yml +49 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction.yml +25 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_arg_env_expression.yml +25 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_cleanup.yml +27 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_cleanup_default.yml +26 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_env_invalid_context.yml +13 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_init.yml +27 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_init_default.yml +26 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfileaction_noargs_noenv_noentrypoint.yml +19 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerfilerelativeaction.yml +25 -0 collapsed to keep the page light
A action/testdata/cases/runner_dockerhubaction.yml +25 -0 collapsed to keep the page light
A action/testdata/cases/runner_node16action.yml +20 -0 collapsed to keep the page light
A action/testdata/cases/runner_node20action.yml +20 -0 collapsed to keep the page light
A action/testdata/cases/runner_node24action.yml +20 -0 collapsed to keep the page light
A action/testdata/cases/runner_nodeaction.yml +20 -0 collapsed to keep the page light
A action/testdata/cases/runner_nodeaction_cleanup.yml +22 -0 collapsed to keep the page light
A action/testdata/cases/runner_nodeaction_cleanup_default.yml +21 -0 collapsed to keep the page light
A action/testdata/cases/runner_nodeaction_init.yml +22 -0 collapsed to keep the page light
A action/testdata/cases/runner_nodeaction_init_default.yml +21 -0 collapsed to keep the page light
A action/testdata/cases/runner_noderelativeaction.yml +19 -0 collapsed to keep the page light
A action/testdata/cases/runner_pluginaction.yml +18 -0 collapsed to keep the page light
A action/testdata/cases/runner_self_repository_composite_action.yml +25 -0 collapsed to keep the page light
A action/testdata/cases/runner_self_repository_ref_composite_action.yml +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | name: 'Invalid Self Repository Ref' | |
| 2 | + | description: 'Test rejection of a ref on a self-repository action' | |
| 3 | + | runs: | |
| 4 | + | using: "composite" | |
| 5 | + | steps: | |
| 6 | + | - uses: $/foo@v1 | |