├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md └── workflows │ └── publish.yml ├── .gitignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── cli ├── args.js ├── commands │ ├── generate │ │ ├── generate.js │ │ ├── generateConfigFiles.js │ │ ├── getDetectableSettings.js │ │ ├── index.js │ │ ├── internalGenerateHooks.js │ │ └── triggerGenerateHooks.js │ ├── help.js │ ├── index.js │ └── reset │ │ ├── deleteEnvDirs.js │ │ ├── index.js │ │ ├── reset.js │ │ └── triggerResetHooks.js ├── index.js └── outputs │ ├── debug.js │ ├── error.js │ ├── goodbye.js │ ├── index.js │ ├── info.js │ ├── success.js │ └── welcome.js ├── config ├── build.js ├── default.js ├── index.js ├── load.js └── providers.js ├── core ├── dependencies.js ├── docker.js ├── file.js ├── index.js ├── questions.js └── templates.js ├── flow.pdf ├── index.js ├── package-lock.json ├── package.json ├── providers ├── aws │ └── .gitkeep ├── digitalocean │ └── .gitkeep ├── google │ └── .gitkeep ├── heroku │ ├── authentication.js │ ├── destroyHerokuApp.js │ ├── generateHerokuServices.js │ ├── heroku.js │ ├── herokuTemplate.js │ ├── index.js │ └── templates │ │ └── heroku.liquid └── render │ ├── index.js │ ├── render.js │ ├── renderFile.js │ └── templates │ └── render.liquid └── utils ├── bugreport.js ├── detection.js ├── index.js ├── spinner.js └── stats.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:import/recommended"], 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | }, 11 | "rules": { 12 | "no-empty": "off", 13 | "indent": ["warn", 2, { "SwitchCase": 1 }], 14 | 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "backtick"], 17 | "semi": ["error", "always"], 18 | "no-unused-vars": "warn" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Eventyret 4 | open_collective: strapi 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: "If something isn't working as expected \U0001F914." 4 | title: '' 5 | labels: 'i: bug, i: needs triage' 6 | assignees: 'eventyret' 7 | --- 8 | 9 | ## 🐛 Bug Report 10 | 11 | ## 🤷‍♀️ What did you do 12 | 13 | 14 | 15 | ## ⛔️ Error log 16 | 17 | ### 🕵️‍♀️ Stack trace 18 | 19 | ```bash 20 | Stack Trace 21 | ``` 22 | 23 | ``` 24 | 25 | ## 🙇‍♀️ Expected behavior/code 26 | 27 | A clear and concise description of what you expected to happen (or code). 28 | 29 | ## 👩‍💻 Environment 30 | 31 | - 📦 Node version: 32 | - 💻 OS: 33 | 34 | ## 💡 Possible Solution 35 | 36 | 37 | 38 | ## 📺 Additional context/Screenshots 39 | 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: "I have a suggestion (and may want to implement it \U0001F642)!" 4 | title: '' 5 | labels: 'i: enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ## Feature Request 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | 13 | 14 | 15 | **Describe the solution you'd like** 16 | 17 | 18 | 19 | **Describe alternatives you've considered** 20 | 21 | 22 | 23 | **Teachability, Documentation, Adoption, Migration Strategy** 24 | 25 | 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Github & NPM Release 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.ref == 'refs/heads/main' }} 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: 'Running Semantic Release' 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm ci 18 | - run: npm run semantic-release 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.G_TOKEN }} 21 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM # 2 | ########## 3 | # Ignore all directories called node_modules in current folder and any subfolders. 4 | node_modules/ 5 | /node_modules/ 6 | 7 | # Packages # 8 | ############ 9 | *.7z 10 | *.dmg 11 | *.gz 12 | *.bz2 13 | *.iso 14 | *.jar 15 | *.rar 16 | *.tar 17 | *.zip 18 | *.tgz 19 | *.map 20 | 21 | # Logs and databases # 22 | ###################### 23 | *.log 24 | *.sql 25 | *.env 26 | 27 | # OS generated files # 28 | ###################### 29 | **.DS_Store* 30 | ehthumbs.db 31 | Icon? 32 | Thumbs.db 33 | ._* 34 | **settings.dat* 35 | 36 | # Vim generated files # 37 | ###################### 38 | *.un~ 39 | 40 | # SASS # 41 | ########## 42 | **/.sass-cache 43 | **/.sass-cache/* 44 | **/.map 45 | 46 | # Composer # 47 | ########## 48 | !assets/js/vendor/ 49 | wpcs/ 50 | /vendor/ 51 | 52 | # Bower # 53 | ########## 54 | assets/bower_components/* 55 | 56 | # Codekit # 57 | ########## 58 | /codekit-config.json 59 | *.codekit 60 | **.codekit-cache/* 61 | 62 | # Compiled Files and Build Dirs # 63 | ########## 64 | /README.html 65 | 66 | # PhpStrom Project Files # 67 | .idea/ 68 | library/vendors/composer 69 | assets/img/.DS_Store 70 | 71 | # VSCode related files # 72 | # .vscode 73 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "arrowParens": "avoid", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "useTabs": false, 7 | "tabWidth": 2, 8 | "semi": true 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.validate": ["javascript"] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

@Strapi-community/deployify

3 | 4 |

Easily deploy a Strapi Project to cloud platforms 🚀

5 | 6 |

7 | 8 | Strapi Discord 9 | 10 | 11 | NPM Version 12 | 13 | 14 | Monthly download on NPM 15 | 16 |

17 |
18 | 19 | ## Table of Contents 20 | 21 | - [🚦 Current Status](#---current-status) 22 | - [✨ Usage](#--usage) 23 | - [🧹 Resetting project](#---resetting-project) 24 | - [🚀 Features](#---features) 25 | - [🖐 Requirements](#---requirements) 26 | - [🎗 Contributing](#---contributing) 27 | - [⭐️ Show your support](#---show-your-support) 28 | - [🔗 Links](#---links) 29 | - [🌎 Community support](#---community-support) 30 | - [🙋‍♀️ Authors](#------authors) 31 | - [🔖 License](#---license) 32 | 33 | ## 🚦 Current Status 34 | 35 | This package is currently under development and should be consider **BETA** in terms of state. I/We are currently accepting contributions and/or dedicated contributors to help develop and maintain this package. 36 | 37 | For more information on contributing please see [the contrib message below](#contributing). 38 | 39 | ## ✨ Usage 40 | 41 | ```bash 42 | npx @strapi-community/deployify 43 | ``` 44 | 45 | ### 🧹 Resetting project 46 | 47 | ```bash 48 | @strapi-community/deployify reset 49 | ``` 50 | 51 | _Note_ that **RESET** will delete the `everything` from heroku related to the projectname, if heroku is selected as a provider 52 | 53 | ## 🚀 Features 54 | 55 | - Easy deploy of your project to some cloud platforms 56 | 57 | ## 🌤 Cloud Support 58 | 59 | Default - Create enviroments for strapi project, user will need to push to version control and deploy to cloud provider 60 | 🚀 - Automatic - Creating apps, databases, setting up env variables. 61 | 🚝 - Semi Automatic - Creating configuration files. Some manual steps required. 62 | 63 | | **Provider** | Is Supported | Semi Automatic 🚝 / Automatic 🚀 | ⚙️ Notes | 64 | | ------------- | ------------ | -------------------------------- | -------------------------------------------- | 65 | | Heroku | ✅ | 🚀 | Manual deployment | 66 | | Render | ✅ | 🚝 | Push to repo, connect repo to render website | 67 | | AWS | ❌ | ❌ | Not Added yet | 68 | | Google | ❌ | ❌ | Not Added yet | 69 | | Digital Ocean | ❌ | ❌ | Not Added yet | 70 | | Platform.sh | ❌ | ❌ | Not Added yet | 71 | | Railway.app | ❌ | ❌ | Not Added yet | 72 | 73 | ## 🖐 Requirements 74 | 75 | Supported Strapi Versions: 76 | 77 | | Strapi Version | Is Compatible | 78 | | -------------- | ------------- | 79 | | v3 | ❌ | 80 | | v4 | ✅ | 81 | 82 | **This tool will not work with Strapi v3 projects as it utilizes the V4 folder format that don't exist in the v3!** 83 | 84 | ## 🎗 Contributing 85 | 86 | I/We are actively looking for contributors, maintainers, and others to help shape this package. As this plugins sole purpose within the Strapi community is to be used by other developers and plugin maintainers to get fast responses time. 87 | 88 | If interested please feel free to email the lead maintainer Simen at: simen@dehlin.dev or ping `Cookie Monster#6725` on Discord. 89 | 90 | ### Contributors 91 | 92 | - [@ComfortablyCoding / Daedalus](https://github.com/ComfortablyCoding) 93 | 94 | ## ⭐️ Show your support 95 | 96 | Give a star if this project helped you. 97 | 98 | ## 🔗 Links 99 | 100 | - [NPM package](https://www.npmjs.com/package/@strapi-community/deployify) 101 | - [GitHub repository](https://github.com/strapi-community/strapi-tool-deployify) 102 | 103 | ## 🌎 Community support 104 | 105 | - For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). 106 | - For support with this plugin you can DM me in the Strapi Discord [channel](https://discord.strapi.io/). 107 | 108 | ## 🙋‍♀️ Authors 109 | 110 | - [@Eventyret / Simen Daehlin](https://github.com/Eventyret) 111 | 112 | ## 🔖 License 113 | 114 | See the [LICENSE](./LICENSE.md) file for licensing information. 115 | -------------------------------------------------------------------------------- /cli/args.js: -------------------------------------------------------------------------------- 1 | const meow = require(`meow`); 2 | const meowHelp = require(`cli-meow-help`); 3 | 4 | const flags = { 5 | clear: { 6 | type: `boolean`, 7 | default: true, 8 | desc: `Clear the console` 9 | }, 10 | version: { 11 | type: `boolean`, 12 | alias: `v`, 13 | desc: `Print CLI version` 14 | } 15 | }; 16 | const commands = { 17 | help: { desc: `Print help info` }, 18 | reset: { desc: `Reset the project` } 19 | }; 20 | 21 | const helpText = meowHelp({ 22 | name: `strapi-tool-deployify`, 23 | flags, 24 | commands 25 | }); 26 | 27 | const options = { 28 | inferType: true, 29 | description: false, 30 | hardRejection: false, 31 | flags 32 | }; 33 | 34 | module.exports = { 35 | getArgs: () => meow(helpText, options) 36 | }; 37 | -------------------------------------------------------------------------------- /cli/commands/generate/generate.js: -------------------------------------------------------------------------------- 1 | const { buildConfig } = require(`../../../config`); 2 | const { askGenerateQuestions } = require(`../../../core/questions`); 3 | const { getDetectableSettings } = require(`./getDetectableSettings`); 4 | const { triggerGenerateHooks } = require(`./triggerGenerateHooks`); 5 | 6 | const generate = async () => { 7 | const { packageManager, projectType } = await getDetectableSettings(); 8 | 9 | const generateAnswers = await askGenerateQuestions(); 10 | 11 | // load configs 12 | let config = buildConfig({ 13 | ...generateAnswers, 14 | packageManager, 15 | projectType 16 | }); 17 | 18 | await triggerGenerateHooks(config); 19 | }; 20 | 21 | module.exports = { 22 | generate 23 | }; 24 | -------------------------------------------------------------------------------- /cli/commands/generate/generateConfigFiles.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { pathExists } = require(`fs-extra`); 3 | const path = require(`path`); 4 | const { spinner } = require(`../../../utils`); 5 | const { file, getTemplate } = require(`../../../core`); 6 | const outputs = require(`../../outputs`); 7 | 8 | const generateConfigFiles = async config => { 9 | const fileExtension = config.projectType; 10 | const environment = config.env; 11 | const dirPath = path.join(process.cwd(), `config`, `env`, config.env); 12 | 13 | for (const fileName of config.files) { 14 | const projectFilePath = `config/env/${environment}/${fileName}.${fileExtension}`; 15 | const filePath = path.join(dirPath, `${fileName}.${fileExtension}`); 16 | 17 | spinner.start(`Checking for existing ${projectFilePath}`); 18 | const hasExistingConfigFile = await pathExists(filePath); 19 | if (hasExistingConfigFile) { 20 | spinner.stopAndPersist({ 21 | symbol: `🕵️‍♀️ `, 22 | text: `Detected ${chalk.yellow(`${projectFilePath}`)} \n` 23 | }); 24 | 25 | const backedUp = await file.backup(filePath); 26 | spinner.stopAndPersist({ 27 | text: `${chalk.yellow(projectFilePath)} was backed up ${ 28 | backedUp ? `successfully` : `unsuccessfuly` 29 | } \n` 30 | }); 31 | } 32 | 33 | const generatedFile = await file.generate( 34 | filePath, 35 | getTemplate(fileName, fileExtension) 36 | ); 37 | if (generatedFile) { 38 | spinner.stopAndPersist({ 39 | symbol: `⚙️ `, 40 | text: `Configured ${chalk.bold.green(`${projectFilePath}`)} \n` 41 | }); 42 | } else { 43 | outputs.error(`Unable to generate the ${fileName} configuration file`); 44 | } 45 | } 46 | }; 47 | 48 | module.exports = { 49 | generateConfigFiles 50 | }; 51 | -------------------------------------------------------------------------------- /cli/commands/generate/getDetectableSettings.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner, detect } = require(`../../../utils`); 3 | 4 | const getDetectableSettings = async () => { 5 | // package manager 6 | spinner.start(` 💻 Detecting package manager... `); 7 | const packageManager = await detect.packageManager(); 8 | spinner.stopAndPersist({ 9 | symbol: `📦 `, 10 | text: `${chalk.bold.yellow(packageManager.toUpperCase())} detected \n` 11 | }); 12 | 13 | // project type 14 | spinner.start(` 💻 Detecting Project type... `); 15 | const projectType = await detect.projectType(); 16 | const projectTypeColoredText = 17 | projectType === `ts` 18 | ? chalk.bold.blueBright(`TypeScript`) 19 | : chalk.bold.yellow(`JavaScript`); 20 | spinner.stopAndPersist({ 21 | symbol: `🍿 `, 22 | text: `${projectTypeColoredText} project detected \n` 23 | }); 24 | 25 | return { 26 | packageManager, 27 | projectType 28 | }; 29 | }; 30 | 31 | module.exports = { 32 | getDetectableSettings 33 | }; 34 | -------------------------------------------------------------------------------- /cli/commands/generate/index.js: -------------------------------------------------------------------------------- 1 | const { generate } = require(`./generate`); 2 | 3 | module.exports = { name: `generate`, invoke: generate }; 4 | -------------------------------------------------------------------------------- /cli/commands/generate/internalGenerateHooks.js: -------------------------------------------------------------------------------- 1 | const { generateDockerFile, installDependencies } = require(`../../../core`); 2 | const { generateConfigFiles } = require(`./generateConfigFiles`); 3 | 4 | module.exports = { 5 | async internalprebuild({ config }) { 6 | await generateConfigFiles(config); 7 | await installDependencies(config.packageManager); 8 | }, 9 | 10 | async internalpostbuild({ config }) { 11 | if (config.useDocker) { 12 | await generateDockerFile(config); 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /cli/commands/generate/triggerGenerateHooks.js: -------------------------------------------------------------------------------- 1 | const { loadProviderConfig } = require(`../../../config`); 2 | const internalGenerateHooks = require(`./internalGenerateHooks`); 3 | 4 | const triggerGenerateHooks = async config => { 5 | const providerConfig = loadProviderConfig(config.provider); 6 | 7 | // setup hooks 8 | const { hooks } = require(`${config.providersDir}/${config.provider}`); 9 | 10 | // init hooks 11 | config.hooks.addHooks({ 12 | ...hooks, 13 | ...internalGenerateHooks 14 | }); 15 | 16 | // trigger provider setup 17 | // provider specific pre build 18 | await config.hooks.callHook(`prebuild`, { providerConfig, config }); 19 | 20 | // general internal pre build 21 | await config.hooks.callHook(`internalprebuild`, { providerConfig, config }); 22 | 23 | // provider specific build 24 | await config.hooks.callHook(`build`, { providerConfig, config }); 25 | 26 | // provider specific post build 27 | await config.hooks.callHook(`postbuild`, { providerConfig, config }); 28 | 29 | // general internal pre build 30 | await config.hooks.callHook(`internalpostbuild`, { providerConfig, config }); 31 | }; 32 | 33 | module.exports = { 34 | triggerGenerateHooks 35 | }; 36 | -------------------------------------------------------------------------------- /cli/commands/help.js: -------------------------------------------------------------------------------- 1 | const help = args => { 2 | args.showHelp(0); 3 | }; 4 | 5 | module.exports = { name: `help`, invoke: help }; 6 | -------------------------------------------------------------------------------- /cli/commands/index.js: -------------------------------------------------------------------------------- 1 | const generate = require(`./generate`); 2 | const reset = require(`./reset`); 3 | const help = require(`./help`); 4 | const commands = { 5 | generate, 6 | reset, 7 | help 8 | }; 9 | 10 | module.exports = commands; 11 | -------------------------------------------------------------------------------- /cli/commands/reset/deleteEnvDirs.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const path = require(`path`); 3 | const { file } = require(`../../../core`); 4 | const { spinner } = require(`../../../utils`); 5 | const outputs = require(`../../outputs`); 6 | 7 | const deleteEnvDirs = async environments => { 8 | const directory = `${process.cwd()}/config/env`; 9 | 10 | for (const env of environments) { 11 | const folderPath = path.join(directory, env); 12 | const removedEnv = await file.remove(folderPath); 13 | 14 | if (removedEnv) { 15 | spinner.stopAndPersist({ 16 | symbol: `🧹 `, 17 | text: `Cleand up ${chalk.yellow(`${folderPath}`)} folder \n` 18 | }); 19 | } else { 20 | outputs.error(`Unable to clean ${chalk.yellow(`${folderPath}`)}`); 21 | } 22 | } 23 | }; 24 | 25 | module.exports = { 26 | deleteEnvDirs 27 | }; 28 | -------------------------------------------------------------------------------- /cli/commands/reset/index.js: -------------------------------------------------------------------------------- 1 | const { reset } = require(`./reset`); 2 | 3 | module.exports = { name: `reset`, invoke: reset }; 4 | -------------------------------------------------------------------------------- /cli/commands/reset/reset.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner, detect } = require(`../../../utils`); 3 | const { buildConfig } = require(`../../../config`); 4 | const { askResetQuestions } = require(`../../../core/questions`); 5 | const { deleteEnvDirs } = require(`./deleteEnvDirs`); 6 | const { triggerResetHooks } = require(`./triggerResetHooks`); 7 | 8 | const reset = async () => { 9 | const projectType = await detect.projectType(); 10 | 11 | const detectedProviderName = await detect.provider(); 12 | const { environments, provider } = await askResetQuestions( 13 | detectedProviderName 14 | ); 15 | 16 | const config = buildConfig({ projectType, provider }); 17 | 18 | spinner.start(` 🦄 ${chalk.yellow(`Searching for our directories...`)} `); 19 | await deleteEnvDirs(environments); 20 | spinner.stopAndPersist({ 21 | symbol: `🦄`, 22 | text: ` Directory search ${chalk.yellow(` completed`)} \n` 23 | }); 24 | await triggerResetHooks(config); 25 | 26 | spinner.stopAndPersist({ 27 | symbol: `🧹 `, 28 | text: `Project ${chalk.yellow(`cleaned`)} \n` 29 | }); 30 | }; 31 | 32 | module.exports = { reset }; 33 | -------------------------------------------------------------------------------- /cli/commands/reset/triggerResetHooks.js: -------------------------------------------------------------------------------- 1 | const { loadProviderConfig } = require(`../../../config`); 2 | 3 | const triggerResetHooks = async config => { 4 | const providerConfig = loadProviderConfig(config.provider); 5 | // setup hooks 6 | const { hooks } = require(`${config.providersDir}/${providerConfig.name}`); 7 | // init provider hooks 8 | config.hooks.addHooks(hooks); 9 | // trigger provider specific destroy 10 | await config.hooks.callHook(`destroy`, { providerConfig, config }); 11 | }; 12 | 13 | module.exports = { 14 | triggerResetHooks 15 | }; 16 | -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | const { getArgs } = require(`./args`); 2 | const commands = require(`./commands`); 3 | const outputs = require(`./outputs`); 4 | 5 | module.exports = { 6 | getArgs, 7 | commands, 8 | outputs 9 | }; 10 | -------------------------------------------------------------------------------- /cli/outputs/debug.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const symbols = require(`log-symbols`); 4 | 5 | const debugOutput = message => { 6 | spinner.start(``); 7 | spinner.stopAndPersist({ 8 | symbol: symbols.warning, 9 | text: `${chalk.bold.orange.bold(`${message}`)} \n` 10 | }); 11 | }; 12 | 13 | module.exports = debugOutput; 14 | -------------------------------------------------------------------------------- /cli/outputs/error.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const symbols = require(`log-symbols`); 4 | 5 | const errorOuput = message => { 6 | spinner.start(``); 7 | spinner.stopAndPersist({ 8 | symbol: symbols.error, 9 | text: `${chalk.bold.red.bold(`${message}`)} \n` 10 | }); 11 | }; 12 | 13 | module.exports = errorOuput; 14 | -------------------------------------------------------------------------------- /cli/outputs/goodbye.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const pkg = require(`../../package.json`); 4 | const { getGithubStats } = require(`../../utils/stats`); 5 | 6 | const goodbyeOutput = async ({ quit = false }) => { 7 | if (quit) { 8 | spinner.stopAndPersist({ 9 | symbol: `☝️`, 10 | text: ` ${chalk.yellow(`Strapi`)} is now ${chalk.bold.blueBright( 11 | `deployified` 12 | )} 🐳 - have a look at the logs above for more info. 🚀 \n` 13 | }); 14 | } 15 | spinner.stopAndPersist({ 16 | symbol: `⭐️`, 17 | text: ` ${chalk.bold.green( 18 | `Star the project on GitHub if you liked this tool 🙏 \n` 19 | )}` 20 | }); 21 | 22 | try { 23 | const { stars } = await getGithubStats(); 24 | spinner.stopAndPersist({ 25 | symbol: `🎉`, 26 | text: ` ${chalk.bold.yellow( 27 | `We now have got ${stars || 0} 🌟 and counting... \n` 28 | )} ` 29 | }); 30 | } catch (error) {} 31 | 32 | console.log(`👉 ${pkg.url} 👈 \n`); 33 | }; 34 | 35 | module.exports = goodbyeOutput; 36 | -------------------------------------------------------------------------------- /cli/outputs/index.js: -------------------------------------------------------------------------------- 1 | const success = require(`./success`); 2 | const debug = require(`./debug`); 3 | const error = require(`./error`); 4 | const info = require(`./info`); 5 | const welcome = require(`./welcome`); 6 | const goodbye = require(`./goodbye`); 7 | 8 | module.exports = { 9 | success, 10 | info, 11 | debug, 12 | error, 13 | welcome, 14 | goodbye 15 | }; 16 | -------------------------------------------------------------------------------- /cli/outputs/info.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const symbols = require(`log-symbols`); 4 | 5 | const infoOuput = message => { 6 | spinner.start(``); 7 | spinner.stopAndPersist({ 8 | symbol: symbols.info, 9 | text: `${chalk.bold.blue.bold(`${message}`)} \n` 10 | }); 11 | }; 12 | 13 | module.exports = infoOuput; 14 | -------------------------------------------------------------------------------- /cli/outputs/success.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const symbols = require(`log-symbols`); 4 | 5 | const successOuput = message => { 6 | spinner.start(``); 7 | spinner.stopAndPersist({ 8 | symbol: symbols.success, 9 | text: `${chalk.bold.blue.bold(`${message}`)} \n` 10 | }); 11 | }; 12 | 13 | module.exports = successOuput; 14 | -------------------------------------------------------------------------------- /cli/outputs/welcome.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const pkg = require(`../../package.json`); 4 | const stats = require(`../../utils/stats`); 5 | 6 | const welcomeOuput = async () => { 7 | const bg = chalk.hex(`#ffffff`).inverse.bold; 8 | const clr = chalk.hex(`#000000`).bold; 9 | 10 | // Do it. 11 | console.log(); 12 | console.log( 13 | `${clr(`${bg(` ${`@strapi-community/deployify`} `)}`)} v${ 14 | pkg.version 15 | } ${chalk.dim(`by Simen Daehlin`)}\n${chalk.dim( 16 | `${pkg.description}\n${pkg.url}` 17 | )}` 18 | ); 19 | console.log(); 20 | 21 | try { 22 | const { downloads } = await stats.getNPMStats(); 23 | spinner.stopAndPersist({ 24 | symbol: `🎉`, 25 | text: ` ${chalk.bold.yellow(`You`)}, and ${chalk.bold.green( 26 | downloads || `large amount of` 27 | )} other people have used this tool this month\n` 28 | }); 29 | } catch (error) {} 30 | }; 31 | 32 | module.exports = welcomeOuput; 33 | -------------------------------------------------------------------------------- /config/build.js: -------------------------------------------------------------------------------- 1 | const { loadConfig } = require(`./load`); 2 | 3 | const buildConfig = (...options) => { 4 | return loadConfig(...options); 5 | }; 6 | 7 | module.exports = { 8 | buildConfig 9 | }; 10 | -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`); 2 | const crypto = require(`crypto`); 3 | const { createHooks } = require(`hookable`); 4 | 5 | const defaultConfig = { 6 | url: `https://github.com/strapi-community/strapi-tool-deployify`, 7 | providersDir: path.join(__dirname, `../providers`), 8 | provider: `heroku`, 9 | outDir: path.join(process.cwd()), 10 | env: `development`, 11 | useDocker: false, 12 | useDockerTool: false, 13 | packageManager: `yarn`, 14 | projectType: `js`, 15 | projectName: ``, 16 | files: [`server`, `database`], 17 | strapiSecrets: { 18 | appKeys: crypto.randomBytes(64).toString(`hex`), 19 | apiTokenSalt: crypto.randomBytes(32).toString(`hex`), 20 | adminJwtSecret: crypto.randomBytes(32).toString(`hex`), 21 | jwtSecret: crypto.randomBytes(32).toString(`hex`) 22 | }, 23 | hooks: createHooks() 24 | }; 25 | 26 | module.exports = defaultConfig; 27 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const { buildConfig } = require(`./build`); 2 | const { loadProviderConfig, loadProviders } = require(`./load`); 3 | 4 | module.exports = { 5 | buildConfig, 6 | loadProviderConfig, 7 | loadProviders 8 | }; 9 | -------------------------------------------------------------------------------- /config/load.js: -------------------------------------------------------------------------------- 1 | const { defu } = require(`defu`); 2 | const defaultConfig = require(`./default`); 3 | const { providers } = require(`./providers`); 4 | 5 | const loadConfig = (...options) => { 6 | return defu(...options, defaultConfig); 7 | }; 8 | 9 | const loadProviderConfig = name => { 10 | return providers[name.toLowerCase()]; 11 | }; 12 | 13 | const loadProviders = () => { 14 | return { ...providers }; 15 | }; 16 | 17 | module.exports = { 18 | loadConfig, 19 | loadProviders, 20 | loadProviderConfig 21 | }; 22 | -------------------------------------------------------------------------------- /config/providers.js: -------------------------------------------------------------------------------- 1 | const providers = { 2 | heroku: { 3 | name: `Heroku`, 4 | description: `Heroku Platform`, 5 | apiToken: ``, 6 | herokuCLI: false, 7 | outputFileName: `heroku.yml`, 8 | regions: [ 9 | { 10 | title: `US`, 11 | value: `us` 12 | }, 13 | { 14 | title: `EU`, 15 | value: `eu` 16 | } 17 | ], 18 | defaultRegion: `global`, 19 | enabled: true 20 | }, 21 | render: { 22 | name: `Render`, 23 | description: `Render Platform`, 24 | outputFileName: `render.yaml`, 25 | regions: [ 26 | { 27 | title: `Oregon`, 28 | value: `oregon`, 29 | description: `US` 30 | }, 31 | { 32 | title: `Ohio`, 33 | value: `ohio`, 34 | description: `US` 35 | }, 36 | { 37 | title: `Frankfurt`, 38 | value: `frankfurt`, 39 | description: `EU` 40 | }, 41 | { 42 | title: `Singapore`, 43 | value: `singapore`, 44 | description: `Asia` 45 | } 46 | ], 47 | defaultRegion: `global`, 48 | enabled: true 49 | }, 50 | aws: { 51 | name: `AWS`, 52 | description: `Amazon Web Services`, 53 | enabled: false 54 | }, 55 | digitalocean: { 56 | name: `Digital Ocean`, 57 | description: `Digital Ocean App Platform`, 58 | enabled: false 59 | }, 60 | google: { 61 | name: `Google`, 62 | description: `Google Cloud Platform`, 63 | enabled: false 64 | } 65 | }; 66 | 67 | module.exports = { 68 | providers 69 | }; 70 | -------------------------------------------------------------------------------- /core/dependencies.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { pathExists } = require(`fs-extra`); 3 | const shell = require(`shelljs`); 4 | const outputs = require(`../cli/outputs`); 5 | const { spinner } = require(`../utils`); 6 | 7 | const installDependencies = async packageManager => { 8 | try { 9 | spinner.start(`📦 Checking if dependencies already installed ...`); 10 | const dependenciesInstalled = await hasOldDependencies(); 11 | if (dependenciesInstalled) { 12 | spinner.stopAndPersist({ 13 | symbol: `📦 `, 14 | text: `dependencies already installed, skipping install \n` 15 | }); 16 | return; 17 | } 18 | 19 | spinner.start( 20 | ` 📦 Installing dependencies using ${chalk.bold.yellow( 21 | packageManager.toUpperCase() 22 | )}...` 23 | ); 24 | 25 | const installCommand = packageManager === `yarn` ? `add` : `install`; 26 | shell.exec(`${packageManager} ${installCommand} pg pg-connection-string`, { 27 | silent: true 28 | }); 29 | outputs.success(`All dependencies installed`); 30 | } catch (err) { 31 | outputs.error(err); 32 | } 33 | }; 34 | 35 | const hasOldDependencies = async () => { 36 | const isReadablePkg = await pathExists(`package.json`); 37 | if (!isReadablePkg) { 38 | return false; 39 | } 40 | 41 | const pkg = require(`${process.cwd()}/package.json`); 42 | if (pkg.dependencies[`pg`] || pkg.dependencies[`pg-connection-string`]) { 43 | return true; 44 | } 45 | }; 46 | 47 | module.exports = { installDependencies }; 48 | -------------------------------------------------------------------------------- /core/docker.js: -------------------------------------------------------------------------------- 1 | const shell = require(`shelljs`); 2 | const { spinner, chalk } = require(`../utils`); 3 | 4 | const generateDockerFile = async config => { 5 | shell.exec( 6 | `npx @strapi-community/dockerize new --usecompose=false --env=${config.env} --projecttype=${config.projectType} --packageManager=${config.packageManager} --dbtype=postgres`, 7 | { silent: true } 8 | ); 9 | 10 | spinner.stopAndPersist({ 11 | symbol: `🐳`, 12 | text: ` Adding ${chalk.blue.bold( 13 | `Dockerfile${config.env === `production` ? `.prod` : ``}` 14 | )} file to project \n` 15 | }); 16 | }; 17 | 18 | module.exports = { generateDockerFile }; 19 | -------------------------------------------------------------------------------- /core/file.js: -------------------------------------------------------------------------------- 1 | const { 2 | copy: copyFile, 3 | remove: removeFile, 4 | outputFile 5 | } = require(`fs-extra`); 6 | const { readFile } = require(`fs/promises`); 7 | 8 | const generate = async (filePath, data) => { 9 | try { 10 | await outputFile(filePath, data); 11 | return true; 12 | } catch (error) { 13 | return false; 14 | } 15 | }; 16 | 17 | const remove = async filePath => { 18 | try { 19 | await removeFile(filePath); 20 | return true; 21 | } catch (error) { 22 | return false; 23 | } 24 | }; 25 | 26 | const copy = async (srcFilePath, dstFilePath) => { 27 | try { 28 | await copyFile(srcFilePath, dstFilePath); 29 | return true; 30 | } catch (error) { 31 | return false; 32 | } 33 | }; 34 | 35 | const read = async filePath => { 36 | try { 37 | return readFile(filePath); 38 | } catch (error) { 39 | return false; 40 | } 41 | }; 42 | 43 | const backup = async filePath => { 44 | const filePathParts = filePath.split(`.`); 45 | const fileExtension = filePathParts.pop(); 46 | const filePathSuffix = fileExtension 47 | ? `${Date.now()}.${fileExtension}` 48 | : `${Date.now()}`; 49 | 50 | const backupFilePath = `${filePathParts.join(`.`)}.backup${filePathSuffix}`; 51 | 52 | return copy(filePath, backupFilePath); 53 | }; 54 | 55 | module.exports = { 56 | generate, 57 | remove, 58 | copy, 59 | read, 60 | backup 61 | }; 62 | -------------------------------------------------------------------------------- /core/index.js: -------------------------------------------------------------------------------- 1 | const { installDependencies } = require(`./dependencies`); 2 | const { generateDockerFile } = require(`./docker`); 3 | const file = require(`./file`); 4 | const { askGenerateQuestions, askResetQuestions } = require(`./questions`); 5 | const { getTemplate } = require(`./templates`); 6 | module.exports = { 7 | installDependencies, 8 | generateDockerFile, 9 | file, 10 | askGenerateQuestions, 11 | askResetQuestions, 12 | getTemplate 13 | }; 14 | -------------------------------------------------------------------------------- /core/questions.js: -------------------------------------------------------------------------------- 1 | const prompts = require(`prompts`); 2 | const { loadProviders, loadProviderConfig } = require(`../config`); 3 | const chalk = require(`chalk`); 4 | 5 | const askGenerateQuestions = async () => { 6 | let initialQuestions = [ 7 | { 8 | type: `text`, 9 | name: `projectName`, 10 | message: `Project Name`, 11 | validate: value => (value ? true : `Project name is required`) 12 | }, 13 | { 14 | type: `select`, 15 | name: `provider`, 16 | message: `What provider do you want to use?`, 17 | warn: `Not enabled yet`, 18 | choices: getProviders() 19 | } 20 | ]; 21 | 22 | const initialAnswers = await prompts(initialQuestions); 23 | 24 | let supplementaryQuestions = [ 25 | { 26 | type: `select`, 27 | name: `env`, 28 | message: `What enviroments do you want to configure?`, 29 | choices: [ 30 | { 31 | title: `Development`, 32 | value: `development`, 33 | description: `Creates a development environment` 34 | }, 35 | { 36 | title: `Production`, 37 | value: `production`, 38 | description: `Creates a production environment` 39 | } 40 | ] 41 | }, 42 | { 43 | name: `useDocker`, 44 | type: `confirm`, 45 | message: `Are you using Docker for deployment? 🐳`, 46 | initial: false 47 | }, 48 | { 49 | type: prev => (prev.useDocker ? `confirm` : null), 50 | name: `useDockerTool`, 51 | message: `Do you have a Docker.prod file or do you want us to create one?`, 52 | initial: false 53 | } 54 | ]; 55 | 56 | const providerRegions = loadProviderConfig(initialAnswers.provider).regions; 57 | if (providerRegions) { 58 | supplementaryQuestions.unshift({ 59 | type: `select`, 60 | name: `region`, 61 | message: `What region do you want to deploy to? 🌍`, 62 | choices: providerRegions 63 | }); 64 | } 65 | const supplementaryAnswers = await prompts(supplementaryQuestions); 66 | 67 | return { 68 | ...initialAnswers, 69 | ...supplementaryAnswers 70 | }; 71 | }; 72 | 73 | const askResetQuestions = async detectedProvider => { 74 | let { environments } = await prompts([ 75 | { 76 | type: `multiselect`, 77 | name: `environments`, 78 | message: `Pick the environments to clean`, 79 | choices: [ 80 | { title: `Development`, value: `development` }, 81 | { title: `Production`, value: `production` } 82 | ], 83 | min: 1, 84 | hint: `- Space to select. Return to submit` 85 | } 86 | ]); 87 | if (detectedProvider) { 88 | const { providerConfirmation } = await prompts([ 89 | { 90 | type: `confirm`, 91 | name: `providerConfirmation`, 92 | initial: true, 93 | message: `Is ${chalk.yellow.bold( 94 | detectedProvider 95 | )} the provider you want to reset?` 96 | } 97 | ]); 98 | if (providerConfirmation) { 99 | return { environments, provider: detectedProvider }; 100 | } 101 | } 102 | 103 | const { provider } = await prompts([ 104 | { 105 | type: `select`, 106 | name: `provider`, 107 | message: `What provider do you want to use?`, 108 | warn: `Not enabled yet`, 109 | choices: getProviders() 110 | } 111 | ]); 112 | return { environments, provider }; 113 | }; 114 | 115 | const getProviders = () => { 116 | let providerChoices = []; 117 | for (const providerKey in loadProviders()) { 118 | const provider = loadProviderConfig(providerKey); 119 | 120 | providerChoices.push({ 121 | title: provider.name, 122 | value: providerKey, 123 | description: provider.description, 124 | disabled: !provider.enabled 125 | }); 126 | } 127 | return providerChoices; 128 | }; 129 | 130 | module.exports = { 131 | askGenerateQuestions, 132 | askResetQuestions 133 | }; 134 | -------------------------------------------------------------------------------- /core/templates.js: -------------------------------------------------------------------------------- 1 | const strapiServerTemplate = fileExtension => { 2 | return `${ 3 | fileExtension === `ts` ? `export default` : `module.exports = ` 4 | } ({ env }) => ({ 5 | url: env('WEBSITE_URL'), 6 | port: process.env.PORT, 7 | }); 8 | `; 9 | }; 10 | 11 | const strapiDatabaseTemplate = fileExtension => { 12 | return `const parse = require('pg-connection-string').parse; 13 | const config = parse(process.env.DATABASE_URL); 14 | 15 | ${ 16 | fileExtension === `ts` ? `export default` : `module.exports = ` 17 | } ({ env }) => ({ 18 | connection: { 19 | client: "postgres", 20 | connection: { 21 | host: config.host, 22 | port: config.port, 23 | database: config.database, 24 | user: config.user, 25 | password: config.password, 26 | ssl: { 27 | rejectUnauthorized: false, 28 | }, 29 | }, 30 | debug: false, 31 | }, 32 | }); 33 | `; 34 | }; 35 | 36 | const getTemplate = (name, fileExtension) => { 37 | let template; 38 | switch (name) { 39 | case `server`: 40 | template = strapiServerTemplate(fileExtension); 41 | break; 42 | case `database`: 43 | template = strapiDatabaseTemplate(fileExtension); 44 | break; 45 | default: 46 | template = ``; 47 | break; 48 | } 49 | 50 | return template; 51 | }; 52 | 53 | module.exports = { getTemplate }; 54 | -------------------------------------------------------------------------------- /flow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/strapi-tool-deployify/c64a82609be6f62b5fc88ea561f2b5a95628a3ca/flow.pdf -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { commands, outputs, getArgs } = require(`./cli`); 3 | const args = getArgs(); 4 | const { input, flags } = args; 5 | const { clear, debug } = flags; 6 | 7 | (async () => { 8 | // clear console 9 | if (clear) { 10 | console.clear(); 11 | } 12 | 13 | await outputs.welcome(); 14 | 15 | if (debug) { 16 | outputs.debug(args); 17 | } 18 | 19 | // retrieve command 20 | let cmd = commands[input]; 21 | 22 | // default to generate 23 | if (!cmd) { 24 | cmd = commands[`generate`]; 25 | } 26 | 27 | let commandSucess = true; 28 | try { 29 | // invoke command found 30 | await cmd.invoke(args); 31 | } catch (error) { 32 | commandSucess = false; 33 | outputs.error(error); 34 | } 35 | 36 | outputs.goodbye({ quit: commandSucess }); 37 | })(); 38 | 39 | process.on(`unhandledRejection`, err => { 40 | outputs.error(err); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strapi-community/deployify", 3 | "description": "Easily deploy a Strapi Project to cloud platforms", 4 | "version": "0.0.0-development", 5 | "license": "MIT", 6 | "bin": { 7 | "strapi-strapi-tool-deployify": "index.js", 8 | "strapi-deployify": "index.js" 9 | }, 10 | "author": "Simen Daehlin (https://dehlin.dev)", 11 | "keywords": [ 12 | "strapi-tool-deployify", 13 | "Simen Daehlin", 14 | "Strapi", 15 | "Strapi Community" 16 | ], 17 | "files": [ 18 | "index.js", 19 | "utils", 20 | "cli", 21 | "config", 22 | "core", 23 | "providers", 24 | "utils" 25 | ], 26 | "scripts": { 27 | "format": "prettier --write \"./**/*.{js,json}\"", 28 | "semantic-release": "semantic-release --branches main", 29 | "commit": "git-cz", 30 | "acp": "git add . && npm run commit", 31 | "lint": "eslint --ext .js --fix \"./**/*.js\"" 32 | }, 33 | "dependencies": { 34 | "chalk": "^4.1.2", 35 | "cli-meow-help": "^3.1.0", 36 | "defu": "^6.1.0", 37 | "fs-extra": "^10.1.0", 38 | "hookable": "^5.3.0", 39 | "liquidjs": "^9.41.0", 40 | "log-symbols": "4.1.0", 41 | "meow": "^9.0.0", 42 | "new-github-issue-url": "^0.2.1", 43 | "node-fetch": "^2.6.7", 44 | "open": "^8.4.0", 45 | "ora": "^5.4.1", 46 | "prompts": "^2.4.2", 47 | "shelljs": "^0.8.5" 48 | }, 49 | "devDependencies": { 50 | "cz-conventional-changelog": "^3.3.0", 51 | "eslint": "^8.21.0", 52 | "eslint-plugin-import": "^2.26.0", 53 | "prettier": "^2.7.1", 54 | "semantic-release": "^19.0.3" 55 | }, 56 | "main": "index.js", 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/strapi-community/strapi-tool-deployify.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/strapi-community/strapi-tool-deployify/issues" 63 | }, 64 | "homepage": "https://github.com/strapi-community/strapi-tool-deployify#readme", 65 | "url": "https://github.com/strapi-community/strapi-tool-deployify", 66 | "config": { 67 | "commitizen": { 68 | "path": "./node_modules/cz-conventional-changelog" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /providers/aws/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/strapi-tool-deployify/c64a82609be6f62b5fc88ea561f2b5a95628a3ca/providers/aws/.gitkeep -------------------------------------------------------------------------------- /providers/digitalocean/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/strapi-tool-deployify/c64a82609be6f62b5fc88ea561f2b5a95628a3ca/providers/digitalocean/.gitkeep -------------------------------------------------------------------------------- /providers/google/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/strapi-tool-deployify/c64a82609be6f62b5fc88ea561f2b5a95628a3ca/providers/google/.gitkeep -------------------------------------------------------------------------------- /providers/heroku/authentication.js: -------------------------------------------------------------------------------- 1 | const shell = require(`shelljs`); 2 | const os = require(`os`); 3 | const path = require(`path`); 4 | const { access } = require(`fs/promises`); 5 | const { loadProviderConfig } = require(`../../config`); 6 | const { spinner } = require(`../../utils/spinner`); 7 | const childProcess = require(`child_process`); 8 | 9 | const getApiKey = async () => { 10 | const herokuProvider = loadProviderConfig(`heroku`); 11 | try { 12 | const apiToken = await shell 13 | .cat(netRCPath()) 14 | .grep( 15 | `[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}` 16 | ) 17 | .substring(11, 47); 18 | herokuProvider.apiToken = apiToken; 19 | } catch (error) { 20 | spinner.stopAndPersist({ 21 | symbol: `❌`, 22 | text: `Unable to get API key from Heroku. Please make sure you are logged in to Heroku.` 23 | }); 24 | } 25 | }; 26 | 27 | const isWindowsPlatform = () => { 28 | return os.platform() === `win32` ? true : false; 29 | }; 30 | 31 | const herokuAuthenticate = async () => { 32 | if (isWindowsPlatform()) { 33 | childProcess.execSync(`heroku login`, { 34 | shell: true 35 | }); 36 | } else { 37 | childProcess.execSync(`heroku`, [`login`], { 38 | stdio: `inherit` 39 | }); 40 | } 41 | }; 42 | 43 | const netRCPath = () => { 44 | const netRC = isWindowsPlatform() ? `_netrc` : `.netrc`; 45 | return path.join(os.homedir(), netRC); 46 | }; 47 | 48 | const netrcExists = async () => { 49 | try { 50 | const test = await access(path.join(os.homedir(), netRCPath())); 51 | console.log(test); 52 | return true; 53 | } catch (error) { 54 | return false; 55 | } 56 | }; 57 | 58 | module.exports = { getApiKey, herokuAuthenticate, netRCPath, netrcExists }; 59 | -------------------------------------------------------------------------------- /providers/heroku/destroyHerokuApp.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const shell = require(`shelljs`); 3 | const { spinner } = require(`../../utils`); 4 | const prompts = require(`prompts`); 5 | 6 | const destroyHerokuApp = async ({ config, herokuConfig }) => { 7 | const test = shell.exec( 8 | `HEROKU_API_KEY="${herokuConfig.apiToken}" heroku apps --json -p`, 9 | { silent: true } 10 | ); 11 | 12 | // map over test and return title and value 13 | const herokuApps = JSON.parse(test.stdout).map(app => { 14 | return { 15 | title: app.name, 16 | value: app.name 17 | }; 18 | }); 19 | let { apps } = await prompts([ 20 | { 21 | type: `multiselect`, 22 | name: `apps`, 23 | message: `Pick the environments to clean`, 24 | choices: herokuApps, 25 | min: 1, 26 | hint: `- Space to select. Return to submit - ctrl/cmd + c to cancel` 27 | } 28 | ]); 29 | if (!apps) { 30 | spinner.stopAndPersist({ 31 | symbol: `🦄`, 32 | text: ` Nothing selected so not destroying any apps \n` 33 | }); 34 | return; 35 | } 36 | for (const app of apps) { 37 | spinner.start(` 🦄 Tearing down ${chalk.magenta.bold(app.toUpperCase())}`); 38 | shell.exec( 39 | `HEROKU_API_KEY="${herokuConfig.apiToken}" heroku apps:destroy ${app} --confirm ${app}`, 40 | { silent: true } 41 | ); 42 | spinner.stopAndPersist({ 43 | symbol: `💀`, 44 | text: ` Teared ${chalk.yellow(app)} down from strapi \n` 45 | }); 46 | } 47 | 48 | spinner.stopAndPersist({ 49 | symbol: `🤠 `, 50 | text: `Note from ${chalk.magenta( 51 | `heroku` 52 | )} here are the apps that are left on your heroku account 👇 \n` 53 | }); 54 | shell.exec(`HEROKU_API_KEY="${herokuConfig.apiToken}" heroku apps`, {}); 55 | console.log(`\n`); 56 | }; 57 | 58 | module.exports = { 59 | destroyHerokuApp 60 | }; 61 | -------------------------------------------------------------------------------- /providers/heroku/generateHerokuServices.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const shell = require(`shelljs`); 3 | const { spinner } = require(`../../utils`); 4 | 5 | const generateHerokuServices = async ({ config, herokuConfig }) => { 6 | await generateApp({ config, herokuConfig }); 7 | if (config.useDocker) await generateContainer({ config, herokuConfig }); 8 | await generateEnv({ config, herokuConfig }); 9 | await generateDB({ config, herokuConfig }); 10 | }; 11 | 12 | const generateEnv = async ({ config, herokuConfig }) => { 13 | shell.exec( 14 | `HEROKU_API_KEY="${herokuConfig.apiToken}" heroku config:set WEBSITE_URL=$(heroku info -s --app=${config.projectName} | grep web_url | cut -d= -f2) APP_KEYS=${config.strapiSecrets.appKeys} API_TOKEN_SALT=${config.strapiSecrets.apiTokenSalt} ADMIN_JWT_SECRET=${config.strapiSecrets.adminJwtSecret} JWT_SECRET=${config.strapiSecrets.jwtSecret} NODE_ENV=${config.env} --app ${config.projectName}`, 15 | { silent: true } 16 | ); 17 | spinner.stopAndPersist({ 18 | symbol: `⚙️`, 19 | text: ` Configuring ${chalk.magenta.bold( 20 | `${config.projectName.toUpperCase()}'s` 21 | )} enviroment variables on ${chalk.magenta.bold( 22 | `Heroku` 23 | )} (${chalk.blue.bold(config.region.toUpperCase())})` 24 | }); 25 | }; 26 | 27 | const generateApp = async ({ config, herokuConfig }) => { 28 | spinner.stopAndPersist({ 29 | symbol: `🌍`, 30 | text: ` Spinning up ${chalk.magenta.bold( 31 | config.projectName.toUpperCase() 32 | )} app on ${chalk.magenta.bold(`Heroku`)} (${chalk.blue.bold( 33 | config.region.toUpperCase() 34 | )})` 35 | }); 36 | shell.exec( 37 | `HEROKU_API_KEY="${herokuConfig.apiToken}" heroku create ${config.projectName} --region ${config.region}` 38 | ); 39 | }; 40 | 41 | const generateDB = async ({ config, herokuConfig }) => { 42 | shell.exec( 43 | `HEROKU_API_KEY="${herokuConfig.apiToken}" heroku addons:create heroku-postgresql:hobby-dev --app ${config.projectName}`, 44 | { silent: true } 45 | ); 46 | spinner.stopAndPersist({ 47 | symbol: `🧚`, 48 | text: ` Spinning up a PostgresSQL database on ${chalk.magenta.bold( 49 | `Heroku` 50 | )} (${chalk.blue.bold(config.region.toUpperCase())})` 51 | }); 52 | spinner.stopAndPersist({ 53 | symbol: `🔗`, 54 | text: ` Linking your new Database to ${chalk.magenta.bold( 55 | config.projectName.toUpperCase() 56 | )} project on ${chalk.magenta.bold(`Heroku`)} (${chalk.blue.bold( 57 | config.region.toUpperCase() 58 | )})` 59 | }); 60 | }; 61 | 62 | const generateContainer = async ({ config, herokuConfig }) => { 63 | shell.exec( 64 | `HEROKU_API_KEY="${herokuConfig.apiToken}" stack:set container --app ${config.projectName}`, 65 | { silent: true } 66 | ); 67 | spinner.stopAndPersist({ 68 | symbol: `🐳`, 69 | text: ` Configuring ${chalk.magenta.bold( 70 | `${config.projectName.toUpperCase()}'s` 71 | )} to use Docker on ${chalk.magenta.bold(`Heroku`)} (${chalk.blue.bold( 72 | config.region.toUpperCase() 73 | )})` 74 | }); 75 | }; 76 | 77 | module.exports = { 78 | generateHerokuServices 79 | }; 80 | -------------------------------------------------------------------------------- /providers/heroku/heroku.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner, detect } = require(`../../utils`); 3 | const { generateHerokuTemplate } = require(`./herokuTemplate`); 4 | const outputs = require(`../../cli/outputs`); 5 | const { generateHerokuServices } = require(`./generateHerokuServices`); 6 | const { destroyHerokuApp } = require(`./destroyHerokuApp`); 7 | const { herokuAuthenticate, netrcExists } = require(`./authentication`); 8 | const { file } = require(`../../core`); 9 | 10 | const herokuSetup = async ({ config, herokuConfig }) => { 11 | outputs.info(`Generating heroku configuration file`); 12 | 13 | const generatedTemplate = await generateHerokuTemplate({ 14 | config, 15 | herokuConfig 16 | }); 17 | 18 | if (!generatedTemplate) { 19 | outputs.error(`Unable to generate render template`); 20 | } 21 | 22 | spinner.stopAndPersist({ 23 | symbol: `⚙️ `, 24 | text: `Added and configured ${chalk.bold.green( 25 | config.provider.toUpperCase() 26 | )} to project \n` 27 | }); 28 | }; 29 | 30 | module.exports = { 31 | herokuHooks: { 32 | async prebuild() { 33 | await outputs.info(`This tool will only create NEW project on heroku`); 34 | await detect.herokuCLI(); 35 | if (!netrcExists()) await herokuAuthenticate(); 36 | }, 37 | async build({ config, providerConfig }) { 38 | if (config.useDocker) { 39 | await herokuSetup({ config, herokuConfig: providerConfig }); 40 | } 41 | }, 42 | async postbuild({ config, providerConfig }) { 43 | await generateHerokuServices({ config, herokuConfig: providerConfig }); 44 | }, 45 | async destroy({ config, providerConfig }) { 46 | await file.remove(providerConfig.outputFileName); 47 | await destroyHerokuApp({ config, herokuConfig: providerConfig }); 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /providers/heroku/herokuTemplate.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`); 2 | const { resolve } = require(`path`); 3 | const { Liquid } = require(`liquidjs`); 4 | const { file } = require(`../../core`); 5 | 6 | const liquidEngine = new Liquid({ 7 | root: resolve(__dirname, `templates`), 8 | extname: `.liquid` 9 | }); 10 | 11 | const generateHerokuTemplate = ({ config, herokuConfig }) => { 12 | const template = liquidEngine.renderFileSync(`heroku`, { 13 | dockerFile: config.env === `production` ? `Dockerfile.prod` : `Dockerfile`, 14 | env: config.env 15 | }); 16 | const filePath = path.join(config.outDir, herokuConfig.outputFileName); 17 | return file.generate(filePath, template); 18 | }; 19 | 20 | module.exports = { generateHerokuTemplate }; 21 | -------------------------------------------------------------------------------- /providers/heroku/index.js: -------------------------------------------------------------------------------- 1 | const { herokuHooks } = require(`./heroku`); 2 | 3 | module.exports = { 4 | hooks: herokuHooks 5 | }; 6 | -------------------------------------------------------------------------------- /providers/heroku/templates/heroku.liquid: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: {{dockerFile}} 4 | config: 5 | NODE_ENV: {{env}} 6 | DATABASE_URL: $DATABASE_URL 7 | WEBSITE_URL: $WEBSITE_URL 8 | PORT: $PORT 9 | 10 | run: 11 | web: yarn start 12 | -------------------------------------------------------------------------------- /providers/render/index.js: -------------------------------------------------------------------------------- 1 | const { renderHooks } = require(`./render`); 2 | 3 | module.exports = { 4 | hooks: renderHooks 5 | }; 6 | -------------------------------------------------------------------------------- /providers/render/render.js: -------------------------------------------------------------------------------- 1 | const chalk = require(`chalk`); 2 | const { spinner } = require(`../../utils`); 3 | const outputs = require(`../../cli/outputs`); 4 | const { generateRenderTemplate } = require(`./renderFile`); 5 | const { file } = require(`../../core`); 6 | 7 | const renderSetup = async ({ config, renderConfig }) => { 8 | outputs.info(`Generating render configuration file`); 9 | 10 | const generatedTemplate = await generateRenderTemplate({ 11 | config, 12 | renderConfig 13 | }); 14 | 15 | if (!generatedTemplate) { 16 | outputs.error(`Unable to generate render template`); 17 | } 18 | 19 | spinner.stopAndPersist({ 20 | symbol: `⚙️ `, 21 | text: `Added and configured ${chalk.bold.green( 22 | config.provider.toUpperCase() 23 | )} to project \n` 24 | }); 25 | 26 | spinner.stopAndPersist({ 27 | symbol: `🚀 `, 28 | text: `Project is now ready, just push to your version control provider. 29 | 🚀 visit https://dashboard.render.com/select-repo?type=blueprint 30 | 🚀 where you can connect your repo and deploy your app \n` 31 | }); 32 | }; 33 | 34 | module.exports = { 35 | renderHooks: { 36 | async build({ config, providerConfig }) { 37 | await renderSetup({ config, renderConfig: providerConfig }); 38 | }, 39 | async destroy({ config, providerConfig }) { 40 | await file.remove(providerConfig.outputFileName); 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /providers/render/renderFile.js: -------------------------------------------------------------------------------- 1 | const { Liquid } = require(`liquidjs`); 2 | const path = require(`path`); 3 | const { resolve } = require(`path`); 4 | const { file } = require(`../../core`); 5 | 6 | const liquidEngine = new Liquid({ 7 | root: resolve(__dirname, `templates`), 8 | extname: `.liquid` 9 | }); 10 | 11 | const generateRenderTemplate = ({ config, renderConfig }) => { 12 | const template = liquidEngine.renderFileSync(`render`, { 13 | name: config.projectName, 14 | env: config.env, 15 | nodeVersion: +process.version.match(/^v(\d+\.\d+)/)[1], 16 | region: config.region, 17 | docker: config.useDocker 18 | }); 19 | const filePath = path.join(config.outDir, renderConfig.outputFileName); 20 | return file.generate(filePath, template); 21 | }; 22 | 23 | module.exports = { 24 | generateRenderTemplate 25 | }; 26 | -------------------------------------------------------------------------------- /providers/render/templates/render.liquid: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: {{ name }} 4 | {%- if docker %} 5 | env: docker 6 | {%- if env == 'production' %} 7 | dockerfilePath: ./Dockerfile.prod 8 | {%- else %} 9 | dockerfilePath: ./Dockerfile{%- endif %} 10 | {%- else %} 11 | env: node 12 | {% endif %} 13 | plan: starter 14 | region: {{ region }} 15 | buildCommand: yarn install && yarn build 16 | startCommand: yarn start 17 | healthCheckPath: /_health 18 | disk: 19 | name: {{ name }}-uploads 20 | mountPath: /opt/render/project/src/public/uploads 21 | sizeGB: 1 22 | envVars: 23 | - key: NODE_VERSION 24 | value: {{ nodeVersion }} 25 | - key: NODE_ENV 26 | value: {{ env }} 27 | - key: DATABASE_URL 28 | fromDatabase: 29 | name: {{ name }} 30 | property: connectionString 31 | - key: JWT_SECRET 32 | generateValue: true 33 | - key: ADMIN_JWT_SECRET 34 | generateValue: true 35 | - key: APP_KEYS 36 | generateValue: true 37 | - key: API_TOKEN_SALT 38 | generateValue: true 39 | 40 | databases: 41 | - name: {{ name }} 42 | plan: starter 43 | -------------------------------------------------------------------------------- /utils/bugreport.js: -------------------------------------------------------------------------------- 1 | const open = require(`open`); 2 | const newGithubIssueUrl = require(`new-github-issue-url`); 3 | 4 | const generateError = async error => { 5 | const url = newGithubIssueUrl({ 6 | repo: `strapi-tool-deployify`, 7 | user: `strapi-community`, 8 | title: `Error: ${error.message}`, 9 | template: `BUG_REPORT.md`, 10 | assignee: `eventyret`, 11 | body: `## 🐛 Bug Report 12 | 13 | ## 🤷‍♀️ What did you do 14 | 15 | 16 | 17 | ## ⛔️ Error log 18 | 19 | ${error.message} 20 | 21 | ### 🕵️‍♀️ Stack trace 22 | 23 | \`\`\`bash 24 | ${error.stack} 25 | \`\`\` 26 | 27 | ## 🙇‍♀️ Expected behavior/code 28 | 29 | A clear and concise description of what you expected to happen (or code). 30 | 31 | ## 👩‍💻 Environment 32 | 33 | - 📦 Node version: ${process.version} 34 | - 💻 OS: ${process.platform} ${process.arch} 35 | 36 | ## 💡 Possible Solution 37 | 38 | 39 | 40 | ## 📺 Additional context/Screenshots 41 | 42 | ` 43 | }); 44 | await open(url); 45 | }; 46 | 47 | module.exports = generateError; 48 | -------------------------------------------------------------------------------- /utils/detection.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`); 2 | const shell = require(`shelljs`); 3 | const chalk = require(`chalk`); 4 | const { pathExists } = require(`fs-extra`); 5 | const { buildConfig } = require(`../config`); 6 | const { spinner } = require(`./spinner`); 7 | const child_process = require(`child_process`); 8 | const { getApiKey } = require(`../providers/heroku/authentication`); 9 | const { loadProviders, loadProviderConfig } = require(`../config`); 10 | 11 | const projectType = async () => { 12 | const isTS = await pathExists(path.join(process.cwd(), `tsconfig.json`)); 13 | 14 | if (isTS) { 15 | return `ts`; 16 | } 17 | 18 | return `js`; 19 | }; 20 | 21 | const provider = async () => { 22 | const providers = loadProviders(); 23 | 24 | let providerName = null; 25 | for (const provider in providers) { 26 | try { 27 | const providerConfig = loadProviderConfig(provider); 28 | const hasProviderGeneratedFile = await pathExists( 29 | providerConfig.outputFileName 30 | ); 31 | if (hasProviderGeneratedFile) { 32 | providerName = providerConfig.name; 33 | break; 34 | } 35 | } catch (error) {} 36 | } 37 | 38 | return providerName; 39 | }; 40 | 41 | const packageManager = async () => { 42 | const [isYarn, isNPM] = await Promise.all([ 43 | pathExists(`yarn.lock`), 44 | pathExists(`package-lock.json`) 45 | ]); 46 | 47 | if (isYarn) { 48 | return `yarn`; 49 | } 50 | 51 | if (isNPM) { 52 | return `npm`; 53 | } 54 | 55 | return `unknown`; 56 | }; 57 | 58 | const herokuCLI = async () => { 59 | const herokuCLI = await shell.which(`heroku`); 60 | if (herokuCLI) { 61 | buildConfig({ herokuCLI: true }); 62 | 63 | spinner.stopAndPersist({ 64 | symbol: `💻`, 65 | text: ` ${chalk.bold.magenta(`Heroku`)} CLI detected \n` 66 | }); 67 | await getApiKey(); 68 | const providers = loadProviders(); 69 | if (!providers.heroku.apiToken) { 70 | child_process.execFileSync(`heroku`, [`login`], { stdio: `inherit` }); 71 | await getApiKey(); 72 | } 73 | } else { 74 | spinner.stopAndPersist({ 75 | symbol: `💻`, 76 | text: ` ${chalk.bold.magenta( 77 | `Heroku` 78 | )} CLI not detected, installing the tool \n` 79 | }); 80 | shell.exec(`npm install -g heroku`, { silent: true }); 81 | spinner.stopAndPersist({ 82 | symbol: `🪄`, 83 | text: ` Please login to ${chalk.magenta.bold(`Heroku`)} to continue 👇 \n` 84 | }); 85 | child_process.execFileSync(`heroku`, [`login`], { stdio: `inherit` }); 86 | } 87 | }; 88 | 89 | module.exports = { 90 | packageManager, 91 | projectType, 92 | provider, 93 | herokuCLI 94 | }; 95 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | const generateError = require(`./bugreport`); 2 | const { spinner } = require(`./spinner`); 3 | const detect = require(`./detection`); 4 | 5 | module.exports = { 6 | spinner, 7 | generateError, 8 | detect 9 | }; 10 | -------------------------------------------------------------------------------- /utils/spinner.js: -------------------------------------------------------------------------------- 1 | const ora = require(`ora`); 2 | const spinner = ora({ text: `` }); 3 | 4 | module.exports = { 5 | spinner 6 | }; 7 | -------------------------------------------------------------------------------- /utils/stats.js: -------------------------------------------------------------------------------- 1 | const fetch = require(`node-fetch`); 2 | 3 | const NPM_PACKAGE_STATS_URL = `https://api.npmjs.org/downloads/point/last-month/@strapi-community/deployify`; 4 | const GITHUB_STATS_URL = `https://api.github.com/repos/strapi-community/strapi-tool-deployify`; 5 | 6 | const getNPMStats = async () => { 7 | const res = await fetch(NPM_PACKAGE_STATS_URL); 8 | const npm = await res.json(); 9 | return { 10 | downloads: npm.downloads 11 | }; 12 | }; 13 | 14 | const getGithubStats = async () => { 15 | const res = await fetch(GITHUB_STATS_URL); 16 | const github = await res.json(); 17 | return { 18 | stars: github.stargazers_count 19 | }; 20 | }; 21 | 22 | module.exports = { 23 | getNPMStats, 24 | getGithubStats 25 | }; 26 | --------------------------------------------------------------------------------