diff --git a/.github/workflows/build-push-images-dev.yaml b/.github/workflows/build-push-images-dev.yaml index 74407845..3578e7a2 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 00000000..1d1cd37c --- /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: + do_work: + runs-on: ubuntu-latest + name: Close + 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 }} + PR_BRANCH_NAME: ${{ github.head_ref }} + ZANE_USERNAME: ${{ secrets.ZANE_USERNAME }} + ZANE_PASSWORD: ${{ secrets.ZANE_PASSWORD }} diff --git a/package.json b/package.json index 000d176c..9f73e5c6 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/public/images/copy-public-ssh-key.png b/public/images/copy-public-ssh-key.png new file mode 100644 index 00000000..b2a108c7 Binary files /dev/null and b/public/images/copy-public-ssh-key.png differ diff --git a/public/images/create-ssh-key.png b/public/images/create-ssh-key.png new file mode 100644 index 00000000..2d2f2dbc Binary files /dev/null and b/public/images/create-ssh-key.png differ diff --git a/public/images/login-via-ssh-key.png b/public/images/login-via-ssh-key.png new file mode 100644 index 00000000..b5f61205 Binary files /dev/null and b/public/images/login-via-ssh-key.png differ diff --git a/public/images/server-shell.png b/public/images/server-shell.png new file mode 100644 index 00000000..865ca53c Binary files /dev/null and b/public/images/server-shell.png differ diff --git a/public/images/settings-dropdown.png b/public/images/settings-dropdown.png new file mode 100644 index 00000000..22aa3c9a Binary files /dev/null and b/public/images/settings-dropdown.png differ diff --git a/public/images/ssh-key-list.png b/public/images/ssh-key-list.png new file mode 100644 index 00000000..602c06a9 Binary files /dev/null and b/public/images/ssh-key-list.png differ diff --git a/public/videos/persistent-cache.mp4 b/public/videos/persistent-cache.mp4 new file mode 100644 index 00000000..b9388cfc Binary files /dev/null and b/public/videos/persistent-cache.mp4 differ diff --git a/public/videos/upgrade-with-shell.mp4 b/public/videos/upgrade-with-shell.mp4 new file mode 100644 index 00000000..1807149b Binary files /dev/null and b/public/videos/upgrade-with-shell.mp4 differ diff --git a/public/videos/webshell.mp4 b/public/videos/webshell.mp4 new file mode 100644 index 00000000..1ab6cc4c Binary files /dev/null and b/public/videos/webshell.mp4 differ diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..9b1ee42e --- /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 00000000..b8d932e6 --- /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 00000000..dfc426f8 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 00000000..60d8ab62 --- /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 00000000..f2ed3649 --- /dev/null +++ b/scripts/common.ts @@ -0,0 +1,130 @@ +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 }>; + unapplied_changes: Array<{ + id: string; + field: string; + new_value: { 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 00000000..c1d4e03f --- /dev/null +++ b/scripts/deploy-pr.ts @@ -0,0 +1,212 @@ +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)} ✅`); + const getEnvRequest = await fetch( + `${DASHBOARD_URL}/api/projects/${PROJECT_SLUG}/environment-details/${ENV_NAME}/`, + { + method: "GET", + headers: { + "x-csrftoken": csrfToken, + cookie: requestCookie, + ...extraHeaders + } + } + ); + + envResponse = await getEnvRequest.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); +} + +let url = docsService.urls[0]; +if (!url) { + url = docsService.unapplied_changes.filter((ch) => ch.field === "urls")[0] + ?.new_value; +} + +const file = Bun.file("./service-url"); +const writer = file.writer(); +writer.write(url.domain); +writer.flush(); +writer.end(); +console.log(`Service deployed to ${colors.green(url.domain)} !`); diff --git a/scripts/index.ts b/scripts/index.ts new file mode 100644 index 00000000..f67b2c64 --- /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 00000000..08366705 --- /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 00000000..238655f2 --- /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 00000000..286892fb --- /dev/null +++ b/src/content/docs/changelog/v1.10.mdx @@ -0,0 +1,87 @@ +--- +title: ZaneOps v1.10 +description: 'Sh...🤫 ells !' +--- +import {Aside} from '@astrojs/starlight/components'; + +25 May 2025 by [**Fred KISSIE**](https://github.com/Fredkiss3) + +Today we release ZaneOps v1.10, introducing a web terminal for deployments and a web terminal to the server. + +**To install:** + +1. via the UI: + clone environment modal + clone environment modal + project environments page + project environments page + +2. 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 + +In this new version, we are introducing a new terminal directly to your service, allowing you to inspect containers created for your service by ZaneOps and debug or run any arbitrary script within it. + + + +### Shell to the server + +As well as terminals to your deployments, you can now connect to your server via SSH directly from the web UI. + +In the demo below, you can see the user connecting via SSH to their server and triggering an update of ZaneOps via the shell directly. +*Pretty cool, huh?* + + + + + +### Persistent cache + +In v1.10, we introduce a new performance improvement on the frontend. +Between each new version, you will have persistent cache on the browser, so that next time you access your app, navigation is very snappy even if you refresh the browser. + +Here is a demo where you can see: +- On top: without persistent storage +- On the bottom: with persistent storage + + + +### Anonymous Telemetry + +We introduced a new anonymous telemetry feature to help us better understand the real usage of ZaneOps in production. + +It works by sending a simple `PING` request at most every 30 minutes to the ZaneOps CDN ([cdn.zaneops.dev](https://cdn.zaneops.dev)). + +The source code of the CDN can be found [here](https://github.com/zane-ops/cdn/tree/main/src/index.ts). + +Anonymous telemetry can be disabled by modifying the env variable in `.env`: + +```shell +# /var/www/zaneops/.env +TELEMETRY_ENABLED=false # or true +``` + + + +### Docker Image Size reduction + +The ZaneOps Docker image is lighter and has a smaller number of layers, going from **1.5GB** to **902MB**. + +So downloading new versions of ZaneOps should be faster in future releases ⚡️. + +### Thank you! 🫶 + +- We now have more than **500 stars** ⭐️ on the repository +- And **100+ more installs** 🔝 on ZaneOps + +Thank you all for making this happen 🙏 \ No newline at end of file diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 3ac298f4..ad9796d7 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/src/content/docs/knowledge-base/anonymous-telemetry.mdx b/src/content/docs/knowledge-base/anonymous-telemetry.mdx new file mode 100644 index 00000000..70140166 --- /dev/null +++ b/src/content/docs/knowledge-base/anonymous-telemetry.mdx @@ -0,0 +1,29 @@ +--- +title: Anonymous Telemetry +description: Learn how and what ZaneOps collects with telemetry +--- + +> Since [**v1.10**](/changelog/v110) + +## How does it work? + +The anonymous telemetry feature helps us better understand how ZaneOps is actually used in production environments. + +It works by sending a simple `PING` request at most every 30 minutes to the ZaneOps CDN ([cdn.zaneops.dev](https://cdn.zaneops.dev)). + +The complete source code of the CDN can be found [here](https://github.com/zane-ops/cdn/tree/main/src/index.ts). + +## How to disable? + +You can easily disable anonymous telemetry by modifying the environment variable in your `.env` file: + +```shell +# /var/www/zaneops/.env +TELEMETRY_ENABLED=false # or true +``` + +## What does it collect? + +When a PING request is sent to the CDN, we capture the requester's IP address and hash it twice: first using a SHA-256 hash, then a second time using HMAC-256 with a secret key that only the CDN knows. This process is irreversible, ensuring the data remains completely anonymous. + +You can review the hashing implementation [here](https://github.com/zane-ops/cdn/blob/ab602e62e7013b3dc55c60eb29b5312e847c93fc/src/index.ts#L67-L79). \ No newline at end of file diff --git a/src/content/docs/knowledge-base/ssh-keys.mdx b/src/content/docs/knowledge-base/ssh-keys.mdx new file mode 100644 index 00000000..9e474228 --- /dev/null +++ b/src/content/docs/knowledge-base/ssh-keys.mdx @@ -0,0 +1,77 @@ +--- +title: SSH keys +description: How ZaneOps uses SSH keys to login to your server from the web UI +--- + +import {Steps, Aside} from '@astrojs/starlight/components'; + +> Since [**v1.10**](/changelog/v110) + +To access your server through the ZaneOps web UI, you will need to use SSH keys. +Each key is associated with a user on your server. + +![server shell screenshot](/images/server-shell.png) + +### How to use SSH Keys + + +1. You can access the SSH keys in the settings. + ![settings dropdown](/images/settings-dropdown.png) + +2. Once there, you can create a new key with a **valid user on your server** and a name for the key. + ![Create new SSH key](/images/create-ssh-key.png) + +3. After clicking _"Add new Key"_, ZaneOps will generate a public/private key pair. + You can then use the public key on your server. + ![SSH key list](/images/ssh-key-list.png) + +4. Next, you will need to add the key to your server in your user directory: + ```shell + # create `~/.ssh` folder if it doesn't exist + mkdir -p $HOME/.ssh + # create `~/.ssh/authorized_keys` file if it doesn't exist + touch $HOME/.ssh/authorized_keys + # Allow permissions to use `authorized_keys` for authentication with SSH + chmod 600 $HOME/.ssh/authorized_keys + ``` + +5. Copy the public SSH key and add it at the end of the file on a new line in `$HOME/.ssh/authorized_keys` + + ![Copy public SSH key](/images/copy-public-ssh-key.png) + + + +6. You can now use the key to SSH to your server from ZaneOps: + ![Login using SSH key](/images/login-via-ssh-key.png) + + + +### Troubleshooting + +1. **Cannot connect to PORT 22**: + + ```shell + Failed to connect to port 22: Connection refused + ``` + + You will need to modify the SSH port to **22** to have access through the web UI: + ```shell + # /etc/ssh/sshd_config + #... other options + Port 22 + #... other options + ``` + +2. **Public key authentication is disabled**: + + ```shell + Failed publickey for from 192.168.1.100 port 22 ssh2: RSA SHA256:... + ``` + You will need to enable it in the SSH configuration: + ```shell + # /etc/ssh/sshd_config + PubkeyAuthentication yes + AuthorizedKeysFile .ssh/authorized_keys + ``` \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b7243b92..2cc8a25a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react" - } + }, + "exclude": ["scripts/**"] }