{"id":61438,"date":"2021-04-07T12:27:43","date_gmt":"2021-04-07T20:27:43","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/devops\/?p=61438"},"modified":"2021-04-08T10:24:32","modified_gmt":"2021-04-08T18:24:32","slug":"building-your-first-github-action","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/devops\/building-your-first-github-action\/","title":{"rendered":"Building Your First GitHub Action"},"content":{"rendered":"<p>This is a guest post from one of our friends at GitHub, John Bohannon. Welcome John!<\/p>\n<h2>Introduction<\/h2>\n<p>Hello from the friendly octocats at GitHub! By now, you might have used <a href=\"https:\/\/github.com\/features\/actions\">GitHub Actions<\/a> for automation, CI\/CD, and more \u2013 and you know it&#8217;s powered by thousands of modular, community-developed building blocks called <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\">actions<\/a>. In this guide, you&#8217;ll use TypeScript to build a GitHub action to interact with an existing service, and then learn how to publish it to GitHub Marketplace for discovery by the larger GitHub community.<\/p>\n<p>So why would you want to write your own action? Reasons include:<\/p>\n<ul>\n<li>Facilitate developer interaction with an existing API, service, or product. <\/li>\n<li>Contribute to the DevSecOps ecosystem, reaching the 56+ million developers on GitHub. <\/li>\n<li>Use <a href=\"https:\/\/docs.microsoft.com\/azure\/devops\/pipelines\/ecosystems\/github-actions?view=azure-devops?WT.mc_id=devops-23615-stmuraws\">Azure DevOps and GitHub Actions together<\/a> in a complementary fashion or while <a href=\"https:\/\/docs.github.com\/en\/actions\/learn-github-actions\/migrating-from-azure-pipelines-to-github-actions\">migrating<\/a> step by step<\/li>\n<\/ul>\n<p>The action we build in this guide will make it easy to upload files to <a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/storage\/blobs\/?WT.mc_id=devops-23615-stmuraws\">Azure Blob Storage<\/a>, a service for massively scalable and secure storage of files like images, videos, logs, and backups. Let&#8217;s begin!<\/p>\n<h2>Method<\/h2>\n<h3>1\ufe0f\u20e3 Design the user experience<\/h3>\n<p>Start by designing a good user experience (UX):<\/p>\n<ul>\n<li>How do we want someone to <em>consume<\/em> the action? <\/li>\n<li>How might someone already be used to <em>interacting<\/em> with Azure Blob Storage programmatically, whether by <a href=\"https:\/\/docs.microsoft.com\/cli\/azure\/storage\/blob?view=azure-cli-latest&amp;WT.mc_id=devops-23615-stmuraws#az_storage_blob_upload\">Azure CLI<\/a>, <a href=\"https:\/\/docs.microsoft.com\/azure\/storage\/blobs\/reference#javascript-client-libraries?WT.mc_id=devops-23615-stmuraws\">popular SDKs<\/a>, or <a href=\"https:\/\/github.com\/marketplace?type=actions&amp;query=azure\">existing actions<\/a>? <\/li>\n<li>What other actions or <a href=\"https:\/\/docs.github.com\/en\/actions\/reference\/workflow-syntax-for-github-actions#jobsjob_idstepsrun\">run steps<\/a> might commonly <em>precede or follow<\/em> it?<\/li>\n<\/ul>\n<p>From some quick research, it looks like accepting a set of files (maybe supporting globbing) and an upload destination (maybe a storage account and container) is a good set of functionality to begin with. And maybe the action could be used in conjunction with some of the existing <a href=\"https:\/\/github.com\/azure\/actions\">Azure actions<\/a>&#8230; \ud83e\udd14<\/p>\n<p>An action takes <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/metadata-syntax-for-github-actions#inputs\"><code>inputs<\/code><\/a> and produces <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/metadata-syntax-for-github-actions#outputs\"><code>outputs<\/code><\/a>, a model which lets actions be piped together with other actions, like <a href=\"https:\/\/github.com\/marketplace?type=actions\">these in the GitHub Marketplace<\/a>, in a <a href=\"https:\/\/docs.github.com\/en\/actions\/learn-github-actions\/introduction-to-github-actions#workflows\">workflow<\/a>. We can start with outputting the <code>filename<\/code> and <code>url<\/code> of the uploaded blob.<\/p>\n<p>So, a user experience like this should do the trick. Here&#8217;s an imaginary workflow calling the yet-to-be-built <code>upload-azure-blob<\/code> action:<\/p>\n<pre><code class=\"yaml\"># GitHub Actions repository workflow file, e.g .github\/workflows\/upload.yml\n\n# ...\n# previous steps to choose a runner type, authenticate to Azure, prepare files, etc\n# ...\n\n# Upload `.png`s to Azure Blob Storage\nname: Upload all PNGs to Azure Blob Storage\nid: upload\nuses: github-developer\/upload-azure-blob@v1\nwith:\n  account: octodex\n  destination: octocats\n  source: **\/*.png\n\n# Print out the urls to uploaded files\nname: Print URLs\nrun: echo $URLS # { [\"filename\":\"hulatocat.png\",\"url\":\"https:\/\/octodex.blob.core.windows.net\/octocats\/hulatocat.png\"] }\nenv:\n  URLS: ${{ steps.upload.outputs.urls }}\n<\/code><\/pre>\n<p>Quick aside: an action is &#8220;just&#8221; a GitHub repository with a <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/metadata-syntax-for-github-actions\">metadata file called <code>action.yml<\/code><\/a>. Workflows can call them using a <a href=\"https:\/\/docs.github.com\/en\/actions\/reference\/workflow-syntax-for-github-actions#example-using-a-public-action-in-a-subdirectory\">pattern<\/a> like <code>uses: {owner}\/{repo}\/{path}@{ref}<\/code> \u2013 in the example above, <code>uses: github-developer\/upload-azure-blob@v1<\/code>.<\/p>\n<p>To match the workflow above, the <code>action.yml<\/code> metadata file for <code>upload-azure-blob<\/code> can look something like this. Progress!<\/p>\n<pre><code class=\"yaml\"># GitHub action metadata file, action.yml\n\n# ... name, description, branding\n\ninputs:\n  account:\n    description: |-\n      Storage account name, e.g. 'mystorageaccount'\n    required: true\n\n  destination:\n    description: |-\n      Name of container to upload blob to, e.g. '$web' to upload a static website.\n    required: true\n\n  source:\n    description: |-\n      Path to file(s) to upload to \"destination\", e.g. '.' to upload all files in the current directory.\n      Supports globbing, e.g. 'images\/**.png'.\n      For more information, please refer to https:\/\/www.npmjs.com\/package\/glob.\n    required: true\n\noutputs:\n  urls:\n    description: |-\n      URL(s) to the uploaded blob(s), if successful\n\n# ...\n<\/code><\/pre>\n<p>With the metadata description complete, now we consider the action logic and come to a fork in the road.<\/p>\n<p>There are advantages to building a <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/creating-a-docker-container-action\">container action<\/a>, but arguably more for <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/creating-a-javascript-action\">JavaScript actions<\/a>. They load faster, work on all runner types, and build upon a thriving community of tooling like <a href=\"https:\/\/github.com\/actions\/toolkit\"><code>actions\/toolkit<\/code><\/a>! JavaScript also happens to be the most-used language across GitHub (read the <a href=\"https:\/\/octoverse.github.com\/\">Octoverse 2020 report<\/a>), so chances are, choosing JavaScript will open doors to community contribution to our action, too.<\/p>\n<p>And you know what? Anything that <em>compiles<\/em> to JavaScript is eligible, so let&#8217;s go with <del>CoffeeScript<\/del> errr nah, <a href=\"https:\/\/www.typescriptlang.org\/\">TypeScript<\/a>, for some extra DX love \ud83d\ude03. I&#8217;ve been wanting to try <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/release-notes\/typescript-3-7.html#optional-chaining\">optional chaining<\/a> anyway.<\/p>\n<h3>2\ufe0f\u20e3 Get to coding<\/h3>\n<h4>Start with a template<\/h4>\n<p>The GitHub Actions team provides a nice set of <a href=\"https:\/\/github.com\/actions?q=template&amp;type=&amp;language=&amp;sort=\">template repos<\/a>, ready to help us get started! By <a href=\"https:\/\/docs.github.com\/en\/github\/creating-cloning-and-archiving-repositories\/creating-a-repository-from-a-template\">generating<\/a> a new repo from <a href=\"https:\/\/github.com\/actions\/typescript-action\/\"><code>actions\/typescript-action<\/code><\/a>, we start with a functioning action. Note that the name of this repository forms part of the path to your action. I generated mine into <code>github-developer\/upload-azure-blob<\/code>, so it can be referenced that way across GitHub. Not to leave out all you container action devs, there&#8217;s a <a href=\"https:\/\/github.com\/actions\/container-action\">template or two<\/a> for you too.<\/p>\n<p>And wow, to TypeScript eyes this action looks very&#8230;normal, except for that one stray metadata file, <code>action.yml<\/code>, most of which we designed before \u261d\ufe0f. Copy that in now.<\/p>\n<pre><code>.\n\u251c\u2500\u2500 .github\n\u2502   \u2514\u2500\u2500 workflows\n\u2502       \u2514\u2500\u2500 test.yml\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 LICENSE\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 __tests__\n\u2502   \u2514\u2500\u2500 main.test.ts\n\u251c\u2500\u2500 action.yml\n\u251c\u2500\u2500 jest.config.js\n\u251c\u2500\u2500 lib\n\u2502   \u251c\u2500\u2500 main.js\n\u2502   \u2514\u2500\u2500 wait.js\n\u251c\u2500\u2500 package-lock.json\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 src\n\u2502   \u251c\u2500\u2500 main.ts\n\u2502   \u2514\u2500\u2500 wait.ts\n\u2514\u2500\u2500 tsconfig.json\n<\/code><\/pre>\n<p>This action isn&#8217;t very useful for uploading to Azure Storage though \u2013 it just waits the number of <code>milliseconds<\/code> we input. But it&#8217;s a great base, since it does include compilation support, tests, a validation workflow, publishing, and versioning guidance!<\/p>\n<h4>Leverage official libraries and actions<\/h4>\n<p>We won&#8217;t get very far without access to an Azure SDK of some sort, like <a href=\"https:\/\/docs.microsoft.com\/azure\/storage\/blobs\/reference#javascript-client-libraries?WT.mc_id=devops-23615-stmuraws\">@azure\/storage-blob<\/a>. <a href=\"https:\/\/docs.microsoft.com\/javascript\/api\/overview\/azure\/storage-blob-readme?view=azure-node-latest&amp;WT.mc_id=devops-23615-stmuraws#create-the-blob-service-client\">Used together<\/a> with <a href=\"https:\/\/www.npmjs.com\/package\/@azure\/identity\"><code>@azure\/identity<\/code><\/a>, we should be able to get an authenticated client to start make function \/ API calls. But &#8220;authenticated&#8221;&#8230; with what tokens? Whose identity? Well, we&#8217;re covered there, too \ud83d\ude0c.<\/p>\n<p>This TypeScript code we&#8217;re writing will be compiled and then called from a GitHub Actions workflow, right? And we saw earlier that there is an <a href=\"https:\/\/github.com\/azure\/actions\">extensive set of actions just for Azure<\/a>. Well, one of these actions happens to facilitate secure, programmatic authentication with Azure on Actions <a href=\"https:\/\/docs.github.com\/en\/actions\/learn-github-actions\/introduction-to-github-actions#runners\">runners<\/a>!<\/p>\n<p>So in our repository documentation (e.g. <code>README<\/code>), we can direct our users to call <a href=\"https:\/\/github.com\/Azure\/login\"><code>azure\/login<\/code><\/a> in their workflows before <code>upload-azure-blob<\/code>, like this:<\/p>\n<pre><code class=\"yaml\"># GitHub Actions repository workflow file, e.g .github\/workflows\/upload.yml\n\n# ...\n# previous steps to choose a runner type, authenticate to Azure, prepare files, etc\n# ...\n\n# Log into Azure using repository secret with service principal credentials\n# https:\/\/github.com\/Azure\/login#configure-deployment-credentials\n- uses: azure\/login@v1\n  with:\n    creds: ${{ secrets.AZURE_CREDENTIALS }}\n\n# Upload `.png`s to Azure Storage Blob\n- uses: github-developer\/upload-azure-blob@v1\n  with:\n    account: mystorageaccount\n    destination: nameofstoragecontainer\n    source: **\/*.png\n<\/code><\/pre>\n<p>Is that cool or what? Our function calls like these will suddenly work:<\/p>\n<pre><code class=\"ts\">\/\/ src\/upload.ts\nexport const getBlobServiceClient = (account: string): BlobServiceClient =&gt; {\n  \/\/ https:\/\/github.com\/Azure\/azure-sdk-for-js\/blob\/master\/sdk\/identity\/identity\/README.md#defaultazurecredential\n  const defaultAzureCredential = new DefaultAzureCredential()\n  const blobServiceClient = new BlobServiceClient(\n    `https:\/\/${account}.blob.core.windows.net`,\n    defaultAzureCredential\n  )\n  return blobServiceClient\n}\n<\/code><\/pre>\n<h4>Write the upload logic<\/h4>\n<p>Now we can write the logic to upload globbed blob (I just wanted to type that) paths like <code>*.log<\/code>, <code>**\/*.png<\/code>. Yet again, we build on the shoulders of giants by pulling in a <a href=\"https:\/\/www.npmjs.com\/package\/glob\">trusty npm package, <code>glob<\/code><\/a>.<\/p>\n<p>To save us some time, and to spare you a very messy commit history (what, just me?!), here&#8217;s how the top-level <code>main.ts<\/code> shapes up, with the core functionality in <code>upload.ts<\/code>. Refer to the full code in the <a href=\"https:\/\/github.com\/github-developer\/upload-azure-blob\">the open source repo<\/a>.<\/p>\n<pre><code class=\"ts\">\/\/ src\/main.ts\nimport * as core from '@actions\/core'\nimport { getInputs, getBlobServiceClient, uploadBlobs, Inputs, Output } from '.\/upload'\n\nasync function run (): Promise&lt;void&gt; {\n  try {\n    \/\/ Retrieve user inputs to action\n    const inputs: Inputs = getInputs()\n    \/\/ Retrieve authenticated container client\n    const containerClient = getBlobServiceClient(inputs.account).getContainerClient(inputs.destination)\n    \/\/ Upload files to container\n    const output: Output[] = await uploadBlobs(containerClient, inputs.source)\n\n    core.setOutput('urls', JSON.stringify(output))\n  } catch (error) {\n    core.setFailed(error.message)\n  }\n}\n\n\/\/ eslint-disable-next-line @typescript-eslint\/no-floating-promises\nrun()\n<\/code><\/pre>\n<p>One distinction actions have with regular JavaScript projects is that dependent packages are <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/creating-a-javascript-action#commit-tag-and-push-your-action-to-github\">committed alongside the code<\/a>, usually in a minified form using a compiler \/ bundler like <a href=\"https:\/\/github.com\/vercel\/ncc\">@vercel\/ncc<\/a> or <a href=\"https:\/\/parceljs.org\/\">Parcel<\/a>. The <code>typescript-action<\/code> template already includes <code>ncc<\/code>, so by running <code>npm run package<\/code>, you get a nice, compiled <code>dist\/index.js<\/code> file. This is the entry point to the action and should match <code>main<\/code> in <code>action.yml<\/code>.<\/p>\n<h4>Test and automate<\/h4>\n<p>Of course, we should add some Jest unit tests to raise confidence in future changes \ud83d\ude07, and then we can use GitHub Actions as a test runner&#8230;to test our action! Let&#8217;s start with some quick sanity checks of <code>uploadBlobs<\/code>, just like you would with any other TypeScript project:<\/p>\n<pre><code class=\"ts\">\/\/ __tests__\/upload.test.ts\n\n\/\/ ...\n\/\/ test files message1.txt, message2.txt exist in repo\n\/\/ ...\n\ntest('uploadBlobs doesn\\'t call uploadBlob with zero files', async () =&gt; {\n  await upload.uploadBlobs(client, 'bananas.txt')\n  expect(mockedUploadBlob.mock.calls.length).toBe(0)\n})\n\ntest('uploadBlobs calls uploadBlob with one file', async () =&gt; {\n  await upload.uploadBlobs(client, 'message.txt')\n  expect(mockedUploadBlob.mock.calls.length).toBe(1)\n})\n\ntest('uploadBlobs calls uploadBlob with two files', async () =&gt; {\n  await upload.uploadBlobs(client, 'message*.txt')\n  expect(mockedUploadBlob.mock.calls.length).toBe(2)\n})\n\n\/\/ ...\n<\/code><\/pre>\n<p>You <em>could<\/em> try remember to run these tests locally or in <a href=\"https:\/\/github.com\/features\/codespaces\">GitHub Codespaces<\/a>, but it&#8217;s far better for them to run automatically every time a push happens or a pull request is opened. Sounds like a job for GitHub Actions! A <a href=\"https:\/\/github.com\/github-developer\/upload-azure-blob\/blob\/main\/.github\/workflows\/ci.yml\">sample continuous integration (CI) workflow<\/a> is included to build, test, and push compiled changes back to the repo.<\/p>\n<p>To view all the source code and see how you can pull in the action to your very own workflow, refer to <a href=\"https:\/\/github.com\/github-developer\/upload-azure-blob\">this open source repo<\/a> \ud83e\udd17.<\/p>\n<h3>3\ufe0f\u20e3 The action in action<\/h3>\n<p>So far, we have:<\/p>\n<ul>\n<li>Designed <\/li>\n<li>Developed <\/li>\n<li>Tested <\/li>\n<li>And automated CI for <code>upload-azure-blob<\/code> \ud83c\udf89<\/li>\n<\/ul>\n<p>Now that we&#8217;ve built the engine, let&#8217;s see this thing go \ud83d\ude80! After creating an Azure <a href=\"https:\/\/docs.microsoft.com\/azure\/storage\/common\/storage-account-create?WT.mc_id=devops-23615-stmuraws&amp;tabs=azure-portal\">storage account and container<\/a> and <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/about-actions#using-release-management-for-actions\">releasing <code>v1<\/code> of the action<\/a>, here&#8217;s me uploading some favorite octocats to my very own <a href=\"https:\/\/octodex.github.com\/\">Octodex<\/a>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/user-images.githubusercontent.com\/2993937\/112677130-b377f880-8e3f-11eb-8880-ad0c6b1dde1d.gif\" alt=\"upload\" \/><\/p>\n<h3>4\ufe0f\u20e3 Release and publish to GitHub Marketplace<\/h3>\n<p>As it stands, <code>upload-azure-blob<\/code> is instructional but not necessarily production-ready. Here are just a few ideas of how it could be improved: &#8211; Automate release and maintenance (e.g. by following this <a href=\"https:\/\/partner.github.com\/integration-resources\/2021\/03\/19\/pattern-releasing-and-maintaining-actions.html\">guide<\/a>) &#8211; Remove requirement to have to create the <code>destination<\/code> before uploading to it &#8211; Increase test coverage<\/p>\n<p>When we&#8217;re happy with the way things look, we can start to distribute and promote. How can I get this in more hands? Well for starters, anyone can <a href=\"https:\/\/docs.github.com\/en\/actions\/creating-actions\/publishing-actions-in-github-marketplace\">publish actions<\/a> to GitHub Marketplace!<\/p>\n<p>When drafting a new release, it&#8217;s as simple as accepting the terms and clicking a checkbox:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/user-images.githubusercontent.com\/2993937\/112678286-20d85900-8e41-11eb-9853-36ab18965b9f.gif\" alt=\"publish-to-github-marketplace\" \/><\/p>\n<p>To further promote and partner with GitHub, consider joining the <a href=\"https:\/\/partner.github.com\/technology-partners\">GitHub Technology Partner Program<\/a>.<\/p>\n<h2>Wrapping up<\/h2>\n<p>Today, we started with an idea (&#8220;upload to Azure Blob Storage&#8221;) and ended with a working GitHub action published in GitHub Marketplace \ud83e\udd73, all while building upon open source tooling and libraries.<\/p>\n<p>We encourage you to try it out by building and publishing actions that facilitate your own DevOps journey. See you around!<\/p>\n<p>John from GitHub Partnerships<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello from the friendly octocats at GitHub! By now, you might have used [GitHub Actions][1] for automation, CI\/CD, and more.  In this guide, you&#8217;ll use TypeScript to build a GitHub action to interact with an existing service, and then learn how to publish it to GitHub Marketplace for discovery by the larger GitHub community.<\/p>\n","protected":false},"author":19707,"featured_media":61441,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[224,226],"tags":[],"class_list":["post-61438","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure","category-ci"],"acf":[],"blog_post_summary":"<p>Hello from the friendly octocats at GitHub! By now, you might have used [GitHub Actions][1] for automation, CI\/CD, and more.  In this guide, you&#8217;ll use TypeScript to build a GitHub action to interact with an existing service, and then learn how to publish it to GitHub Marketplace for discovery by the larger GitHub community.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/posts\/61438","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/users\/19707"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/comments?post=61438"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/posts\/61438\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/media\/61441"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/media?parent=61438"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/categories?post=61438"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/tags?post=61438"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}