Skip to content

Commit

success(), failure(), cancelled() and always() work only in an if:, as on GitHub. Anywhere else, such as a step's env, g1t now refuses them with GitHub's own error, "Unrecognized function: 'success'"; before, g1t accepted them everywhere, and the Conformance workflow, which read success() in its env, ran on g1t and failed on GitHub. The workflow no longer reports it, and the Actions guide says where the status functions may be called.

syntaqxcommitted Parenta83a675Browse files
4 files+48−150/4 viewed
+0−1
105105 CHECK_EQUALITY: ${{ 1 == '1' }} ${{ 'A' == 'a' }} ${{ null == '' }}
106106 CHECK_FALSY_OR: ${{ '' || 'fallback' }}
107107 CHECK_HASH_FILES: ${{ hashFiles('conformance/report.mjs') }}
108− CHECK_SUCCESS: ${{ success() }}
109108 CHECK_MATRIX: ${{ matrix.n }}
110109 CHECK_JOB_TOTAL: ${{ strategy.job-total }}
111110 CHECK_EVENT_NAME: ${{ github.event_name }}
+0−1
105105 CHECK_EQUALITY: ${{ 1 == '1' }} ${{ 'A' == 'a' }} ${{ null == '' }}
106106 CHECK_FALSY_OR: ${{ '' || 'fallback' }}
107107 CHECK_HASH_FILES: ${{ hashFiles('conformance/report.mjs') }}
108− CHECK_SUCCESS: ${{ success() }}
109108 CHECK_MATRIX: ${{ matrix.n }}
110109 CHECK_JOB_TOTAL: ${{ strategy.job-total }}
111110 CHECK_EVENT_NAME: ${{ github.event_name }}
+1−1
6161 | `strategy.matrix` with `include` and `exclude`, `fail-fast`, `max-parallel`, a matrix from `fromJSON(needs.…)` | The same. |
6262 | `concurrency` with `cancel-in-progress`, for the workflow or for one job | The same: one run, or one job, of a group at a time. |
6363 | `permissions:` for the workflow or for one job, `read-all`, `write-all` | The same: they decide what [the job's token](#the-jobs-token) may do. |
64−| `${{ }}` expressions: every operator, function and context | The same, including `hashFiles`, `success()`, `failure()`, `always()` and `cancelled()`. |
64+| `${{ }}` expressions: every operator, function and context | The same, including `hashFiles`, and `success()`, `failure()`, `always()` and `cancelled()` in `if:` (anywhere else they are an error, as on GitHub). |
6565 | `run:` with `bash`, `sh`, `python`, `pwsh` or a custom shell | The same, with GitHub's file for each: `.sh` for `bash` and `sh`, `.py` for `python`, `.ps1` for PowerShell (stopping at the first error, and exiting with the last program's code), `.cmd` for `cmd`, and no extension for another program. `python` runs `python`, or `python3` on a machine without it. |
6666 | JavaScript actions (`uses: owner/repo@v7`, `owner/repo/path@v7`) | Fetched from that repository on g1t when g1t has it and your repository may use it, otherwise from GitHub, and run as they are, on Node 24, the runtime current actions declare. An action that declares `node20` runs on Node 20 where the machine has one. See [actions and workflows from other repositories](#actions-and-workflows-from-other-repositories) and [an action's pre and post](#an-actions-pre-and-post). |
6767 | Composite actions | The same. |
+47−12
7373 ];
7474
7575 /// Evaluates one expression (the text between `${{` and `}}`, or a bare `if:`).
76+/// The status functions exist only in `if:`; anywhere else they are unrecognized, as on GitHub.
7677 pub fn evaluate(expression: &str, scope: &Scope) -> Result<Value, String> {
77− let ast = parse(expression, scope.contexts)?;
78+ let ast = parse(expression, scope.contexts, false)?;
7879 Ok(eval(&ast, scope)?.into_value())
7980 }
8081
8788 // Text around or between expressions makes the whole thing a string,
8889 // as on GitHub: it is true whenever it interpolates to anything.
8990 None if has_expression(trimmed) => {
90− let text = interpolate(trimmed, scope)?;
91+ let text = interpolate_in(trimmed, scope, true)?;
9192 return Ok(success && !text.is_empty());
9293 }
9394 None => trimmed,
9596 if source.trim().is_empty() {
9697 return Ok(success);
9798 }
98− let ast = parse(source, scope.contexts)?;
99+ let ast = parse(source, scope.contexts, true)?;
99100 if uses_status(&ast) {
100101 Ok(eval(&ast, scope)?.truthy())
101102 } else {
105106
106107 /// Replaces each `${{ expr }}` in text with the value converted to a string. Text without `${{` is returned unchanged.
107108 pub fn interpolate(text: &str, scope: &Scope) -> Result<String, String> {
109+ interpolate_in(text, scope, false)
110+}
111+
112+/// `interpolate`, with the status functions allowed when `status` (inside an `if:`).
113+fn interpolate_in(text: &str, scope: &Scope, status: bool) -> Result<String, String> {
108114 if !has_expression(text) {
109115 return Ok(text.to_string());
110116 }
119125 "The expression is not closed. An unescaped ${{{{ sequence was found, but the closing }}}} sequence was not found: {text}"
120126 )
121127 })?;
122− let value = evaluate(&text[body..close], scope)?;
128+ let ast = parse(&text[body..close], scope.contexts, status)?;
129+ let value = eval(&ast, scope)?.into_value();
123130 out.push_str(&to_text(&value));
124131 from = close + 2;
125132 }
525532 Call(Func, Vec<Expr>),
526533 }
527534
528−fn parse(src: &str, contexts: &Map<String, Value>) -> Result<Expr, String> {
535+/// `status`: whether success(), failure(), cancelled() and always() may be called.
536+fn parse(src: &str, contexts: &Map<String, Value>, status: bool) -> Result<Expr, String> {
529537 let toks = lex(src)?;
530538 if toks.is_empty() {
531539 return Err(format!("An expression was expected: '{src}'"));
535543 i: 0,
536544 src,
537545 contexts,
546+ status,
538547 };
539548 let expr = parser.or()?;
540549 if parser.i < parser.toks.len() {
548557 i: usize,
549558 src: &'s str,
550559 contexts: &'s Map<String, Value>,
560+ status: bool,
551561 }
552562
553563 impl Parser<'_> {
711721 let Some(&(canonical, func, min, max)) = FUNCTIONS
712722 .iter()
713723 .find(|(n, ..)| n.eq_ignore_ascii_case(name))
724+ .filter(|(_, func, ..)| self.status || !is_status(*func))
714725 else {
715726 return Err(located(
716727 &format!("Unrecognized function: '{name}'"),
749760 }
750761 }
751762
763+fn is_status(func: Func) -> bool {
764+ matches!(
765+ func,
766+ Func::Success | Func::Failure | Func::Cancelled | Func::Always
767+ )
768+}
769+
752770 /// Whether the expression calls success(), failure(), cancelled() or always().
753771 fn uses_status(expr: &Expr) -> bool {
754772 match expr {
757775 Expr::Index(a, b) | Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => {
758776 uses_status(a) || uses_status(b)
759777 }
760− Expr::Call(func, args) => {
761− matches!(
762− func,
763− Func::Success | Func::Failure | Func::Cancelled | Func::Always
764− ) || args.iter().any(uses_status)
765− }
778+ Expr::Call(func, args) => is_status(*func) || args.iter().any(uses_status),
766779 }
767780 }
768781
15201533 fn function_errors() {
15211534 assert!(err("nope(1)").starts_with("Unrecognized function: 'nope'"));
15221535 assert!(err("contains('a')").starts_with("Too few parameters supplied: 'contains'"));
1523− assert!(err("success(1)").starts_with("Too many parameters supplied: 'success'"));
1536+ assert!(cond(Status::Success, "success()"));
1537+ let extra = with(Status::Success, |s| condition("success(1)", s)).unwrap_err();
1538+ assert!(extra.starts_with("Too many parameters supplied: 'success'"));
1539+ assert!(err("toJSON()").starts_with("Too few parameters supplied: 'toJSON'"));
1540+ }
1541+
1542+ #[test]
1543+ fn status_functions_only_in_conditions() {
1544+ // GitHub: "Unrecognized function: 'success'" in env, with, run and the rest.
1545+ for f in [
1546+ "success()",
1547+ "failure()",
1548+ "cancelled()",
1549+ "always()",
1550+ "!SUCCESS()",
1551+ ] {
1552+ assert!(err(f).contains("Unrecognized function:"), "{f}");
1553+ }
1554+ let interpolated = with(Status::Success, |s| interpolate("x ${{ always() }}", s));
1555+ assert!(interpolated
1556+ .unwrap_err()
1557+ .starts_with("Unrecognized function: 'always'"));
1558+ assert!(cond(Status::Success, "${{ success() }} and text"));
15241559 assert!(err("toJSON()").starts_with("Too few parameters supplied: 'toJSON'"));
15251560 }
15261561