diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..84c8223b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +# .github/workflows/deploy.yml +name: Deploy openDAW studio + +on: + workflow_dispatch: + inputs: + dry: + description: "Dry run (no upload)" + required: false + default: 'true' + type: boolean + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + environment: production + + steps: + - name: ⬇️ Checkout main repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ⬇️ Checkout private submodule + uses: actions/checkout@v4 + with: + repository: andremichelle/opendaw-lib + path: lib + ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} + persist-credentials: false + + - name: 🦄 Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: 📦 Install dependencies + run: npm ci + + - name: 🏗️ Build project + shell: bash + run: | + bash ./rebuild.sh + + - name: 🚚 Run deploy script + env: + TERM: xterm-256color + SFTP_HOST: ${{ secrets.SFTP_HOST }} + SFTP_PORT: ${{ secrets.SFTP_PORT }} + SFTP_USERNAME: ${{ secrets.SFTP_USERNAME }} + SFTP_PASSWORD: ${{ secrets.SFTP_PASSWORD }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + DRY_RUN: ${{ inputs.dry && '1' || '0' }} + run: | + npx ts-node --transpile-only deploy/run.ts ${{ inputs.dry && '--dry' || '' }} \ No newline at end of file diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml new file mode 100644 index 00000000..41ab6762 --- /dev/null +++ b/.github/workflows/discord.yml @@ -0,0 +1,17 @@ +name: Notify Discord + +on: + workflow_dispatch: + +jobs: + notify: + runs-on: ubuntu-latest + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - run: npm ci + - run: npx ts-node --transpile-only deploy/discord.ts \ No newline at end of file diff --git a/deploy/discord.ts b/deploy/discord.ts new file mode 100644 index 00000000..88ca198c --- /dev/null +++ b/deploy/discord.ts @@ -0,0 +1,18 @@ +(async () => { + const webhookUrl = process.env.DISCORD_WEBHOOK + if (!webhookUrl) { + console.error("Missing DISCORD_WEBHOOK") + process.exit(1) + } + const content = "🧪 Discord test from GitHub Actions" + const res = await fetch(webhookUrl, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({content}) + }) + if (!res.ok) { + console.error(`Failed to post: ${res.status}`, await res.text()) + process.exit(1) + } + console.log("✅ Discord message sent") +})() \ No newline at end of file diff --git a/deploy/run.ts b/deploy/run.ts new file mode 100644 index 00000000..bafb2f65 --- /dev/null +++ b/deploy/run.ts @@ -0,0 +1,85 @@ +import SftpClient from "ssh2-sftp-client" +import * as fs from "fs" +import * as path from "path" + +const config = { + host: process.env.SFTP_HOST, + port: Number(process.env.SFTP_PORT), + username: process.env.SFTP_USERNAME, + password: process.env.SFTP_PASSWORD +} as const + +const DRY_RUN = process.env.DRY_RUN === "1" || process.argv.includes("--dry") +console.info(`DRY_RUN: ${DRY_RUN}`) +const env = Object.entries({ + SFTP_HOST: process.env.SFTP_HOST, + SFTP_PORT: process.env.SFTP_PORT, + SFTP_USERNAME: process.env.SFTP_USERNAME, + SFTP_PASSWORD: process.env.SFTP_PASSWORD, + DISCORD_WEBHOOK: process.env.DISCORD_WEBHOOK +}) +const missing = env.filter(([, v]) => !v).map(([k]) => k) +if (missing.length > 0) { + throw new Error(`Missing secrets/vars: ${missing.join(", ")}`) +} +if (DRY_RUN) { + console.log("✅ All secrets & variables are set. Nothing was uploaded (dry-run).") + process.exit(0) +} +const sftp = new SftpClient() +const staticFolders = ["/viscious-speed"] + +async function deleteDirectory(remoteDir: string) { + const items = await sftp.list(remoteDir) + for (const item of items) { + const remotePath = path.posix.join(remoteDir, item.name) + if (staticFolders.includes(remotePath)) continue + if (item.type === "d") { + await deleteDirectory(remotePath) + await sftp.rmdir(remotePath, true) + } else { + await sftp.delete(remotePath) + } + } +} + +async function uploadDirectory(localDir: string, remoteDir: string) { + for (const file of fs.readdirSync(localDir)) { + const localPath = path.join(localDir, file) + const remotePath = path.posix.join(remoteDir, file) + if (fs.lstatSync(localPath).isDirectory()) { + await sftp.mkdir(remotePath, true).catch(() => {/* exists */}) + if (staticFolders.includes(remotePath)) continue + await uploadDirectory(localPath, remotePath) + } else { + console.log(`upload ${remotePath}`) + await sftp.put(localPath, remotePath) + } + } +} + +// --------------------- main ------------------------------------------------- +(async () => { + console.log(`⏩ upload…`) + await sftp.connect(config) + await deleteDirectory("/") + await uploadDirectory("./packages/app/studio/dist", "/") + await sftp.end() + const webhookUrl = process.env.DISCORD_WEBHOOK + if (webhookUrl) { + console.log("posting to discord...") + const now = Math.floor(Date.now() / 1000) // in seconds + const content = `🚀 **openDAW** has been deployed to .` + try { + const response = await fetch(webhookUrl, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({content}) + }) + console.log(response) + } catch (error) { + console.warn(error) + } + } + console.log("deploy complete") +})() diff --git a/deploy/tsconfig.json b/deploy/tsconfig.json new file mode 100644 index 00000000..3df0bac4 --- /dev/null +++ b/deploy/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true + }, + "files": [ + "run.ts" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4d018b10..152d3018 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,14 @@ "packages/**/*" ], "devDependencies": { + "@types/ssh2-sftp-client": "^9.0.5", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "lerna": "^8.2.3", "prettier": "^3.6.2", + "ssh2-sftp-client": "^12.0.1", "turbo": "^2.5.5", "typescript": "^5.9.2", "vitest": "^3.1.3" @@ -2797,6 +2799,43 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-sftp-client": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/ssh2-sftp-client/-/ssh2-sftp-client-9.0.5.tgz", + "integrity": "sha512-cpUO6okDusnfLw2hnmaBiomlSchIWNVcCdpywLRsg/h9Q1TTiUSrzhkn5sJeeyTM8h6xRbZEZZjgWtUXFDogHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^1.0.0" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.121", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.121.tgz", + "integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.6", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.6.tgz", @@ -3379,6 +3418,16 @@ "node": ">=0.10.0" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3442,6 +3491,16 @@ ], "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -3530,6 +3589,16 @@ "dev": true, "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", @@ -4142,6 +4211,21 @@ } } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7841,6 +7925,14 @@ "dev": true, "license": "ISC" }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9926,6 +10018,42 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, + "node_modules/ssh2-sftp-client": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-12.0.1.tgz", + "integrity": "sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "concat-stream": "^2.0.0", + "ssh2": "^1.16.0" + }, + "engines": { + "node": ">=18.20.4" + }, + "funding": { + "type": "individual", + "url": "https://square.link/u/4g7sPflL" + } + }, "node_modules/ssri": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", @@ -10631,6 +10759,13 @@ "win32" ] }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 411aae1f..4fa356f3 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "publish-sdk": "lerna publish" }, "devDependencies": { + "@types/ssh2-sftp-client": "^9.0.5", + "ssh2-sftp-client": "^12.0.1", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "eslint": "^8.57.0",