Implementing a private JavaScript GitHub action

February 14th 2025 GitHub JavaScript

In my quest to automate announcing of new blog posts to social media using GitHub Actions I managed to find existing actions for most steps in the process. However, the act of composing the final message from the collected data was just too specific to my needs and there was no way around writing some custom code. I ended up creating a private JavaScript action for that.

This approach is a good fit for me because:

  • It allows me to keep the code I want to execute as part of the workflow in the same repository. This makes sense since I can't imagine reusing the same code somewhere else. It's just too specific to the task at hand.
  • It requires minimum ceremony to get my custom code running. Even more so, since I already need Node.js to build my blog before deployment.

I followed the official guidance and placed my action in a subfolder of .github/actions. Inside that folder I had to:

  • Initialize npm, i.e., create a package.json file:
    npm init -y
    
  • Create an action.yml metadata file, most importantly describing the action inputs and outputs, and declaring how the action is to be run:
    name: "Compose social media message"
    description: "Compose a message for social media, using the title, the URL and the tags of a new post"
    inputs:
      title:
        description: "Title of the post"
        required: true
      url:
        description: "URL of the post"
        required: true
      tags:
        description: "Tags of the post"
        required: true
    outputs:
      message:
        description: "Composed message for social media"
    runs:
      using: "node20"
      main: "index.js"
    
  • Install the @actions/core package for handling action inputs and outputs in code. Of course, I could install any other packages needed by my code:
    npm install @actions/core
    
  • Write the actual JavaScript code I wanted to run (along with some extra calls to get the inputs, set the output, and handle errors):

    const core = require("@actions/core");
    
    try {
      const title = core.getInput("title");
      const url = core.getInput("url");
      const tags = core.getInput("tags");
      const hashTags = JSON.parse(tags)
        .map(
          (tag) =>
            `#${tag
              .toLowerCase()
              .replace(/^\./, "dot") // replace leading dot with 'dot'
              .replace(/#/g, "sharp") // replace '#' with 'sharp'
              .replace(/\s/g, "")}` // remove whitespace
        )
        .join(" ");
      const message = `${title}\n\n${url}\n\n${hashTags}`;
      core.setOutput("message", message);
    } catch (error) {
      core.setFailed(error.message);
    }
    
  • Commit all of these files (and of course the generated package-lock.json file) in my git repository.

From my workflow, I could run such an action the same way as any public action published in the marketplace. The only difference was how it is being referenced in uses property: via its path instead of via its repository:

- name: Compose social media message
  id: social-media-message
  if: ${{ env.NEW_POST == 'true' }}
  uses: ./.github/actions/compose-post
  with:
    title: ${{ steps.post-frontmatter.outputs.title }}
    url: ${{ env.POST_URL }}
    tags: ${{ steps.post-frontmatter.outputs.tags }}

However, there was still one step missing, therefore the code in my action failed to access the referenced npm package:

Error: Cannot find module @actions/core

That's because GitHub Actions runner doesn't install the npm packages. The documentation suggests two approaches:

  • either commit the node_modules folder in your repository,
  • or compile your code and the packages into distribution files and commit those.

I chose a third approach: since my package is private, and I already install Node.js in my workflow to build the blog, I simply install the packages for my action before running it (notice that working-directory is set to the folder with my action):

- name: Install dependencies for compose-post action
  if: ${{ env.NEW_POST == 'true' }}
  run: npm ci
  working-directory: ./.github/actions/compose-post

Once you know exactly what you need to do, it's really simple to run some custom code inside your GitHub Actions workflow, at least if you're familiar with JavaScript. Even more so, if the project you're creating the workflow for is also JavaScript-based.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License