fix: matrix-job data races + outputs, leaner offline test suite (#994)

Running the full suite under `-race` (dropping `-short`) exposed pre-existing data races in parallel matrix-job execution, fixed by not sharing mutable state across combinations:

- `containerDaemonSocket()`/`validVolumes()` derive per-job values instead of mutating shared `Config`
- `getWorkflowSecrets` builds a fresh map, `rc.steps()` clones each step, and go-git workdir access is serialized
- every write to a shared `Job`'s result/outputs runs under a per-`Job` lock, each combo interpolating outputs from a pristine snapshot (last wins, as on GitHub)

### Test suite

- capability gates (docker / network / host-tools / Linux) replace the `-short` skips, and the suite runs offline via local fixtures (the artifact flow uses an in-process loopback server, only the docker-action force-pull needs the network)
- drops redundant tests, adds a regression test for https://gitea.com/gitea/runner/issues/981 and a docker-in-docker harness (`make test-dind`)

---
This PR was written with the help of Claude Opus 4.7

Reviewed-on: https://gitea.com/gitea/runner/pulls/994
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-05-29 05:23:10 +00:00
committed by silverwind
parent 0b9f251b6a
commit 270ea41232
69 changed files with 969 additions and 1176 deletions

View File

@@ -0,0 +1,34 @@
name: local-reusable-workflow
on:
workflow_call:
inputs:
string_required:
required: true
type: string
bool_required:
required: true
type: boolean
number_required:
required: true
type: number
secrets:
secret:
required: true
outputs:
output:
value: ${{ jobs.reusable.outputs.output }}
jobs:
reusable:
runs-on: ubuntu-latest
outputs:
output: ${{ steps.gen.outputs.output }}
steps:
- name: check inputs and secret arrived
run: |
[ "${{ inputs.string_required }}" = "string" ]
[ "${{ inputs.bool_required }}" = "true" ]
[ "${{ inputs.number_required }}" = "1" ]
[ "${{ secrets.secret }}" = "keep_it_private" ]
- id: gen
run: echo "output=${{ inputs.string_required }}" >> $GITHUB_OUTPUT

View File

@@ -5,10 +5,11 @@ jobs:
env:
MYGLOBALENV3: myglobalval3
steps:
- uses: actions/checkout@v4
- run: |
echo MYGLOBALENV1=myglobalval1 > $GITHUB_ENV
echo "::set-env name=MYGLOBALENV2::myglobalval2"
- uses: nektos/act-test-actions/script@main
- uses: ./actions/script
with:
main: |
env

View File

@@ -1,48 +1,31 @@
on: push
jobs:
# State saved in main (via the $GITHUB_STATE file and the ::save-state command) must surface
# as $STATE_* in the action's post step.
_:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/script@main
- uses: actions/checkout@v4
- uses: ./actions/script
with:
pre: |
env
echo mystate0=mystateval > $GITHUB_STATE
echo "::save-state name=mystate1::mystateval"
main: |
env
echo mystate2=mystateval > $GITHUB_STATE
echo "::save-state name=mystate3::mystateval"
post: |
env
[ "$STATE_mystate0" = "mystateval" ]
[ "$STATE_mystate1" = "mystateval" ]
[ "$STATE_mystate2" = "mystateval" ]
[ "$STATE_mystate3" = "mystateval" ]
# State must be isolated per action instance even when two steps use the same action.
test-id-collision-bug:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/script@main
- uses: actions/checkout@v4
- uses: ./actions/script
id: script
with:
pre: |
env
echo mystate0=mystateval > $GITHUB_STATE
echo "::save-state name=mystate1::mystateval"
main: |
env
echo mystate2=mystateval > $GITHUB_STATE
echo "::save-state name=mystate3::mystateval"
post: |
env
[ "$STATE_mystate0" = "mystateval" ]
[ "$STATE_mystate1" = "mystateval" ]
[ "$STATE_mystate2" = "mystateval" ]
[ "$STATE_mystate3" = "mystateval" ]
- uses: nektos/act-test-actions/script@main
main: echo mystate=val1 > $GITHUB_STATE
post: '[ "$STATE_mystate" = "val1" ]'
- uses: ./actions/script
id: pre-script
with:
main: |
env
echo mystate0=mystateerror > $GITHUB_STATE
echo "::save-state name=mystate1::mystateerror"
main: echo mystate=val2 > $GITHUB_STATE
post: '[ "$STATE_mystate" = "val2" ]'

View File

@@ -9,7 +9,3 @@ jobs:
- uses: actions/checkout@v3
- uses: './actions-environment-and-context-tests/js'
- uses: './actions-environment-and-context-tests/docker'
- uses: 'nektos/act-test-actions/js@main'
- uses: 'nektos/act-test-actions/docker@main'
- uses: 'nektos/act-test-actions/docker-file@main'
- uses: 'nektos/act-test-actions/docker-relative-context/action@main'

View File

@@ -0,0 +1,15 @@
name: 'script'
description: 'Run the shell scripts passed as inputs across the pre/main/post lifecycle'
inputs:
main:
description: 'shell script to run in the main step'
required: false
default: ''
post:
description: 'shell script to run in the post step'
required: false
default: ''
runs:
using: 'node24'
main: 'index.js'
post: 'post.js'

View File

@@ -0,0 +1,9 @@
import {execFileSync} from 'node:child_process';
// Run the `main` input as a bash script; its stdout (workflow commands like
// ::set-output / ::save-state) and $GITHUB_ENV / $GITHUB_STATE writes are
// processed by the runner, exactly like the remote script action this replaces.
const script = process.env.INPUT_MAIN;
if (script) {
execFileSync('bash', ['-eo', 'pipefail', '-c', script], {stdio: 'inherit'});
}

View File

@@ -0,0 +1,5 @@
{
"name": "script",
"private": true,
"type": "module"
}

View File

@@ -0,0 +1,6 @@
import {execFileSync} from 'node:child_process';
const script = process.env.INPUT_POST;
if (script) {
execFileSync('bash', ['-eo', 'pipefail', '-c', script], {stdio: 'inherit'});
}

View File

@@ -4,7 +4,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: |
FROM ubuntu:latest
FROM node:24-bookworm-slim
ENV PATH="/opt/texlive/texdir/bin/x86_64-linuxmusl:${PATH}"
ENV ORG_PATH="${PATH}"
ENTRYPOINT [ "bash", "-c", "echo \"PATH=$PATH\" && echo \"ORG_PATH=$ORG_PATH\" && [[ \"$PATH\" = \"$ORG_PATH\" ]]" ]

View File

@@ -1,13 +0,0 @@
on: push
env:
variable: "${{ github.repository_owner }}"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: print env.variable
run: |
echo ${{ env.variable }}
exit ${{ (env.variable == 'nektos') && '0' || '1'}}

View File

@@ -9,24 +9,13 @@ jobs:
steps:
- name: My first false step
if: "endsWith('Should not', 'o1')"
uses: actions/checkout@v2.0.0
with:
ref: refs/pull/${{github.event.pull_request.number}}/merge
fetch-depth: 5
run: exit 1
- name: My first true step
if: ${{endsWith('Hello world', 'ld')}}
uses: actions/hello-world-javascript-action@main
with:
who-to-greet: "Renst the Octocat"
run: echo "Renst the Octocat"
- name: My second false step
if: "endsWith('Should not evaluate', 'o2')"
uses: actions/checkout@v2.0.0
with:
ref: refs/pull/${{github.event.pull_request.number}}/merge
fetch-depth: 5
run: exit 1
- name: My third false step
if: ${{endsWith('Should not evaluate', 'o3')}}
uses: actions/checkout@v2.0.0
with:
ref: refs/pull/${{github.event.pull_request.number}}/merge
fetch-depth: 5
run: exit 1

View File

@@ -1,31 +1,21 @@
name: issue-598
on: push
jobs:
my_first_job:
runs-on: ubuntu-latest
steps:
- name: My first false step
if: "endsWith('Hello world', 'o1')"
uses: actions/hello-world-javascript-action@main
with:
who-to-greet: 'Mona the Octocat'
run: exit 1
- name: My first true step
if: "!endsWith('Hello world', 'od')"
uses: actions/hello-world-javascript-action@main
with:
who-to-greet: "Renst the Octocat"
run: echo "Renst the Octocat"
- name: My second false step
if: "endsWith('Hello world', 'o2')"
uses: actions/hello-world-javascript-action@main
with:
who-to-greet: 'Act the Octocat'
run: exit 1
- name: My third false step
if: "endsWith('Hello world', 'o2')"
uses: actions/hello-world-javascript-action@main
with:
who-to-greet: 'Git the Octocat'
run: exit 1

View File

@@ -5,6 +5,7 @@ jobs:
test:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:runner-latest # image with user 'runner:runner' built on tag 'act-latest'
image: node:24-bookworm-slim
options: --user 1000
steps:
- run: echo PASS

View File

@@ -24,4 +24,3 @@ jobs:
args: ${{format('"{0}"', 'Mona is not the Octocat') }}
who-to-greet: 'Mona the Octocat'
- run: '[[ "${{ env.SOMEVAR }}" == "Mona is not the Octocat" ]]'
- uses: ./localdockerimagetest_

View File

@@ -30,11 +30,6 @@ runs:
who-to-greet: ${{inputs.who-to-greet}}
- run: '[[ "${{ env.SOMEVAR }}" == "Mona is not the Octocat" ]]'
shell: bash
- uses: ./localdockerimagetest_
# Also test a remote docker action here
- uses: actions/hello-world-docker-action@v2
with:
who-to-greet: 'Mona the Octocat'
# Test if GITHUB_ACTION_PATH is set correctly after all steps
- run: stat $GITHUB_ACTION_PATH/push.yml
shell: bash

View File

@@ -5,5 +5,5 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: nektos/test-override@a
- uses: https://github.com/nektos/test-override@a
- uses: nektos/test-override@b

View File

@@ -1,31 +0,0 @@
name: matrix-include-exclude
on: push
jobs:
build:
name: PHP ${{ matrix.os }} ${{ matrix.node}}
runs-on: ${{ matrix.os }}
steps:
- run: echo ${NODE_VERSION} | grep ${{ matrix.node }}
env:
NODE_VERSION: ${{ matrix.node }}
strategy:
matrix:
os: [ubuntu-18.04, macos-latest]
node: [4, 6, 8, 10]
exclude:
- os: macos-latest
node: 4
include:
- os: ubuntu-16.04
node: 10
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [8.x, 10.x, 12.x, 13.x]
steps:
- run: echo ${NODE_VERSION} | grep ${{ matrix.node }}
env:
NODE_VERSION: ${{ matrix.node }}

View File

@@ -18,12 +18,4 @@ jobs:
runs:
using: composite
shell: cp {0} action.yml
- uses: ./
remote-invalid-step:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/invalid-composite-action/invalid-step@main
remote-missing-steps:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/invalid-composite-action/missing-steps@main
- uses: ./

View File

@@ -27,7 +27,7 @@ jobs:
exit 1
fi
- uses: nektos/act-test-actions/composite@main
- uses: ./path-handling/
with:
input: some input

View File

@@ -1,8 +0,0 @@
name: remote-action-composite-action-ref
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/composite-assert-action-ref-action@main

View File

@@ -1,23 +0,0 @@
name: remote-action-composite-js-pre-with-defaults
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/composite-js-pre-with-defaults/js@main
with:
in: nix
- uses: nektos/act-test-actions/composite-js-pre-with-defaults@main
with:
in: secretval
- uses: nektos/act-test-actions/composite-js-pre-with-defaults@main
with:
in: secretval
- uses: nektos/act-test-actions/composite-js-pre-with-defaults/js@main
with:
pre: "true"
in: nix
- uses: nektos/act-test-actions/composite-js-pre-with-defaults/js@main
with:
in: nix

View File

@@ -1,10 +0,0 @@
name: remote-action-docker
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/hello-world-docker-action@v1
with:
who-to-greet: 'Mona the Octocat'

View File

@@ -1,30 +0,0 @@
name: remote-action-js
on: push
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:24-bookworm-slim
options: --user node
steps:
- name: check permissions of env files
id: test
run: |
echo "USER: $(id -un) expected: node"
[[ "$(id -un)" = "node" ]]
echo "TEST=Value" >> $GITHUB_OUTPUT
shell: bash
- name: check if file command worked
if: steps.test.outputs.test != 'Value'
run: |
echo "steps.test.outputs.test=${{ steps.test.outputs.test || 'missing value!' }}"
exit 1
shell: bash
- uses: actions/hello-world-javascript-action@v1
with:
who-to-greet: 'Mona the Octocat'
- uses: cloudposse/actions/github/slash-command-dispatch@0.14.0

View File

@@ -1,12 +0,0 @@
name: remote-action-js
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/hello-world-javascript-action@v1
with:
who-to-greet: 'Mona the Octocat'
- uses: cloudposse/actions/github/slash-command-dispatch@0.14.0

View File

@@ -1,24 +0,0 @@
name: runs-on
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: env
- run: echo ${GITHUB_ACTOR}
- run: echo ${GITHUB_ACTOR} | grep nektos/act
many:
runs-on: [ubuntu-latest]
steps:
- run: env
- run: echo ${GITHUB_ACTOR}
- run: echo ${GITHUB_ACTOR} | grep nektos/act
selfmany:
runs-on: [self-hosted, ubuntu-latest]
steps:
- run: env
- run: echo ${GITHUB_ACTOR}
- run: echo ${GITHUB_ACTOR} | grep nektos/act

View File

@@ -1,14 +0,0 @@
name: services-host-network
on: push
jobs:
services-host-network:
runs-on: ubuntu-latest
services:
nginx:
image: "nginx:latest"
ports:
- "8080:80"
steps:
- run: apt-get -qq update && apt-get -yqq install --no-install-recommends curl net-tools
- run: netstat -tlpen
- run: curl -v http://localhost:8080

View File

@@ -5,12 +5,11 @@ jobs:
runs-on: ubuntu-latest
# https://docs.github.com/en/actions/using-containerized-services/about-service-containers#running-jobs-in-a-container
container:
image: "ubuntu:latest"
image: "node:24-bookworm-slim"
services:
nginx:
image: "nginx:latest"
ports:
- "8080:80"
image: "nginx:alpine"
steps:
- run: apt-get -qq update && apt-get -yqq install --no-install-recommends curl
# reach the service over the shared job network by its alias, no host port needed
- run: curl -v http://nginx:80

View File

@@ -6,18 +6,9 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12
env:
POSTGRES_USER: runner
POSTGRES_PASSWORD: mysecretdbpass
POSTGRES_DB: mydb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
image: nginx:alpine
ports:
- 5432:5432
- 80
steps:
- name: Echo the Postgres service ID / Network / Ports
run: |

View File

@@ -8,13 +8,6 @@ jobs:
- shell: ${{ env.MY_SHELL }}
run: |
$PSVersionTable
check-container:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:pwsh-latest
steps:
- shell: ${{ env.MY_SHELL }}
run: |
$PSVersionTable
check-job-default:
runs-on: ubuntu-latest
defaults:

View File

@@ -1,28 +0,0 @@
on: push
env:
MY_SHELL: python
jobs:
check:
runs-on: ubuntu-latest
steps:
- shell: ${{ env.MY_SHELL }}
run: |
import platform
print(platform.python_version())
check-container:
runs-on: ubuntu-latest
container: node:24-bookworm
steps:
- shell: ${{ env.MY_SHELL }}
run: |
import platform
print(platform.python_version())
check-job-default:
runs-on: ubuntu-latest
defaults:
run:
shell: ${{ env.MY_SHELL }}
steps:
- run: |
import platform
print(platform.python_version())

View File

@@ -1,7 +0,0 @@
name: "last action check"
description: "last action check"
runs:
using: "node24"
main: main.js
post: post.js

View File

@@ -1,17 +0,0 @@
const pre = process.env['ACTION_OUTPUT_PRE'];
const main = process.env['ACTION_OUTPUT_MAIN'];
const post = process.env['ACTION_OUTPUT_POST'];
console.log({pre, main, post});
if (pre !== 'pre') {
throw new Error(`Expected 'pre' but got '${pre}'`);
}
if (main !== 'main') {
throw new Error(`Expected 'main' but got '${main}'`);
}
if (post !== 'post') {
throw new Error(`Expected 'post' but got '${post}'`);
}

View File

@@ -1,15 +0,0 @@
name: uses-action-with-pre-and-post-step
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./uses-action-with-pre-and-post-step/last-action
- uses: nektos/act-test-actions/js-with-pre-and-post-step@main
with:
pre: true
post: true
- run: |
cat $GITHUB_ENV

View File

@@ -1,7 +0,0 @@
name: uses-github-root
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/hello-world-docker-action@b136eb8894c5cb1dd5807da824be97ccdf9b5423

View File

@@ -1,7 +0,0 @@
name: uses-github-path
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: sergioramos/yarn-actions/install@v6

View File

@@ -1,7 +0,0 @@
name: uses-github-root
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/hello-world-docker-action@b136eb8

View File

@@ -1,63 +0,0 @@
---
name: "Test Composite Action"
description: "Test action uses composite"
inputs:
test_input_optional:
description: Test
runs:
using: "composite"
steps:
- uses: actions/setup-node@v6
with:
node-version: '24'
- run: |
console.log(process.version);
console.log("Hi from node");
console.log("${{ inputs.test_input_optional }}");
if("${{ inputs.test_input_optional }}" !== "Test") {
console.log("Invalid input test_input_optional expected \"Test\" as value");
process.exit(1);
}
if(!process.version.startsWith('v16')) {
console.log("Expected node v16, but got " + process.version);
process.exit(1);
}
shell: node {0}
- uses: ./uses-composite/composite_action
id: composite
with:
test_input_required: 'test_input_required_value'
test_input_optional: 'test_input_optional_value'
test_input_optional_with_default_overriden: 'test_input_optional_with_default_overriden'
test_input_required_with_default: 'test_input_optional_value'
test_input_required_with_default_overriden: 'test_input_required_with_default_overriden'
secret_input: ${{inputs.test_input_optional}}
env:
secret_input: ${{inputs.test_input_optional}}
- run: |
echo "steps.composite.outputs.test_output=${{ steps.composite.outputs.test_output }}"
[[ "${{steps.composite.outputs.test_output == 'test_output_value'}}" = "true" ]] || exit 1
shell: bash
- run: |
echo "steps.composite.outputs.secret_output=${{ steps.composite.outputs.secret_output }}"
[[ "${{steps.composite.outputs.secret_output == format('{0}/{0}', inputs.test_input_optional)}}" = "true" ]] || exit 1
shell: bash
# Now test again with default values
- name: ./uses-composite/composite_action with defaults
uses: ./uses-composite/composite_action
id: composite2
with:
test_input_required: 'test_input_required_value'
test_input_optional_with_default_overriden: 'test_input_optional_with_default_overriden'
test_input_required_with_default_overriden: 'test_input_required_with_default_overriden'
- run: |
echo "steps.composite2.outputs.test_output=${{ steps.composite2.outputs.test_output }}"
[[ "${{steps.composite2.outputs.test_output == 'test_output_value'}}" = "true" ]] || exit 1
shell: bash
- run: |
echo "steps.composite.outputs.secret_output=$COMPOSITE_ACTION_ENV_OUTPUT"
[[ "${{env.COMPOSITE_ACTION_ENV_OUTPUT == 'my test value' }}" = "true" ]] || exit 1
shell: bash

View File

@@ -1,15 +0,0 @@
name: uses-docker-url
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./uses-nested-composite/composite_action2
with:
test_input_optional: Test
- run: |
echo "steps.composite.outputs.secret_output=$COMPOSITE_ACTION_ENV_OUTPUT"
[[ "${{env.COMPOSITE_ACTION_ENV_OUTPUT == 'my test value' }}" = "true" ]] || exit 1
shell: bash

View File

@@ -1,42 +0,0 @@
name: local-reusable-workflows
on: pull_request
jobs:
reusable-workflow:
uses: ./.github/workflows/local-reusable-workflow.yml
with:
string_required: string
bool_required: ${{ true }}
number_required: 1
secrets:
secret: keep_it_private
reusable-workflow-with-inherited-secrets:
uses: ./.github/workflows/local-reusable-workflow.yml
with:
string_required: string
bool_required: ${{ true }}
number_required: 1
secrets: inherit
reusable-workflow-with-on-string-notation:
uses: ./.github/workflows/local-reusable-workflow-no-inputs-string.yml
reusable-workflow-with-on-array-notation:
uses: ./.github/workflows/local-reusable-workflow-no-inputs-array.yml
output-test:
runs-on: ubuntu-latest
needs:
- reusable-workflow
- reusable-workflow-with-inherited-secrets
steps:
- name: output with secrets map
run: |
echo reusable-workflow.output=${{ needs.reusable-workflow.outputs.output }}
[[ "${{ needs.reusable-workflow.outputs.output == 'string' }}" = "true" ]] || exit 1
- name: output with inherited secrets
run: |
echo reusable-workflow-with-inherited-secrets.output=${{ needs.reusable-workflow-with-inherited-secrets.outputs.output }}
[[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == 'string' }}" = "true" ]] || exit 1

View File

@@ -1,8 +1,11 @@
on: push
# Exercises the reusable-workflow caller path against a local reusable workflow: passing typed
# inputs and secrets (both an explicit map and `inherit`), and reading the called workflow's
# outputs back through `needs`.
jobs:
reusable-workflow:
uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
uses: ./.github/workflows/local-reusable-workflow.yml
with:
string_required: string
bool_required: ${{ true }}
@@ -11,7 +14,7 @@ jobs:
secret: keep_it_private
reusable-workflow-with-inherited-secrets:
uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
uses: ./.github/workflows/local-reusable-workflow.yml
with:
string_required: string
bool_required: ${{ true }}
@@ -24,12 +27,5 @@ jobs:
- reusable-workflow
- reusable-workflow-with-inherited-secrets
steps:
- name: output with secrets map
run: |
echo reusable-workflow.output=${{ needs.reusable-workflow.outputs.output }}
[[ "${{ needs.reusable-workflow.outputs.output == 'string' }}" = "true" ]] || exit 1
- name: output with inherited secrets
run: |
echo reusable-workflow-with-inherited-secrets.output=${{ needs.reusable-workflow-with-inherited-secrets.outputs.output }}
[[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == 'string' }}" = "true" ]] || exit 1
- run: '[[ "${{ needs.reusable-workflow.outputs.output == ''string'' }}" = "true" ]] || exit 1'
- run: '[[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == ''string'' }}" = "true" ]] || exit 1'