├── .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 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------