Mock Github
Provides a bunch of tools to configure and create a local github environment to test your custom github actions in without having to clutter your github with test repositories, actions or hitting github api rate limits.
Using this library along with kiegroup/act-js can provide you with all the tools you need to test your workflow files as well as your customs locally.
Table of Content
Moctokit
[!WARNING]
Moctokit is currently not compatible with Node 18's native fetch
implementation since it uses nock
under the hood See nock/nock#2397
Allows you to mock octokit using an octokit like interface.
Example
const moctokit = new Moctokit();
mock.rest.repos
.get({
owner: "kie",
repo: /build.*/,
})
.reply({ status: 200, data: { full_name: "it worked" } });
Initialization options
By default a moctokit instance uses https://api.github.com
as the base url
const moctokit = new Moctokit();
You can also specify a base url when a creating a new instance. Useful when you have an enterprise github api url.
const moctokit = new Moctokit("http://localhost:8000");
You can enable other APIs to pass through if there is no exact match
const moctokit = new Moctokit(undefined, true);
Mock an api
You can mock all the github api exactly how @octokit/rest library is used to make an actual call to the corresponding api.
Mock the entire endpoint
You can mock an entire endpoint by simply passing no arguments.
const moctokit = new Moctokit();
moctokit.rest.projects
.createForRepo()
.reply({ status: 200, data: { owner_url: "whatever url" } });
Mock an endpoint for specific parameter(s) and header(s)
You can mock an endpoint for certain paramters. So only if the call to the api has parameters which match the values you defined it will be get the mocked response.
const moctokit = new Moctokit();
moctokit.rest.projects
.createForRepo({ owner: "kie", repo: /d.+/, name: "project" })
.reply({ status: 200, data: { owner_url: "whatever url" } });
moctokit.rest.projects
.createForRepo({ owner: "kie", repo: /d.+/, name: "project" })
.matchReqHeaders({"custom-header": "value", "test-header": /\d+/})
.reply({ status: 200, data: { owner_url: "whatever url" } });
Replying with a response
The endpoint isn't actually mocked with calling reply
with response you want send back if your application makes an api call to that particular endpoint.
Reply once
You can reply with a response exactly once i.e. the 1st api call to the mocked endpoint will respond with whatever response you set and the 2nd api call won't be mocked.
const moctokit = new Moctokit();
moctokit.rest.projects
.createForRepo()
.reply({ status: 200, data: { owner_url: "whatever url" }, headers: {"some-header": "value"} });
Reply N times
You can repeat the same response n times i.e. n consecutive calls to the mocked api will get the same response back
const moctokit = new Moctokit();
moctokit.rest.projects
.createForRepo()
.reply({ status: 200, data: { owner_url: "whatever url" }, repeat: 5 });
Setting response and replying later
You can set an array of responses but actually mock the api later on. Responses are sent in order of their position in the array.
const moctokit = new Moctokit();
const mockedCreateForRepo = moctokit.rest.projects.createForRepo()
.setResponse({
status: 200,
data: {owner_url: "whatever url"}, repeat: 5
});
mockedCreateForRepo.setResponse([
{status: 201, data: {owner_url: "something"}, headers: {"some-header": "value"}},
{status: 400, data: {owner_url: "something else"}, repeat: 2}
{status: 404, data: {owner_url: "something completely difference"}}
]);
mockedCreateForRepo.reply();
Chaining responses
You can chain multiple responses together
const moctokit = new Moctokit();
moctokit.rest.projects
.createForRepo()
.reply({
status: 200,
data: { owner_url: "whatever url" },
repeat: 5,
})
.setResponse([
{ status: 201, data: { owner_url: "something" } },
{ status: 400, data: { owner_url: "something else" }, repeat: 2 },
])
.reply()
.reply({
status: 404,
data: { owner_url: "something completely difference" },
});
Typescript Support
When using with typescript, for each endpoint typescript will tell you what paramters are allowed and what status and corresponding data you can set as response. This way you will be forced to pass paramters that are actually accepted either as path, query or body params by the api and set responses according to the api's response schema.
Note no key in either params or response data will be a required key. All keys are optional. This merely checks that no key which is not defined in the openapi specification is passed in either params or response data for the given endpoint. It also enforces datatypes for any key defined in the openapi specification for the given endpoint.
MockGithub
This class is used to create local repositories and mimic github environment, action inputs and artifact archiving. To configure this local "github", it reads a configuration object by passing the path of a json file or directly to the constructor arguments.
Create everything in a directory you want
const github = new MockGithub(
"path to config json file",
"path to a setup directory"
);
await github.setup();
await github.teardown();
Use the default setup directory - process.cwd()
const github = new MockGithub("path to config json file");
await github.setup();
await github.teardown();
The configuration object/file has 3 sections and all of them are optional. So you can configure an combination of repositories, env and actions.
{
repo:
env:
action:
}
Requirements
To use MockGithub you need to have git
version 2.28 or up installed
Repositories
Local repositories can be configured and created by adding a key to the repo
section of the configuration. The key is the name of the repository. The value for that key is an object with fields described in the table below. All the repositories are created in ${setupPath}/repo
where setupPath
is the path which is passed to MockGithub
Attribute Name | Type | Required | Default | Description |
---|
pushedBranches | string[] | No | | Contains the names of branches that are already pushed and available on "origin". "main" branch is always pushed |
localBranches | string[] | No | | Contains the names of branches that haven't been pushed to "origin" |
currentBranch | string | No | main | Sets the current branch to whatever you want |
owner | string | No | $LOGNAME if defined otherwise "" | sets the owner of the repository |
forkedFrom | string | No | | Sets the name of the repository it was forked from. This implies that the repository was a fork |
files | Files[] | No | | Create all the files for the repository. Refer to Files table |
history | History[] | No | | Create a git history for the repository using the actions mentioned in this array chronologically starting from array at index 0. Refer to History table below |
Files
Attribute Name | Type | Required | Default | Description |
---|
src | string | Yes | | Specifies a path to a directory or a file that is to be copied into the repository. If the path is to a directory, then it will copy all the files and subdirectories in it |
dest | string | Yes | | Specifies the location inside the repository, where the files from src need to be copied into. If dest was "foo/bar/blah" then this equivalent to "path_to_repository/foo/bar/blah" . Further more, it will create any directory in its path if it does not exist in the repository. If src is a path to a directory, for example "/home/somedir" , and say if dest is "/foo/bar/blah" then all of the contents of somedir will be copied into the blah directory inside the repository. If src is a path to a file, for example "/home/test.txt" , and say if dest is "/foo/bar/renamed.txt" then test.txt will be copied into the bar directory inside the repository and renamed as renamed.txt |
filter | string[] | No | | Specify glob patterns to exclude certains files/directories when copying from src to dest |
History
Each History
object is either a Push
object or a Merge
object. See below
Push
Attribute Name | Type | Required | Default | Description |
---|
action | "push" | Yes | | Identifies this history object as a "push" object |
branch | string | Yes | | The branch on which the push has to be performed |
files | Files[] | No | dummy-file-${number} | Create all the files that you want to push. Refer to Files table. If not defined, it will push a dummy file |
commitMessage | string | No | adding files to mimic history at index ${number} | Define a custom commit message for the push |
Merge
Attribute Name | Type | Required | Default | Description |
---|
action | "merge" | Yes | | Identifies this history object as a "merge" object |
head | string | Yes | | The branch you want to merge from |
base | string | Yes | | The branch you want to merge into |
commitMessage | string | No | Merging ${head} to ${base} | Define a custom commit message for the merge |
Example
const github = new MockGithub({
repo: {
repoA: {
pushedBranches: ["branch1", "branch2"],
localBranches: ["branch3"],
currentBranch: "branch2",
files: [
{
src: "/home/workflows",
dest: ".github/workflows",
},
{
src: "/home/code",
dest: "src/",
},
],
history: [
{
action: "push",
branch: "branch1",
files: [
{
src: "/home/test",
dest: "test/",
},
],
},
{
action: "merge",
head: "branch1",
base: "branch2",
},
],
},
repoB: {},
},
});
await github.setup();
Utility functions
There multiple utility functions available to extract information about the state of the repositories and perform some action on them.
const github = new MockGithub("path to config");
github.repo.getState("repoA");
await github.setup();
const state = github.repo.getState("repoA");
const fork = github.repo.getForkedFrom("repoA");
const isFork = github.repo.isFork("repoA");
const path = github.repo.getPath("repoA");
const owner = github.repo.getOwner("repoA");
const branches = github.repo.getBranchState("repoA");
const repoFs = await github.repo.getFileSystemState("repoA");
await github.repo.checkout("repoA", "branchA");
Env
Used to set github environment variable. Adds the prefix GITHUB_
to all the variables. Specify it in the env
section of the config.
const github = new MockGithub({
env: {
hello: "world",
},
});
await github.setup();
Utility functions
There utility functions available to dynamically update these variables as well
const github = new MockGithub({
env: {
hello: "world"
}
});
github.env.update("hello", "update");
await github.setup();
github.env.update("hello", "update");
github.env.update("foo", "bar");
const delete = github.env.delete("hello");
const value = github.env.get("hello");
const valueAll = github.env.get();
Action
Comprises of 2 sections - input and archive
Input
Mimics how github actions are set. Adds the prefix INPUT_
to all the variables.
const github = new MockGithub({
action: {
input: {
hello: "world",
},
},
});
await github.setup();
Utility functions
There utility functions available to dynamically update these variables as well
const github = new MockGithub({
action: {
input: {
hello: "world"
}
}
});
github.action.input.update("hello", "update");
await github.setup();
github.action.input.update("hello", "update");
github.action.input.update("foo", "bar");
const delete = github.action.input.delete("hello");
const value = github.action.input.get("hello");
const valueAll = github.action.input.get();
Archive artifacts
Starts an express server that mimics the server actually used by github action to archive artifacts. Constructed from nektos/act. The artifacts are stored in ${setupPath}/store
where setupPath
is the path passed to MockGithub
const github = new MockGithub({
action: {
archive: {
serverPort: "8000",
},
},
});
await github.setup();
Attribute Name | Type | Required | Default | Description |
---|
serverPort | string | Yes | | The port of the server which will receive requests to upload/download artifacts. If no port is specified, then the server won't start |
Utility functions
There are utility functions that return the location of the artifact store and runner id being used.
const github = new MockGithub({
action: {
archive: {
serverPort: "8000",
},
},
});
github.action.archiver.getArtifactStore();
await github.setup();
const store = github.action.archiver.getArtifactStore();
const runId = github.action.archiver.getRunId();
Example with act-js
You can use this library along with act-js to test your workflow files as well as your custom actions.
Here are some examples on how to do so.
You can also take look at how the workflow files are being tested in the act-js repository - ci-check.yaml