مرور خودت را تکرار نکن0:00
You just skipped the homework, didn't you? You just finished that last episode and you started watching this one right after. No, no. And I don't want to hear the excuses. Stop. Go back and do the homework. I'm trying to teach you the pain of writing GitHub Actions step by step, of copying Actions. You will not understand this lesson if you don't... Go back and do the homework. Go.
Why Actions Get Repetitive0:20
Go back and do the homework. Go. Hopefully you realized that was a joke. If not, I am sorry for any school PTSD I've just brought back. But no, if you did the homework, you will start to understand that when you create GitHub Actions, you repeat yourself a lot, right? You have to write the same steps over and over again, which doesn't feel very programmery. Like we don't do that, right? We like to extract to shared functions, shared methods.
Like we don't do that, right? We like to extract to shared functions, shared methods. In fact, most of our life is spent trying to remove a duplicate code. So when it comes to the setup of, well, the cache environment for php, or setting up php on a certain version with Composer, or making sure that the cache for our Composer dependencies is in place, you're going to want to do that in each and every workflow. It gets very boring having to repeat yourself. So why don't we improve that? Because we can actually create our own little Composite Action
Creating a Composite Action1:19
So why don't we improve that? Because we can actually create our own little Composite Action that allows us to simplify all of these steps into just a single step we can drop into any of our workflows. It's pretty cool. Let's play with it in this episode. The first thing we're going to do is head to the GitHub directory, and we'll create a new subdirectory called Actions. That's just my preference. There's no actual paradigm there, but I think it makes a lot of sense.
That's just my preference. There's no actual paradigm there, but I think it makes a lot of sense. That's what we're going to be creating, a Composite Action. Let's create another directory underneath that, and this is going to house our setup action that we're creating. Now, this bit is paradigm. Inside the directory, you need a YAML file called action.yml, right? It has to be called action because GitHub Actions is going to look for that file in order to be able to execute this action when you ask for it to run. Okay, first required parameter is a name,
in order to be able to execute this action when you ask for it to run. Okay, first required parameter is a name, and I'll keep it simple. I'm just going to call mine setup. We also want a description. So, let's say sets up PHP and Composer. Next, we're going to define the runs key. This is sort of equivalent to the jobs key inside a workflow file, but obviously, you can only have a single job inside one of these extracted actions. So, it's called runs, and we also have to say what type of action it is.
but obviously, you can only have a single job inside one of these extracted actions. So, it's called runs, and we also have to say what type of action it is. So, we have Docker actions. We have Node actions. In our case, because we're taking an existing set of steps and essentially just placing them, dropping them into one unified location, this is known as a composite action, and you'll see that it's already autofilled underneath this steps key, which is very similar to the steps key that we have inside our workflow file. So, here's what we'll do.
Migrating Steps into Action3:04
which is very similar to the steps key that we have inside our workflow file. So, here's what we'll do. We'll take from setup cache environment in test.yaml, and we'll drag down until we get to the end of caching Composer dependencies. Let's cut that out, and then we'll head back into our brand new exciting composite action, and we'll drop those in. Let's see if everything works. So, we have set up cache environment. Yeah, we're relying on some external environment variables here, but we'll come back to that in just a moment.
Yeah, we're relying on some external environment variables here, but we'll come back to that in just a moment. No worries about that for now. We cache the extensions. We set up php and Composer. Again, relying on those variables. We'll clean that up later. We have an actual error here, which is when we get the Composer cache directory. So, this is because we're making use of run. Nothing wrong with doing that inside a composite action,
So, this is because we're making use of run. Nothing wrong with doing that inside a composite action, but you do have to define what type of shell you're using. If in doubt, you're using bash, okay? So, we'll drop bash in there using the shell key, and everything else looks to actually be okay. So, why don't we see if we can actually make this work? I'm going to head to test.yaml, and where I've cut out those manual steps, we'll create a new step called setup,
Using and Testing Action4:13
and where I've cut out those manual steps, we'll create a new step called setup, and we can now make use of our brand new exciting action. So, we reference this as a local path .github/actions/setup, and you don't need to go to action.yaml. That's going to be figured out for you by GitHub Actions itself. Let's go ahead, commit this code, make sure we include our new file, and then we'll see if it actually runs correctly inside GitHub. See you in a second. Hey, it worked.
See you in a second. Hey, it worked. We have a successful test run with our new setup block. It took 13 seconds to execute, and you can see it's caching the extensions, restoring the cache, goes ahead and sets up PHP. Then further down, we have the composer cache being prepared, ready for installing project dependencies. It just does exactly what you'd expect. So, it's pretty cool that with minimal effort,
It just does exactly what you'd expect. So, it's pretty cool that with minimal effort, we can extract those common steps into a composite action. There are a few caveats here. As we said, currently, these steps are relying on the fact that there are environment variables configured. So, that means that if we haven't declared these environment variables up here, we're going to have a little issue. If we try to take this step, and then we head to our, I don't know, our Laravel Pint yaml file, right?
Replacing Env with Inputs5:29
If we try to take this step, and then we head to our, I don't know, our Laravel Pint YAML file, right? And then we come down here. Let's get rid of setup.php and composer. And instead, we'll just drop this setup step in. If we attempted to actually execute this at this stage, this would fail. For obvious reasons, there are no environment variables configured. So, in order to circumnavigate that potential problem, what we can actually do is move away from environment variables for those steps, and create custom input that we will provide to our composite action.
what we can actually do is move away from environment variables for those steps, and create custom input that we will provide to our composite action. Let's take a look. So, we're currently relying on three environment variables, PHP version, cache key, and PHP extensions. We'll focus on the PHP version and extensions first, and then we'll come back to cache key a little later on because I think we can actually improve that. So, let's go into action.yaml. And just above runs, we're going to define a new key called inputs.
So, let's go into action.yaml. And just above runs, we're going to define a new key called inputs. Each input has a unique key. In this case, why don't we say PHP version? Then underneath, we provide a description. So, let's say the PHP version, and I'll put a little S in there because you could technically use a matrix to run against multiple PHP versions. So, the PHP versions you wish to use. Then we're going to define whether or not this is required. And in this case, I want to make sure you have to pass a PHP version.
Then we're going to define whether or not this is required. And in this case, I want to make sure you have to pass a php version. Okay. Once we've done that, we can define our second piece of input, which is going to be php extensions. In this case, our description will be the php extensions you want to install. And of course, we don't necessarily have to require this because we know that default list that we would always want to use. So, let's say required false, and we'll set a default up, which is going to be equal to this list here.
So, let's say required faults, and we'll set a default up, which is going to be equal to this list here. So, we'll come back, grab this list, and then let's just drop that in as the default. Okay. So, we have our two inputs defined, php version, php extensions. How do we make use of them? It's actually really simple. Anywhere we define env. and then an environment variable, we just switch it out for inputs. followed by the input that we've created. So, in this case, inputs.php version or inputs.php extensions.
we just switch it out for inputs.php followed by the input that we've created. So, in this case, inputs.php version or inputs.php extensions. Little further down, we have the same thing. Let's change that. inputs.php version, inputs. and we're looking for PHP extensions. Very nice. I think the only other usage we have of environment is the cache key, which again, we'll come back to in just a second. If we go back to test.yaml, we can now make use of this. So, where we have steps, you'll see actually it's now throwing a validation error.
If we go back to test.yaml, we can now make use of this. So, where we have steps, you'll see actually it's now throwing a validation error because we need to provide some custom input. So, the first piece of custom input we want to provide is the PHP version, which is 8.3. And of course, because it's not required, we don't have to provide PHP extensions, but we could if we wanted to. I'm actually going to leave that out. And once we've done that, we can remove this line here and this line here because we no longer reference that anywhere else inside this action.
And once we've done that, we can remove this line here and this line here because we no longer reference that anywhere else inside this action. It's now all handled inside this one simple setup composite action, which again, I think is really cool. Now, the reason I've left cache key until this point is because there are a few questions that it raises. We're manually bumping the cache key. No problem with that, as we've discussed, apart from the fact that now we have extensions defined in multiple places because we can override the php extensions that we install.
apart from the fact that now we have extensions defined in multiple places because we can override the PHP extensions that we install by defining it as an input to the setup action. But if we don't define it, it's going to fall back to this list here. What happens if we, for example, remove GMP from this list? We need to bust the cache, but the cache key is defined not in the setup action, but in the workflow file. So we'd have to remember to go to every workflow file and manually bump the cache version. Start to see the issue.
Auto-Generating Cache Key9:36
and manually bump the cache version. Start to see the issue. We've got a little bit of a chicken egg scenario. It would just make more sense to actually automate the process of generating that cache key. And I think we have all the knowledge to be able to do that now. In our previous episode, we already discussed how it's possible to run a bash command, save the output to GitHub, and then retrieve that in a subsequent step.
save the output to GitHub, and then retrieve that in a subsequent step. So what if we replace this command here with a command that would generate a hash based on a string of php extensions provided? I think that would work. Let's see if we can implement it. I'm going to copy this as a template, and we'll head up to the top here, and just above the step where we're making use
and we'll head up to the top here, and just above the step where we're making use of the cache key environment variable, we'll drop this in place. Let's update the name, get php extension cache hash, and then we can change the ID as well, get cache hash. We'll change our variable name to hash, and then we can remove this subcommand.
We'll change our variable name to hash, and then we can remove this subcommand. And let's have a think about what we'd actually want to perform inside this subcommand. On macOS, there's a very handy MD5 function that you can call from the terminal passing a string, and it will generate an MD5 hash, but that's not available inside Linux containers. Instead, we have a function called md5sum. It does much the same,
Instead, we have a function called md5sum. It does much the same, but it's designed to work on files. Now, we can kind of trick it by piping a string into it. So we could say something like echo test, and then we'll pipe that to md5sum, and it will return a hash. But notice that it also has this little hyphen at the end. That would usually be the file name if you'd passed a file in. So we need to do a little bit of manipulation here.
That would usually be the file name if you'd passed a file in. So we need to do a little bit of manipulation here. Now, thankfully, there's a pretty cool scripting language we can make use of called awk. So we'll pass a couple of quotes as awk, then we'll pass curly braces, and we want to print out the first variable from the previous command. So the first variable is going to be $1. If you run that, you're left with the hash.
So the first variable is going to be $1. If you run that, you're left with the hash. Just to show you what happens, by the way, if you print out the second variable, you're going to be left with the hyphen. So it essentially takes that command output, it splits up what's returned, and then we're able to say, just return to me a certain portion of that script. In this case, the md5 hash.
just return to me a certain portion of that script. In this case, the md5 hash. Now, let's take this one step further. Let's come and grab a few PHP extensions, and then we'll run the command again, but instead of test, I'm going to pass those extensions in. We're given a hash. No matter how many times we go ahead and run this command, as long as those extensions are the same,
No matter how many times we go ahead and run this command, as long as those extensions are the same, the hash remains consistent. But let's say we remove the zip extension, where all of a sudden our hash changes, that would cause the cache to be bust, and it would set up again from fresh. In other words, we've just created the perfect solution. So let's implement it. First of all, we need to echo out the PHP extensions.
So let's implement it. First of all, we need to echo out the php extensions. I'm going to do that via environment variables, which we'll fix in a moment. So php extensions, we'll pipe that into the md5 sum function. And then again, I'm going to use awk here in order to create a little script that essentially is going to print out the first variable from the output of md5 sum.
that essentially is going to print out the first variable from the output of md5 sum. And that should be everything we need. Now we can make use of this. So inside our output here, instead of reaching for environment, let's go for steps.getCacheHash outputs, and we're looking for the hash variable. And I would always recommend using a little prefix here. So why don't we say PHP extensions,
And I would always recommend using a little prefix here. So why don't we say php extensions, and then that will just give us a prefix to make sure that we always know that this cache key is dealing with those php extensions. All right, let's head back into our test.yaml workflow. We should now be able to remove all global environment variables, which is pretty cool. We're left with this seamless,
which is pretty cool. We're left with this seamless, single, easy action to make use of. We can also update our Laravel Pint workflow. We'll make sure we're first of all, passing our php version of 8.3. And if we take a quick look at the Laravel documentation for Pint, there are just a few extensions required to get Pint working.
there are just a few extensions required to get Pint working. So we'll make sure that we declare those extensions using our brand new php extensions input. It's gonna load the default in, in PHPStorm, but we can remove that default and then replace it with just these five requirements. All right, I think at this, we're actually good to go. Let's push it up and see if it works. And it worked.
Let's push it up and see if it works. And it worked. In fact, if we take a quick look at setup, took quite a while, but I would expect that because we're generating a new cache key here. For cache extensions, here's the key that we generated. So basically for this set list of extensions, this key will always be generated. Pretty nice. Yeah, I like that a lot.
Pretty nice. Yeah, I like that a lot. This is actually a really cool episode because what you've actually created is, well, a GitHub Action, an action that you could package up, you could move out of this repository and you could place on the GitHub Action marketplace, if you so desired. That's essentially all a GitHub Action is.
if you so desired. That's essentially all a GitHub action is. I think that's pretty cool. And certainly if you do too, you might want to explore what else you can build using actions. At the very least, you now know how to de-duplicate code across workflows in your repository, which is also a huge benefit. So with that knowledge, let's move on.
So with that knowledge, let's move on.
