Centralizing Git Submodules with GitHub Actions and Octokit


Posted on Sat, Jun 17, 2023 devops git actions

A Custom GitHub Action

Git Submodules are a powerful way to manage dependencies in your projects. However, they can be difficult to manage manually. GitHub Actions and Octokit can help to streamline the management of Git Submodules.

GitHub Actions are a way to automate tasks in your GitHub repository. Octokit is a library that allows you to interact with GitHub from code. By using GitHub Actions and Octokit, you can create a workflow that automatically updates your submodules whenever you push to your repository.

This tutorial will show you how to streamline Git Submodules with GitHub Actions and Octokit.

Understanding the Problem πŸ”Ž

Managing a repository that incorporates multiple submodules can present significant challenges when it comes to keeping track of changes and pipelines. Submodules are separate repositories embedded within a main repository, allowing for modular code organization and reusability.

However, as the number of submodules increases, manually monitoring their individual changes and pipeline statuses becomes increasingly burdensome and error-prone.

While submodules offer numerous advantages, they also introduce specific challenges that developers must understand and address effectively. In this section, we will explore some key problems that arise when managing a repository with multiple submodules and provide insights on how to mitigate them. Below are such problems :

1. Dependency Management πŸ‘»

One of the primary challenges when working with submodules is ensuring proper dependency management. Each submodule may have its own dependencies and version requirements, which can lead to conflicts and compatibility issues.

Tracking and synchronizing dependencies across multiple submodules becomes crucial to maintain a stable and functional codebase.

2. Repository Cloning and Initialization πŸ˜‡

When cloning a repository with submodules, additional steps are required to initialize and fetch the submodule content. This process can be cumbersome, particularly for new team members or when dealing with a large number of submodules.

πŸ”₯

Failure to properly initialize submodules can result in broken or incomplete codebases, leading to debugging and deployment challenges.

3. Version Control and Updates πŸ’«

Managing versions and updates across submodules can become intricate. Each submodule may have its own release cycle and versioning scheme. Coordinating updates, bug fixes, and feature enhancements across multiple submodules can be time-consuming and error-prone.

Additionally, ensuring that the main repository and submodules remain in sync requires careful planning and execution.

4. Collaboration and Communication πŸ‘‹

When multiple developers are working on a repository with submodules, effective collaboration and communication become paramount. Without proper coordination, conflicts can arise when developers modify the same submodule simultaneously. Clear guidelines and communication channels are essential to avoid conflicts and streamline collaborative efforts.

To address these challenges, a solution is needed to automate the process of tracking changes and pipeline statuses across submodules. The next section will introduce a GitHub action specifically designed to manage, track, automate, and streamline repository management and provide an efficient and reliable solution.

What’s GitHub Action? 🀷

GitHub actions have revolutionized the way developers collaborate on projects hosted on the GitHub platform. These actions allow users to automate various tasks, enhance workflows, and streamline development processes. The fun part of this tool is that you can create a custom Action that automates module tracking and management.

Overview of GitHub OctoKit

Octokit is a GitHub API client library for JavaScript. It allows you to interact with GitHub from code, and it provides a number of methods for interacting with items from GitHub.

Some of the items that you can compute from GitHub with Octokit include :

  • Repositories : You can get information about repositories, such as their name, description, and stars.
  • Issues : You can get information about issues, such as their title, description, and comments.
  • Pull requests : You can get information about pull requests, such as their title, description, and commits.
  • Commits : You can get information about commits, such as their message, author, and date.

To compute items from GitHub with Octokit, you first need to create a client object. You can do this by calling the octokit.client() method. Once you have a client object, you can use the methods that are provided by Octokit to compute the items that you need.

For example, to get information about a repository, you would use the octokit.getRepository() method. This method takes the name of the repository as its argument, and it returns a Repository object. The Repository object contains information about the repository, such as its name, description, and stars.

GitLab, Azure, GitHub provide native CI/CD support. These platforms provide a centralized location within organizations to view all workflows.

To enhance this functionality further and customize it according to your specific needs, we can leverage OctoKit and build a custom GitHub Actions solution. In the next section, we’ll be moving to the fun part (i.e setup, coding, and implementation πŸ˜€)

Setting Up the Custom GitHub Action πŸ‘·

GitHub Actions allow you to automate various tasks and workflows within your GitHub repositories using yaml files.

In this guide, we will walk you through the process of setting up a custom GitHub Action with code samples and hook up your actions to octokit so you can perform any tasks that your creativity comes up with!

lets get started ! πŸš€

Step 1 : Create a New Workflow File

  1. Open your GitHub repository in your preferred code editor or directly on the GitHub website. You can access the repository used in this content here.
  2. Navigate to the .github/workflows or create directory within the root directory if it’s not there already.
  3. Create a new file within the .github/workflows and name it custom-action.yml.

Step 2 : Define the Workflow

  1. Open the custom-action.yml file.
  2. Add the following code to define the workflow :

name: Custom Action

on:
  pull_request:
    branches:
      - main

This configuration specifies that the custom action will be triggered whenever a pull request is made to the main branch. You can modify the `branches` field to target a different branch if needed.

Step 3 : Configure the Job πŸ‘·β€β™€οΈ

Add the following code to configure the job within the workflow :

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 14

      - name: Run Custom Action
        uses: ./ 
        env:
          root_token: ${{ secrets.ROOT_TOKEN }}
          ci_token: ${{ secrets.CI_TOKEN }}
          target_owner: YourTargetOwner
          target_submodules: |
            submodule1
            submodule2
          target_branch: YourTargetBranch
          target_workflow: YourTargetWorkflow

⚠️

make sure to replace YourTargetOwner, YourTargetBranch, YourTargetWorkflow, and other placeholders with your actual values.

Step 4 : Add the Custom Action Code

  1. Create a new file in your repository's root directory and name it main.ts.
  2. Then add your custom action code. You can use the code sample below to get started.

Copy and paste the code into your main.ts file :

import * as core from '@actions/core'
import * as github from "@actions/github"
import { Octokit, } from "octokit";

async function getTargetPrData(targetOwner: string, targetRepo: string, targetBranch: string) {
  const api = new Octokit({
    auth: core.getInput('root_token'),
  });

  const { data } = await api.rest.pulls.list({
    owner: targetOwner,
    repo: targetRepo,
    head: targetOwner + ":" + targetBranch,
    sort: "updated",
    state: "all"
  })

  if (data.length === 0) return null;
  const prData = data[0]
  return prData;
}

async function removePreviousBotComment() {
  const api = new Octokit({
    auth: core.getInput('ci_token'),
  });

  const { data } = await api.rest.issues.listComments({
    owner: github.context.issue.owner,
    repo: github.context.issue.repo,
    issue_number: github.context.issue.number
  })

  let comment_id;

  for (let comment of data) {
    if (comment.body.includes("action info")) {
      comment_id = comment.id;
      break;
    }
  }

  if (comment_id === undefined) return;

  await api.rest.issues.deleteComment({
    owner: github.context.issue.owner,
    repo: github.context.issue.repo,
    comment_id
  })
}

async function pushCommentOnPr(targetPrDataList: {
  title: string;
  html_url: string;
  state: string;
  owner: string;
  repo: string;
  branch: string;
}[]) {
  const api = new Octokit({
    auth: core.getInput('ci_token'),
  });

  let body = `# action info \n`

  for (let targetPrData of targetPrDataList) {
    body += `## [${targetPrData.repo} - ${targetPrData.title}](${targetPrData.html_url})\n**pull request status**: ${targetPrData.state}\n**pipeline status**: ![worflow](https://github.com/${targetPrData.owner}/${targetPrData.repo}/actions/workflows/${core.getInput('target_workflow')}/badge.svg?branch=${targetPrData.branch})\n`
  }

  await api.rest.issues.createComment({
    owner: github.context.issue.owner,
    repo: github.context.issue.repo,
    issue_number: github.context.issue.number,
    body
  })
}

async function run(): Promise<void> {
  try {
    const owner = core.getInput('target_owner');
    const submodules = core.getMultilineInput("target_submodules")
    const targetBranch = core.getInput("target_branch")
    const targetPrDataList = []

    for (let submodule of submodules) {
      const targetPrData = await getTargetPrData(owner, submodule, targetBranch)
      if (targetPrData) targetPrDataList.push({ ...targetPrData, owner: owner, repo: submodule, branch: targetBranch })
    }

    await removePreviousBotComment()
    await pushCommentOnPr(targetPrDataList)

  } catch (error) {
    core.setFailed(error.message);
  }
}

run()

Below is a breakdown of what the code above does :

1. Imports ☁️

2. Functions πŸ“¦

  • retrieves the data of the target pull request.
  • It creates an instance of the Octokit class using the provided root_token from the GitHub repository secrets.
  • It uses the api.rest.pulls.list() method to fetch the pull requests matching the given parameters (owner, repo, head, sort, and state).
  • If the data array is empty, it returns null. Otherwise, it returns the first pull request data from the array.

  • removes the previous bot comment from the GitHub issue.
  • It creates an instance of the Octokit class using the provided ci_token from the GitHub repository secrets.
  • It uses the api.rest.issues.listComments() method to fetch the comments of the current GitHub issue.
  • It iterates over the comments and checks if any comment's body includes the string "action info".
  • If a matching comment is found, it stores its ID and breaks the loop.
  • If a comment ID is found, it uses the api.rest.issues.deleteComment() method to delete the comment.

  • pushes a comment on the pull request with relevant information.
  • It creates an instance of the Octokit class using the provided ci_token from the GitHub repository secrets.
  • It generates a body string for the comment, including details about the target pull request.
  • It iterates over the targetPrDataList array, adding information about each pull request to the body string.
  • Finally, it uses the api.rest.issues.createComment() method to create the comment on the GitHub issue.

  • This is the main function that orchestrates the custom GitHub Action.
  • It utilizes the core module to retrieve inputs such as target_owner, target_submodules, target_branch, etc.
  • It calls the getTargetPrData() function for each submodule, gathers the data, and adds it to the targetPrDataList.
  • It then removes the previous bot comment using the removePreviousBotComment() function.
  • Finally, it pushes a new comment on the pull request with the relevant information using the pushCommentOnPr() function.

3. Execution ⚑️

  • This executes the run() function, kicking off the custom GitHub Action.
  • It wraps the function call within a try/catch block to handle any potential errors.
  • If an error occurs, it sets the failure message using core.setFailed().

Before committing and pushing your codes, create a package.json file and insert the code below into it :

{
  "name": "action-submodules",
  "version": "0.0.0",
  "private": true,
  "description": "TypeScript template action",
  "main": "lib/main.js",
  "scripts": {
    "build": "tsc",
    "format": "prettier --write '**/*.ts'",
    "format-check": "prettier --check '**/*.ts'",
    "package": "ncc build --source-map --license licenses.txt",
    "all": "npm run build && npm run format && npm run lint && npm run package && npm test"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/actions/typescript-action.git"
  },
  "keywords": [
    "actions",
    "node",
    "setup"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@actions/core": "^1.10.0",
    "@actions/github": "^5.1.1",
    "octokit": "^2.0.14"
  },
  "devDependencies": {
    "@types/node": "^18.15.13",
    "@vercel/ncc": "^0.36.1",
    "js-yaml": "^4.1.0",
    "prettier": "^2.8.7",
    "typescript": "^5.0.4"
  }
}

run npm init to initialize and get the dependencies and packages in our package.json file

Step 5 : Commit and Push 🌠

  1. Save all the changes you made to the files.
  2. Commit the changes and push them to your repository.

Step 6 : Configure Secrets 🀫

Step 7 : Test the Custom Action πŸ§ͺ

  1. Create or modify a pull request on the main branch to trigger the custom action.
  2. Wait for the workflow to run.
  3. Check the pull request for a new comment that includes the information about the target pull request's data and pipeline status.

πŸ––

Congratulations ! You have successfully set up a custom GitHub Action using the provided code sample.

πŸ”₯

You can further customize the action and workflow according to your specific requirements by modifying the code and workflow configuration.

the final render should look something like this :

this project is also available from the zip below :

xotoscript-action-submodules.zip609.9KB

Conclusion βœ…

In this tutorial, we have seen how to streamline Git Submodules with custom GitHub Actions and Octokit. This allows us to automatically update our submodules whenever we push to our repository. This can save us a lot of time and effort, and it can also help to ensure that our submodules are always up-to-date.

If you are using Git Submodules in your projects, I encourage you to try out this tutorial. It is a simple way to improve the way you manage your submodules.