diff --git a/generator/konfig-dash/.changeset/grumpy-cougars-jam.md b/generator/konfig-dash/.changeset/grumpy-cougars-jam.md new file mode 100644 index 000000000..9d908a11f --- /dev/null +++ b/generator/konfig-dash/.changeset/grumpy-cougars-jam.md @@ -0,0 +1,7 @@ +--- +'konfig-kill-port': patch +'konfig-cli': patch +'konfig-lib': patch +--- + +Fix kill port in containers diff --git a/generator/konfig-dash/.earthlyignore b/generator/konfig-dash/.earthlyignore new file mode 100644 index 000000000..b0a5c349c --- /dev/null +++ b/generator/konfig-dash/.earthlyignore @@ -0,0 +1,2 @@ +/node_modules/ +/dist/ diff --git a/generator/konfig-dash/Earthfile b/generator/konfig-dash/Earthfile new file mode 100644 index 000000000..5753c10b5 --- /dev/null +++ b/generator/konfig-dash/Earthfile @@ -0,0 +1,35 @@ +VERSION 0.7 +FROM node:16-slim +WORKDIR /konfig-dash + +build-konfig-dash: + RUN apt-get update + RUN apt-get install -y openssl # not present in node:16-slim but required by prisma + # Copy everything we need to run `yarn` without copying the source code so that dependencies are cached + COPY package.json yarn.lock .yarnrc.yml redwood.toml . + COPY .yarn .yarn + COPY api/package.json api/package.json + COPY web/package.json web/package.json + COPY packages/konfig-cli/package.json packages/konfig-cli/package.json + COPY packages/konfig-kill-port/package.json packages/konfig-kill-port/package.json + COPY packages/konfig-lib/package.json packages/konfig-lib/package.json + COPY packages/konfig-openapi-spec/package.json packages/konfig-openapi-spec/package.json + COPY packages/konfig-postman-to-openapi/package.json packages/konfig-postman-to-openapi/package.json + COPY packages/konfig-release-it/package.json packages/konfig-release-it/package.json + COPY packages/konfig-spectral-ruleset/package.json packages/konfig-spectral-ruleset/package.json + COPY packages/konfig-swagger2openapi/package.json packages/konfig-swagger2openapi/package.json + COPY packages/konfig-typescript-sdk/package.json packages/konfig-typescript-sdk/package.json + COPY packages/konfig-zod-to-openapi/package.json packages/konfig-zod-to-openapi/package.json + RUN --secret NPM_TOKEN yarn + # Now that dependencies are installed, copy the source code and build + COPY api api + COPY web web + COPY packages packages + COPY bash-scripts/build.sh bash-scripts/build.sh + RUN --secret NPM_TOKEN yarn build + +run-konfig-api: + FROM +build-konfig-dash + ENTRYPOINT ["yarn", "rw", "deploy", "render", "api"] + EXPOSE 8911 + SAVE IMAGE konfig-api:latest diff --git a/generator/konfig-dash/api/db/dataMigrations/20221121050642-create-dummy-generate-config-row.ts b/generator/konfig-dash/api/db/dataMigrations/20221121050642-create-dummy-generate-config-row.ts deleted file mode 100644 index de929f0b4..000000000 --- a/generator/konfig-dash/api/db/dataMigrations/20221121050642-create-dummy-generate-config-row.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { PrismaClient } from '@prisma/client' - -export default async ({ db }: { db: PrismaClient }) => { - const dummyOrganization = await db.organization.create({ - data: { - name: 'dummyOrg', - }, - }) - const dummySpace = await db.space.create({ - data: { - name: 'dummySpace', - organizationId: dummyOrganization.id, - }, - }) - - const dummyUser = await db.user.create({ - data: { - email: 'dummyemail2@dummy.com', - hashedPassword: 'dummy', - salt: 'dummy', - currentSpaceId: dummySpace.id, - currentOrganizationId: dummyOrganization.id, - }, - }) - - await db.generateExecution.create({ - data: { - s3Key: 'dummy', - spaceId: dummySpace.id, - userId: dummyUser.id, - }, - }) - - await db.generateConfig.create({ - data: { - id: 'dummy', - konfigyaml: 'dummy', - userId: dummyUser.id, - spaceId: dummySpace.id, - }, - }) -} diff --git a/generator/konfig-dash/packages/konfig-cli/src/util/execute-test-command.ts b/generator/konfig-dash/packages/konfig-cli/src/util/execute-test-command.ts index 3116f717d..d9d5dd604 100644 --- a/generator/konfig-dash/packages/konfig-cli/src/util/execute-test-command.ts +++ b/generator/konfig-dash/packages/konfig-cli/src/util/execute-test-command.ts @@ -74,9 +74,8 @@ export async function executeTestCommand({ // if this process exits in any way, kill the mock server const handleTermination = async () => { - CliUx.ux.log('🛑 Killing mock server') + CliUx.ux.log('Exit hook - Ensuring mock server is not running.') await kill(mockServerPort) - process.exit() } process.on('exit', handleTermination) @@ -197,7 +196,8 @@ export async function executeTestCommand({ // If we made it here then we successfully ran all tests CliUx.ux.info('Successfully ran all tests!') - process.exit() + CliUx.ux.log('🛑 Killing mock server') + await kill(mockServerPort) } const defaultTestScripts: Record< diff --git a/generator/konfig-dash/packages/konfig-cli/src/util/get-default-branch.ts b/generator/konfig-dash/packages/konfig-cli/src/util/get-default-branch.ts index b7a915f37..224e271c5 100644 --- a/generator/konfig-dash/packages/konfig-cli/src/util/get-default-branch.ts +++ b/generator/konfig-dash/packages/konfig-cli/src/util/get-default-branch.ts @@ -3,9 +3,16 @@ import * as shell from 'shelljs' * Get usually "master" or "main" */ export function getDefaultBranch({ cwd }: { cwd: string }) { - const result = shell.exec( - "git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", - { silent: true, cwd } - ) - return result.stdout.trim() + try { + const result = shell.exec( + "git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", + { silent: true, cwd } + ) + return result.stdout.trim() + } catch (e) { + console.error( + 'Warning! Encountered error when trying to use git. If running in CI, this can be safely ignored.' + ) + } + return 'main' } diff --git a/generator/konfig-dash/packages/konfig-cli/src/util/is-submodule.ts b/generator/konfig-dash/packages/konfig-cli/src/util/is-submodule.ts index 318521677..44062399d 100644 --- a/generator/konfig-dash/packages/konfig-cli/src/util/is-submodule.ts +++ b/generator/konfig-dash/packages/konfig-cli/src/util/is-submodule.ts @@ -29,7 +29,9 @@ export async function isSubmodule({ const isSameRemoteUrl = url.includes(gitConfigUrl) return !isSameRemoteUrl } catch (e) { - console.error(e) + console.error( + 'Warning! Encountered error when trying to use git. If running in CI, this can be safely ignored.' + ) } return false } diff --git a/generator/konfig-dash/packages/konfig-kill-port/index.js b/generator/konfig-dash/packages/konfig-kill-port/index.js index 07f371a7a..cfc6c8b96 100644 --- a/generator/konfig-dash/packages/konfig-kill-port/index.js +++ b/generator/konfig-dash/packages/konfig-kill-port/index.js @@ -47,9 +47,9 @@ module.exports = function (port, method = 'tcp') { return Promise.reject(new Error('No process running on port')) return sh( - `lsof -i ${method === 'udp' ? 'udp' : 'tcp'}:${port} | grep ${ + `pid=$(lsof -i ${method === 'udp' ? 'udp' : 'tcp'}:${port} | grep ${ method === 'udp' ? 'UDP' : 'LISTEN' - } | awk '{print $2}' | xargs kill -9` + } | awk '{print $2}') && kill -9 $pid` ) }) } diff --git a/generator/konfig-dash/packages/konfig-lib/generate-json-schema.ts b/generator/konfig-dash/packages/konfig-lib/generate-json-schema.ts index cec47b9ad..ac7442abf 100644 --- a/generator/konfig-dash/packages/konfig-lib/generate-json-schema.ts +++ b/generator/konfig-dash/packages/konfig-lib/generate-json-schema.ts @@ -14,5 +14,7 @@ const docOutputPath = path.join( 'static', 'konfig-yaml.schema.json' ) -fs.writeFileSync(outputPath, JSON.stringify(jsonSchema, undefined, 2)) -fs.writeFileSync(docOutputPath, JSON.stringify(jsonSchema, undefined, 2)) +if (fs.existsSync(outputPath)) + fs.writeFileSync(outputPath, JSON.stringify(jsonSchema, undefined, 2)) +if (fs.existsSync(docOutputPath)) + fs.writeFileSync(docOutputPath, JSON.stringify(jsonSchema, undefined, 2)) diff --git a/generator/konfig-generator-api/Earthfile b/generator/konfig-generator-api/Earthfile new file mode 100644 index 000000000..5f5c05ca3 --- /dev/null +++ b/generator/konfig-generator-api/Earthfile @@ -0,0 +1,16 @@ +VERSION 0.7 +FROM maven:3.8.6-jdk-11-slim +WORKDIR /konfig-generator-api + +build-generator: + COPY src src + COPY pom.xml . + RUN mvn -f pom.xml clean package + SAVE ARTIFACT target/openapi-generator-api-1.0.0.jar + +run-generator: + FROM openjdk:11-jre-slim + COPY +build-generator/openapi-generator-api-1.0.0.jar /usr/local/lib/openapi-generator-api.jar + EXPOSE 8080 + ENTRYPOINT ["java","-jar","/usr/local/lib/openapi-generator-api.jar", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED"] + SAVE IMAGE konfig-generator:latest \ No newline at end of file diff --git a/generator/konfig-integration-tests/.earthlyignore b/generator/konfig-integration-tests/.earthlyignore new file mode 100644 index 000000000..3bb3a5066 --- /dev/null +++ b/generator/konfig-integration-tests/.earthlyignore @@ -0,0 +1 @@ +generate-id.txt \ No newline at end of file diff --git a/generator/konfig-integration-tests/.gitignore b/generator/konfig-integration-tests/.gitignore index de878483a..355961333 100644 --- a/generator/konfig-integration-tests/.gitignore +++ b/generator/konfig-integration-tests/.gitignore @@ -191,4 +191,6 @@ dist # and uncomment the following lines # .pnp.* -# End of https://www.toptal.com/developers/gitignore/api/node,yarn,macos \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/node,yarn,macos + +*.pyc \ No newline at end of file diff --git a/generator/konfig-integration-tests/Earthfile b/generator/konfig-integration-tests/Earthfile new file mode 100644 index 000000000..064468644 --- /dev/null +++ b/generator/konfig-integration-tests/Earthfile @@ -0,0 +1,50 @@ +VERSION 0.7 +FROM ../konfig-dash+build-konfig-dash +WORKDIR /konfig-integration-tests + +konfig-test-dependencies: + ### NODE comes from base image + ### PYTHON + ENV PYTHONDONTWRITEBYTECODE=1 # Don't write .pyc files, which are not needed in a container + ENV PIP_DEFAULT_TIMEOUT=100 + ENV POETRY_VERSION=1.5.1 + + RUN apt-get update + # lsof required for killing mock server after test + RUN apt-get install -y lsof python3 python3-pip + RUN pip3 install poetry==$POETRY_VERSION + # Clean up python installations + RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +integration-tests: + FROM +konfig-test-dependencies + COPY package.json yarn.lock . + RUN yarn + COPY tsconfig.json jest.config.ts util.ts . + COPY sdks sdks + COPY tests tests + ENTRYPOINT ["yarn", "test", "--no-cache"] # TODO: see if we can speed this up more + SAVE IMAGE konfig-integration-tests:latest + +test: + FROM earthly/dind:alpine + COPY compose/.env . + COPY compose/compose.yaml . + WITH DOCKER \ + --compose compose.yaml \ + --load konfig-python-formatter:latest=../konfig-python-formatter-server-blackd+run-python-formatter \ + --load konfig-generator:latest=../konfig-generator-api+run-generator \ + --load konfig-api:latest=../konfig-dash+run-konfig-api \ + --load konfig-integration-tests:latest=+integration-tests + RUN --secret NPM_TOKEN docker run \ + --network="konfig-network" \ + -e "KONFIG_API_URL=http://konfig-api:8911" \ + -e "NPM_TOKEN=${NPM_TOKEN}" \ + konfig-integration-tests:latest + END + +build-all: + BUILD ../konfig-python-formatter-server-blackd+run-python-formatter + BUILD ../konfig-generator-api+run-generator + BUILD ../konfig-dash+run-konfig-api + BUILD +integration-tests \ No newline at end of file diff --git a/generator/konfig-integration-tests/README.md b/generator/konfig-integration-tests/README.md index a16a8d73a..dce443c38 100644 --- a/generator/konfig-integration-tests/README.md +++ b/generator/konfig-integration-tests/README.md @@ -1,11 +1,25 @@ # konfig-integration-tests - - test SDK directories under `sdks/` - - each map to test under `tests/` - - see `util.ts` for implementation of tests +- test SDK directories under `sdks/` +- each map to test under `tests/` +- see `util.ts` for implementation of tests -## To run rests +## To run tests 1. `cd` into `konfig-integration-tests` 2. Run `yarn` 3. Run `yarn test` + +## To run tests using earthly (local) + +1. `cd` into `konfig-integration-tests` +2. Ensure `EARTHLY_SECRETS` env var is set + - EARTHLY_SECRETS: `NPM_TOKEN=xxx` + - This is used during the container building phase for konfig-dash +3. Ensure `compose/.env` file is present and contains the following: + - `NPM_TOKEN`, `SESSION_SECRET`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` + - Each line should be formatted as key=value + - These are passed to docker compose and are used by while running containerized services +4. Run `earthly -P +test` + - `-P` runs earthly in privileged mode (required for earthly image) + - `+test` is the name of the earthly target we are running diff --git a/generator/konfig-integration-tests/compose/compose.yaml b/generator/konfig-integration-tests/compose/compose.yaml new file mode 100644 index 000000000..fea57efd1 --- /dev/null +++ b/generator/konfig-integration-tests/compose/compose.yaml @@ -0,0 +1,48 @@ +version: "3.8" +services: + konfig-api: + image: konfig-api:latest + environment: + # prisma has trouble connecting to postgres from node:16 base image. Adding connect_timeout to url fixes it + # https://stackoverflow.com/questions/68476229/m1-related-prisma-cant-reach-database-server-at-database5432 + DATABASE_URL: postgresql://konfig:konfig@konfig-db:5432/konfig?connect_timeout=300 + SKIP_INSTALL_DEPS: "true" + NODE_VERSION: 16 + GENERATOR_API_HOST_PORT: konfig-generator:8080 + BLACKD_API_HOST_PORT: konfig-python-formatter:10000 + NPM_TOKEN: ${NPM_TOKEN} + SESSION_SECRET: ${SESSION_SECRET} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + ports: + - "8911:8911" + depends_on: + - konfig-db + + konfig-python-formatter: + image: konfig-python-formatter:latest + ports: + - "10000:10000" + + konfig-generator: + image: konfig-generator:latest + environment: + PORT: 8080 + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + ports: + - "8080:8080" + + konfig-db: + image: postgres + restart: always + environment: + POSTGRES_USER: konfig + POSTGRES_PASSWORD: konfig + ports: + - "5432:5432" + +networks: + default: + name: konfig-network + attachable: true diff --git a/generator/konfig-integration-tests/tests/__snapshots__/leap-workflows-sdks.test.ts.snap b/generator/konfig-integration-tests/tests/__snapshots__/leap-workflows-sdks.test.ts.snap index e8abc552e..759685cd5 100644 --- a/generator/konfig-integration-tests/tests/__snapshots__/leap-workflows-sdks.test.ts.snap +++ b/generator/konfig-integration-tests/tests/__snapshots__/leap-workflows-sdks.test.ts.snap @@ -7,7 +7,6 @@ The Leap Workflows API allows developers to run workflows, fetch workflow runs, [![PyPI](https://img.shields.io/badge/PyPI-v1.0.0-blue)](https://pypi.org/project/leap-workflows-python-sdk/1.0.0) -[![GitHub last commit](https://img.shields.io/github/last-commit/leap-ai/workflows-sdks.svg)](https://github.com/leap-ai/workflows-sdks/commits) [![README.md](https://img.shields.io/badge/README-Click%20Here-green)](https://github.com/leap-ai/workflows-sdks/tree/main/sdks/python#readme) [![More Info](https://img.shields.io/badge/More%20Info-Click%20Here-orange)](https://tryleap.ai/) @@ -207,7 +206,6 @@ exports[`leap-workflows-sdks 2`] = ` The Leap Workflows API allows developers to run workflows, fetch workflow runs, and provide other utility functions related to workflow runs. Please use the X-Api-Key for authenticated requests. [![npm](https://img.shields.io/badge/npm-v1.0.0-blue)](https://www.npmjs.com/package/@leap-ai/workflows/v/1.0.0) -[![GitHub last commit](https://img.shields.io/github/last-commit/leap-ai/workflows-sdks/tree/main/sdks/typescript.svg)](https://github.com/leap-ai/workflows-sdks/tree/main/sdks/typescript/commits) [![More Info](https://img.shields.io/badge/More%20Info-Click%20Here-orange)](https://tryleap.ai/) ## Table of Contents diff --git a/generator/konfig-integration-tests/tests/__snapshots__/python-dataclass-responses.test.ts.snap b/generator/konfig-integration-tests/tests/__snapshots__/python-dataclass-responses.test.ts.snap index a3054244b..921ef47ae 100644 --- a/generator/konfig-integration-tests/tests/__snapshots__/python-dataclass-responses.test.ts.snap +++ b/generator/konfig-integration-tests/tests/__snapshots__/python-dataclass-responses.test.ts.snap @@ -7,7 +7,6 @@ A simple API based on python dataclass responses. [![PyPI](https://img.shields.io/badge/PyPI-v1.0.0-blue)](https://pypi.org/project/python-dataclass-responses-python-sdk/1.0.0) -[![GitHub last commit](https://img.shields.io/github/last-commit/konfig-dev/konfig.svg)](https://github.com/konfig-dev/konfig/commits) [![README.md](https://img.shields.io/badge/README-Click%20Here-green)](https://github.com/konfig-dev/konfig/tree/main/python#readme) [![More Info](https://img.shields.io/badge/More%20Info-Click%20Here-orange)](http://example.com/support) diff --git a/generator/konfig-integration-tests/tests/__snapshots__/python-typeddict-responses.test.ts.snap b/generator/konfig-integration-tests/tests/__snapshots__/python-typeddict-responses.test.ts.snap index b94664a2f..67bb66c9d 100644 --- a/generator/konfig-integration-tests/tests/__snapshots__/python-typeddict-responses.test.ts.snap +++ b/generator/konfig-integration-tests/tests/__snapshots__/python-typeddict-responses.test.ts.snap @@ -7,7 +7,6 @@ A simple API based on python typeddict responses. [![PyPI](https://img.shields.io/badge/PyPI-v1.0.0-blue)](https://pypi.org/project/python-typeddict-responses-python-sdk/1.0.0) -[![GitHub last commit](https://img.shields.io/github/last-commit/konfig-dev/konfig.svg)](https://github.com/konfig-dev/konfig/commits) [![README.md](https://img.shields.io/badge/README-Click%20Here-green)](https://github.com/konfig-dev/konfig/tree/main/python#readme) [![More Info](https://img.shields.io/badge/More%20Info-Click%20Here-orange)](http://example.com/support) diff --git a/generator/konfig-integration-tests/tests/leap-workflows-sdks.test.ts b/generator/konfig-integration-tests/tests/leap-workflows-sdks.test.ts index 9ed90f15e..bdc94d7c0 100644 --- a/generator/konfig-integration-tests/tests/leap-workflows-sdks.test.ts +++ b/generator/konfig-integration-tests/tests/leap-workflows-sdks.test.ts @@ -2,4 +2,4 @@ import { e2e } from "../util"; test("leap-workflows-sdks", async () => { await e2e(4010); -}); +}, 900000); diff --git a/generator/konfig-integration-tests/tests/python-dataclass-responses.test.ts b/generator/konfig-integration-tests/tests/python-dataclass-responses.test.ts index 3b0955d5e..6b7ac01ea 100644 --- a/generator/konfig-integration-tests/tests/python-dataclass-responses.test.ts +++ b/generator/konfig-integration-tests/tests/python-dataclass-responses.test.ts @@ -2,4 +2,4 @@ import { e2e } from "../util"; test("python-dataclass-responses", async () => { await e2e(4011); -}); +}, 900000); diff --git a/generator/konfig-integration-tests/tests/python-typeddict-responses.test.ts b/generator/konfig-integration-tests/tests/python-typeddict-responses.test.ts index 8349b6124..c801fe132 100644 --- a/generator/konfig-integration-tests/tests/python-typeddict-responses.test.ts +++ b/generator/konfig-integration-tests/tests/python-typeddict-responses.test.ts @@ -2,4 +2,4 @@ import { e2e } from "../util"; test("python-typeddict-responses", async () => { await e2e(4012); -}); +}, 900000); diff --git a/generator/konfig-integration-tests/util.ts b/generator/konfig-integration-tests/util.ts index d6c76b12d..e9c8b7cde 100644 --- a/generator/konfig-integration-tests/util.ts +++ b/generator/konfig-integration-tests/util.ts @@ -59,10 +59,19 @@ export async function e2e(mockServerPort: number) { ]; for (const generator of generators) { expect( - fs.readFileSync( - path.join(sdkDir, generator.outputDirectory, "README.md"), - "utf-8" + normalizeDocumentation( + fs.readFileSync( + path.join(sdkDir, generator.outputDirectory, "README.md"), + "utf-8" + ) ) ).toMatchSnapshot(); } } + +// Removes the [GitHub last commit] line from the README +function normalizeDocumentation(readme: string) { + // matches [GitHub last commit] to the end of the line + const pattern = /\[!\[GitHub last commit\].*$\n?/gm; + return readme.replace(pattern, ""); +} diff --git a/generator/konfig-python-formatter-server-blackd/Earthfile b/generator/konfig-python-formatter-server-blackd/Earthfile new file mode 100644 index 000000000..1289a1b48 --- /dev/null +++ b/generator/konfig-python-formatter-server-blackd/Earthfile @@ -0,0 +1,15 @@ +VERSION 0.7 +FROM python:3.11-slim +WORKDIR /konfig-python-formatter-server-blackd + +run-python-formatter: + COPY pyproject.toml . + COPY README.md . # Required because pyproject.toml references it + COPY main.py . + COPY python_formatter_server_blackd python_formatter_server_blackd + COPY build.sh . + RUN ./build.sh + COPY start-ci.sh . + EXPOSE 10000 + ENTRYPOINT ["./start-ci.sh"] + SAVE IMAGE konfig-python-formatter:latest \ No newline at end of file diff --git a/generator/konfig-python-formatter-server-blackd/build-render.sh b/generator/konfig-python-formatter-server-blackd/build.sh similarity index 100% rename from generator/konfig-python-formatter-server-blackd/build-render.sh rename to generator/konfig-python-formatter-server-blackd/build.sh diff --git a/generator/konfig-python-formatter-server-blackd/start-ci.sh b/generator/konfig-python-formatter-server-blackd/start-ci.sh new file mode 100755 index 000000000..44b297c6b --- /dev/null +++ b/generator/konfig-python-formatter-server-blackd/start-ci.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +./venv/bin/poetry run uvicorn main:app --host 0.0.0.0 --port 10000 & +./venv/bin/poetry run blackd --bind-host 0.0.0.0 --bind-port 9090 \ No newline at end of file diff --git a/render.yaml b/render.yaml index 49b71b137..c21612600 100644 --- a/render.yaml +++ b/render.yaml @@ -248,7 +248,7 @@ services: - type: pserv name: konfig-python-formatter env: python - buildCommand: ./build-render.sh + buildCommand: ./build.sh startCommand: ./start.sh repo: https://github.com/konfig-dev/konfig rootDir: generator/konfig-python-formatter-server-blackd