Automate commits with LaunchAgents & JavaScript
Tags: node, git, macos, github
Published: 2023-02-02 21:43:22

In this article, I will guide you through setting up a LaunchAgent that will automatically run a JavaScript project to update & commit to GitHub. This is a fun way to keep that Github "grass" green.

LaunchAgents contain `.plist` files that your mac will look for and run on login. 

##Important notes: 
- *LaunchAgents are only on macos*
- *Node.js & Git are required* 
- *Any code editor of your choice*

## Table of Contents <a name="contents"></a>
- [Setup Node project](#node-setup)
- [Setup Github repo](#github-repo)
- [Write some JavaScript pt.1](#javascript)
- [Write some JavaScript pt.2](#javascript2)
- [Setup LaunchAgent](#la)

## Setup new Node.js project with git <a name="node-setup"></a>
Let's first start with a fresh node project in a new directory(*using auto-commit in this example*) in your desired location on your mac. With your terminal open, run the following:
```
mkdir auto-commit
cd auto-commit
npm init -y
```
The above command will initialize the project and skip all the required details required by the `package.json` file. You can set these configuration details later if/when you’re ready to add them. 

After running this, you should see `package.json` contents printed in your terminal.

Now we can open this new project and we're going to install one package, [shelljs](https://www.npmjs.com/package/shelljs)
`Shelljs` is a great Command Line Utility for interacting with the command line in JavaScript. 
```
npm i shelljs
```

After this, you will see a `package-lock.json` file and `node_modules` appear in your project. Your `package.json` should look like this:
```javascript
{
  "name": "auto-commit",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "shelljs": "^0.8.5"
  }
}
```

Let's now create our `index.js` file:
```
touch index.js
```

Now we need `git` in our project, so we can push to a remote repo. We're also going to create a `.gitignore` file so we don't push up our `node_modules`:
```
git init
touch .gitignore
```

Inside your new `.gitignore` file, type in `node_modules`.

You should have the following files in your project:
```
node_modules
.gitignore
index.js
package-lock.json
package.json
```

Now it's time to move onto the next step and create an empty repository in Github. 

## Setup new Github repository <a name="github-repo"></a>
We're going to create a new repository on Github. For this example, I am going to name it the same name as my project, `auto-commit`. No other info is needed and you can make it public or private:
![Repo-example](https://i.imgur.com/5gUtbb3.png)

Once it is created, you will now need to copy the remote origin line:
![Repo-example](https://i.imgur.com/dRb1zcI.png)
& paste it in the terminal at the root of your `auto-commit` directory.

Once you do this, you can run the following to see your Github remote link that you just pasted in:
```
git remote -v
```

Now within our project, let's push everything we currently have up to Github. 
```
git add .
git commit -m"first commit with initial file creation"
git push -u origin master
```
Now if you go back to your repo on Github, you will see your files. 

## Time to write some JavaScript <a name="javascript"></a>
######[Table of Contents](#contents)
For this tutorial, we're going to write some code that renames & increments a file name every time the project is ran. 

Let's create a new empty file called `renameMe-0.js` at the root of your project:
```
touch renameMe-0.js
```
Let's push this new file up to Github:
```
git add renameMe-0.js
git commit -m"pushes file up at number 0"
git push
```

Now let's create another file called `nameChange.js` where we will write the logic to find `renameMe-0.js` and update it. 
```
touch nameChange.js
```

At the top of `nameChange.js`, let's bring in `shelljs`
```javascript
const shell = require('shelljs');
```

Let's create our first function and look for the files in our project:
![file-search](https://i.imgur.com/U73JNMB.png)

If you run `node nameChange.js` you will see this function log our projects JavaScript files in addition to the output that this shelljs method gives out within an array in the terminal.
![filesPrinted](https://i.imgur.com/PFPLxFX.png)

Now that we have our array of files & output, all we need is the file named `renameMe-0.js`, so we can use the `filter` array method to return only that file in an array. 
![renamemefile](https://i.imgur.com/Gz87YPy.png)

Now that we have isolated our file, let's create a new function that gets the number from the file name and adds to it so that it can be renamed:
![renamingFile](https://i.imgur.com/zG3WwTs.png)
If you run `node nameChange.js`, you will see `renameMe-1.js` printed to the terminal. 

We can now use `shell` to update our previous file name with the new one & if you run `node nameChange.js` again in the terminal, you will see the file be renamed:
![newName](https://i.imgur.com/RiqCgLk.png)

You will see the file get renamed as well as 3 git changes. 
1. we haven't committed `nameChange.js` yet
2. `renameMe-0.js` was technically deleted
3. `renameMe-1.js` was created 

We should currently have these files in our project:
You should have the following files in your project:
```
node_modules
.gitignore
index.js
nameChange.js.  <-------- added during this section
package-lock.json
package.json
renameMe-1.js   <-------- added during this section
```
Before we move on, let's remove the `setNewFileName()` function call from the end of the file & export `setNewFileName`. Our final `nameChange.js` file should look like this:
```javascript
const shell = require('shelljs');

const getFileNames = () => {
    const fileNames = shell.ls('*.js');
    return fileNames.filter(fileName => fileName.includes('renameMe'));
}

exports.setNewFileName = () => {
    const [fileToRename] = getFileNames(); 
    const fileNumber = fileToRename.split('-')[1]; 

    const newFileName = `renameMe-${parseInt(fileNumber) + 1}.js`; 
    
    shell.mv(fileToRename, newFileName);
    return { fileToRename, newFileName };
}
```
At this point, we are ready to move one & get `git` involved. 

## Let's write some JavaScript pt. 2 <a name="javascript2"></a>
######[Table of Contents](#contents)

Let's start by creating a new file at the root of the project called `gitCommands.js`. We need to bring in `shelljs` and our `setNewFileName` function:
```javascript
const shell = require('shelljs');
const { setNewFileName } = require('./nameChange');
```

We're going to create a new function expression called `commitNewFile` and destructure the object we returned in `setNewFileName`:

```javascript
const commitNewFile = () => {
    const { fileToRename, newFileName } = setNewFileName();
}
```

Now let's setup "state-like" variables to help debug in the future if necessary & setup three command string variables to `git add`,`git commit` & `git push` the files. 
```javascript
const commitNewFile = () => {
  const { fileToRename, newFileName } = setNewFileName();
  let isFileAdded = false;
  let error = { message: "", code: "" };

  const gitAddFiles = `git add ${fileToRename} ${newFileName}`;
  const gitCommitFiles = `git commit -m "renamed ${fileToRename} to ${newFileName}"`;
  const gitPushFiles = `git push`;
};
```

Now let's use `shell` to execute these commands`(add, commit, push)`:
```javascript
const shell = require("shelljs");
const { setNewFileName } = require("./nameChange");

const commitNewFile = () => {
  const { fileToRename, newFileName } = setNewFileName();
  let isFileAdded = false;
  let error = { message: "", code: "" };

  const gitAddFiles = `git add ${fileToRename} ${newFileName}`;
  const gitCommitFiles = `git commit -m "renamed ${fileToRename} to ${newFileName}"`;
  const gitPushFiles = `git push`;

  shell.exec(gitAddFiles, (code, stdout, stderr) => {
    // code is 0 for success, 1 for error
    code === 0 ? (isFileAdded = true) : (error = { message: stderr, code });

    console.log(`File added to git: ${isFileAdded}`);

    if (isFileAdded) {
      shell.exec(gitCommitFiles, (code) => {
        code === 0 ? shell.exec(gitPushFiles) : console.log(error);
      });
    }
  });
};
```

Before we try this code out, let's manually commit & push up our `renameMe-1.js` file. 
```
git add renameMe-1.js
git commit -m"adds manual push of renameMe file"
git push
```

Now that file should have a clean git slate. 

Lets export our `commitNewFile`, import it & invoke it within `index.js`
```javascript
exports.commitNewFile = () => ....
```
Inside `index.js`:
```javascript
const { commitNewFile } = require("./gitCommands");

commitNewFile();
```

Now if we run `node index.js`, you should see everything in action. The file should be renamed to `renameMe-2.js`, committed and pushed to Github. 

Let's add the rest of our files and move on to the next step. 
```
git add .
git commit -m"adds nameChange and gitCommands functionality"
git push
```


## Setup LaunchAgent <a name="la"></a>
######[Table of Contents](#contents)

Now within the root of our project let's create a `plist` file named `com.autocommit.plist` that we will later move to our LaunchAgents directory. 

With this file open, let's paste this boilerplate in:
```javascript
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.autocommit</string>
    <key>ProgramArguments</key>
    <array>
        <string>__{NODE LOCATION WILL GO HERE}__</string>
        <string>__{PROJECT FILE ENTRY WILL GO HERE}__</string>
    </array>
    <key>WorkingDirectory</key>
    <string>__{PROJECT ROOT LOCATION WILL GO HERE}__</string>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
    <string>__{OUTPUT LOCATION}__</string>
    <key>StandardErrorPath</key>
    <string>__{ERROR OUTPUT LOCATION}__</string>
</dict>
</plist>
```

There are five areas of this `plist` file that we need to update:

1. NODE LOCATION 
2. PROJECT FILE ENTRY
3. PROJECT ROOT LOCATION 
4. OUTPUT LOCATION
5. ERROR OUTPUT LOCATION

1. Updating NODE LOCATION:
If we open up a terminal window and run the following, you will get the file path to your node version:
```
which node
``` 
The output should look something like this:
```
/Users/jared.long/.nvm/versions/node/v16.16.0/bin/node
```
*I have `nvm` (node version manager) installed so my output may look slightly different*

Lets take that output and replace the node location line:
```javascript
REPLACE:
<string>__{NODE LOCATION WILL GO HERE}__</string>

WITH:
<string>/Users/jared.long/.nvm/versions/node/v16.16.0/bin/node</string>
```

2. Updating PROJECT FILE ENTRY
This is the main file that we are running our project from, which in this case is `index.js`
```javascript

REPLACE:
<string>__{PROJECT FILE ENTRY WILL GO HERE}__</string>

WITH:
<string>index.js</string>
```

3. Updating PROJECT ROOT LOCATION
This is referring to the root file location of the app that you are creating now. Running the `pwd` command in the terminal is all you'll need for this. Grab that path and replace the following string with it:
```javascript
REPLACE:
<string>__{PROJECT ROOT LOCATION WILL GO HERE}__</string>

***** WITH YOUR PATH TO YOUR PROJECT :
<string>/Users/jared.long/article-auto-commit</string>
```

4 & 5. Updating OUTPUT & ERROR OUTPUT LOCATION
These are files that your app will print output & errors from this LaunchAgent action into, which we can put directly into the project. We can grab your project path from above and add log files to the end of the path:
```javascript
REPLACE
<key>StandardOutPath</key>
    <string>__{OUTPUT LOCATION}__</string>
<key>StandardErrorPath</key>
    <string>__{ERROR OUTPUT LOCATION}__</string>

WITH
<key>StandardOutPath</key>
    <string>/Users/jaredlong/article-auto-commit/logs/autocommit.log</string>
<key>StandardErrorPath</key>
    <string>/Users/jaredlong/article-auto-commit/logs/autocommit_err.log</string>
```

With your own file paths, our final `plist` file should look something like this:
```javascript
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.autocommit.la</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/jaredlong/.nvm/versions/node/v16.14.2/bin/node</string>
        <string>index.js</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/Users/jaredlong/auto-commit</string>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/jaredlong/auto-commit/logs/autocommit.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/jaredlong/auto-commit/logs/autocommiterr.log</string>
</dict>
</plist>
```
Our `com.autocommit.plist` file is complete and needs to be moved into our LaunchAgents directory, so that it will `RunAtLoad`.

LaunchAgents are found at the this file path: 
`~/Library/LaunchAgents`

Using the terminal, lets move our `plist` file to the LaunchAgents directory. Your first file path may look different than mine, as this is where your `com.autocommit.plist` is found on your machine:
```
mv ~/article-auto-commit/com.autocommit.plist ~/Library/LaunchAgents/com.autocommit.plist
```

Now we can double check that it has been moved to the LaunchAgents by running the following:
```
cd ~/Library/LaunchAgents
ls 
```

You should see your `com.autocommit.plist` file here. 

Now for the final test, reboot your mac, login and check your Github repo. You should see it get updated (may take a minute).

Notes to end on:
- *if you use nvm and you uninstall the version of node that this is pointing to, your node file path will need to be updated in your `plist` file*