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 :
+
+
+
+
+
+
+
+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/**"]
}