Switch back to the dfinity fork of actions-setup-docker. #27704
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: Build and test | |
# We use `push` events so that we have the actual commit. In `pull_request` | |
# events we get a merge commit with main instead. The merge commit can be | |
# useful to check that the code would pass tests once merged, but here it just | |
# creates confusion and doesn't add anything since the branch must be up to | |
# date before merge. It's also nice to have CI running on branches without PRs. | |
on: | |
push: | |
workflow_dispatch: | |
inputs: | |
no_cache: | |
description: 'no-cache' | |
default: false | |
type: boolean | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.ref }} | |
cancel-in-progress: true | |
defaults: | |
run: | |
shell: bash -euxlo pipefail {0} | |
jobs: | |
build: | |
runs-on: ubuntu-latest-m | |
timeout-minutes: 45 | |
env: | |
GH_TOKEN: ${{ github.token }} | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Skip build for testing | |
# Set to true and set a recent `run_id` below to reuse an existing build | |
# instead of building. | |
if: false | |
id: skip_build | |
run: | | |
echo "skip_build=true" >> "$GITHUB_OUTPUT" | |
mkdir out | |
# The run ID is the number at the end of a URL like this: | |
# https://github.com/dfinity/nns-dapp/actions/runs/5801187848 | |
run_id=5801187848 | |
gh run download "$run_id" --dir ./out -n out | |
- name: Build nns-dapp repo | |
if: steps.skip_build.outputs.skip_build != 'true' | |
uses: ./.github/actions/build_nns_dapp | |
with: | |
token: ${{ secrets.GITHUB_TOKEN }} | |
- name: 'Upload nns-dapp wasm module' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: nns-dapp | |
path: out/nns-dapp.wasm.gz | |
retention-days: 3 | |
- name: 'Upload nns-dapp test wasm module' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: nns-dapp_test | |
path: out/nns-dapp_test.wasm.gz | |
retention-days: 3 | |
- name: 'Upload sns_aggregator wasm module' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: sns_aggregator | |
path: out/sns_aggregator.wasm.gz | |
retention-days: 3 | |
- name: 'Upload sns_aggregator_dev wasm module' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: sns_aggregator_dev | |
path: out/sns_aggregator_dev.wasm.gz | |
retention-days: 3 | |
- name: 'Upload whole out directory' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: out | |
path: out | |
retention-days: 3 | |
test-playwright-e2e-shard-1-of-2: | |
needs: build | |
runs-on: ubuntu-latest-m | |
timeout-minutes: 30 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Run Playwright e2e test shard 1/2 | |
uses: ./.github/actions/test-e2e | |
with: | |
shard_number: 1 | |
shard_count: 2 | |
test-playwright-e2e-shard-2-of-2: | |
needs: build | |
runs-on: ubuntu-latest-m | |
timeout-minutes: 30 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Run Playwright e2e test shard 2/2 | |
uses: ./.github/actions/test-e2e | |
with: | |
shard_number: 2 | |
shard_count: 2 | |
test-downgrade-upgrade: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 40 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get nns-dapp_test | |
uses: actions/download-artifact@v4 | |
with: | |
name: nns-dapp_test | |
- name: Start snapshot environment | |
uses: ./.github/actions/start_dfx_snapshot | |
with: | |
nns_dapp_wasm: 'nns-dapp_test.wasm.gz' | |
logfile: 'dfx-test-downgrade-upgrade.log' | |
- name: Downgrade nns-dapp to prod and upgrade back again | |
run: ./scripts/nns-dapp/downgrade-upgrade-test -w nns-dapp_test.wasm.gz | |
- name: Count upgrade cycles | |
run: scripts/nns-dapp/estimate-upgrade-cycles | tee -a $GITHUB_STEP_SUMMARY | |
test-upgrade-map: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 40 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get nns-dapp_test | |
uses: actions/download-artifact@v4 | |
with: | |
name: out | |
path: out | |
- name: Install ic-wasm | |
uses: ./.github/actions/install_ic_wasm | |
- name: Install dfx | |
uses: dfinity/setup-dfx@main | |
- name: Install tools | |
run: | | |
sudo apt-get update -yy && sudo apt-get install -yy moreutils && command -v sponge | |
cargo binstall --no-confirm "idl2json_cli@$(jq -r .defaults.build.config.IDL2JSON_VERSION dfx.json)" && idl2json --version | |
- name: Start dfx | |
run: dfx start --clean --background &>test-upgrade-map-dfx.log | |
- name: Put assets in the default location | |
run: | | |
ls | |
ls out | |
- name: Upgrade to self, keeping the storage schema as map | |
run: ./scripts/nns-dapp/migration-test --schema1 Map --schema2 Map --accounts 10000 | |
- name: Upload dfx logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-upgrade-map-dfx.log | |
path: test-upgrade-map-dfx.log | |
test-upgrade-stable: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 40 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get nns-dapp_test | |
uses: actions/download-artifact@v4 | |
with: | |
name: out | |
path: out | |
- name: Install ic-wasm | |
uses: ./.github/actions/install_ic_wasm | |
- name: Install dfx | |
uses: dfinity/setup-dfx@main | |
- name: Install tools | |
run: | | |
sudo apt-get update -yy && sudo apt-get install -yy moreutils && command -v sponge | |
cargo binstall --no-confirm "idl2json_cli@$(jq -r .defaults.build.config.IDL2JSON_VERSION dfx.json)" && idl2json --version | |
- name: Start dfx | |
run: dfx start --clean --background &>test-upgrade-stable-dfx.log | |
- name: Downgrade nns-dapp to prod and upgrade back again | |
run: ./scripts/nns-dapp/migration-test --schema1 AccountsInStableMemory --schema2 AccountsInStableMemory --accounts 1000 --chunk 100 | |
- name: Upload dfx logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-upgrade-stable-dfx.log | |
path: test-upgrade-stable-dfx.log | |
test-upgrade-map-stable: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 40 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get nns-dapp_test | |
uses: actions/download-artifact@v4 | |
with: | |
name: out | |
path: out | |
- name: Install ic-wasm | |
uses: ./.github/actions/install_ic_wasm | |
- name: Install dfx | |
uses: dfinity/setup-dfx@main | |
- name: Install tools | |
run: | | |
sudo apt-get update -yy && sudo apt-get install -yy moreutils && command -v sponge | |
cargo binstall --no-confirm "idl2json_cli@$(jq -r .defaults.build.config.IDL2JSON_VERSION dfx.json)" && idl2json --version | |
- name: Start dfx | |
run: dfx start --clean --background &>test-upgrade-stable-dfx.log | |
- name: Upgrade nns-dapp from Map to AccountsInStableMemory and back again | |
run: ./scripts/nns-dapp/migration-test --schema1 Map --schema2 AccountsInStableMemory --accounts 20 --chunk 20 | |
- name: Upload dfx logs | |
if: failure() | |
uses: actions/upload-artifact@v3 | |
with: | |
name: test-upgrade-stable-dfx.log | |
path: test-upgrade-stable-dfx.log | |
test-test-account-api: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 40 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get nns-dapp_test | |
uses: actions/download-artifact@v4 | |
with: | |
name: nns-dapp_test | |
- name: Start empty nns-dapp | |
# As long as the snapshot environment can be installed with no accounts, we can use that. | |
# The snapshot action also installs `idl2json` and `jq`; commands that we will need. | |
uses: ./.github/actions/start_dfx_snapshot | |
with: | |
nns_dapp_wasm: 'nns-dapp_test.wasm.gz' | |
logfile: 'dfx-test-test-account-api.log' | |
- name: Check that test accounts can be created and read | |
run: ./scripts/nns-dapp/test-account.test | |
test-rest: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 40 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get nns-dapp | |
uses: actions/download-artifact@v4 | |
with: | |
name: nns-dapp | |
- name: Get sns_aggregator | |
uses: actions/download-artifact@v4 | |
with: | |
name: sns_aggregator | |
- name: Get sns_aggregator_dev | |
uses: actions/download-artifact@v4 | |
with: | |
name: sns_aggregator_dev | |
- name: Start snapshot environment | |
uses: ./.github/actions/start_dfx_snapshot | |
with: | |
nns_dapp_wasm: 'nns-dapp.wasm.gz' | |
sns_aggregator_wasm: 'sns_aggregator_dev.wasm.gz' | |
logfile: 'dfx-test-rest.log' | |
- name: Add go and SNS scripts to the path | |
run: | | |
echo "$PWD/snsdemo/bin" >> $GITHUB_PATH | |
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH | |
- name: Install command line HTML parser | |
run: | | |
go install github.com/ericchiang/pup@latest | |
pup --version | |
- name: Verify that arguments are set in index.html | |
run: | | |
for ((i=5; i>0; i--)); do | |
( | |
timeout 60 curl --fail --silent --retry 10 --connect-timeout 5 "http://$(dfx canister id nns-dapp).localhost:8080/" > index.html | |
file index.html | |
< index.html gunzip | pup 'head meta[name="nns-dapp-vars"] json{}' | tee nns_dapp_args_in_page.json | |
) || { echo "Failed. Retrying..." ; sleep 5 ; continue ; } | |
break | |
done | |
echo "Check a few values:" | |
for key in data-own-canister-id data-fetch-root-key data-identity-service-url ; do | |
# Verify that the key is non-trivial: | |
# `jq -e` returns an error code if the value is missing | |
# `grep ...` fails if the value is implausibly short. | |
key="$key" jq -re '.[0][env.key]' nns_dapp_args_in_page.json | grep -E ... | |
done | |
- name: Install ic-wasm | |
uses: ./.github/actions/install_ic_wasm | |
- name: Check that metadata is present | |
run: | | |
scripts/dfx-wasm-metadata-add.test --verbose | |
- name: Verify that metrics are present | |
run: scripts/nns-dapp/e2e-test-metrics-present | |
- name: Release | |
run: | | |
for tag in $(git tag --points-at HEAD) ; do | |
: Creates or updates a release for the tag | |
if gh release view "$tag" | |
then gh release upload --repo dfinity/nns-dapp --clobber "$tag" nns-dapp.wasm.gz || true | |
else gh release create --title "Release for tags/$tag" --draft --notes "Build artefacts from tag: $tag" "$tag" nns-dapp.wasm.gz | |
fi | |
: If the tag is for a proposal or nightly, make it public | |
[[ "$tag" != proposal-* ]] && [[ "$tag" != nightly-* ]] || { echo "Making release public" ; gh release edit "$tag" --draft=false ; } | |
done | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
- name: Get the postinstall instruction count | |
run: | | |
dfx canister install --upgrade-unchanged nns-dapp --wasm nns-dapp.wasm.gz --mode upgrade --argument "$(cat nns-dapp-arg-local.did)" --yes | |
postinstall_instructions="$(scripts/backend/get_upgrade_instructions)" | |
echo "Installation consumed ${postinstall_instructions} instructions." | |
echo "Cycles consumed are instructions * some factor that depends on subnet. There is no guarantee that that formula will not change." | |
- name: Stop replica | |
run: dfx stop | |
network_independent_wasm: | |
name: "Same wasms for mainnet and local" | |
# Note: The dockerfile structure SHOULD guarantee that the network is not used in any Wasm build commands. | |
# As long as that holds, this test is not needed. | |
needs: build | |
runs-on: ubuntu-latest-m | |
timeout-minutes: 45 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Check dockerfile for changes | |
id: dockerfile_changed | |
run: | | |
common_parent_commit="$(git merge-base HEAD origin/main)" | |
if git diff "$common_parent_commit" Dockerfile | grep -q . | |
then echo "dockerfile_changed=true" >> "$GITHUB_OUTPUT" | |
fi | |
- name: Set up docker buildx | |
if: steps.dockerfile_changed.outputs.dockerfile_changed == 'true' | |
uses: docker/setup-buildx-action@v3 | |
- name: Create a blank global config | |
run: echo "{}" > global-config.json | |
- name: Build wasms | |
if: steps.dockerfile_changed.outputs.dockerfile_changed == 'true' | |
uses: docker/build-push-action@v5 | |
with: | |
context: . | |
file: Dockerfile | |
build-args: | | |
DFX_NETWORK=local | |
COMMIT=${{ github.sha }} | |
cache-from: type=gha,scope=cached-stage | |
# Exports the artefacts from the final stage | |
outputs: ./out-mainnet | |
- name: Get nns-dapp | |
if: steps.dockerfile_changed.outputs.dockerfile_changed == 'true' | |
uses: actions/download-artifact@v4 | |
with: | |
name: nns-dapp | |
path: out-local | |
- name: Get sns_aggregator | |
if: steps.dockerfile_changed.outputs.dockerfile_changed == 'true' | |
uses: actions/download-artifact@v4 | |
with: | |
name: sns_aggregator | |
path: out-local | |
- name: Get sns_aggregator_dev | |
if: steps.dockerfile_changed.outputs.dockerfile_changed == 'true' | |
uses: actions/download-artifact@v4 | |
with: | |
name: sns_aggregator_dev | |
path: out-local | |
- name: Compare wasms | |
if: steps.dockerfile_changed.outputs.dockerfile_changed == 'true' | |
run: | | |
set -x | |
ls -l | |
artefacts="sns_aggregator_dev.wasm.gz sns_aggregator.wasm.gz nns-dapp.wasm.gz" | |
networks=(mainnet local) | |
for network in "${networks[@]}" ; do | |
ls -l "out-$network" | |
(cd "out-$network" && sha256sum ${artefacts[@]} ; ) > "${network}_hashes.txt" | |
done | |
diff local_hashes.txt mainnet_hashes.txt || { | |
echo "ERROR: wasm hashes differ between mainnet and local." | |
} | |
aggregator_test: | |
needs: build | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 60 | |
steps: | |
- name: Checkout nns-dapp | |
uses: actions/checkout@v4 | |
- name: Get sns_aggregator_dev | |
uses: actions/download-artifact@v4 | |
with: | |
name: sns_aggregator_dev | |
- name: Start snapshot environment | |
uses: ./.github/actions/start_dfx_snapshot | |
with: | |
sns_aggregator_wasm: 'sns_aggregator_dev.wasm.gz' | |
logfile: 'dfx-aggregator-test.log' | |
- name: Get the earliest data from the sns aggregator | |
run: | | |
AGGREGATOR_CANISTER_ID="$(dfx canister id sns_aggregator)" | |
# Wait for the aggregator to be up: | |
for (( try=300; try>0; try-- )); do | |
if curl -Lf "http://${AGGREGATOR_CANISTER_ID}.localhost:8080/v1/sns/list/latest/slow.json" | tee aggregate-1.json; then | |
break | |
fi | |
sleep 2 | |
done | |
expect=10 | |
actual="$(jq length aggregate-1.json)" | |
# Later we expect 10 SNSs. Make sure that when we do, it's because we | |
# actually collected the data and it wasn't preloaded from the | |
# snapshot. | |
(( actual < expect )) || { | |
echo ERROR: Should not yet have $expected SNS before collecting. | |
scripts/sns/aggregator/get_log | |
} | |
- name: Verify that configuration is as provided | |
run: scripts/sns/aggregator/test-config | |
- name: Make the aggregator collect data quickly | |
run: dfx canister call sns_aggregator reconfigure '(opt record { update_interval_ms = 100; fast_interval_ms = 1_000_000_000; })' | |
- name: Wait for the aggregator to get data | |
run: sleep 120 | |
# sleep time > 12 SNS & 2 block heights each + a few extra calls. | |
# TODO: The aggregator can be installed and populated in the saved state, so this sleep is not needed. | |
- name: Get the latest data from the sns aggregator | |
run: | | |
AGGREGATOR_CANISTER_ID="$(dfx canister id sns_aggregator)" | |
curl -Lf "http://${AGGREGATOR_CANISTER_ID}.localhost:8080/v1/sns/list/latest/slow.json" | tee aggregate-1.json | |
expect=10 | |
actual="$(jq length aggregate-1.json)" | |
(( expect == actual )) || { | |
echo ERROR: Expected to have $expect SNS in the aggregator but found $actual. | |
scripts/sns/aggregator/get_log | |
} | |
- name: Test the paginated endpoint | |
run: scripts/sns/aggregator/test-pagination --num 12 | |
- name: Get logs | |
run: | | |
scripts/sns/aggregator/get_log > ,logs | |
LOG_LINES="$(wc -l <,logs)" | |
(( LOG_LINES > 10 )) || { | |
echo "ERROR: Expected a non-trivial number of lines to have been logged by now but found only ${LOG_LINES}" | |
cat ,logs | |
exit 1 | |
} | |
- name: Upgrade the aggregator to self with a slow refresh rate | |
run: dfx canister install --mode upgrade --wasm sns_aggregator_dev.wasm.gz --upgrade-unchanged sns_aggregator '(opt record { update_interval_ms = 1_000_000_000; fast_interval_ms = 1_000_000_000; })' --yes | |
- name: Expect the paginated data to be retained over the upgrade | |
run: scripts/sns/aggregator/test-pagination --num 12 | |
- name: Expect the latest data to be retained over the upgrade | |
run: | | |
AGGREGATOR_CANISTER_ID="$(dfx canister id sns_aggregator)" | |
curl -Lf "http://${AGGREGATOR_CANISTER_ID}.localhost:8080/v1/sns/list/latest/slow.json" | tee aggregate-1.json | |
expect=10 | |
actual="$(jq length aggregate-1.json)" | |
(( expect == actual )) || { | |
echo ERROR: Expected to have $expect SNS in the aggregator but found $actual. | |
} | |
- name: Expect the upstream data to be retained over the upgrade | |
run: | | |
./scripts/sns/aggregator/get_stable_data | |
expect=12 | |
actual="$(jq '.sns_cache.upstream_data | length' stable_data.json)" | |
(( expect == actual )) || { | |
echo ERROR: Expected to have $expect SNS in the aggregator upstream data but found $actual. | |
} | |
- name: Downgrade sns_aggregator to prod and upgrade back again | |
run: | | |
set -euxo pipefail | |
git fetch --depth 1 origin tag aggregator-prod | |
diff="$(git diff tags/aggregator-prod rs/sns_aggregator .github/workflows/build.yml)" | |
if test -n "${diff:-}" | |
then ./scripts/sns/aggregator/downgrade-upgrade-test -w sns_aggregator_dev.wasm.gz --verbose | |
else echo "Skipping test as there are no relevant code changes" | |
fi | |
- name: Verify that fast data is updated fast | |
run: | | |
pushd snsdemo | |
# Install tools such as quill | |
bin/dfx-sns-demo-install | |
# Set canister IDs | |
bin/dfx-nns-import --network local | |
popd | |
scripts/sns/aggregator/test-fast | |
- name: Stop replica | |
run: dfx stop | |
assets: | |
name: "Upload assets" | |
needs: build | |
runs-on: ubuntu-20.04 | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Get docker build outputs | |
uses: actions/download-artifact@v4 | |
with: | |
name: out | |
path: out | |
- name: Print the hash of all assets | |
run: find out -type f | xargs sha256sum | |
- name: 'Record the git commit and any tags' | |
run: git log | head -n1 > out/commit.txt | |
- name: 'Upload ${{ matrix.BUILD_NAME }} nns-dapp wasm module' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: nns-dapp for ${{ matrix.BUILD_NAME }} | |
path: | | |
out/commit.txt | |
out/nns-dapp.wasm.gz | |
out/nns-dapp-arg-${{ matrix.DFX_NETWORK }}.did | |
out/nns-dapp-arg-${{ matrix.DFX_NETWORK }}.bin | |
out/frontend-config.sh | |
out/deployment-config.json | |
- name: 'Upload sns_aggregator wasm module' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: sns_aggregator for ${{ matrix.BUILD_NAME }} | |
path: | | |
out/sns_aggregator.wasm.gz | |
out/sns_aggregator_dev.wasm.gz | |
- name: Release | |
uses: ./.github/actions/release_nns_dapp | |
with: | |
assets_dir: 'out' | |
token: ${{ secrets.GITHUB_TOKEN }} | |
- name: 'Upload frontend assets' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: NNS frontend assets | |
path: | | |
out/assets.tar.xz | |
out/sourcemaps.tar.xz | |
- name: "Link the build sha to this commit" | |
run: | | |
: Set up git | |
git config user.name "GitHub Actions Bot" | |
git config user.email "<>" | |
: Make a note of the WASM shasum. | |
NOTE="refs/notes/mainnet/wasm-sha" | |
SHA="$(sha256sum < "out/nns-dapp.wasm.gz")" | |
git fetch origin "+${NOTE}:${NOTE}" | |
if git notes --ref="wasm-sha" add -m "$SHA" | |
then git push origin "${NOTE}:${NOTE}" || true | |
else echo SHA already set | |
fi | |
- name: "Verify that the WASM module is small enough to deploy" | |
run: | | |
wasm_size="$(wc -c < "out/nns-dapp.wasm.gz")" | |
max_size=3145728 | |
( | |
echo "## NNS Dapp WASM stats" | |
humreadable_size="$(numfmt --to=iec-i --suffix=B --format="%.3f" $wasm_size)" | |
humreadable_max="$(numfmt --to=iec-i --suffix=B --format="%.3f" $max_size )" | |
humreadable_ratio="$(( (wasm_size * 100) / max_size ))%" | |
humreadable_free="$(numfmt --to=iec-i --suffix=B --format="%.3f" $(( max_size - wasm_size )))" | |
echo "**WASM size:** $humreadable_size / $humreadable_max = $humreadable_ratio ($humreadable_free free)" | |
) | tee -a $GITHUB_STEP_SUMMARY | |
(( wasm_size <= max_size )) || { echo "The WASM is too large" ; exit 1 ; } | |
build-pass: | |
needs: ["build", "test-playwright-e2e-shard-1-of-2", "test-playwright-e2e-shard-2-of-2", "test-rest", "network_independent_wasm", "aggregator_test", "assets", "test-downgrade-upgrade", "test-test-account-api", "test-upgrade-map", "test-upgrade-stable", "test-upgrade-map-stable"] | |
if: ${{ always() }} | |
runs-on: ubuntu-20.04 | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: ./.github/actions/needs_success | |
with: | |
needs: '${{ toJson(needs) }}' |