-
Notifications
You must be signed in to change notification settings - Fork 15
refactor(PLATENG-800): replace platform-sdk-fetch with integration-sdk-http-client #1188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tokio-on-jupiter
wants to merge
64
commits into
main
Choose a base branch
from
feat/PLATENG-800-replace-lifeomic-alpha
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
40618dd
feat(PLATENG-800): Replace @lifeomic/alpha with @jupiterone/platform-…
tokio-on-jupiter ad5d7a0
feat: Create standalone canary release workflow for public repo
tokio-on-jupiter d82c3d9
refactor: Address Copilot review feedback
tokio-on-jupiter 7bef637
security: Add authorization checks and environment protection
tokio-on-jupiter 0c31352
refactor: Use jq direct file read instead of cat pipe
tokio-on-jupiter 51bebd5
fix: Update platform-sdk-fetch to released version and fix compression
tokio-on-jupiter af21390
fix: Remove GitHub packages registry override from canary workflow
tokio-on-jupiter 48619c7
fix: Resolve TypeScript errors with bunyan logger method spreads
tokio-on-jupiter b7442be
fix: resolve TypeScript build errors from platform-sdk-fetch types
tokio-on-jupiter b5e27f4
test: update tests to remove axios dependency
tokio-on-jupiter d60c2d1
fix(canary): resolve backtick escaping issue in comment script
tokio-on-jupiter fb443fc
fix(canary): compact JSON output to avoid multiline string issues
tokio-on-jupiter 1a7aeae
fix: Use platform-sdk-fetch canary with rawBody support
tokio-on-jupiter f0af857
chore: trigger new canary release for rawBody fix
tokio-on-jupiter e814783
fix: Default compressUploads to true to match Alpha behavior
tokio-on-jupiter 3a8c1a3
chore: Update platform-sdk-fetch to 6.0.5-canary-490-1.0
tokio-on-jupiter a905dab
fix: Address Copilot review comments
tokio-on-jupiter 4bc1694
chore: Update package-lock.json for new canary version
tokio-on-jupiter 8972ea7
chore: Update platform-sdk-fetch to released version 6.0.5
tokio-on-jupiter 73a0dda
Address Copilot review feedback
tokio-on-jupiter 9fc2a04
Update packages/integration-sdk-testing/src/logger.ts
tokio-on-jupiter 11264b4
Fix npm install by overriding platform-sdk-logging version
tokio-on-jupiter e4af020
Fix npm auth in CI workflow
tokio-on-jupiter bfcf5a8
Fix test mocks to return proper RequestClientResponse objects
tokio-on-jupiter 1502eda
Fix remaining test mock type issues
tokio-on-jupiter 642fe86
fix: pin transitive dependencies and update tests for compression def…
tokio-on-jupiter bc290c0
fix(canary): use run_id for unique canary version and commit before p…
tokio-on-jupiter a9bc0fa
fix: address PR review comments
tokio-on-jupiter 80eb827
fix: address mscottford review feedback
tokio-on-jupiter dabba5a
feat: add createMockApiClient utility to integration-sdk-testing
tokio-on-jupiter 9352b6b
fix: address PR review comments (items 1-6)
tokio-on-jupiter 5ba653b
fix: remove all as any violations from PR diff
tokio-on-jupiter 36c3e56
fix: replace err: any with unknown in cleanRequestError
tokio-on-jupiter 5e020e4
fix: add skipLibCheck to tsconfig.json files for CI build compatibility
tokio-on-jupiter 47cae91
fix: use isRequestClientError type guard in cleanRequestError
tokio-on-jupiter 3792b7b
fix: warn instead of throw for deprecated alphaOptions/proxyUrl
tokio-on-jupiter 654b394
fix: use process.emitWarning instead of console.warn
tokio-on-jupiter 6a2b879
fix: upgrade fast-xml-parser to 5.3.4 for CVE-2026-25128
tokio-on-jupiter e02282b
refactor: add mockApiClient subpath export to avoid polly conflict
tokio-on-jupiter 93d2f56
Update package.json
tokio-on-jupiter 07a2892
refactor: migrate from Lerna to NX build system
tokio-on-jupiter 4831886
fix: retain lerna.json for canary workflow compatibility
tokio-on-jupiter cfc3954
chore: remove unnecessary npm overrides
tokio-on-jupiter b4cfd98
feat(runtime): add JupiterOneApiClient scaffold with constructor and …
tokio-on-jupiter 924ef34
feat(runtime): add post/get methods with response mapping and rawBody…
tokio-on-jupiter 23dc4e1
test(runtime): add gzip rawBody tests for JupiterOneApiClient
tokio-on-jupiter 4614506
refactor(runtime): replace platform-sdk-fetch with JupiterOneApiClient
tokio-on-jupiter 7e5148a
chore(runtime): remove platform-sdk-fetch dependency
tokio-on-jupiter f11f976
fix(runtime): remove unnecessary await on sync getAuthorizationHeaders
tokio-on-jupiter d41679a
fix(testing): remove stale ts-expect-error directive in logger
tokio-on-jupiter d0792f1
fix(cli): replace platform-sdk-fetch type imports with runtime ApiClient
tokio-on-jupiter 3084c95
fix(build): add http-client to runtime tsconfig references and fix bu…
tokio-on-jupiter de71780
fix(cli): remove stale platform-sdk-fetch reference from test comment
tokio-on-jupiter aa95cc1
fix(deps): bump axios 1.13.4 → 1.13.5 (CVE-2026-25639)
tokio-on-jupiter e5c68af
refactor(runtime): address PR review feedback
tokio-on-jupiter 2f9e32c
pinned nx to 22.5.3
tokio-on-jupiter 0eef9df
fix(deps): regenerate lockfile for nx 22.5.3 with npm 10
tokio-on-jupiter d2adc8e
fix(deps): pin minimatch to 10.2.4 (CVE-2026-27903, CVE-2026-27904)
tokio-on-jupiter 24dafad
fix(deps): align @sinclair/typebox to 0.32.30 across workspace
tokio-on-jupiter 0291c7d
chore: format files to pass prettier check
tokio-on-jupiter 2b8ebe0
fix(runtime): address PR review — restore retryOptions/proxyUrl, refa…
tokio-on-jupiter 1a5eb80
fix(ci): replace lerna with nx release in canary workflow
tokio-on-jupiter aa25d72
fix(runtime): remove all as-any casts and eslint-disable overrides fr…
tokio-on-jupiter 2d8cded
fix(runtime): remove unnecessary async from TestableApiClient overrid…
tokio-on-jupiter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
tokio-on-jupiter marked this conversation as resolved.
Show resolved
Hide resolved
|
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,10 @@ jobs: | |
| node-version: '20' | ||
| registry-url: 'https://registry.npmjs.org' | ||
|
|
||
| - name: Configure npm authentication | ||
| run: | | ||
| echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" >> .npmrc | ||
|
|
||
tokio-on-jupiter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - name: Install dependencies | ||
| run: npm ci --include=optional | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed — will add |
||
| env: | ||
|
|
@@ -106,72 +110,109 @@ jobs: | |
| git config --global user.email "automation@jupiterone.com" | ||
| git config --global user.name "Automation" | ||
|
|
||
| - name: Get changed packages | ||
| - name: Detect affected packages | ||
| id: changed | ||
| run: | | ||
| # Get list of packages changed since main (compact JSON for single-line output) | ||
| CHANGED=$(npx lerna changed --json 2>/dev/null | jq -c '.' || echo "[]") | ||
| echo "Changed packages: $CHANGED" | ||
|
|
||
| # Output as single line (no heredoc needed for compact JSON) | ||
| echo "packages=$CHANGED" >> $GITHUB_OUTPUT | ||
| AFFECTED=$(npx nx show projects --affected --base=origin/main --json 2>/dev/null || echo "[]") | ||
| echo "Affected packages: $AFFECTED" | ||
| echo "packages=$AFFECTED" >> $GITHUB_OUTPUT | ||
|
|
||
| # Check if there are any changed packages | ||
| if [ "$CHANGED" = "[]" ]; then | ||
| if [ "$AFFECTED" = "[]" ] || [ -z "$AFFECTED" ]; then | ||
| echo "has_changes=false" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "has_changes=true" >> $GITHUB_OUTPUT | ||
| fi | ||
|
|
||
| - name: Build packages | ||
| if: steps.changed.outputs.has_changes == 'true' | ||
| run: npm run build:dist | ||
|
|
||
| - name: Publish canary versions | ||
| id: publish | ||
| - name: Bump canary versions | ||
| if: steps.changed.outputs.has_changes == 'true' | ||
| run: | | ||
| PRERELEASE_ID="canary-${{ github.event.issue.number }}-${{ github.run_attempt }}" | ||
| node <<'SCRIPT' | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| const preid = process.env.PRERELEASE_ID; | ||
| const packagesDir = 'packages'; | ||
| const dirs = fs.readdirSync(packagesDir).filter(d => | ||
| fs.existsSync(path.join(packagesDir, d, 'package.json')) | ||
| ); | ||
|
|
||
| // Load all packages | ||
| const packages = new Map(); | ||
| for (const dir of dirs) { | ||
| const pkgPath = path.join(packagesDir, dir, 'package.json'); | ||
| const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); | ||
| packages.set(pkg.name, { dir, pkg, pkgPath }); | ||
| } | ||
|
|
||
| # Version bump with canary prerelease | ||
| npx lerna version prerelease --preid "$PRERELEASE_ID" --no-git-tag-version --no-push --yes | ||
| // Bump non-private packages to canary version | ||
| const bumped = new Map(); | ||
| for (const [name, entry] of packages) { | ||
| if (entry.pkg.private) { | ||
| console.log(`Skipping private: ${name}`); | ||
| continue; | ||
| } | ||
| const canaryVersion = `${entry.pkg.version}-${preid}.0`; | ||
| entry.pkg.version = canaryVersion; | ||
| bumped.set(name, canaryVersion); | ||
| console.log(`${name} -> ${canaryVersion}`); | ||
| } | ||
|
|
||
| # Commit version changes locally to satisfy lerna publish | ||
| # These changes won't be pushed back to the PR | ||
| # Only stage lerna-modified files (package.json, lerna.json, package-lock.json) | ||
| git add lerna.json package-lock.json packages/*/package.json | ||
| git commit -m "chore: canary version bump [skip ci]" --no-verify | ||
| // Update internal cross-references | ||
| for (const [, entry] of packages) { | ||
| let changed = false; | ||
| for (const depType of ['dependencies', 'devDependencies', 'peerDependencies']) { | ||
| if (!entry.pkg[depType]) continue; | ||
| for (const depName of Object.keys(entry.pkg[depType])) { | ||
| if (bumped.has(depName)) { | ||
| entry.pkg[depType][depName] = bumped.get(depName); | ||
| changed = true; | ||
| } | ||
| } | ||
| } | ||
| if (changed || bumped.has(entry.pkg.name)) { | ||
| fs.writeFileSync(entry.pkgPath, JSON.stringify(entry.pkg, null, 2) + '\n'); | ||
| } | ||
| } | ||
| SCRIPT | ||
| env: | ||
| PRERELEASE_ID: canary-${{ github.event.issue.number }}-${{ github.run_id }} | ||
|
|
||
| # Publish with canary tag | ||
| npx lerna publish from-package --dist-tag canary --yes 2>&1 | tee publish-output.txt | ||
| - name: Build packages | ||
| if: steps.changed.outputs.has_changes == 'true' | ||
| run: npx nx run-many -t build:dist | ||
|
|
||
| # Extract published versions | ||
| PUBLISHED=$(grep -E "Successfully published" publish-output.txt || echo "") | ||
| echo "published<<EOF" >> $GITHUB_OUTPUT | ||
| echo "$PUBLISHED" >> $GITHUB_OUTPUT | ||
| echo "EOF" >> $GITHUB_OUTPUT | ||
| - name: Publish canary versions | ||
| id: publish | ||
| if: steps.changed.outputs.has_changes == 'true' | ||
| run: npx nx release publish --tag canary | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} | ||
| GH_TOKEN: ${{ secrets.AUTO_GITHUB_PAT_TOKEN }} | ||
| NPM_CONFIG_REGISTRY: https://registry.npmjs.org | ||
|
|
||
| - name: Comment with published versions | ||
| if: steps.changed.outputs.has_changes == 'true' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const packages = JSON.parse('${{ steps.changed.outputs.packages }}'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| // Build versions list and install commands from package.json files | ||
| // Read all non-private package versions | ||
| let versionsList = []; | ||
| let installCommands = []; | ||
|
|
||
| for (const pkg of packages) { | ||
| const pkgPath = pkg.location.startsWith(process.env.GITHUB_WORKSPACE) | ||
| ? pkg.location | ||
| : `${process.env.GITHUB_WORKSPACE}/${pkg.location}`; | ||
| const pkgJson = require(`${pkgPath}/package.json`); | ||
| versionsList.push(`- \`${pkg.name}@${pkgJson.version}\``); | ||
| installCommands.push(`npm install ${pkg.name}@${pkgJson.version}`); | ||
| const packagesDir = path.join(process.env.GITHUB_WORKSPACE, 'packages'); | ||
| const dirs = fs.readdirSync(packagesDir); | ||
|
|
||
| for (const dir of dirs) { | ||
| const pkgPath = path.join(packagesDir, dir, 'package.json'); | ||
| if (fs.existsSync(pkgPath)) { | ||
| const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); | ||
| if (!pkg.private && pkg.version.includes('canary')) { | ||
| versionsList.push(`- \`${pkg.name}@${pkg.version}\``); | ||
| installCommands.push(`npm install ${pkg.name}@${pkg.version}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const versionsText = versionsList.join('\n'); | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,3 +16,7 @@ yarn-error.log | |
| *.bak.* | ||
|
|
||
| tsconfig.tsbuildinfo | ||
| *.tsbuildinfo | ||
|
|
||
| # NX | ||
| .nx/ | ||
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
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
167 changes: 167 additions & 0 deletions
167
docs/plans/2026-02-06-platform-sdk-public-publishing-design.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # Platform SDK Public Publishing Design | ||
|
|
||
| **Date:** 2026-02-06 **Status:** Approved for implementation **Authors:** Toks | ||
| Fawibe, Ryan McAfee (security analysis) **Context:** PLATENG-800 — PR #1188 adds | ||
| `@jupiterone/platform-sdk-fetch` (private) as a dependency of `@jupiterone/sdk` | ||
| (public). External consumers cannot install private transitive deps. | ||
|
|
||
| --- | ||
|
|
||
| ## Problem | ||
|
|
||
| `@jupiterone/sdk` is public on npm. PR #1188 replaces `@lifeomic/alpha` with | ||
| `@jupiterone/platform-sdk-fetch`, which has | ||
| `publishConfig.access: "restricted"`. Five of its transitive `@jupiterone/*` | ||
| dependencies are also restricted. External consumers cannot `npm install` the | ||
| SDK after this change merges. | ||
|
|
||
| ## Decision | ||
|
|
||
| Make 17 platform-sdk packages public with MPL-2.0 license. This was chosen over: | ||
|
|
||
| - **Vendoring the RequestClient** (~400 lines) — Viable but creates maintenance | ||
| burden and divergence from upstream. | ||
| - **Using `undici` directly** — Requires rewriting ~460 lines + tests. | ||
| Unnecessary complexity. | ||
| - **Bundling with tsdown/tsup** — 8-15 days effort, fragile DTS inlining, | ||
| massive bundle from unused AWS SDK clients. Not recommended. | ||
| - **Native `fetch`** — Experimental on Node 18-20 (SDK's target range). Not | ||
| viable until engine constraint is raised to >=21. | ||
|
|
||
| ## Security Assessment | ||
|
|
||
| Ryan McAfee performed a full assessment of all 21 platform-sdk packages. | ||
| Independent verification audit confirmed his findings across 107+ source files. | ||
|
|
||
| ### Classification | ||
|
|
||
| | Tier | Count | Packages | | ||
| | ------------------------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | Safe to publish | 14 | config-reader, errors, fetch, graphql, koa, logging, message-codec, service, service-plugin-errors, service-plugin-health, service-types, sqs-consumer, test-tools, framework | | ||
| | Safe after minor cleanup | 3 | aws, headers, iam | | ||
| | Keep restricted | 2 | elasticsearch, observability | | ||
| | Keep private | 2 | benchmark, examples | | ||
|
|
||
| ### Findings Summary | ||
|
|
||
| - Zero HIGH or CRITICAL issues across all 17 packages | ||
| - All hardcoded credentials found are verified LocalStack mocks (`test`/`test`) | ||
| - All URLs in source/tests are generic placeholders (`example.com`, `localhost`) | ||
| - Common LOW finding: `development@jupiterone.com` team email in `package.json` | ||
| author fields (standard npm convention) | ||
| - Common INFORMATIONAL: GitHub usernames and Jira ticket prefixes in CHANGELOGs | ||
|
|
||
| ## Implementation Plan | ||
|
|
||
| ### Execution Order | ||
|
|
||
| Packages must be published bottom-up (dependencies before dependents): | ||
|
|
||
| ``` | ||
| Layer 1 (no @jupiterone deps): | ||
| config-reader, errors, service-types, test-tools, headers*, iam* | ||
|
|
||
| Layer 1.5: | ||
| aws* (depends on config-reader) | ||
|
|
||
| Layer 2 (depends on Layer 1): | ||
| logging (-> errors) | ||
|
|
||
| Layer 3 (depends on Layer 2): | ||
| fetch (-> logging, errors, aws) | ||
| message-codec (-> logging, errors) | ||
| koa (-> logging, errors) | ||
| graphql (-> errors) | ||
| service-plugin-errors (-> errors) | ||
| service-plugin-health (-> errors) | ||
| sqs-consumer (-> logging) | ||
| service (-> logging, errors) | ||
|
|
||
| Layer 4 (depends on Layer 3): | ||
| framework (-> config-reader, errors, iam, logging) | ||
|
|
||
| * = caution packages requiring minor code fixes | ||
| ``` | ||
|
|
||
| Since platform-sdk uses NX with independent versioning in a single monorepo, all | ||
| changes go in one PR and publish together. | ||
|
|
||
| ### Changes | ||
|
|
||
| **All 17 packages** — `package.json`: | ||
|
|
||
| ```diff | ||
| - "license": "UNLICENSED", | ||
| + "license": "MPL-2.0", | ||
|
|
||
| "publishConfig": { | ||
| - "access": "restricted" | ||
| + "access": "public" | ||
| } | ||
| ``` | ||
|
|
||
| **`platform-sdk-aws`** — `src/config.ts:44`: | ||
|
|
||
| ```diff | ||
| - (awsProfile === 'jupiterone-dev' ? 'us-east-1' : undefined) | ||
| + (process.env.AWS_DEFAULT_REGION || undefined) | ||
| ``` | ||
|
|
||
| **`platform-sdk-headers`** — `src/index.ts:120-121`: | ||
|
|
||
| ```diff | ||
| - // The JupiterOne-Forwards-acirciapo header is set by our CF distribution and gateways | ||
| - // based on how many additional forwards there are between the CF distribution and the lambda: | ||
| + // The JupiterOne-Forwards-acirciapo header is set by the CDN distribution and gateways | ||
| + // based on how many additional forwards there are between the CDN distribution and the handler: | ||
| ``` | ||
|
|
||
| **`platform-sdk-iam`** — No code change. The `lifeomic-*` header names are a | ||
| runtime contract across all consumers. Renaming would be a breaking change. Not | ||
| a security vulnerability. | ||
|
|
||
| **Root** — Add `LICENSE` file with MPL-2.0 text. Update root `package.json` | ||
| license to `MPL-2.0`. | ||
|
|
||
| ### PR Strategy | ||
|
|
||
| Single PR to `platform-sdk` repo: | ||
|
|
||
| - Title: `chore: publish 17 packages as public with MPL-2.0 license` | ||
| - 17 `package.json` updates | ||
| - 2 code fixes (aws, headers) | ||
| - Root LICENSE file + root package.json license field | ||
|
|
||
| ### Validation | ||
|
|
||
| **Before merge:** | ||
|
|
||
| 1. CI passes (all existing tests) | ||
| 2. Verify `platform-sdk-aws` config change doesn't break tests | ||
| 3. `npm pack` dry-run on a few packages to inspect tarball contents | ||
|
|
||
| **After platform-sdk publishes:** 4. Verify public access from unauthenticated | ||
| environment: | ||
|
|
||
| ``` | ||
| npm view @jupiterone/platform-sdk-fetch | ||
| npm view @jupiterone/platform-sdk-errors | ||
| npm view @jupiterone/platform-sdk-logging | ||
| ``` | ||
|
|
||
| 5. Trigger new SDK canary on PR #1188, deploy to dev, check NR logs | ||
| 6. External consumer simulation: | ||
| `npm install @jupiterone/integration-sdk-runtime@canary` in a clean directory | ||
| without `.npmrc` auth | ||
|
|
||
| ### Rollback | ||
|
|
||
| npm allows deprecating or unpublishing within 72 hours. Security audit confirmed | ||
| no sensitive data, so rollback is unlikely to be needed. | ||
|
|
||
| ### Timeline | ||
|
|
||
| 1. Create platform-sdk PR — ~1 hour | ||
| 2. Review & merge — same day | ||
| 3. Publish completes — automated via CI | ||
| 4. Validate SDK canary — ~30 minutes |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.