Skip to main content

Overview

GitHub Motion is a GitHub Action that allows you to automate your GitHub workflow by creating a motion to perform a specific task. This action is designed to be used in conjunction with the GitHub Actions workflow.

Publishing Package(s)

Publishing with an Executor

Converting your script into a custom Nx executor is a great idea. It will make your publishing logic reusable across different projects and Nx workspaces. Here's a general approach to creating a custom Nx executor:

  1. Set up the executor: Create the necessary file structure for your executor package.

  2. Define the executor configuration: Write your executor's logic inside an executor function and export it.

  3. Add schema for options: Define a JSON schema for configuration options.

  4. Pack and distribute: Once your executor is ready, you can publish it as an npm package.

Here's a step-by-step outline for implementing this:

1. File Structure

Create a new directory for your executor, e.g., nx-publish-executor, with a structure like this:

nx-publish-executor/
├── src/
│ ├── executor.ts
│ ├── schema.json
└── package.json

2. Executor Logic (executor.ts)

Refactor the script into an executor function:

import { ExecutorContext } from '@nx/devkit';
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import * as path from 'path';

interface NpmPublishExecutorOptions {
name: string;
version: string;
tag?: string;
}

export default async function runExecutor(
options: NpmPublishExecutorOptions,
context: ExecutorContext
) {
const { name, version, tag = 'next' } = options;

// Perform validation
const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/;
invariant(
version && validVersion.test(version),
`No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.`
);

// Load project graph and find the project
const graph = context.projectGraph;
const project = graph.nodes[name];

invariant(
project,
`Could not find project "${name}" in the workspace. Is the project.json configured correctly?`
);

const outputPath = project.data?.targets?.build?.options?.outputPath;
invariant(
outputPath,
`Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`
);

process.chdir(outputPath);

// Update the version in "package.json"
try {
const json = JSON.parse(readFileSync('package.json').toString());
json.version = version;
writeFileSync('package.json', JSON.stringify(json, null, 2));
} catch (e) {
console.error(`Error reading package.json file from library build output.`);
return { success: false };
}

// Execute "npm publish"
try {
execSync(`npm publish --tag ${tag}`);
} catch (e) {
console.error('Publishing failed: ', e.message);
return { success: false };
}

return { success: true };
}

function invariant(condition: boolean, message: string) {
if (!condition) {
console.error(message);
throw new Error(message);
}
}

3. Schema (schema.json)

Describe the input options schema:

{
"$schema": "http://json-schema.org/schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the project to publish."
},
"version": {
"type": "string",
"description": "The version to publish."
},
"tag": {
"type": "string",
"description": "The tag to publish under.",
"default": "next"
}
},
"required": ["name", "version"]
}

4. package.json

Configure the package for distribution:

{
"name": "nx-publish-executor",
"version": "1.0.0",
"main": "src/executor",
"types": "src/executor.d.ts",
"devDependencies": {
"@nx/devkit": "^latest",
"@types/node": "^latest"
},
"peerDependencies": {
"@nx/devkit": "^latest"
}
}

5. Usage

Once packaged and published, you can use this executor in other Nx workspaces by configuring it in project.json or workspace.json.

You can then run the executor using Nx CLI commands, providing the necessary options. This makes your publish logic portable and reusable across different Nx projects.

Versioning with an Executor

Creating a GitHub Action with your publishing and versioning logic is indeed a smart move, especially for automating CI/CD workflows. Here's how you can convert your logic into a custom GitHub Action:

Steps to Create a GitHub Action

  1. Organize your action files: Set up a directory in your repository dedicated to GitHub Actions.

  2. Define the action metadata: Create an action.yml file to define inputs, outputs, and other metadata for your action.

  3. Implement the action logic: Write the script for the core logic.

  4. Set up a workflow: Create a .github/workflows YAML file that uses your custom action.

1. File Structure

Create a directory named .github/actions/npm-publish with the following structure:

.github/
└── actions/
└── npm-publish/
├── Dockerfile
├── action.yml
└── entrypoint.sh

2. Action Metadata (action.yml)

Define the action and its inputs:

name: 'NPM Publish with Version Update'
description: 'Publishes an npm package with versioning'

inputs:
name:
description: 'The name of the package'
required: true
version:
description: 'The version to set for the package'
required: true
tag:
description: 'NPM publish tag'
required: false
default: 'next'

runs:
using: 'docker'
image: 'Dockerfile'

3. Dockerfile

Use a Docker image for isolation and environment consistency:

FROM node:alpine

WORKDIR /app

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

4. Script Logic (entrypoint.sh)

Implement the logic for versioning and publishing:

#!/bin/sh -l

set -e

# Inputs
PACKAGE_NAME="$1"
PACKAGE_VERSION="$2"
PUBLISH_TAG="$3"

# Validate version format
if ! echo "${PACKAGE_VERSION}" | grep -qE '^\d+\.\d+\.\d+(-\w+\.\d+)?$'; then
echo "Invalid version format: ${PACKAGE_VERSION}"
exit 1
fi

# Navigate to the package directory
cd "${GITHUB_WORKSPACE}/${PACKAGE_NAME}"

# Update package version
jq ".version=\"${PACKAGE_VERSION}\"" package.json > tmp.json && mv tmp.json package.json

# Publish the package
npm publish --tag "${PUBLISH_TAG}"

5. Workflow Setup (.github/workflows/publish.yml)

Create a workflow file to use the action:

name: Publish Package

on:
push:
branches: [ main ]

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '16'

- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Publish with Version Update
uses: ./.github/actions/npm-publish
with:
name: 'path/to/package'
version: ${{ secrets.PACKAGE_VERSION }}
tag: 'latest'
# Adjust 'name' and 'version' as needed

Explanation

  • Docker-based action: This structure uses Docker to ensure a consistent environment.
  • entrypoint.sh: Handles argument parsing, version validation, package update, and publishing.
  • Workflow file: Triggers on pushes to the main branch, checks out the repository, and uses your custom action with specified inputs.

This setup encapsulates the publish and version update logic into a reusable GitHub Action, allowing for streamlined and automated package deployment processes. You can trigger this action in a workflow on specific events, like code merges or tag creations, to automate the release pipeline.