How to automatically push every change to an external repository

Automation setup is a crucial aspect for enterprise adoption of any tool. Business Central has several ways to be automated, and this post will guide you how to automate the content replication from Business Central to an external git infrastructure like GitHub.

This post will cover a push-based integration pattern, where Business Central is the source of the truth and, on every data change, the content will be replicated to an external infrastructure. This pattern can be used as a real-time backup/replication strategy.

Architecture Overview

post-commit hook push schema

The architecture of this push-based integration pattern is straightforward, on every change of a Business Central repository content, in any branch, a post-commit hook will replicate that content using the standard git push command to an external repository.

The caveat is how to define the git push origin repository where the content of the Business Central repository will be replicate to.

Imported Projects

When you import a project from an existing external source, Business Central will preserve the origin information in git config. The post-commit hook code will rely on this information and will use the source as the target of the git push.

New Projects

New project is a little bit more trick use-case, due to the fact there is no pre-defined origin to rely on. For this case, the post-commit hook will have to create a new repository in the external git infrastructure (in this case GitHub), generate the origin configuration entry on git config and use it for the git push command.

Integration Code

As we learned in a previous post, the post-commit hook is a bash script that is executed after every commit. We could write all the logic using bash. However, I prefer to use Java to code the integration logic.

So here is the post-commit hook template that executes a Java program:

#!/bin/bash
java -jar $APP_SERVER_HOME/hooks/git-push-1.0-SNAPSHOT.jar // (1)
  1. Using bash to execute a java program

Note that I’ll be using $APP_SERVER_HOME as a variable to reference the application server home that Business Central is running.

Java Logic

One of the most significant advantage of using Java to code the integration logic is that we can take advantage of rich Java ecosystem. For the post-commit hook code the following two key libraries are used:

  • JGit to interact with internal Business Central git repositories

  • GitHub API for Java, to communicate with GitHub.

The following code snippet can be considered a standard template for almost any post-commit hook coded in java. The specifics for each integration pattern will be contained in line 44 (item 9).

public class GitHook {

    public static void main(String[] args) throws IOException, GitAPIException {
        // collect the repository location (1)
        final Path currentPath = new File("").toPath().toAbsolutePath();
        final String parentFolderName = currentPath.getParent().getName(currentPath.getParent().getNameCount() - 1).toString();
        // ignoring system space (2)
        if (parentFolderName.equalsIgnoreCase("system")) {
            return;
        }

        // setup GitHub credentials and integration  (3)
        final GitHubCredentials ghCredentials = new GitHubCredentials();
        final GitHubIntegration integration = new GitHubIntegration();

        // setup the JGit repository access  (4)
        final Repository repo = new FileRepositoryBuilder()
                .setGitDir(currentPath.toFile())
                .build();
        final Git git = new Git(repo);

        // collect all remotes for the current repository  (5)
        final StoredConfig storedConfig = repo.getConfig();
        final Set remotes = storedConfig.getSubsections("remote");
        if (remotes.isEmpty()) {
           //create a remote repository, if it does not exist (6)
            new SetupRemote(ghCredentials, integration).execute(git, currentPath);
        }

        // mechanism to find the latest commit (7)
        final List branches = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call();
        final RevWalk revWalk = new RevWalk(git.getRepository()); (8)

        branches.stream()
                .map(branch -> {
                    try {
                        return revWalk.parseCommit(branch.getObjectId());
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                })
                .max(comparing((RevCommit commit) -> commit.getAuthorIdent().getWhen()))
                .ifPresent(latestCommit -> {
                // the integration here (9)
                });
    }
}
  1. The first thing is to identify the repository location by using the working dir, for this an instance of File with an empty string is created

  2. This integration will ignore changes in the system Space, the goal is to automate the external integration of users projects

  3. Here a couple of GitHub integration are set up. GitHubCredentials provides credentials to GitHub access, and GitHubIntegration is responsible for creating a repository in GitHub using the GitHub public rest API.

  4. Here JGit is set up to access the git repository collected in the first step

  5. Check if the current repository has any remote information

  6. If no remote repository config exists, it means that it’s necessary to create a repository in GitHub and add it to a remote config section to the current Business Central repository

  7. As we learned in previous posts, Business Central uses the bare format of git repositories, so it’s needed to find the latest commit (without using the HEAD shortcut). This code goes over all branches and collects the most recent commit

  8. Don’t worry much about this code, as this can be considered just a template to get access to what we really need (the last commit id). I’m planning, in a near future, blog a bit about how to use JGit API for some advanced git operations beyond the porcelain commands.

  9. Here we have the latestCommit and we’re ready to execute the external integration needed

 

The above comments should be enough for a good understanding of the code. I want to re-emphasize the importance of lines 23-28 (items 5 and 6), as those are the lines that will create the external repository if it doesn’t already exist.

The following code now that contains the real logic for the push-based integration pattern:

//get the branches where this commit is referenced (1)
final Map branchesAffected = git
        .nameRev()
        .addPrefix("refs/heads")
        .add(latestCommit)
        .call();

//iterate over all remote repositories (2)
for (String remoteName : remotes) {
    final String remoteURL = storedConfig.getString("remote", remoteName, "url");
    for (String ref : branchesAffected.values()) { (3)
        // push changes to the remote repository (4)
        git.push()
                .setRefSpecs(new RefSpec(ref + ":" + ref))
                .setRemote(remoteURL)
                .setCredentialsProvider(ghCredentials.getCredentials())
                .call();

        //check if the branch has a remote config (5)
        final String remote = storedConfig.getString("branch", ref, "remote");
        if (remote == null) {
            //branch had no remote info, now needs to be update  (6)
            storedConfig.setString("branch", ref, "remote", remoteName);
            storedConfig.setString("branch", ref, "merge", "refs/heads/" + ref);
            storedConfig.save();
        }
    }
}
  1. The latestCommit could be referenced in multiple branches, so to properly replicate the content it’s necessary to get all branches that this commit is associated with

  2. In the previous snipped we collected all the remote repositories (it’s possible to have more than one), so now the integration code has to interact overall remotes to replicate the content for each

  3. The external integration has to be executed per remote repository (so the reason for the item 2), but also needs to be executed per each branch (collected on item 1)

  4. For each remote and branch, the git push command is executed. As this is pushing to GitHub, we have to provide credentials information

  5. Check if the current branch has a remote config

  6. If no remote information is found for the current branch, it’s necessary to update it, as it might just be created by the item 4

Walkthrough

Here’s a quick walkthrough video that shows the above integration in action

bc-hook-push-walkthrough

The java code for this blog post is available here in my GitHub. For more information on how to setup it in your Business Central, follow the README.me instructions.

Wrapping up

This post is part of a series of posts about Business Central and Git. In this post, we learned how to use Java to code a post-commit hook logic and the push-based integration pattern. This pattern can be used to automate Business Central content replication.

rhba business-central git hook external integration push