gh-125666: Avoid PyREPL exiting when a null byte is in input (#125732) #130513
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Tests | |
on: | |
workflow_dispatch: | |
push: | |
branches: | |
- 'main' | |
- '3.*' | |
pull_request: | |
branches: | |
- 'main' | |
- '3.*' | |
permissions: | |
contents: read | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}-reusable | |
cancel-in-progress: true | |
jobs: | |
check_source: | |
name: Change detection | |
# To use boolean outputs from this job, parse them as JSON. | |
# Here's some examples: | |
# | |
# if: fromJSON(needs.check_source.outputs.run-docs) | |
# | |
# ${{ | |
# fromJSON(needs.check_source.outputs.run_tests) | |
# && 'truthy-branch' | |
# || 'falsy-branch' | |
# }} | |
# | |
uses: ./.github/workflows/reusable-change-detection.yml | |
check-docs: | |
name: Docs | |
needs: check_source | |
if: fromJSON(needs.check_source.outputs.run-docs) | |
uses: ./.github/workflows/reusable-docs.yml | |
check_autoconf_regen: | |
name: 'Check if Autoconf files are up to date' | |
# Don't use ubuntu-latest but a specific version to make the job | |
# reproducible: to get the same tools versions (autoconf, aclocal, ...) | |
runs-on: ubuntu-24.04 | |
container: | |
image: ghcr.io/python/autoconf:2024.10.16.11360930377 | |
timeout-minutes: 60 | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
steps: | |
- name: Install Git | |
run: | | |
apt install git -yq | |
git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
- uses: actions/checkout@v4 | |
with: | |
fetch-depth: 1 | |
- name: Runner image version | |
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV | |
- name: Check Autoconf and aclocal versions | |
run: | | |
grep "Generated by GNU Autoconf 2.71" configure | |
grep "aclocal 1.16.5" aclocal.m4 | |
grep -q "runstatedir" configure | |
grep -q "PKG_PROG_PKG_CONFIG" aclocal.m4 | |
- name: Regenerate autoconf files | |
# Same command used by Tools/build/regen-configure.sh ($AUTORECONF) | |
run: autoreconf -ivf -Werror | |
- name: Check for changes | |
run: | | |
git add -u | |
changes=$(git status --porcelain) | |
# Check for changes in regenerated files | |
if test -n "$changes"; then | |
echo "Generated files not up to date." | |
echo "Perhaps you forgot to run make regen-all or build.bat --regen. ;)" | |
echo "configure files must be regenerated with a specific version of autoconf." | |
echo "$changes" | |
echo "" | |
git diff --staged || true | |
exit 1 | |
fi | |
check_generated_files: | |
name: 'Check if generated files are up to date' | |
# Don't use ubuntu-latest but a specific version to make the job | |
# reproducible: to get the same tools versions (autoconf, aclocal, ...) | |
runs-on: ubuntu-22.04 | |
timeout-minutes: 60 | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-python@v5 | |
with: | |
python-version: '3.x' | |
- name: Runner image version | |
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV | |
- name: Restore config.cache | |
uses: actions/cache@v4 | |
with: | |
path: config.cache | |
# Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python | |
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ needs.check_source.outputs.config_hash }}-${{ env.pythonLocation }} | |
- name: Install Dependencies | |
run: sudo ./.github/workflows/posix-deps-apt.sh | |
- name: Add ccache to PATH | |
run: echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV | |
- name: Configure ccache action | |
uses: hendrikmuhs/[email protected] | |
with: | |
save: false | |
- name: Configure CPython | |
run: | | |
# Build Python with the libpython dynamic library | |
./configure --config-cache --with-pydebug --enable-shared | |
- name: Build CPython | |
run: | | |
make -j4 regen-all | |
make regen-stdlib-module-names regen-sbom | |
- name: Check for changes | |
run: | | |
git add -u | |
changes=$(git status --porcelain) | |
# Check for changes in regenerated files | |
if test -n "$changes"; then | |
echo "Generated files not up to date." | |
echo "Perhaps you forgot to run make regen-all or build.bat --regen. ;)" | |
echo "configure files must be regenerated with a specific version of autoconf." | |
echo "$changes" | |
echo "" | |
git diff --staged || true | |
exit 1 | |
fi | |
- name: Check exported libpython symbols | |
run: make smelly | |
- name: Check limited ABI symbols | |
run: make check-limited-abi | |
- name: Check for unsupported C global variables | |
if: github.event_name == 'pull_request' # $GITHUB_EVENT_NAME | |
run: make check-c-globals | |
build_windows: | |
name: >- | |
Windows | |
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} | |
needs: check_source | |
if: fromJSON(needs.check_source.outputs.run_tests) | |
strategy: | |
matrix: | |
arch: | |
- Win32 | |
- x64 | |
- arm64 | |
free-threading: | |
- false | |
- true | |
uses: ./.github/workflows/reusable-windows.yml | |
with: | |
arch: ${{ matrix.arch }} | |
free-threading: ${{ matrix.free-threading }} | |
build_windows_msi: | |
name: >- # ${{ '' } is a hack to nest jobs under the same sidebar category | |
Windows MSI${{ '' }} | |
needs: check_source | |
if: fromJSON(needs.check_source.outputs.run-win-msi) | |
strategy: | |
matrix: | |
arch: | |
- x86 | |
- x64 | |
- arm64 | |
uses: ./.github/workflows/reusable-windows-msi.yml | |
with: | |
arch: ${{ matrix.arch }} | |
build_macos: | |
name: >- | |
macOS | |
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
strategy: | |
fail-fast: false | |
matrix: | |
# Cirrus and macos-14 are M1, macos-13 is default GHA Intel. | |
# macOS 13 only runs tests against the GIL-enabled CPython. | |
# Cirrus used for upstream, macos-14 for forks. | |
os: | |
- ghcr.io/cirruslabs/macos-runner:sonoma | |
- macos-14 | |
- macos-13 | |
is-fork: # only used for the exclusion trick | |
- ${{ github.repository_owner != 'python' }} | |
free-threading: | |
- false | |
- true | |
exclude: | |
- os: ghcr.io/cirruslabs/macos-runner:sonoma | |
is-fork: true | |
- os: macos-14 | |
is-fork: false | |
- os: macos-13 | |
free-threading: true | |
uses: ./.github/workflows/reusable-macos.yml | |
with: | |
config_hash: ${{ needs.check_source.outputs.config_hash }} | |
free-threading: ${{ matrix.free-threading }} | |
os: ${{ matrix.os }} | |
build_ubuntu: | |
name: >- | |
Ubuntu | |
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
strategy: | |
matrix: | |
free-threading: | |
- false | |
- true | |
uses: ./.github/workflows/reusable-ubuntu.yml | |
with: | |
config_hash: ${{ needs.check_source.outputs.config_hash }} | |
free-threading: ${{ matrix.free-threading }} | |
build_ubuntu_ssltests: | |
name: 'Ubuntu SSL tests with OpenSSL' | |
runs-on: ${{ matrix.os }} | |
timeout-minutes: 60 | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
strategy: | |
fail-fast: false | |
matrix: | |
os: [ubuntu-22.04] | |
openssl_ver: [3.0.15, 3.1.7, 3.2.3, 3.3.2] | |
env: | |
OPENSSL_VER: ${{ matrix.openssl_ver }} | |
MULTISSL_DIR: ${{ github.workspace }}/multissl | |
OPENSSL_DIR: ${{ github.workspace }}/multissl/openssl/${{ matrix.openssl_ver }} | |
LD_LIBRARY_PATH: ${{ github.workspace }}/multissl/openssl/${{ matrix.openssl_ver }}/lib | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Runner image version | |
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV | |
- name: Restore config.cache | |
uses: actions/cache@v4 | |
with: | |
path: config.cache | |
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ needs.check_source.outputs.config_hash }} | |
- name: Register gcc problem matcher | |
run: echo "::add-matcher::.github/problem-matchers/gcc.json" | |
- name: Install Dependencies | |
run: sudo ./.github/workflows/posix-deps-apt.sh | |
- name: Configure OpenSSL env vars | |
run: | | |
echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> $GITHUB_ENV | |
echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> $GITHUB_ENV | |
echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}/lib" >> $GITHUB_ENV | |
- name: 'Restore OpenSSL build' | |
id: cache-openssl | |
uses: actions/cache@v4 | |
with: | |
path: ./multissl/openssl/${{ env.OPENSSL_VER }} | |
key: ${{ matrix.os }}-multissl-openssl-${{ env.OPENSSL_VER }} | |
- name: Install OpenSSL | |
if: steps.cache-openssl.outputs.cache-hit != 'true' | |
run: python3 Tools/ssl/multissltests.py --steps=library --base-directory $MULTISSL_DIR --openssl $OPENSSL_VER --system Linux | |
- name: Add ccache to PATH | |
run: | | |
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV | |
- name: Configure ccache action | |
uses: hendrikmuhs/[email protected] | |
with: | |
save: false | |
- name: Configure CPython | |
run: ./configure CFLAGS="-fdiagnostics-format=json" --config-cache --enable-slower-safety --with-pydebug --with-openssl=$OPENSSL_DIR | |
- name: Build CPython | |
run: make -j4 | |
- name: Display build info | |
run: make pythoninfo | |
- name: SSL tests | |
run: ./python Lib/test/ssltests.py | |
build_wasi: | |
name: 'WASI' | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
uses: ./.github/workflows/reusable-wasi.yml | |
with: | |
config_hash: ${{ needs.check_source.outputs.config_hash }} | |
test_hypothesis: | |
name: "Hypothesis tests on Ubuntu" | |
runs-on: ubuntu-22.04 | |
timeout-minutes: 60 | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' && needs.check_source.outputs.run_hypothesis == 'true' | |
env: | |
OPENSSL_VER: 3.0.15 | |
PYTHONSTRICTEXTENSIONBUILD: 1 | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Register gcc problem matcher | |
run: echo "::add-matcher::.github/problem-matchers/gcc.json" | |
- name: Install Dependencies | |
run: sudo ./.github/workflows/posix-deps-apt.sh | |
- name: Configure OpenSSL env vars | |
run: | | |
echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> $GITHUB_ENV | |
echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> $GITHUB_ENV | |
echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}/lib" >> $GITHUB_ENV | |
- name: 'Restore OpenSSL build' | |
id: cache-openssl | |
uses: actions/cache@v4 | |
with: | |
path: ./multissl/openssl/${{ env.OPENSSL_VER }} | |
key: ${{ runner.os }}-multissl-openssl-${{ env.OPENSSL_VER }} | |
- name: Install OpenSSL | |
if: steps.cache-openssl.outputs.cache-hit != 'true' | |
run: python3 Tools/ssl/multissltests.py --steps=library --base-directory $MULTISSL_DIR --openssl $OPENSSL_VER --system Linux | |
- name: Add ccache to PATH | |
run: | | |
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV | |
- name: Configure ccache action | |
uses: hendrikmuhs/[email protected] | |
with: | |
save: false | |
- name: Setup directory envs for out-of-tree builds | |
run: | | |
echo "CPYTHON_RO_SRCDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-ro-srcdir)" >> $GITHUB_ENV | |
echo "CPYTHON_BUILDDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-builddir)" >> $GITHUB_ENV | |
- name: Create directories for read-only out-of-tree builds | |
run: mkdir -p $CPYTHON_RO_SRCDIR $CPYTHON_BUILDDIR | |
- name: Bind mount sources read-only | |
run: sudo mount --bind -o ro $GITHUB_WORKSPACE $CPYTHON_RO_SRCDIR | |
- name: Runner image version | |
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV | |
- name: Restore config.cache | |
uses: actions/cache@v4 | |
with: | |
path: ${{ env.CPYTHON_BUILDDIR }}/config.cache | |
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ needs.check_source.outputs.config_hash }} | |
- name: Configure CPython out-of-tree | |
working-directory: ${{ env.CPYTHON_BUILDDIR }} | |
run: | | |
../cpython-ro-srcdir/configure \ | |
--config-cache \ | |
--with-pydebug \ | |
--enable-slower-safety \ | |
--with-openssl=$OPENSSL_DIR | |
- name: Build CPython out-of-tree | |
working-directory: ${{ env.CPYTHON_BUILDDIR }} | |
run: make -j4 | |
- name: Display build info | |
working-directory: ${{ env.CPYTHON_BUILDDIR }} | |
run: make pythoninfo | |
- name: Remount sources writable for tests | |
# some tests write to srcdir, lack of pyc files slows down testing | |
run: sudo mount $CPYTHON_RO_SRCDIR -oremount,rw | |
- name: Setup directory envs for out-of-tree builds | |
run: | | |
echo "CPYTHON_BUILDDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-builddir)" >> $GITHUB_ENV | |
- name: "Create hypothesis venv" | |
working-directory: ${{ env.CPYTHON_BUILDDIR }} | |
run: | | |
VENV_LOC=$(realpath -m .)/hypovenv | |
VENV_PYTHON=$VENV_LOC/bin/python | |
echo "HYPOVENV=${VENV_LOC}" >> $GITHUB_ENV | |
echo "VENV_PYTHON=${VENV_PYTHON}" >> $GITHUB_ENV | |
./python -m venv $VENV_LOC && $VENV_PYTHON -m pip install -r ${GITHUB_WORKSPACE}/Tools/requirements-hypothesis.txt | |
- name: 'Restore Hypothesis database' | |
id: cache-hypothesis-database | |
uses: actions/cache@v4 | |
with: | |
path: ${{ env.CPYTHON_BUILDDIR }}/.hypothesis/ | |
key: hypothesis-database-${{ github.head_ref || github.run_id }} | |
restore-keys: | | |
hypothesis-database- | |
- name: "Run tests" | |
working-directory: ${{ env.CPYTHON_BUILDDIR }} | |
run: | | |
# Most of the excluded tests are slow test suites with no property tests | |
# | |
# (GH-104097) test_sysconfig is skipped because it has tests that are | |
# failing when executed from inside a virtual environment. | |
${{ env.VENV_PYTHON }} -m test \ | |
-W \ | |
-o \ | |
-j4 \ | |
-x test_asyncio \ | |
-x test_multiprocessing_fork \ | |
-x test_multiprocessing_forkserver \ | |
-x test_multiprocessing_spawn \ | |
-x test_concurrent_futures \ | |
-x test_socket \ | |
-x test_subprocess \ | |
-x test_signal \ | |
-x test_sysconfig | |
- uses: actions/upload-artifact@v4 | |
if: always() | |
with: | |
name: hypothesis-example-db | |
path: ${{ env.CPYTHON_BUILDDIR }}/.hypothesis/examples/ | |
build_asan: | |
name: 'Address sanitizer' | |
runs-on: ubuntu-22.04 | |
timeout-minutes: 60 | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
env: | |
OPENSSL_VER: 3.0.15 | |
PYTHONSTRICTEXTENSIONBUILD: 1 | |
ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0 | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Runner image version | |
run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV | |
- name: Restore config.cache | |
uses: actions/cache@v4 | |
with: | |
path: config.cache | |
key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ needs.check_source.outputs.config_hash }} | |
- name: Register gcc problem matcher | |
run: echo "::add-matcher::.github/problem-matchers/gcc.json" | |
- name: Install Dependencies | |
run: sudo ./.github/workflows/posix-deps-apt.sh | |
- name: Set up GCC-10 for ASAN | |
uses: egor-tensin/setup-gcc@v1 | |
with: | |
version: 10 | |
- name: Configure OpenSSL env vars | |
run: | | |
echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> $GITHUB_ENV | |
echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> $GITHUB_ENV | |
echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}/lib" >> $GITHUB_ENV | |
- name: 'Restore OpenSSL build' | |
id: cache-openssl | |
uses: actions/cache@v4 | |
with: | |
path: ./multissl/openssl/${{ env.OPENSSL_VER }} | |
key: ${{ matrix.os }}-multissl-openssl-${{ env.OPENSSL_VER }} | |
- name: Install OpenSSL | |
if: steps.cache-openssl.outputs.cache-hit != 'true' | |
run: python3 Tools/ssl/multissltests.py --steps=library --base-directory $MULTISSL_DIR --openssl $OPENSSL_VER --system Linux | |
- name: Add ccache to PATH | |
run: | | |
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV | |
- name: Configure ccache action | |
uses: hendrikmuhs/[email protected] | |
with: | |
save: ${{ github.event_name == 'push' }} | |
max-size: "200M" | |
- name: Configure CPython | |
run: ./configure --config-cache --with-address-sanitizer --without-pymalloc | |
- name: Build CPython | |
run: make -j4 | |
- name: Display build info | |
run: make pythoninfo | |
- name: Tests | |
run: xvfb-run make test | |
build_tsan: | |
name: 'Thread sanitizer' | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
uses: ./.github/workflows/reusable-tsan.yml | |
with: | |
config_hash: ${{ needs.check_source.outputs.config_hash }} | |
options: ./configure --config-cache --with-thread-sanitizer --with-pydebug | |
suppressions_path: Tools/tsan/supressions.txt | |
tsan_logs_artifact_name: tsan-logs-default | |
build_tsan_free_threading: | |
name: 'Thread sanitizer (free-threading)' | |
needs: check_source | |
if: needs.check_source.outputs.run_tests == 'true' | |
uses: ./.github/workflows/reusable-tsan.yml | |
with: | |
config_hash: ${{ needs.check_source.outputs.config_hash }} | |
options: ./configure --config-cache --disable-gil --with-thread-sanitizer --with-pydebug | |
suppressions_path: Tools/tsan/suppressions_free_threading.txt | |
tsan_logs_artifact_name: tsan-logs-free-threading | |
# CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/ | |
cifuzz: | |
name: CIFuzz | |
runs-on: ubuntu-latest | |
timeout-minutes: 60 | |
needs: check_source | |
if: needs.check_source.outputs.run_cifuzz == 'true' | |
permissions: | |
security-events: write | |
strategy: | |
fail-fast: false | |
matrix: | |
sanitizer: [address, undefined, memory] | |
steps: | |
- name: Build fuzzers (${{ matrix.sanitizer }}) | |
id: build | |
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master | |
with: | |
oss-fuzz-project-name: cpython3 | |
sanitizer: ${{ matrix.sanitizer }} | |
- name: Run fuzzers (${{ matrix.sanitizer }}) | |
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master | |
with: | |
fuzz-seconds: 600 | |
oss-fuzz-project-name: cpython3 | |
output-sarif: true | |
sanitizer: ${{ matrix.sanitizer }} | |
- name: Upload crash | |
uses: actions/upload-artifact@v4 | |
if: failure() && steps.build.outcome == 'success' | |
with: | |
name: ${{ matrix.sanitizer }}-artifacts | |
path: ./out/artifacts | |
- name: Upload SARIF | |
if: always() && steps.build.outcome == 'success' | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: cifuzz-sarif/results.sarif | |
checkout_path: cifuzz-sarif | |
all-required-green: # This job does nothing and is only used for the branch protection | |
name: All required checks pass | |
if: always() | |
needs: | |
- check_source # Transitive dependency, needed to access `run_tests` value | |
- check-docs | |
- check_autoconf_regen | |
- check_generated_files | |
- build_macos | |
- build_ubuntu | |
- build_ubuntu_ssltests | |
- build_wasi | |
- build_windows | |
- build_windows_msi | |
- test_hypothesis | |
- build_asan | |
- build_tsan | |
- build_tsan_free_threading | |
- cifuzz | |
runs-on: ubuntu-latest | |
steps: | |
- name: Check whether the needed jobs succeeded or failed | |
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe | |
with: | |
allowed-failures: >- | |
build_ubuntu_ssltests, | |
build_windows_msi, | |
cifuzz, | |
test_hypothesis, | |
allowed-skips: >- | |
${{ | |
!fromJSON(needs.check_source.outputs.run-docs) | |
&& ' | |
check-docs, | |
' | |
|| '' | |
}} | |
${{ | |
needs.check_source.outputs.run_tests != 'true' | |
&& ' | |
check_autoconf_regen, | |
check_generated_files, | |
build_macos, | |
build_ubuntu, | |
build_ubuntu_ssltests, | |
build_wasi, | |
build_windows, | |
build_asan, | |
build_tsan, | |
build_tsan_free_threading, | |
' | |
|| '' | |
}} | |
${{ | |
!fromJSON(needs.check_source.outputs.run_cifuzz) | |
&& ' | |
cifuzz, | |
' | |
|| '' | |
}} | |
${{ | |
!fromJSON(needs.check_source.outputs.run_hypothesis) | |
&& ' | |
test_hypothesis, | |
' | |
|| '' | |
}} | |
jobs: ${{ toJSON(needs) }} |