Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate issues remain in Conda locking, dependency handling, runtime setup, and resolver integration.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds custom Conda environment.yml support with micromamba resolution, explicit locks, and provider-specific installation.
Changes:
- Extends schema and validation for Conda environments.
- Adds Conda solving and lock generation.
- Integrates Conda builds across managed providers and updates conformance checks.
File summaries
| File | Summary |
|---|---|
tests/test_environment_spec.py |
Tests publication rules. |
tests/test_environment_resolve_conda.py |
Tests Conda resolution. |
tests/test_environment_modal_builder.py |
Tests Modal Conda builds. |
tests/test_environment_managed_builders.py |
Tests managed capabilities. |
tests/test_environment_e2b_builder.py |
Tests E2B Conda builds. |
tests/test_environment_daytona_builder.py |
Tests Daytona Conda builds. |
tests/test_environment_datalayer_builder.py |
Tests Datalayer Conda builds. |
tests/test_environment_conformance.py |
Tests GPU gating. |
schemas/environment-v1alpha1.json |
Adds Conda source support. |
code_sandboxes/environments/spec.py |
Adds Conda validation and publication checks. |
code_sandboxes/environments/resolve.py |
Routes Conda resolution. |
code_sandboxes/environments/resolve_conda.py |
Implements Conda parsing, solving, and locks. |
code_sandboxes/environments/conformance.py |
Gates accelerator conformance checks. |
code_sandboxes/environments/adapters/modal.py |
Installs Conda locks on Modal. |
code_sandboxes/environments/adapters/managed.py |
Adds dependency-format capabilities. |
code_sandboxes/environments/adapters/e2b.py |
Installs Conda locks on E2B. |
code_sandboxes/environments/adapters/daytona.py |
Installs Conda locks on Daytona. |
code_sandboxes/environments/adapters/datalayer.py |
Builds Conda locks with Docker. |
Review details
Suppressed comments (5)
code_sandboxes/environments/adapters/managed.py:223
build.source == "dependencyFile"is allowed to reach capability validation before full spec validation, butspec.build.dependency_filecan still beNone. This dereference raisesAttributeErrorinstead of returning a capability report for a draft such as{"source": "dependencyFile"}; guard the missing file before readingsource_format.
source_format = spec.build.dependency_file.source_format
if source_format in self.dependency_formats:
return []
code_sandboxes/environments/resolve.py:1264
- This branch passes only the YAML content and Python version, so the conda path drops the existing package contract:
spec.packages.python.indexesand user constraints are not applied to the YAMLpip:layer, andspec.packages.system.aptis never resolved into the lock. Private pip requirements therefore fall back to the default index, while requested apt packages are silently omitted from every builder.
return resolve_conda_environment(
environment_yml=dependency_file.content,
python_version=environment.spec.language.version,
resolved_bases=resolved_bases,
credential=credential,
code_sandboxes/environments/resolve_conda.py:705
- This resolver has no apt input or output. When
resolve_environmentselects the conda path,spec.packages.system.aptis never pinned or recorded, soapt_pins_in()sees nothing and every adapter silently omits requested system packages. Resolve apt packages in the same base or explicitly reject apt packages for conda sources.
def resolve_conda_environment(
*,
environment_yml: str,
python_version: str,
resolved_bases: Mapping[str, str],
platform: str = CONDA_PLATFORM,
code_sandboxes/environments/resolve_conda.py:731
merge_conda_piponly receives the parsedpip:entries;spec.packages.python.constraintsnever reaches this path. A conda source can therefore bypass user constraints and protected-package conflicts that the normal resolver would merge or reject. Pass the Python constraints through the conda merge/resolve path.
environment = parse_conda_environment(environment_yml)
merged = merge_conda_pip(environment)
for note in merged.notes:
say(note)
rendered = rendered_environment(environment, merged, python_version=python_version)
code_sandboxes/environments/resolve_conda.py:737
CondaResolveRequesthas no index field, and the runners only setPIP_FIND_LINKS. Thuspip:dependencies inenvironment.ymlignorespec.packages.python.indexes(including private indexes) and resolve through pip's ambient/default index instead of the spec. Carry the indexes into the request and configure the pip step with them.
request = CondaResolveRequest(
environment_yml=rendered,
python_version=python_version,
platform=platform,
base_reference=solving_in,
- Files reviewed: 18/18 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The pooled kernel manager moved to the public jupyter-kernels package (jupyter_kernels.pool.mapping.PooledMappingKernelManager, published as 1.2.23). PyPI serves it, so the protected pin resolves from the index and its wheel is dropped from the wheelhouse. Update the contract pin, the wheelhouse README, and the constraint/resolve tests.
…he contract's workdir
Both found by the r1 geospatial drill on 2026-09-16 — the first one
whose smoke test ran the Appendix B core tier for real against a built
artifact. Seven of nine checks passed; these are the two that did not.
- check 7 read "state survived the restart ('True')".
CodeSandboxClient.restart() was stop() + start(), which destroys and
recreates a sandbox this process owns but only drops the websocket of
one attached to somebody else's Jupyter server — the kernel process
keeps running and the reconnect lands in the same interpreter.
JupyterServerSandbox.restart_kernel() now asks the server's own
POST /api/kernels/{id}/restart, as _do_interrupt already does.
- check 2 read "cwd is '/home/datalayer', not '/home/datalayer/content'".
The image declares WORKDIR there and the contract's User row requires
it, but a kernel's cwd is the Jupyter server's to choose and
jupyter-python roots it at $HOME. The contract layer now sets
MappingKernelManager.root_dir, which moves the kernel without moving
the file browser. Channel repinned to sha256:122d3e31f5e2....
Also re-pinned the channel digest and apt snapshot in one place: they
were duplicated across two test files and three base releases had left
both red rather than catching anything.
1049 environment/client/jupyter-server tests pass; pre-commit clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… shared engine contents_build is a first-class field on the environment build spec, baked through the same build_commands engine as the spec's uploaded files (build_entries/files_step) on every provider adapter — verified fetch, checksum that fails the build, environment-contents.json manifest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`attest_artifact` took `size_bytes` from its caller and nobody ever passed one — the builder answers a reference, not a weight — so every artifact was recorded with `sizeBytes: null`, and `environments.artifact.bytes`, the series section 14 tracks the artifact size in, had no point in it although artifacts had been recorded. Seen on r1 on 2026-09-16, reading the section 14 SLOs back through the OTEL query API (PLAN_ENVS.md E1-25). `Attestor.size_of()` asks the registry with the client the scan is already read from. A size that cannot be read is logged, not raised: a missing number on a dashboard is no reason to refuse an artifact that is otherwise signed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lock's header carried `# resolved-at:`, and its digest is over the whole text, so two resolves of the same spec in the same base pinning the same 320 packages produced two different digests. Found by resolving one environment twice on r1: the texts differed in exactly that line, one of 5,388. Section 5's cache key is over the lock digest, so D-12's build cache could never hit — and it never had, `hit=false` twelve times out of twelve. When a lock was resolved is on the lock document Runtimes stores, in `created_at`. The two tests that asserted determinism did it by freezing the clock. They assert it without one now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A published version's page says it shows the licences its SBOM names, and every publication froze an empty list instead: the snapshot read them from the scan summary, and the registry's scanner reports vulnerabilities, not licences. There was nowhere for them to come from. `licenses_of` reads both shapes the ecosystem writes — SPDX, which is what BuildKit's `attest:sbom=` produces, and CycloneDX — taking a concluded licence over a declared one and treating SPDX's NOASSERTION as the non-answer it is. A document it does not understand names nothing rather than raising: a licence list is worth having and never worth failing a build over. `attest` takes the document and freezes what it found onto the artifact, so a publication carries it without reading anything at publish time. Still missing, and it needs registry access this machine does not have: the fetch of the SBOM itself. BuildKit pushes it as an OCI attestation in the image index, while `sbom_ref` is a constructed `…@digest.sbom` string that probably does not resolve, and the blob read is the Docker Registry HTTP API rather than boto3. Until a caller passes the document, the list stays empty — as it already was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each chained builder call leaves an image with an id of its own, and deleting
the artifact does not delete them. Modal offers no call that lists an
account's images — ImageGetOrCreate, ImageFromId, ImageGetByTag,
ImageListTags, ImageTagRevisions, ImagePublish and ImageDelete, and nothing
that enumerates — so an intermediate nobody writes down at build time can
never be found again. That is why E2-05 says `delete` removes "the recorded
intermediates", and it is why this records rather than discovers.
Confirmed against a real Modal account, 2026-09-17:
- `Image.build` hydrates an `object_id` on every image in `deps()`, not only
on the last, so the whole chain is readable once the build finishes.
- `ImageDelete` removes one, and the image is `NotFound` afterwards.
- The bottom of a chain can be an image the workspace does not own: Modal's
own `debian_slim` answers PermissionDenied. An image somebody else owns
was never this artifact's to collect, so it is logged and stepped over.
`ArtifactReference` gained `intermediates`; `build` records them; `delete`
removes them before the artifact, since an intermediate is only reachable
while the record naming it survives. Already-gone is success, the way a
replayed collection has to be. 7 tests, and modal's `delete` leaves the
not-built-yet list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A managed artifact exists only in the account its credential opens, so a managed build needs the owner's own E2B, Daytona or Modal keys — and nothing could fetch them, which is why every managed build refused. `resolve_provider_credential` asks IAM by variant and owner, the same shape `resolve_build_secret` already uses, and answers the environment names the provider's own SDK reads, which is what `BuildCredential` carries and every adapter already reads. Both a JSON value and base64 of that JSON are accepted: the secret routes say clients encode values and nothing enforces it, and a credential that cannot be read is a build that cannot run. An owner with no credential is refused by name and not retried. Never a fallback to whatever keys the worker holds: that would put one owner's artifact in whatever account the deployment happens to have, which is the one thing D-8 exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…D-8) The first version of this invented a convention — one secret per provider holding a JSON blob — beside one that already works. The owner keeps DAYTONA_API_KEY, E2B_API_KEY, MODAL_TOKEN_ID and MODAL_TOKEN_SECRET as ordinary secrets named after the environment variables their own SDKs read, which is exactly the shape a build credential carries. So IAM gathers the credential from those and answers a mapping, decoded, and the resolver takes it as given: the worker is not asked to know how a secret is stored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
E2-01's remaining half, which is what kept every managed build from running at all: `resolve_provider_credential` reads the keys an owner already keeps — DAYTONA_API_KEY, E2B_API_KEY, MODAL_TOKEN_ID, MODAL_TOKEN_SECRET — so a build runs in their own account and never in whatever account the worker happens to hold. Modal records the intermediate layers a build leaves and `delete` collects them, because Modal has no call that lists an account's images: an intermediate nobody writes down can never be found again. Licences are read from an SBOM (SPDX and CycloneDX) for a publication to carry; the registry's scanner reports vulnerabilities, not licences. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`build.source: dockerfile` had nowhere to put the file, so nothing could read it and no variant could say whether it would honour it. `DockerfileSpec` carries the text inline, the way `DependencyFileSpec` carries a requirements.txt: a file the author brings lives in the spec, so the contract and every capability report read it before anything is queued. The bounded upload E3-03 also describes is for the build *context* — the extra files a COPY needs — which `validate_build_context` already checks. All three managed variants take a Dockerfile, each through its own door, and each declares what its own builder will not honour: E2B VOLUME EXPOSE HEALTHCHECK SHELL ONBUILD STOPSIGNAL LABEL MAINTAINER Modal ONBUILD STOPSIGNAL VOLUME Daytona none beyond the contract's own E2B's list is read from its SDK rather than guessed: `e2b.template.dockerfile_parser` branches on FROM, RUN, COPY, ADD, WORKDIR, USER, ENV, ARG, CMD and ENTRYPOINT, and for anything else **prints `Unsupported instruction` and carries on** — so a template built from a Dockerfile naming one comes back without it and reports success. That is the case a capability report exists for. Daytona hands the text to a real Docker builder, so Docker's grammar is its limit. Every refusal names its line, at `validate`, because a Dockerfile is somebody's file and "it was refused" is not a reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first real Daytona build answered "Failed to create snapshot: Region not found". `region_id` was `request.region` — *this platform's* region, `r1` — which Daytona has never heard of. The region that scopes a snapshot is Daytona's, and the owner names it in `compatibility.regions`, which `validate` already refuses more than one of. With none named the field is left out and the account's own default decides, which is what every snapshot in a real Daytona account already has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he spec The first real Daytona build answered "Failed to create snapshot: Region not found": `region_id` was this platform's region, `r1`, which Daytona has never heard of. The region that scopes a snapshot is Daytona's own, named in `compatibility.regions`; with none named the field is left out and the account's default decides, which is what every snapshot in a real account has. `build.source: dockerfile` also gained somewhere to put the file — `build.dockerfile.content`, inline the way a requirements.txt already travels — so the contract and every capability report read it before anything is queued. All three managed variants take a Dockerfile now, each refusing what its own builder will not honour: E2B's list is read from its own parser, which prints "Unsupported instruction" and carries on, so a template would otherwise come back missing what the Dockerfile asked for and report success. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first real Daytona build succeeded at the provider and then failed on our own side: "`51d10ab0-d98d-4117-bdb5-918e98646c92` is not a digest in a repository, so it cannot be attested". The snapshot was built, live in the owner's account, and the build was recorded as failed. `attest_artifact` demanded `registry/repository@sha256:…` of every variant. That is what a Datalayer artifact is: it lives in this platform's registry, the scanner reads it there, cosign signs that digest, and the Operator refuses to start what is unsigned. A managed artifact is none of those things — it lives in the owner's own provider account (D-8) and is named the way that provider names it: a Daytona snapshot uuid, an E2B build id, a Modal `im-…`. There is nothing in ECR to scan or sign, and no Operator starting it. So a managed variant records its artifact without a scan or a signature, and the digest check stays where it means something. Publishing is not weakened: E2-15's gate requires the *Datalayer* artifact to have passed its scan, and that one is still attested. The two tests that pinned the old behaviour asked it of `modal` and `e2b` — the variants this no longer applies to — and now ask `datalayer`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first real Daytona build succeeded at the provider and then failed on our own side, because `attest_artifact` demanded an OCI digest of every variant. The snapshot was built and live in the owner's account; the build was recorded as failed. D-11 is about the Datalayer artifact: it lives in this platform's registry, the scanner reads it there, cosign signs that digest, and the Operator refuses to start what is unsigned. A managed artifact lives in the owner's own provider account and is named the way that provider names it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
E2-04's own `Done when` asks for "a sandbox launched from its id passes the core tier", and `smoke_test` refused through `ManagedBuilder` — while the build workflow calls that step. So **no Daytona build could reach `succeeded`**: attempt six built a snapshot, left it live in the owner's account, and recorded the build failed. Launched by id, never by name: a Daytona sandbox record keeps the snapshot's name, a name is republished, and only the id says which artifact ran (E0-04). Restarted by stopping and starting the sandbox rather than a kernel, because Daytona's own daemon is PID 1 here — the stronger version of check 8's question. Deleted whether the tier passed or not: a smoke test that leaves a sandbox running bills the owner for a check. The seam widened to carry what the core tier needs and an artifact does not: the Python version the spec declared and the packages its lock pinned. The caller has both, having read them to build, so it hands them over rather than every adapter fetching them again. `expected_packages` moves to `conformance`, where `run_core_tier` already lives, so the worker and a builder ask the same question the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n finish `smoke_test` refused through `ManagedBuilder` while the build workflow calls that step, so no Daytona build could reach `succeeded` — a snapshot was built, left live in the owner's account, and the build recorded failed. Launched by id rather than name, restarted by stopping and starting the sandbox (Daytona's daemon is PID 1, so there is no kernel to restart), and deleted whether the core tier passed or not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `Sandbox` reads the environment and the name from its `SandboxConfig`. Passed as bare keywords they land in its `**kwargs` and are dropped there in silence, so `create` answers `running` for a default sandbox under a generated name and says nothing about either. Found against a real Daytona account on 2026-09-17, while looking for a way to launch a repaired artifact: datalayer sandboxes create daytona -e eric/daytona-drift -n envs-drill-launch answered `running`, launched `daytonaio/sandbox:0.8.0` with a blank name — and `-e this-environment-does-not-exist-at-all` did exactly the same. The provider lists `daytona-gpu`, so a person can ask for a GPU by the name the tool itself printed and be handed a CPU sandbox with no indication. `DatalayerSandboxManager` had learned this once, in the words of its own `create`. Every other manager still had the hole, so the fix is in the base class: `_configure` moves the options onto the config, and an environment the provider does not ship is refused naming what it does ship. Daytona applies what it takes besides a card — `daytona-gpu-spot` differs from `daytona-gpu` by preemptible capacity, which nothing on the config carries. One spelling: `environment`, as `SandboxConfig` spells it. The `environment_name` alias is gone from the create path, from the Datalayer manager that carried it and from the test that parametrized over both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…itted `_secret_mount` renders `mountAs: env` as `--mount=type=secret,env=NAME`. That key does not exist before frontend 1.10, and the Dockerfile pinned `docker/dockerfile:1.7`, so BuildKit answered error: failed to solve: unexpected key 'env' in 'env=TILES_LICENSE_KEY' and failed the whole build. The mount and the pin were written against each other's assumptions and never ran together, because until today no build had ever carried a build secret. Checked against both frontends before changing it: 1.7 rejects the key, 1.10 accepts it and the value arrives in the variable. The pin is now one named constant with one reason — raising it is a change to every Datalayer build — and a test reads the version back out of the rendered Dockerfile and refuses anything below the minimum, so the two cannot drift apart again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ockerfile validate kept its own copy of the sources it builds, which never learned `dockerfile`: on r1 the capability set said yes and validate said "does not build `dockerfile` yet" to the same version. One list now.
…rved; the site builds again
…eck 5 reads a dependency file - locked_versions reads the version of a direct reference to a wheel from its file name. uv hashes a wheel only when it fetched it by URL, so a pyproject export pins the fork as jupyter-server @ https://...whl, with no ==, and the protected-pin check found it unlocked. - The pyproject check runs uv with its cache in the scratch directory and Python downloads off: the durable worker's user has no home. - Check 5 was handed spec.packages alone, which a dependencyFile source leaves empty, so it passed a requirements.txt build without importing anything. It now reads the file, or [project].dependencies. - The specification page says which sources build where.
- bases: datalayer/python-cuda:2026.09 resolves to the digest its release pushed (jupyter-python-cuda 0.3.1, CUDA 12.8 pinned), and records its CUDA. - spec: a GPU version's accelerator.cuda must be its base's. - Daytona: the spec's GPU type and count are baked into the snapshot, and a GPU Daytona does not offer is refused at validate, naming the ones it does. - DaytonaSandbox: a sandbox of a GPU snapshot is ephemeral, as Daytona requires of every GPU sandbox; it reads the snapshot's gpu. - Check 11 compares the image's toolkit with the spec, not the driver's highest CUDA, requires the driver to be new enough, and counts the GPUs; the Daytona smoke test adds it for a GPU version, where nothing ran it.
The build step calls a builder's cancel and lets the build's thread finish unheard, and the Daytona builder had no cancel: the snapshot kept building and was recorded by nobody (found by E2-14's drill, dl-backfill-drill-v2). cancel deletes the snapshot by the build's own name, found by its id; one not made yet is deleted by build the moment Daytona hands it back.
…or it Daytona keeps a refused snapshot, in error, under the build's name: one over its 20 GB limit was left there (E2-17). The build looks it up by its own name and deletes it by id before reporting the failure; cancel shares the lookup.
…t run fails it With no secrets set every leg skipped, and the job reported success in under a second every night while it tested nothing. Each leg's outcome goes in the job summary, as the header already promised, and a skipped or absent Daytona leg, the one the matrix must prove, fails the job naming the secrets.
Daytona's SDK 0.205 reads its deprecated DaytonaConfig.server_url inside Daytona(config), and with warnings as errors every live Daytona test failed before reaching Daytona; a nightly that skipped every leg hid it. With it ignored, the leg passes against the real provider: build, launch from the snapshot, the core tier, and the snapshot deleted.
B017: the contents-build test named no exception, so any failure passed it; it expects DL_ENV_SPEC_INVALID naming the missing sha256. ruff-format's own wrapping of two test lines.
The repository holds DATALAYER_API_KEY alone. The providers' keys are Datalayer secrets of its account, read from IAM at run time, each masked before it reaches the job's environment; the AWS pair is a key of the ECR base reader, which can only pull the approved base.
On Modal a GPU is a launch option, not part of the image: a GPU version builds the image any version builds, on the CUDA base; its accelerator names one of Modal's GPUs, checked at validate; and the smoke test launches the image on it (gpu="L4", "H100:2") and adds check 11. Live on an L4: the core tier and check 11 pass. A real artifact also passes all nine checks with a secret for check 9 to scan for, so the break recorded after check 8 no longer reproduces and the live matrix's Modal leg drops its xfail.
…roxy (E1-06) DATALAYER_BUILDKIT_PROXY (or proxy=) gives the Datalayer builder and both BuildKit resolvers the proxy buildkitd's own pod runs, as the frontend's predefined proxy build args: a RUN step sees them without an ARG, they stay out of the image's config and history, and they change no cache key.
…CMD (E2-05) Modal keeps the base's CMD, start-jupyter.sh, under the builder's ENTRYPOINT, so a sandbox started with no command ran a Jupyter server as its main process. It exits within a minute and ends the sandbox: the smoke test's restarted sandbox, started warm, died between checks 8 and 9. The artifact's CMD is now sleep infinity, and ModalSandbox names it when it launches an artifact, which also covers images built before.
…a finding (E2-19)
A failed build, resolve or conda solve reads its log for a refused tunnel,
as each tool writes it through the chart's own Squid: pip, uv, micromamba,
git, curl without -f, wget, and buildkitd's own pulls. The host is the one
the line names or else the last its BuildKit step named. The failure names
it, with detail.findings [{kind: egress_refused, subject: host}], and a
refused index or channel is no longer a retryable provider error.
The fixtures are real buildctl logs from moby/buildkit v0.33.0 through
ubuntu/squid 6.6 with the chart's squid.conf.
sandbox-contract/v1 put a sandbox in `/home/datalayer/content`, a directory below the home the platform mounts folders into: `pwd` and the file browser disagreed, and code that wrote a relative path wrote it where nobody looked. The working directory is now `/home/datalayer` itself, across the contract, the four adapters, the doctor and the generated reference.
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.