DEV Community

Cover image for Automate commits with LaunchAgents & JavaScript
Jared Long
Jared Long

Posted on • Updated on

Automate commits with LaunchAgents & JavaScript

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

Setup new Node.js project with git

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
Enter fullscreen mode Exit fullscreen mode

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
Shelljs is a great Command Line Utility for interacting with the command line in JavaScript.

npm i shelljs
Enter fullscreen mode Exit fullscreen mode

After this, you will see a package-lock.json file and node_modules appear in your project. Your package.json should look like this:

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's now create our index.js file:

touch index.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Setup new Github repository

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

Once it is created, you will now need to copy the remote origin line:
Repo-example
& 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now if you go back to your repo on Github, you will see your files.

Time to write some JavaScript

Table of 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

At the top of nameChange.js, let's bring in shelljs

const shell = require('shelljs');
Enter fullscreen mode Exit fullscreen mode

Let's create our first function and look for the files in our project:
file-search

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

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

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
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

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
Enter fullscreen mode Exit fullscreen mode

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:

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 };
}
Enter fullscreen mode Exit fullscreen mode

At this point, we are ready to move one & get git involved.

Let's write some JavaScript pt. 2

Table of 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:

const shell = require('shelljs');
const { setNewFileName } = require('./nameChange');
Enter fullscreen mode Exit fullscreen mode

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

const commitNewFile = () => {
    const { fileToRename, newFileName } = setNewFileName();
}
Enter fullscreen mode Exit fullscreen mode

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.

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`;
};
Enter fullscreen mode Exit fullscreen mode

Now let's use shell to execute these commands(add, commit, push):

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);
      });
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now that file should have a clean git slate.

Lets export our commitNewFile, import it & invoke it within index.js

exports.commitNewFile = () => ....
Enter fullscreen mode Exit fullscreen mode

Inside index.js:

const { commitNewFile } = require("./gitCommands");

commitNewFile();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Setup LaunchAgent

Table of 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:

<?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>
Enter fullscreen mode Exit fullscreen mode

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

  6. 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
Enter fullscreen mode Exit fullscreen mode

The output should look something like this:

/Users/jared.long/.nvm/versions/node/v16.16.0/bin/node
Enter fullscreen mode Exit fullscreen mode

I have nvm (node version manager) installed so my output may look slightly different

Lets take that output and replace the node location line:

REPLACE:
<string>__{NODE LOCATION WILL GO HERE}__</string>

WITH:
<string>/Users/jared.long/.nvm/versions/node/v16.16.0/bin/node</string>
Enter fullscreen mode Exit fullscreen mode
  1. Updating PROJECT FILE ENTRY This is the main file that we are running our project from, which in this case is index.js

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

WITH:
<string>index.js</string>
Enter fullscreen mode Exit fullscreen mode
  1. 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:
REPLACE:
<string>__{PROJECT ROOT LOCATION WILL GO HERE}__</string>

***** WITH YOUR PATH TO YOUR PROJECT :
<string>/Users/jared.long/article-auto-commit</string>
Enter fullscreen mode Exit fullscreen mode

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:

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>
Enter fullscreen mode Exit fullscreen mode

With your own file paths, our final plist file should look something like this:

<?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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now we can double check that it has been moved to the LaunchAgents by running the following:

cd ~/Library/LaunchAgents
ls 
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)