Write your own Github action and publish it to Marketplace

VS-404
8 min readMay 1, 2022

--

Github action
Github action

I just wrote blog regarding how to feed dynamic input to Github workflow matrix, and Github matrix only accepts stringified version of input which should be sequence in order for Github to run multiple job with input combinations. So I thought why not publish this custom action which takes comma separated values as input and return stringified array to feed it to matrix. So here I am writting another blog to create custom action and publish it to Github marketplace. If you are interested to read through previous blog, you can find it here

Enough with introduction lets spring into action 😅

For this lets follow below steps and we can explore each step in details (Please note this action is written in Javascript)

  1. Create Github repo (Dahhh!!!!!)
  2. Init Github repo with npm init
  3. Install @actions/core dependency to access input provided from workflow and provide stringified output
  4. Create our actions specification in file action.yml and in this we will specify which script to execute for this action.
  5. Create index.js which will have our actions code.
  6. Finally we will push these changes to Github repo and Github will help us to publish this to marketplace.

Done, so easy right? Now lets go through each step in details.

  1. Create Github repo
    Go to github.com and create new repo, and clone it on you computer.
  2. Init Github repo / create package.json
    This is also standard step, once you are done with first step, navigate to repo in your terminal and run npm init , and provide all necessary details.
  3. Install required packages
    For accessing action inputs and returning output to main workflow we will need this package. So go ahead and npm i @actions/core . You can read on usage of this package but we will use only basic things for our use case. So no need to go in details.
  4. Write our actions specifications
    In order for others to use our actions in their workflow we need to have these specification on what action will accept as input and what is returned.
    Lets create action.yaml in our repo and paste below yaml.
name: your-action-name
description: your-action-description
inputs:
input:
description: your-input-description
required: true
schema:
description: your-output-schema-description
required: true
outputs:
stringified:
description: your-output-decsription
runs:
using: node16
main: "index.js"
branding:
icon: "check-circle"
color: "purple"

This is very basic action template where we are accepting 2 inputs (input and schema) you can name them whatever you want. And both inputs are required if missing we will throw error which will exit action with exit code 1.

We have also mentioned outputs our action will provide only one output variable which is named as stringified . And this is transformed values from input with schema from schema . Sounds confusing right? Just hold on till we write our implementation it will clear up all the things.
And Finally which script will be executed when action is used in workflow using node16 .

5. Create index.js (our actions implementation)

Create file index.js, name can be whatever just make sure we update new file in action.yml specifically this entry main: “index.js” to use new file.

  • First things first lets see how our input would look like to process it properly. As it is illustrated in below image that we have to format “OS” which is wrapped in ‘’ single quotes and we get value from input object. Here point to not final output will be like below. And it is sequence so matrix will accept it.
    ‘[“ubuntu-latest” , “windows-latest”]’
Input representation
Input representation
  • First we will import our package which we installed in step 3.
    const core = require(“@actions/core”);
  • We need couple of validation function to determine if value is object or string. So lets add below basic standard type validation functions. Function name is sufficient to know what that function will validate.
const isString = object =>
Object.prototype.toString.call(object) === "[object String]";
const isObject = object =>
Object.prototype.toString.call(object) === "[object Object]";
  • Now lets see how we can get our inputs provided from workflow then we go ahead and start implementing our transformer.
    To get input we just use our core object to call function which we have just imported like below. Notice we have called function getInputand first parameter is name of the input which should be same name as we mentioned in action.yml . And second parameter is InputOptions ,we have asked core to get our input but if it is not provided throw required error.
    const input = core.getInput(“input”, {required: true});
  • Similarly we get both inputs and print values just to show in workflow run that we received provided inputs with values.
const input = core.getInput("input", {required: true});
const schema = core.getInput("schema", { require: true });
core.info(`Action is running with inputs: ${JSON.stringify({input,schema})}`)
  • Just a note on inputs provided all inputs should be flat stringified object as data is passed through env variables so all data would be stringified. That is why we need to parse these objects before we process it to stringify it again with schema. We wrap it with try-catch block to properly exit form action if there is parsing error.
try {
const _input = JSON.parse(input);
const _schema = JSON.parse(schema);
} catch(err) {
core.setFailed(
`Action failed to transform input with error: ${error.message}`
);
}
  • Considering we don’t get any error in parsing, now we have all the data we need to process input and write our transformer. So our transformer will take 2 inputs (schema,input) both are objects and we will get all values from input and stringify it.
const transform = (schema, input) => {
if (isObject(schema)) {
let formattedObj = {};
Object.keys(schema).forEach(key => {
const value = schema[key];
if (isString(key)) {
if (isString(value)) formattedObj[key] = input[value];
else if (Array.isArray(value)) {
formattedObj[key] = value.map(_schema =>
transform(_schema, input)
);
}
} else {
console.info(`${key} is not string`);
}
});
return formattedObj;
}
};
  • We have written this simple transformation function. Lets walk through it to understand what is going on here.
    First we check if schema is object,
    if(isObject(schema)){}
  • if yes then we have declared empty object formattedObj which will be populated and returned . We are looping through keys and get values to transform.
    Object.keys(schema).forEach(key => { const value = schema[key];})
  • We will talk about other cases later. Now we have key and value, lets compare if value is string, if yes then we will have to get actual value from input for that key. If I put it another way value from schema is key to input object just like illustrated in input images above.
  • Now lets compare type of values, if it is string we know that it should be key from parsed input. And like that we got one entry to formattedObj
    if (isString(value)) formattedObj[key] = input[value];
  • If value is array then we just have to repeat steps, here we can reuse same function to map our array values recusively.
else if (Array.isArray(value)) {
formattedObj[key] = value.map(_schema =>
transform(_schema, input)
);
}
  • Now about else cases we have considered if schema is object we have covered almost all scenarios, but if it is array then we can do something like this, we are recursively mapping / transforming values of array and returning array back.
else if (Array.isArray(schema)) {
return schema.map(_schema =>
transform(_schema, input)
);
}
  • If schema is string then just fetch values from input object and return.
else if (isString(schema)) {
return input[schema];
}
  • Finally putting all together we get below function 😅 phewwww that was easy to explain.
const transform = (schema, input) => {
if (isObject(schema)) {
let formattedObj = {};
Object.keys(schema).forEach(key => {
const value = schema[key];
if (isString(key)) {
if (isString(value)) formattedObj[key] = input[value];
else if (Array.isArray(value)) {
let arr = [];
value.forEach(arrKey => {
arr.push(input[arrKey]);
});
formattedObj[key] = arr;
}
} else {
console.info(`${key} is not string`);
}
});
return formattedObj;
} else if (Array.isArray(schema)) {
return schema.map(_schema =>
transform(_schema, input, retainCommasInValues)
);
} else if (isString(schema)) {
return input[schema];
} else return schema;
};

We have transformed input to required schema now we just have to stringify it and pass it on as output to use it in workflow. I will write seperate blog about how I implemented simple version of JSON.stringify for my needs. But for now we can copy below stringify function and hopefully it should work 😐.

const stringify = input => {
let stringifiedInput = "";
if (isObject(input)) {
stringifiedInput = `${stringifiedInput}{`;
Object.keys(input).forEach((key, index) => {
stringifiedInput = `${stringifiedInput}'${key}':${stringify(input[key])}`;
if (Object.keys(input).length - 1 !== index) {
stringifiedInput = `${stringifiedInput},`;
}
});
stringifiedInput = `${stringifiedInput}}`;
} else if (Array.isArray(input)) {
stringifiedInput = `${stringifiedInput}[`;
input.forEach((value, index) => {
stringifiedInput = `${stringifiedInput}${stringify(value)}`;
if (input.length - 1 !== index) {
stringifiedInput = `${stringifiedInput},`;
}
});
stringifiedInput = `${stringifiedInput}]`;
} else {
stringifiedInput = `'${input}'`;
}
return stringifiedInput;
};

And once we put all together this is how index.js will look like.

const core = require("@actions/core");const isString = object =>
Object.prototype.toString.call(object) === "[object String]";
const isObject = object =>
Object.prototype.toString.call(object) === "[object Object]";
const transform = (schema, input) => {
if (isObject(schema)) {
let formattedObj = {};
Object.keys(schema).forEach(key => {
const value = schema[key];
if (isString(key)) {
if (isString(value)) formattedObj[key] = input[value];
else if (Array.isArray(value)) {
let arr = [];
value.forEach(arrKey => {
arr.push(input[arrKey]);
});
formattedObj[key] = arr;
}
} else {
console.info(`${key} is not string`);
}
});
return formattedObj;
} else if (Array.isArray(schema)) {
return schema.map(_schema =>
transform(_schema, input, retainCommasInValues)
);
} else if (isString(schema)) {
return input[schema];
} else return schema;
};
const stringify = input => {
let stringifiedInput = "";
if (isObject(input)) {
stringifiedInput = `${stringifiedInput}{`;
Object.keys(input).forEach((key, index) => {
stringifiedInput = `${stringifiedInput}'${key}':${stringify(input[key])}`;
if (Object.keys(input).length - 1 !== index) {
stringifiedInput = `${stringifiedInput},`;
}
});
stringifiedInput = `${stringifiedInput}}`;
} else if (Array.isArray(input)) {
stringifiedInput = `${stringifiedInput}[`;
input.forEach((value, index) => {
stringifiedInput = `${stringifiedInput}${stringify(value)}`;
if (input.length - 1 !== index) {
stringifiedInput = `${stringifiedInput},`;
}
});
stringifiedInput = `${stringifiedInput}]`;
} else {
stringifiedInput = `'${input}'`;
}
return stringifiedInput;
};
try {
const input = core.getInput("input");
const schema = core.getInput("schema", { require: true });
const transfomedInput = transform(schema, input);
core.info(`transformed input : ${transfomedInput}`);
core.info(`stringified input : ${stringify(transfomedInput)}`);
core.setOutput("stringified", stringify(transfomedInput));
} catch (error) {
core.setFailed(
`Action failed to transform input with error: ${error.message}`
);
}

6. Final step put all together and push it to github repo. As you have action.yamlin your root folder Github will guide you to publish it to marketplace. You can refer to this documentation.
That’s it we have just created our own custom action and we can use it in any workflow. 👍🏻

Take loot at this action on marketplace.

Conclusion: At the time this article was published github does not access normal object as input to matrix in workflow. So we created this action to convert our dynamic inputs to stringified version with schema and we can successfully use it in our workflows.

Refrences:

  1. https://docs.github.com/en/enterprise-server@3.1/actions/creating-actions/creating-a-javascript-action
  2. https://docs.github.com/en/actions/creating-actions/publishing-actions-in-github-marketplace

--

--

VS-404
VS-404

No responses yet