Using GitLab CI for automated builds, as described in the previous article “Front-end Continuous Integration,” has its downsides as it requires compilation and packaging on the server, which takes up server resources. In addition, during the development of the desktop using electron vue some time ago, we needed to transfer the installation package to a certain address on the server for the testers to download, and encountered the following problems: Linux cannot hit the MAC installation package, hit the Windows package also need to install additional plug-ins, this time can only use the most primitive method, local packaging, manual upload to the server. This is still quite inconvenient, online research, there are a lot of automatic deployment schemes, and then according to the needs of their own projects, the implementation of the scheme is as follows:
First, the overall idea
We need to implement a cli tool, when the user to enter a build commands in a terminal, perform the following steps 1, 2, local compressed package and connect to the server. 3, the local package uploaded to the server under a certain path (4), the server receives the package to extract 5 6, disconnect the server, delete local packaging file
The above is the basic idea to complete the automatic deployment. Since we upload to the server for the testers to download, we need to notify the testers to download the latest installation package during each build, so we also add the function of sending emails.
Second, the implementation
1. Project construction
- Create an auto-deploy folder and execute it on the terminal
npm init
Initialize thepackage.json
. package.json
Add the bin field to theauto-deploy
The location of the executable file corresponding to the command.
"bin": {
"auto-deploy": "bin/auto-deploy-cli.js"
}
Copy the code
auto-deploy
Add a bin folder under the folder to store our entry filesauto-deploy-cli.js
.
#! /usr/bin/env node const Deploy = require('.. / SRC /index.js') // create a new object const deploy = new deploy () // Execute the run function under the object deploy.run()Copy the code
Add a line #! /usr/bin/env node specifies that the current script is parsed by Node.js
auto-deploy
Create a SRC folder under the folderindex.js
Module. Exports = class Deploy {constructor() {} run() {console.log(' build successfully ')}}Copy the code
- It is then executed at the end of the project
npm link
Link the NPM package to the global environment so we can use itauto-deploy
The command.
The NPM link command links an NPM package anywhere to the global execution environment so that it can be run directly from the command line anywhere. Briefly, this command does two things: Create a soft link for the NPM package directory and link it to {prefix}/lib/node_modules/ Create a soft link for the executable file (bin) and link it to {prefix}/bin/{name}. In Windows, the two paths are: directory: C:\Users{Username}\AppData\Roaming\ NPM \node_modules
File: C:\Users{Username}\AppData\Roaming\npm
Now let’s execute the command on the terminal
After successfully executing our project by running the auto-deploy command, a basic CLI project is set up.
2. Plan design
When the user enters a build command at the terminal, we need to follow the steps in the whole idea, but what command do we need to package according to, what is the port number of the server, what is the username and password, what is the path to upload to the server, so we need to have a configuration file to record this information, You need to implement the command auto-deploy init to initialize the configuration file. You also need to implement an auto-deploy build command to execute the steps in the overall idea in turn.
Plan a
We can use process.argv.slice(2) to get command line arguments to determine what to do, as follows
// auto-deploy-cli.js #! /usr/bin/env node const Deploy = require('.. /src/index.js') const deploy = new Deploy() const command = process.argv.slice(2) deploy.run(command) // index.js Module. Exports = class Deploy {constructor() {} run(command) {if (command[0] === 'init') {console.log(' init', Else if (command[0] === 'build') {console.log(' build deployment command, build deployment operation ')}}}Copy the code
Terminal input in sequenceauto-deploy init
,auto-deploy build
Command:
If this idea is implemented, it is not conducive to the subsequent maintenance and extension of the code, and the code will be very chaotic when there are more commands. You can use the commander third-party package to add commands.
Scheme 2
Use commander as follows:
Const program = require('commander') program.command (commanName) // Add command.description (command-description) // the description of the command .option('-n, --name <items1> [items2]', 'name description', 'default value') // Define commander options options. action((options) => {// command callback function})Copy the code
- Add commands to the SRC folder where the commands execution files are stored. Add files under this folder
init.js
,build.js
.
init.js
Module.exports = {description: 'initialization command ', run: function () {console.log(' initialization command, initialization operation ')}}Copy the code
build.js
Module. exports = {description: 'build deployment command ', option: '-t, --type <type>', optionDescription: 'setup deploy type', run: Function () {console.log(' build deployment command, perform build deployment operation ')}}Copy the code
- In the index.js file of the SRC file, iterate over the commands execution file and register the corresponding commands
Module. Exports = class Deploy {run() {const program = require('commander') const fs = require('fs') module.exports = class Deploy {run() { __dirname: Const commandsPath = '${__dirname}/commands' // To traverse the command execution file, ReaddirSync (' ${commandsPath} '). ForEach ((fileName) =>{const command = require(' ${commandsPath}/${fileName} ') Const commandName = filename.split ('.')[0] // Register the command if(command.option){program.command (commandName) .description(command.description) .option(command.option,command.optionDescription) .action((options) => { command.run(options.type) }) }else{ program .command(commandName) .description(command.description) .action((options) => {command-run (options.type)})}}) // Parse command line arguments program.parse(process.argv)}}Copy the code
After the command registration is complete, run the help command on the terminal (default help command).
You can see that we have successfully registeredinit
andbuild
The terminal executes the following commands in sequence:
3. Function realization
Above we have implemented the command registration, next complete each command to perform the function
The init command
First, we need to clarify the functions of each command. When we execute init command, if the project already has a configuration file, it will prompt the user that the configuration file already exists, and do nothing. If the project does not have a configuration file, we need to build a basic configuration file for the user. Configure the information we need to execute other commands. From the above general idea, we know that when we execute the build command, we need to know the build command of the local package build, the location of the packaged file, the IP address and port of the server, the user name and password, and the storage path on the server. Therefore, we need to ask the user these information in turn at the terminal. Inquirer is used in this paper to interact with the command.
Inquirer can be used as follows:
The installation
npm install inquirer
Copy the code
The basic use
var inquirer = require('inquirer');
inquirer
.prompt([
/* Pass your questions in here */
])
.then(answers => {
// Use user feedback for... whatever!!
})
.catch(error => {
if(error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else when wrong
}
});
Copy the code
We also need to output logs on the terminal. We can use the simplest console.log(), but it is impossible to distinguish the types of log information, so we can use Chalk to optimize our log output.
Add an util folder under SRC and add index.js to hold our public methods
src/utils/index.js
Exports = {console.log(chalk) => {console.log(chalk) => {console.log(message)) }, // showErrorText:(message) => {console.log(chalk.inverse. Red ('ERROR') + chalk. Red (message))}, // success log type 1 showSuccessText:(message) => {console.log(chalk. Green (message + 'stocking '))}, // SUCCESS log type 2 showBigSuccessText:(message) => {console.log(chalk.inverse. Green ('SUCCESS') + '+ chalk. Green (message))} }Copy the code
Now let’s implement our configuration initialization
src/commands/init.js
const fs = require('fs') const path = require('path') const inquirer = require('inquirer'); const {showErrorText,showSuccessText} = require('.. /utils/index.js') // where the configuration file is stored (process.cwd() : Working directory for the current command-line run time) const deployConfigPath = '${path.join(process.cwd())}/deploy.config.js' // gets the information entered by the user, const GetUserInputInfo = function(){const prompt = [{type: 'input', name: 'script', message: 'please enter package command'}, {type: 'input', name: 'host', message: 'Please enter server address'}, {type: 'number', name: 'port', message: 'Please enter server port number'}, {type: 'input', name: 'username', message: 'please enter the username of the server'}, {type: 'input', name: 'password', message: 'Please enter the password of the server'}, {type: 'input', name: 'localPath', message: 'please enter local package directory'}, {type: 'input', name: 'remotePath', message: 'Please enter the server deployment path'}, {type: 'confirm', name: 'needEmail', message: 'Do you need to add email function'}, {type: 'input', name: 'addressee', message: 'Please enter recipient'}, {type: 'input', name: 'title', message: 'Please enter message title'}, {type: 'input', name: 'content', message: }] return inquirer. Prompt (prompt)} // Create configuration file const createConfigFile = function(fileJson){const STR = `module.exports = ${JSON.stringify({'win':fileJson},null,2)}` fs.writeFileSync(deployConfigPath,str) } module.exports = {description: 'initialization command ', run: Function () {if(fs.existssync (deployConfigPath)){showErrorText('deploy.config.js already exists, Do not repeat initialization ~') process.exit(1)}else{getUserInputInfo().then((configInfo) => {createConfigFile(configInfo) ") process.exit(0)}). Catch (e => {showErrorText(' failed to configure: '+ e) process.exit(1)}); }}}Copy the code
The json.stringify () method is used to convert JavaScript values to JSON strings. Json.stringify (value[, replacer[, space]]) parameter value: required, JavaScript value to convert (usually an object or array). Replacer: Optional. A function or array used to transform the result. If replacer is a function, json.stringify calls that function, passing in the key and value for each member. Use return values instead of original values. If this function returns undefined, the member is excluded. The key of the root object is an empty string: “”. If the replacer is an array, only the members of that array that have key values are converted. The members are converted in the same order as the keys in the array. Space: Optional, the text is indented, whitespace, and newline characters. If space is a number, the return value text is indented by a specified number of Spaces at each level, or 10 Spaces if space is greater than 10. Space can also use non-numerals, as in: \t.
Init command execution file created, let’s execute:
You can then see that it has been added to the project root directorydeploy.config.js
file
An error occurs when we execute the command again:
The build command
In the build command we need to implement the functions in the overall steps one by one
- Pack locally
We need to get the package command in the configuration file to package the local files
const childProcess = require('child_process') const localBuild = async function(config){ try{ const {script} = config ShowInfoText (' Local packaging... ') await new Promise((resolve,reject) => { childProcess.exec( script, {cwd:process.cwd(),maxBuffer:5000*1024}, (e) = > {the if (e) {reject (e)} else {resolve ()}})})} the catch (e) {showErrorText (' packaging failure: + e) process. The exit (1)}}Copy the code
The child_process module is a child process module of NodeJS that can be used to create a child process and perform some tasks. Exec (command[, options][, callback]) command: Command to run, separated by Spaces. Options > CWD: the current working directory of the child process. Default value: null. Options > maxBuffer: Maximum amount of data (in bytes) allowed on stdout or stderr. If the limit is exceeded, the child process is terminated and the output is truncated. See maxBuffer and Unicode considerations. Default value: 1024 x 1024 Callback: Called and passed in output when the process terminates.
- Compress the packed files
Const fs = require('fs') const archiver = require('archiver') const createZip = async (config) => {try { Const {localPath} = config showInfoText('... ') await new Promise((resolve, Reject) => {const output = fs.createWritestream (' ${process.cwd()}/${localPath}.zip ') // Generate archiver object, Zip const archive = archiver('zip', {zlib: {level: 9} // Sets the compression level.}); Output. On ('close', (e) => {if (e) {output. '+ e) reject(e) process.exit(1)} else {showSuccessText(' ${localPath}.zip package successfully') resolve()}}) // Associate the package object with the output stream Archive.pipe (output) // Append files from subdirectories, Place its contents in the root directory of archive archive.directory(config.localPath, False) // End archive archive.finalize()})} catch (e) {showErrorText(' compression failed: '+ e) process.exit(1)}}Copy the code
Archiver is a cross-platform packaging module in NodeJS, which can be zip and tar packages. It is a convenient three-party module.
- Connecting to the server
const { NodeSSH } = require('node-ssh') const ssh = new NodeSSH() const connactSSH = async (config) => { try { ShowInfoText (' Server connected... ') await ssh.connect({ host: config.host, username: config.username, password: config.password, port: Config. Port}) showSuccessText(' server failed ')} catch (e) {showErrorText(' server failed: '+ e) process.exit(1)}}Copy the code
- Upload the compressed package to the server
Const uploadFile = async (config) => {try {showInfoText(' uploadFile to async... ') const { localPath, remotePath } = config await ssh.putFile(`${process.cwd()}/${localPath}.zip`, '${remotePath}.zip ') showSuccessText(' successful ')} Catch (e) {showErrorText(' failed to upload:' + e) process.exit(1)}}Copy the code
- Unzip remote files
Const unzipFile = async (config) => {try {showInfoText('... ') const { remotePath, needEmail } = config const remoteFileName = `${remotePath}.zip` await ssh.execCommand( `unzip -o ${remoteFileName} -d ${remotePath} && rm -rf ${remoteFileName} ') if (needEmail) {await ssh.execcommand (' echo ' '${config. The content}' | mail -s' ${config. The title} ${config. The addressee}} showSuccessText `) (' remote file decompression success ')} the catch (e) { ShowErrorText (' failed to extract remote file: '+ e) process.exit(1)}}Copy the code
The following configuration file (/etc/mail.rc) is installed and configured on the server:
Set SMTP = SMTPS ://smtp.xxx.com:465 # set SMTP =smtps://smtp.xxx.com:465 # Set [email protected] # Set smtp-auth-password=password # Put the password in here, Set ssl-verify=ignore # Ignore certificate warning set nss-config-dir= /etc/pki-nssdb # Certificate directory set [email protected] # Set the sender's email address and nicknameCopy the code
- Deleting a Local File
Const deleteLocalFile = (config) => {try {showInfoText(' Delete from local package... ') let {localPath} = config function deleteFn(path) {if (fs.existssync (path)) { Fs.readdirsync (path). ForEach ((file) => {var curPath = path + "/" + file; if (fs.statSync(curPath).isDirectory()) { deleteFn(curPath); } else { fs.unlinkSync(curPath); }}); fs.rmdirSync(path); }} deleteFn(localPath) fs.unlinksync (' ${localPath}.zip ') showSuccessText(' delete localPath} succeeded ')} catch (e) { ShowErrorText (' Failed to delete local package: '+ e) process.exit(1)}}Copy the code
- Disconnect from the server
const disconnectSSH = () => {
ssh.dispose()
}
Copy the code
Finally, we perform these operations in turn in our entry function
src/commands/build.js
// Where the configuration file is stored (process.cwd() : Working directory for the current command line runtime) const path = require('path') const deployConfigPath = '${path.join(process.cwd())}/deploy.config.js' Module. exports = {description: 'build deployment command ', option: '-t, --type <type>', optionDescription: 'setup deploy type', run: async function (type) { const config = require(deployConfigPath)[type] if (! Type) {showErrorText(' Please select platform to build ') process.exit(1)} else if (! Config) {showErrorText(' no ${type} platform configuration information, Please configure ') process.exit(1)} else {if (fs.existssync (deployConfigPath)) {await localBuild(config) await createZip(config) await connactSSH(config) await uploadFile(config) await unzipFile(config) await deleteLocalFile(config) disconnectSSH() ShowBigSuccessText (' deployed successfully ') process.exit(0)} else {showErrorText('deploy.config.js config file does not exist, Run the auto-deploy init command to create ') process.exit(1)}}}}Copy the code
Run our build command and see
A basic automatic deployment scheme has been realized, which can be extended according to the corresponding requirements.