diff --git a/.github/workflows/build-push-images-dev.yaml b/.github/workflows/build-push-images-dev.yaml index 7440784..3578e7a 100644 --- a/.github/workflows/build-push-images-dev.yaml +++ b/.github/workflows/build-push-images-dev.yaml @@ -1,82 +1,44 @@ -name: Build and Push Images (DEV) +name: PR preview environment on: pull_request: workflow_dispatch: jobs: - build-push-docs: - name: Build and Push Zane docs dev images - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - attestations: write - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Cache pnpm dependencies - uses: actions/cache@v3 - with: - path: ~/.pnpm-store - key: ${{ runner.OS }}-pnpm-cache-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.OS }}-pnpm-cache- - - name: Build docs with node - run: | - npm install -g pnpm@8 - pnpm install --frozen-lockfile - FORCE_COLOR=true pnpm run build - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.CONTAINER_REGISTRY_PAT }} - - name: Build and push - uses: docker/build-push-action@v3 - with: - context: . - file: Dockerfile - push: true - platforms: linux/amd64,linux/arm64 - tags: ghcr.io/zane-ops/docs:pr-${{ github.event.pull_request.number }},ghcr.io/zane-ops/docs:${{ github.sha }} - cache-from: | - type=registry,ref=ghcr.io/zane-ops/docs:pr-${{ github.event.pull_request.number }} - type=registry,ref=ghcr.io/zane-ops/docs:latest - cache-to: type=inline deploy: runs-on: ubuntu-latest name: Deploy - needs: build-push-docs steps: - name: Checkout uses: actions/checkout@v4 - - name: Get commit message - id: get-commit - run: echo "commit_message=$(git log -1 --pretty=format:%s)" >> $GITHUB_OUTPUT - - - name: Bypass Cloudflare for GitHub Action - uses: xiaotianxt/bypass-cloudflare-for-github-action@v1.1.1 + - uses: oven-sh/setup-bun@v2 with: - cf_zone_id: ${{ secrets.CF_ZONE_ID }} - cf_api_token: ${{ secrets.CF_API_TOKEN }} - - name: Deploy to ZaneOps + bun-version: latest + + - name: Install packages and Deploy to zaneops + shell: bash run: | - curl -f -o /dev/null --fail \ - -H "CF-Access-Client-Id: ${{ secrets.CF_CLIENT_ID }}" \ - -H "CF-Access-Client-Secret: ${{ secrets.CF_CLIENT_SECRET }}" \ - -X PUT ${{ secrets.STAGING_DEPLOY_WEBHOOK_URL }} \ - --data '{ - "commit_message": "${{ steps.get-commit.outputs.commit_message }}", - "new_image": "ghcr.io/zane-ops/docs:${{ github.sha }}" - }' + cd scripts + bun install + bun run deploy-pr.ts + env: + CF_CLIENT_ID: ${{ secrets.CF_CLIENT_ID }} + CF_CLIENT_SECRET: ${{ secrets.CF_CLIENT_SECRET }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BRANCH_NAME: ${{ github.head_ref }} + ZANE_USERNAME: ${{ secrets.ZANE_USERNAME }} + ZANE_PASSWORD: ${{ secrets.ZANE_PASSWORD }} + - name: Read service URL + id: read_url + run: | + url=$(cat ./scripts/service-url) + echo "url=$url" >> $GITHUB_OUTPUT + + - name: Comment on PR + uses: unsplash/comment-on-pr@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.COMMENT_PAT }} + with: + msg: 🚀 **PR Preview URL:** https://${{ steps.read_url.outputs.url }} + check_for_duplicate_msg: true \ No newline at end of file diff --git a/.github/workflows/close-pr-env.yaml b/.github/workflows/close-pr-env.yaml new file mode 100644 index 0000000..7a4f723 --- /dev/null +++ b/.github/workflows/close-pr-env.yaml @@ -0,0 +1,31 @@ +name: Close PR environment (DEV) +on: + pull_request: + types: [closed] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Close PR + shell: bash + run: | + cd scripts + bun install + bun run close-pr.ts + env: + CF_CLIENT_ID: ${{ secrets.CF_CLIENT_ID }} + CF_CLIENT_SECRET: ${{ secrets.CF_CLIENT_SECRET }} + PR_NUMBER: ${{ github.event.pull_request.number }} + ZANE_USERNAME: ${{ secrets.ZANE_USERNAME }} + ZANE_PASSWORD: ${{ secrets.ZANE_PASSWORD }} + diff --git a/package.json b/package.json index 000d176..9f73e5c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.0.1", "scripts": { - "dev": "astro dev --port 3000 --host", + "dev": "astro dev --port ${PORT:-3000} --host", "start": "astro dev", "build": "astro check && astro build", "preview": "astro preview", diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..b8d932e --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,15 @@ +# scripts + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.45. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/scripts/bun.lockb b/scripts/bun.lockb new file mode 100755 index 0000000..dfc426f Binary files /dev/null and b/scripts/bun.lockb differ diff --git a/scripts/close-pr.ts b/scripts/close-pr.ts new file mode 100644 index 0000000..60d8ab6 --- /dev/null +++ b/scripts/close-pr.ts @@ -0,0 +1,71 @@ +import { + DASHBOARD_URL, + ENV_NAME, + PROJECT_SLUG, + authenticate, + colors, + env, + extraHeaders, + parseResponseBody +} from "./common"; +const { requestCookie, csrfToken } = await authenticate(); + +/*****************************/ +/* CLOSING PR ENV */ +/*****************************/ + +console.log( + `Creating new environment ${colors.blue( + `pr-${env.PR_NUMBER}` + )} in the project ${colors.blue(PROJECT_SLUG)}...` +); +const getEnvRequest = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/environment-details/${ENV_NAME}/`, + { + method: "GET", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + ...extraHeaders + } + } +); +if (![200, 404].includes(getEnvRequest.status)) { + console.error(colors.red("❌ Failed to GET the environment for the PR ❌")); + console.error( + `Received status code from zaneops API : ${colors.red(getEnvRequest.status)}` + ); + + console.error("Received response from zaneops API : "); + console.dir(await parseResponseBody(getEnvRequest), { depth: null }); + process.exit(1); +} + +if (getEnvRequest.status === 200) { + const deleteEnvRequest = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/environment-details/${ENV_NAME}/`, + { + method: "DELETE", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + ...extraHeaders + } + } + ); + + if (deleteEnvRequest.status !== 204) { + console.error( + colors.red("❌ Failed to archive the environment for the PR ❌") + ); + console.error( + `Received status code from zaneops API : ${colors.red(getEnvRequest.status)}` + ); + + console.error("Received response from zaneops API : "); + console.dir(await parseResponseBody(getEnvRequest), { depth: null }); + process.exit(1); + } +} + +console.log(`Succesfully archived environment ${colors.blue(ENV_NAME)} ✅`); diff --git a/scripts/common.ts b/scripts/common.ts new file mode 100644 index 0000000..8ea1c5a --- /dev/null +++ b/scripts/common.ts @@ -0,0 +1,125 @@ +import * as cookie from "cookie-es"; +import { z } from "zod"; + +const envVariables = z.object({ + ZANE_USERNAME: z.string(), + ZANE_PASSWORD: z.string(), + CF_CLIENT_ID: z.string(), + CF_CLIENT_SECRET: z.string(), + PR_BRANCH_NAME: z.string(), + PR_NUMBER: z.coerce.number() +}); +export const PROJECT_SLUG = "zane-docs"; +export const DASHBOARD_URL = "https://lab.fkiss.me"; +export const SERVICE_SLUG = "zn-docs"; +export type EnvResponse = { + name: string; + services: Array<{ + slug: string; + urls: Array<{ domain: string }>; + }>; +}; + +const Colors = { + GREEN: "\x1b[92m", + BLUE: "\x1b[94m", + ORANGE: "\x1b[38;5;208m", + RED: "\x1b[91m", + GREY: "\x1b[90m", + ENDC: "\x1b[0m" +} as const; + +export const colors = { + green: (input: any) => `${Colors.GREEN}${input}${Colors.ENDC}`, + blue: (input: any) => `${Colors.BLUE}${input}${Colors.ENDC}`, + orange: (input: any) => `${Colors.ORANGE}${input}${Colors.ENDC}`, + red: (input: any) => `${Colors.RED}${input}${Colors.ENDC}`, + grey: (input: any) => `${Colors.GREY}${input}${Colors.ENDC}` +} as const; + +export async function parseResponseBody(response: Response) { + return response.headers.get("content-type") === "application/json" + ? await response.json() + : await response.text(); +} + +export const { + ZANE_USERNAME: username, + ZANE_PASSWORD: password, + ...env +} = envVariables.parse(process.env); +export const ENV_NAME = `pr-${env.PR_NUMBER}`; + +export const extraHeaders = { + "CF-Access-Client-Id": env.CF_CLIENT_ID, + "CF-Access-Client-Secret": env.CF_CLIENT_SECRET +}; + +export async function authenticate() { + /*****************************/ + /* CSRF TOKEN */ + /*****************************/ + console.log( + `Getting the CSRF token on ZaneOps API at ${colors.blue(DASHBOARD_URL)}...` + ); + const csrfResponse = await fetch(`${DASHBOARD_URL}/api/csrf`, { + headers: extraHeaders + }); + if (csrfResponse.status !== 200) { + console.error( + colors.red("❌ Failed to get CSRF token from ZaneOps API ❌") + ); + console.error( + `Received status code from zaneops API : ${colors.red(csrfResponse.status)}` + ); + + console.error("Received response from zaneops API : "); + console.dir(await parseResponseBody(csrfResponse), { depth: null }); + process.exit(1); + } else { + console.log(`Got the CSRF token successfully ✅`); + } + const csrfTokenStr = cookie + .splitSetCookieString(csrfResponse.headers.get("set-cookie") ?? "") + .filter((cookieStr) => cookieStr.startsWith("csrftoken"))[0]; + const csrfToken = cookie.parseSetCookie(csrfTokenStr).value; + + /*****************************/ + /* AUTHENTICATION */ + /*****************************/ + console.log(`Authenticating to ZaneOps API...`); + const authResponse = await fetch(`${DASHBOARD_URL}/api/auth/login`, { + method: "POST", + headers: { + "x-csrftoken": csrfToken, + cookie: `csrftoken=${csrfToken}`, + "content-type": "application/json", + ...extraHeaders + }, + body: JSON.stringify({ username, password }) + }); + if (authResponse.status !== 201) { + console.error(colors.red("❌ Failed to authenticate to ZaneOps API ❌")); + console.error( + `Received status code from zaneops API : ${colors.red(authResponse.status)}` + ); + + console.error("Received response from zaneops API : "); + console.dir(await parseResponseBody(authResponse), { depth: null }); + process.exit(1); + } else { + console.log(`Successfully Authenticated to ZaneOps API ✅`); + } + + const sessionIdCookieStr = cookie + .splitSetCookieString(authResponse.headers.get("set-cookie") ?? "") + .filter((cookieStr) => cookieStr.startsWith("sessionid"))[0]; + + const sessionId = cookie.parseSetCookie(sessionIdCookieStr).value; + const requestCookie = [ + cookie.serialize("sessionid", sessionId), + cookie.serialize("csrftoken", csrfToken), + "" + ].join(";"); + return { requestCookie, csrfToken }; +} diff --git a/scripts/deploy-pr.ts b/scripts/deploy-pr.ts new file mode 100644 index 0000000..24ec5b2 --- /dev/null +++ b/scripts/deploy-pr.ts @@ -0,0 +1,194 @@ +import { + DASHBOARD_URL, + ENV_NAME, + type EnvResponse, + PROJECT_SLUG, + SERVICE_SLUG, + authenticate, + colors, + env, + extraHeaders, + parseResponseBody +} from "./common"; +const { requestCookie, csrfToken } = await authenticate(); + +/*****************************/ +/* CLONING PROD ENV */ +/*****************************/ +console.log( + `Creating new environment ${colors.blue( + `pr-${env.PR_NUMBER}` + )} in the project ${colors.blue(PROJECT_SLUG)}...` +); +const getEnvRequest = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/environment-details/${ENV_NAME}/`, + { + method: "GET", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + ...extraHeaders + } + } +); +if (![200, 404].includes(getEnvRequest.status)) { + console.error(colors.red("❌ Failed to GET the environment for the PR ❌")); + console.error( + `Received status code from zaneops API : ${colors.red(getEnvRequest.status)}` + ); + + console.error("Received response from zaneops API : "); + console.dir(await parseResponseBody(getEnvRequest), { depth: null }); + process.exit(1); +} + +let envResponse: EnvResponse; + +if (getEnvRequest.status === 200) { + envResponse = await getEnvRequest.json(); +} else { + const cloneEnvRequest = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/clone-environment/production/`, + { + method: "POST", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + "content-type": "application/json", + ...extraHeaders + }, + body: JSON.stringify({ + name: `pr-${env.PR_NUMBER}`, + deploy_services: false + }) + } + ); + + if (![201, 409].includes(cloneEnvRequest.status)) { + console.error( + colors.red("❌ Failed to clone the production environment ❌") + ); + console.error( + `Received status code from zaneops API : ${colors.red(cloneEnvRequest.status)}` + ); + + console.error("Received response from zaneops API : "); + console.dir(await parseResponseBody(cloneEnvRequest), { depth: null }); + process.exit(1); + } else { + console.log(`Successfully created environment ${colors.blue(ENV_NAME)} ✅`); + envResponse = await cloneEnvRequest.json(); + } +} + +console.log(`Environment in ${colors.blue(ENV_NAME)}`); + +const docsService = envResponse.services.find( + (srv) => srv.slug === SERVICE_SLUG +); +if (!docsService) { + console.error( + `The cloned environment doesn't have the service "${SERVICE_SLUG}"` + ); + process.exit(1); +} + +const change = { + field: "git_source", + type: "UPDATE", + new_value: { + repository_url: "https://github.com/zane-ops/docs.git", + branch_name: env.PR_BRANCH_NAME, + commit_sha: "HEAD" + } +}; + +console.log( + `Updating the branch name for the service ${colors.orange( + SERVICE_SLUG + )} in the project ${colors.orange(PROJECT_SLUG)}...` +); +const requestChangeResponse = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/${ENV_NAME}/request-service-changes/${SERVICE_SLUG}/`, + { + method: "PUT", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + "content-type": "application/json", + ...extraHeaders + }, + body: JSON.stringify(change) + } +); + +if (requestChangeResponse.status !== 200) { + console.log( + colors.red("❌ Failed to update the image of the service on ZaneOps API ❌") + ); + console.log( + `Received status code from zaneops API : ${colors.red( + requestChangeResponse.status + )}` + ); + + console.log("Received response from zaneops API : "); + console.dir(await parseResponseBody(requestChangeResponse), { + depth: null + }); + // core.setFailed("Failure"); + process.exit(1); +} else { + console.log( + `Successfully Updated the repository branch to ${colors.orange(change.new_value.branch_name)} ✅` + ); +} + +console.log( + `Queuing a new deployment for the service ${colors.orange(SERVICE_SLUG)}...` +); +const deploymentResponse = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/${ENV_NAME}/deploy-service/git/${SERVICE_SLUG}/`, + { + method: "PUT", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + "content-type": "application/json", + ...extraHeaders + } + } +); + +if (deploymentResponse.status >= 200 && deploymentResponse.status <= 299) { + const deployment = await deploymentResponse.json(); + console.log(`Deployment queued succesfully ✅`); + console.log( + `inspect here ➡️ ${colors.blue( + `${DASHBOARD_URL}/project/${PROJECT_SLUG}/${ENV_NAME}/services/${SERVICE_SLUG}/deployments/${deployment.hash}/build-logs` + )}` + ); +} else { + console.log(colors.red("❌ Failed to queue deployment ❌")); + console.log( + `Received status code from zaneops API : ${colors.red( + deploymentResponse.status + )}` + ); + + const response = + deploymentResponse.headers.get("content-type") === "application/json" + ? await deploymentResponse.json() + : await deploymentResponse.text(); + console.log("Received response from zaneops API : "); + console.dir(response); + process.exit(1); +} + +const domain = docsService.urls[0].domain; +const file = Bun.file("./service-url"); +const writer = file.writer(); +writer.write(docsService.urls[0].domain); +writer.flush(); +writer.end(); +console.log(`Service deployed to ${colors.green(domain)} !`); diff --git a/scripts/index.ts b/scripts/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/scripts/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..0836670 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,15 @@ +{ + "name": "scripts", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "^1.2.14" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "cookie-es": "^2.0.0", + "zod": "^3.25.28" + } +} \ No newline at end of file diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/src/content/docs/changelog/v1.10.mdx b/src/content/docs/changelog/v1.10.mdx new file mode 100644 index 0000000..de767ab --- /dev/null +++ b/src/content/docs/changelog/v1.10.mdx @@ -0,0 +1,43 @@ +--- +title: ZaneOps v1.10 +description: 'Sh...🤫 ells !' +--- + +import {Aside} from '@astrojs/starlight/components'; + +21 May 2025 by [**Fred KISSIE**](https://github.com/Fredkiss3) + +Today we release zaneOps v1.10 introducing shells for everyone + + +**To install :** + +1. via the UI : + clone environment modal + clone environment modal + + project environments page + project environments page + + +3. via the shell : + ```shell + # assuming you are at /var/www/zaneops + curl https://cdn.zaneops.dev/makefile > Makefile + make setup + make deploy + ``` + +### Shells to deployments + +> todo + +### Shell to the server + +> todo + + +### Other changes + +- ZaneOps docker image is lighter and with a smaller number of layers ! Downloading new versions of ZaneOps should be faster in the next releases. +- We introduced a new anonymous telemetry to allows us to better see the real usage of ZaneOps in production, [how to configure ?](#) \ No newline at end of file diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 3ac298f..ad9796d 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -4,7 +4,7 @@ description: ZaneOps is a self-hosted, open source platform as a service for hos template: splash banner: content: | - In v1.9, ZaneOps replaces Vercel (well... kinda) + Shells for everyone ! (v1.10) hero: tagline: your all-in-one self-hosted platform for deploying apps with ✨ zen ✨. image: diff --git a/tsconfig.json b/tsconfig.json index b7243b9..2cc8a25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react" - } + }, + "exclude": ["scripts/**"] }