diff --git a/.github/workflows/cross-platform-test.yml b/.github/workflows/cross-platform-test.yml index 46aa680c8f..612402a964 100644 --- a/.github/workflows/cross-platform-test.yml +++ b/.github/workflows/cross-platform-test.yml @@ -26,6 +26,10 @@ on: description: "Name of the langflow-sdk package artifact" required: false type: string + bundles-artifact-name: + description: "Name of the bundles package artifact (contains every src/bundles/*/ wheel)" + required: false + type: string pre_release: description: "Whether this is a pre-release build" required: false @@ -42,6 +46,7 @@ jobs: main-artifact-name: ${{ steps.set-names.outputs.main-artifact-name }} lfx-artifact-name: ${{ steps.set-names.outputs.lfx-artifact-name }} sdk-artifact-name: ${{ steps.set-names.outputs.sdk-artifact-name }} + bundles-artifact-name: ${{ steps.set-names.outputs.bundles-artifact-name }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -74,6 +79,25 @@ jobs: uv build --wheel --out-dir dist - name: Build main package run: make build_langflow args="--wheel" + - name: Build bundle wheels + id: build-bundles + run: | + shopt -s nullglob + bundles=(src/bundles/*/pyproject.toml) + if [ ${#bundles[@]} -eq 0 ]; then + echo "No bundles found under src/bundles/*/" + echo "bundles-found=0" >> $GITHUB_OUTPUT + exit 0 + fi + mkdir -p bundles-dist + BUNDLES_DIST="$PWD/bundles-dist" + for bundle_pyproject in "${bundles[@]}"; do + bundle_dir=$(dirname "$bundle_pyproject") + echo "Building wheel for $bundle_dir" + (cd "$bundle_dir" && uv build --wheel --out-dir "$BUNDLES_DIST") + done + echo "bundles-found=1" >> $GITHUB_OUTPUT + ls -la bundles-dist/ - name: Upload lfx artifact uses: actions/upload-artifact@v6 with: @@ -94,6 +118,12 @@ jobs: with: name: adhoc-dist-main path: dist + - name: Upload bundles artifact + if: steps.build-bundles.outputs.bundles-found == '1' + uses: actions/upload-artifact@v6 + with: + name: adhoc-dist-bundles + path: bundles-dist - name: Set artifact names id: set-names run: | @@ -101,6 +131,11 @@ jobs: echo "main-artifact-name=adhoc-dist-main" >> $GITHUB_OUTPUT echo "lfx-artifact-name=adhoc-dist-lfx" >> $GITHUB_OUTPUT echo "sdk-artifact-name=adhoc-dist-sdk" >> $GITHUB_OUTPUT + if [ "${{ steps.build-bundles.outputs.bundles-found }}" = "1" ]; then + echo "bundles-artifact-name=adhoc-dist-bundles" >> $GITHUB_OUTPUT + else + echo "bundles-artifact-name=" >> $GITHUB_OUTPUT + fi test-installation-stable: name: Install & Run - ${{ matrix.os }} ${{ matrix.arch }} ${{ matrix.python-version }} @@ -215,6 +250,13 @@ jobs: name: ${{ inputs.main-artifact-name || needs.build-if-needed.outputs.main-artifact-name || 'adhoc-dist-main' }} path: ./main-dist + - name: Download bundles package artifact + if: steps.install-method.outputs.method == 'wheel' && (inputs.bundles-artifact-name != '' || needs.build-if-needed.outputs.bundles-artifact-name != '') + uses: actions/download-artifact@v7 + with: + name: ${{ inputs.bundles-artifact-name || needs.build-if-needed.outputs.bundles-artifact-name }} + path: ./bundles-dist + - name: Create fresh virtual environment run: | uv venv test-env --seed @@ -278,6 +320,21 @@ jobs: fi shell: bash + - name: Install bundle packages from wheel (Windows) + if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.bundles-artifact-name != '' || needs.build-if-needed.outputs.bundles-artifact-name != '') + run: | + ls -la ./bundles-dist/ + shopt -s nullglob + BUNDLE_WHEELS=(./bundles-dist/*.whl) + if [ ${#BUNDLE_WHEELS[@]} -gt 0 ]; then + printf '%s\n' "${BUNDLE_WHEELS[@]}" + uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "${BUNDLE_WHEELS[@]}" + else + echo "No bundle wheels found in ./bundles-dist/" + exit 1 + fi + shell: bash + - name: Install main package from wheel (Windows) if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' run: | @@ -349,6 +406,21 @@ jobs: fi shell: bash + - name: Install bundle packages from wheel (Unix) + if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.bundles-artifact-name != '' || needs.build-if-needed.outputs.bundles-artifact-name != '') + run: | + ls -la ./bundles-dist/ + shopt -s nullglob + BUNDLE_WHEELS=(./bundles-dist/*.whl) + if [ ${#BUNDLE_WHEELS[@]} -gt 0 ]; then + printf '%s\n' "${BUNDLE_WHEELS[@]}" + uv pip install --prerelease=allow --python ./test-env/bin/python "${BUNDLE_WHEELS[@]}" + else + echo "No bundle wheels found in ./bundles-dist/" + exit 1 + fi + shell: bash + - name: Install main package from wheel (Unix) if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' run: | @@ -579,6 +651,13 @@ jobs: name: ${{ inputs.main-artifact-name || needs.build-if-needed.outputs.main-artifact-name || 'adhoc-dist-main' }} path: ./main-dist + - name: Download bundles package artifact + if: steps.install-method.outputs.method == 'wheel' && (inputs.bundles-artifact-name != '' || needs.build-if-needed.outputs.bundles-artifact-name != '') + uses: actions/download-artifact@v7 + with: + name: ${{ inputs.bundles-artifact-name || needs.build-if-needed.outputs.bundles-artifact-name }} + path: ./bundles-dist + - name: Create fresh virtual environment run: | uv venv test-env --seed @@ -671,6 +750,21 @@ jobs: fi shell: bash + - name: Install bundle packages from wheel (Windows) + if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.bundles-artifact-name != '' || needs.build-if-needed.outputs.bundles-artifact-name != '') + run: | + ls -la ./bundles-dist/ + shopt -s nullglob + BUNDLE_WHEELS=(./bundles-dist/*.whl) + if [ ${#BUNDLE_WHEELS[@]} -gt 0 ]; then + printf '%s\n' "${BUNDLE_WHEELS[@]}" + uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "${BUNDLE_WHEELS[@]}" + else + echo "No bundle wheels found in ./bundles-dist/" + exit 1 + fi + shell: bash + - name: Install main package from wheel (Windows) if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' run: | @@ -713,6 +807,21 @@ jobs: fi shell: bash + - name: Install bundle packages from wheel (Unix) + if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.bundles-artifact-name != '' || needs.build-if-needed.outputs.bundles-artifact-name != '') + run: | + ls -la ./bundles-dist/ + shopt -s nullglob + BUNDLE_WHEELS=(./bundles-dist/*.whl) + if [ ${#BUNDLE_WHEELS[@]} -gt 0 ]; then + printf '%s\n' "${BUNDLE_WHEELS[@]}" + uv pip install --prerelease=allow --python ./test-env/bin/python "${BUNDLE_WHEELS[@]}" + else + echo "No bundle wheels found in ./bundles-dist/" + exit 1 + fi + shell: bash + - name: Install main package from wheel (Unix) if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' run: | diff --git a/.github/workflows/docker-build-v2.yml b/.github/workflows/docker-build-v2.yml index 86f74f4a7a..68d24a4a36 100644 --- a/.github/workflows/docker-build-v2.yml +++ b/.github/workflows/docker-build-v2.yml @@ -124,7 +124,7 @@ jobs: - name: Determine version id: version run: | - version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | grep -v '^langflow-sdk' | cut -d' ' -f2 | sed 's/^v//') + version=$(uv tree 2>/dev/null | grep -E '^langflow(-nightly)?[[:space:]]' | cut -d' ' -f2 | sed 's/^v//') echo "Main version from pyproject.toml: $version" if [ ${{inputs.pre_release}} == "true" ]; then @@ -604,7 +604,7 @@ jobs: if [[ "${{ inputs.release_type }}" == "base" ]]; then version=$(uv tree 2>/dev/null | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1) else - version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | grep -v '^langflow-sdk' | cut -d' ' -f2 | sed 's/^v//') + version=$(uv tree 2>/dev/null | grep -E '^langflow(-nightly)?[[:space:]]' | cut -d' ' -f2 | sed 's/^v//') fi echo "Using version: $version" echo version=$version >> $GITHUB_OUTPUT diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4c90ad6ec3..4fd92dd6c0 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -150,7 +150,7 @@ jobs: if: ${{ inputs.main_version == '' && (inputs.release_type == 'main' || inputs.release_type == 'main-ep' || inputs.release_type == 'nightly-main' || inputs.release_type == 'main-all' || inputs.release_type == 'nightly-main-all') }} id: get-version-main run: | - version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | grep -v 'langflow-sdk' | awk '{print $2}' | sed 's/^v//') + version=$(uv tree | grep -E '^langflow(-nightly)?[[:space:]]' | awk '{print $2}' | sed 's/^v//') echo version=$version echo version=$version >> $GITHUB_OUTPUT setup: diff --git a/.github/workflows/docker-nightly-build.yml b/.github/workflows/docker-nightly-build.yml index 53a59b9269..7411e5a6b6 100644 --- a/.github/workflows/docker-nightly-build.yml +++ b/.github/workflows/docker-nightly-build.yml @@ -168,7 +168,7 @@ jobs: id: version run: | echo "Extracting main version from pyproject.toml" - version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | grep -v '^langflow-sdk' | cut -d' ' -f2 | sed 's/^v//') + version=$(uv tree 2>/dev/null | grep -E '^langflow(-nightly)?[[:space:]]' | cut -d' ' -f2 | sed 's/^v//') # Verify nightly tag format if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$ ]]; then @@ -263,7 +263,7 @@ jobs: id: version run: | echo "Extracting main version from pyproject.toml" - version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | grep -v '^langflow-sdk' | cut -d' ' -f2 | sed 's/^v//') + version=$(uv tree 2>/dev/null | grep -E '^langflow(-nightly)?[[:space:]]' | cut -d' ' -f2 | sed 's/^v//') # Verify nightly tag format if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$ ]]; then @@ -353,7 +353,7 @@ jobs: if [[ "${{ inputs.release_type }}" == "nightly-base" ]]; then version=$(uv tree 2>/dev/null | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1) else - version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | grep -v '^langflow-sdk' | cut -d' ' -f2 | sed 's/^v//') + version=$(uv tree 2>/dev/null | grep -E '^langflow(-nightly)?[[:space:]]' | cut -d' ' -f2 | sed 's/^v//') fi echo "Using version: $version" echo version=$version >> $GITHUB_OUTPUT diff --git a/.github/workflows/python_test.yml b/.github/workflows/python_test.yml index 4063fec810..40d3a6cf95 100644 --- a/.github/workflows/python_test.yml +++ b/.github/workflows/python_test.yml @@ -93,7 +93,7 @@ jobs: - name: Run unit tests uses: nick-fields/retry@v3 with: - timeout_minutes: 30 + timeout_minutes: 40 max_attempts: 2 command: make unit_tests args="-x -vv --splits ${{ matrix.splitCount }} --group ${{ matrix.group }} --reruns 5 --cov --cov-config=src/backend/.coveragerc --cov-report=xml --cov-report=html" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 504103ac4c..1cade02e7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,11 @@ on: required: false type: boolean default: false + release_bundles: + description: "Release every src/bundles/* package (required when releasing main, since main pins exact bundle versions)" + required: false + type: boolean + default: false build_docker_base: description: "Build Docker Image for Langflow Base" required: true @@ -71,6 +76,7 @@ jobs: echo "release_package_main: ${{ inputs.release_package_main }}" echo "release_lfx: ${{ inputs.release_lfx }}" echo "release_sdk: ${{ inputs.release_sdk }}" + echo "release_bundles: ${{ inputs.release_bundles }}" echo "build_docker_base: ${{ inputs.build_docker_base }}" echo "build_docker_main: ${{ inputs.build_docker_main }}" echo "pre_release: ${{ inputs.pre_release }}" @@ -163,6 +169,12 @@ jobs: exit 1 fi + if [ "${{ inputs.release_package_main }}" = "true" ] && [ "${{ inputs.release_bundles }}" = "false" ]; then + echo "Error: Cannot release Langflow Main without releasing bundles." + echo "Langflow Main pins exact bundle versions (lfx-arxiv, lfx-duckduckgo, ...). Please enable 'release_bundles' or disable 'release_package_main'." + exit 1 + fi + echo "✅ Release dependencies validated successfully." determine-base-version: @@ -245,7 +257,7 @@ jobs: - name: Determine version id: version run: | - version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | grep -v 'langflow-sdk' | awk '{print $2}' | sed 's/^v//') + version=$(uv tree | grep -E '^langflow(-nightly)?[[:space:]]' | awk '{print $2}' | sed 's/^v//') echo "Main version from pyproject.toml: $version" if [ ${{inputs.pre_release}} == "true" ]; then @@ -844,9 +856,63 @@ jobs: name: dist-main path: dist + build-bundles: + name: Build Langflow Bundles + needs: [build-lfx, determine-lfx-version] + if: | + always() && + !cancelled() && + inputs.release_bundles && + needs.build-lfx.result == 'success' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + outputs: + bundles-found: ${{ steps.build.outputs.bundles-found }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.release_tag }} + - name: Setup Environment + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: "3.13" + prune-cache: false + - name: Install the project + run: uv sync + - name: Build bundle wheels + id: build + run: | + shopt -s nullglob + bundles=(src/bundles/*/pyproject.toml) + if [ ${#bundles[@]} -eq 0 ]; then + echo "No bundles found under src/bundles/*/" + echo "bundles-found=0" >> $GITHUB_OUTPUT + exit 0 + fi + mkdir -p bundles-dist + BUNDLES_DIST="$PWD/bundles-dist" + for bundle_pyproject in "${bundles[@]}"; do + bundle_dir=$(dirname "$bundle_pyproject") + echo "Building wheel for $bundle_dir" + (cd "$bundle_dir" && uv build --wheel --out-dir "$BUNDLES_DIST") + done + echo "bundles-found=1" >> $GITHUB_OUTPUT + ls -la bundles-dist/ + - name: Upload bundles artifact + if: steps.build.outputs.bundles-found == '1' + uses: actions/upload-artifact@v6 + with: + name: dist-bundles + path: bundles-dist + test-cross-platform: name: Test Cross-Platform Installation - needs: [build-base, build-main, build-lfx, build-sdk] + needs: [build-base, build-main, build-lfx, build-sdk, build-bundles] if: | always() && !cancelled() && @@ -857,6 +923,7 @@ jobs: main-artifact-name: "dist-main" lfx-artifact-name: "dist-lfx" sdk-artifact-name: ${{ needs.build-sdk.result == 'success' && 'dist-sdk' || '' }} + bundles-artifact-name: ${{ needs.build-bundles.result == 'success' && needs.build-bundles.outputs.bundles-found == '1' && 'dist-bundles' || '' }} pre_release: ${{ inputs.pre_release }} publish-base: @@ -889,10 +956,58 @@ jobs: run: | cd src/backend/base && uv publish dist/*.whl + publish-bundles: + name: Publish Bundles to PyPI + needs: [build-bundles, test-cross-platform, ci, publish-lfx] + if: | + always() && + !cancelled() && + inputs.release_bundles && + needs.build-bundles.result == 'success' && + needs.build-bundles.outputs.bundles-found == '1' && + needs.test-cross-platform.result == 'success' && + needs.ci.result == 'success' && + (needs.publish-lfx.result == 'success' || needs.publish-lfx.result == 'skipped') + runs-on: ubuntu-latest + steps: + - name: Download bundles artifact + uses: actions/download-artifact@v7 + with: + name: dist-bundles + path: bundles-dist + - name: Setup Environment + uses: astral-sh/setup-uv@v6 + with: + enable-cache: false + python-version: "3.13" + - name: Publish bundles to PyPI + if: ${{ !inputs.dry_run }} + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + run: | + shopt -s nullglob + wheels=(bundles-dist/*.whl) + if [ ${#wheels[@]} -eq 0 ]; then + echo "No bundle wheels to publish." + exit 0 + fi + for wheel in "${wheels[@]}"; do + echo "Publishing $wheel" + uv publish "$wheel" + done + publish-main: name: Publish Langflow Main to PyPI - if: ${{ inputs.release_package_main }} - needs: [build-main, test-cross-platform, publish-base, ci] + if: | + always() && + !cancelled() && + inputs.release_package_main && + needs.build-main.result == 'success' && + needs.test-cross-platform.result == 'success' && + needs.publish-base.result == 'success' && + needs.ci.result == 'success' && + (needs.publish-bundles.result == 'success' || needs.publish-bundles.result == 'skipped') + needs: [build-main, test-cross-platform, publish-base, publish-bundles, ci] runs-on: ubuntu-latest steps: - name: Download main artifact diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index c805245a8d..61802de087 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -294,8 +294,8 @@ jobs: - name: Verify Nightly Name and Version id: verify run: | - name=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | grep -v 'langflow-sdk' | awk '{print $1}') - version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | grep -v 'langflow-sdk' | awk '{print $2}') + name=$(uv tree | grep -E '^langflow(-nightly)?[[:space:]]' | awk '{print $1}') + version=$(uv tree | grep -E '^langflow(-nightly)?[[:space:]]' | awk '{print $2}') if [ "$name" != "langflow-nightly" ]; then echo "Name $name does not match langflow-nightly. Exiting the workflow." exit 1 @@ -339,15 +339,67 @@ jobs: name: dist-nightly-main path: dist + build-nightly-bundles: + name: Build Langflow Nightly Bundles + needs: [build-nightly-lfx] + if: ${{ always() && (needs.build-nightly-lfx.result == 'success' || inputs.build_lfx == false) }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash + outputs: + bundles-found: ${{ steps.build.outputs.bundles-found }} + steps: + - name: Check out the code at a specific ref + uses: actions/checkout@v6 + with: + ref: ${{ inputs.nightly_tag_release }} + persist-credentials: true + - name: "Setup Environment" + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: ${{ env.PYTHON_VERSION }} + prune-cache: false + - name: Install the project + run: uv sync + - name: Build bundle wheels + id: build + run: | + shopt -s nullglob + bundles=(src/bundles/*/pyproject.toml) + if [ ${#bundles[@]} -eq 0 ]; then + echo "No bundles found under src/bundles/*/" + echo "bundles-found=0" >> $GITHUB_OUTPUT + exit 0 + fi + mkdir -p bundles-dist + BUNDLES_DIST="$PWD/bundles-dist" + for bundle_pyproject in "${bundles[@]}"; do + bundle_dir=$(dirname "$bundle_pyproject") + echo "Building wheel for $bundle_dir" + (cd "$bundle_dir" && uv build --wheel --out-dir "$BUNDLES_DIST") + done + echo "bundles-found=1" >> $GITHUB_OUTPUT + ls -la bundles-dist/ + - name: Upload bundles artifact + if: steps.build.outputs.bundles-found == '1' + uses: actions/upload-artifact@v6 + with: + name: dist-nightly-bundles + path: bundles-dist + test-cross-platform: name: Test Cross-Platform Installation - needs: [build-nightly-lfx, build-nightly-base, build-nightly-main] + needs: [build-nightly-lfx, build-nightly-base, build-nightly-main, build-nightly-bundles] uses: ./.github/workflows/cross-platform-test.yml with: base-artifact-name: "dist-nightly-base" main-artifact-name: "dist-nightly-main" lfx-artifact-name: "dist-nightly-lfx" sdk-artifact-name: "dist-nightly-sdk" + bundles-artifact-name: ${{ needs.build-nightly-bundles.outputs.bundles-found == '1' && 'dist-nightly-bundles' || '' }} publish-nightly-lfx: name: Publish LFX Nightly to PyPI @@ -434,9 +486,47 @@ jobs: run: | make publish base=true + publish-nightly-bundles: + name: Publish Bundle Nightlies to PyPI + needs: [build-nightly-bundles, test-cross-platform, publish-nightly-lfx] + if: ${{ always() && needs.build-nightly-bundles.result == 'success' && needs.build-nightly-bundles.outputs.bundles-found == '1' && needs.test-cross-platform.result == 'success' && (needs.publish-nightly-lfx.result == 'success' || inputs.build_lfx == false) }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.nightly_tag_release }} + persist-credentials: true + - name: Download bundles artifact + uses: actions/download-artifact@v7 + with: + name: dist-nightly-bundles + path: bundles-dist + - name: Setup Environment + uses: astral-sh/setup-uv@v6 + with: + enable-cache: false + python-version: "3.13" + - name: Publish bundles to PyPI + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }} + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + run: | + shopt -s nullglob + wheels=(bundles-dist/*.whl) + if [ ${#wheels[@]} -eq 0 ]; then + echo "No bundle wheels to publish." + exit 0 + fi + for wheel in "${wheels[@]}"; do + echo "Publishing $wheel" + uv publish "$wheel" + done + publish-nightly-main: name: Publish Langflow Main Nightly to PyPI - needs: [build-nightly-main, test-cross-platform, publish-nightly-base] + needs: [build-nightly-main, test-cross-platform, publish-nightly-base, publish-nightly-bundles] + if: ${{ always() && needs.build-nightly-main.result == 'success' && needs.test-cross-platform.result == 'success' && needs.publish-nightly-base.result == 'success' && (needs.publish-nightly-bundles.result == 'success' || needs.publish-nightly-bundles.result == 'skipped') }} runs-on: ubuntu-latest steps: - name: Checkout code