Conf42 DevSecOps 2022 - Online

CI/CD with Github Actions

Video size:


Learn about building and deploying applications using Github Actions. We will learn diverse ways to trigger our builds and build different of types of applications. After this session you’ll be able to build and deploy your software from GitHub.


  • Chris Ayers is a senior customer engineer at Microsoft. He talks about CI CD with GitHub Actions. Actions are event driven, so there's a large number of events that can trigger if a workflow runs. He also shows some demos and has a fun time playing with them.
  • There's no manual way to trigger a workflow. You have to add workflow dispatch if you want to manually trigger a workflows. Using hosted agents, you can run and do lots of things very quickly across a wide range of languages.
  • GitHub actions and the workflows are an amazing toolkit that lets us kind of automate and trigger our workflows. The GitHub marketplace is where we have all of those great actions that are built by the community. Any action in the marketplace needs to be in a public open source repo.
  • GitHub secrets exist in GitHub under settings. Conditionals are really a form of expression. Some of our actions and expressions are essentially a type of context. The way you access your secrets is relatively simple when you're looking at the workflow.
  • The secrets come from the environment or from the individual actions. It makes it much easier for me to reuse my jobs or to copy and paste them. And I know Linux environment, shell environment versus environment, a little bit of reuse.
  • Matrices are a really cool feature that is available in workflows where maybe you're a library builder or you're building an application. Youll want to be able to test it on multiple versions of software or multiple OSS. Not everybody does, but it's a good to have.
  • Like net, I do similar patterns to what I've done in other pipeline CI CD platforms. I'm a big fan of separating out my build and my deploy jobs stages. These are cool actions that you can look at on GitHub.
  • In GitHub actions, you can log into Docker Hub or ACR for Azure, the Azure container registry. GitHub also provides a superlinter, which looks at all of your code base in pretty much every language you can think of. How do these actions work?


This transcript was autogenerated. To make changes, submit a PR.
Hello, my name is Chris Ayers and today I'm going to talk about CI CD with GitHub Actions. I'm a senior customer engineer at Microsoft and I work on the fast track for Azure team. We're part of Azure engineering and we help customers build and deploy software to Azure securely, reliably. I have a big focus in DevOps and what I do, I'm on Mastodon, Twitter, LinkedIn. I have a blog and all of my code samples are out on GitHub, so feel free to go out to my repo and take a look and I'll be sharing that. So I first want to talk about YaML and then we'll dive into CI CD, go into some of the discussion around actions, and then I want to show some demos and have a fun time playing with those. So GitHub actions are part of workflows and those are all written in Yaml, yet another markup language. And you have lists that can start with a dash. We have key value pairs that are separated by a colon in a space. Then we have objects which are very similar to key value pairs, but all of the properties are below the object and indented. And I'll show some examples of that shortly. I just want to make sure that those three kind of basic things are covered. And something that's very important is if you get your indentation wrong or your spacing wrong, it can cause problems with your workflow. So having good tooling like Vs code with the YAMl plugins and extensions can be really helpful in this. And another thing is I like to talk about, we have CI CD. Well, there's two types of CD that come into play, and you can do all of these things with GitHub actions through the workflows. Like you can run your builds, you can run your unit tests, you can deploy things out, and there are ways to stop for approvals and see if you want to proceed. Maybe you deploy to dev and you want someone to sign off before it goes to QA, or you deploying to QA and you want some approvals or time windows before things go to prod. You can do some of that. But usually I think some of the differences between continuous delivery and continuous deployment are youll do have push button deployment, but things slow down and you have a human in the loop that can approve or not approve. Now, actions are event driven, so there's a large number of events that can trigger if a workflow runs. All GitHub actions workflows live in the GitHub workflows folder and they're all defined in Yaml. Now what's different about GitHub actions from maybe something like an Azure pipelines is in Azure pipelines you have to specifically say I want a new pipeline using this file. But with GitHub, if youll drop a Yaml inside that GitHub workflows file, and it's a valid workflows YAml, it will start running whenever it's supposed to be triggered. Now if that's a manual trigger, if it's on a push, if it's on a schedule, as soon as you put the file in the repo and it's a valid yaml that matches the schema, it'll be run whenever it's supposed to be triggered. Now, events trigger workflows, and I'm going to show you a big list of events. I'll probably bring it up in a moment. But a workflow contains one or more jobs. So we have a job. When the workflow gets triggered, a job is going to be kicked off and that job can have one or more steps. Those can be like command line steps, like a bash script, they can be actions, they can be shell commands, it can be a number of different things. Now, each of those jobs runs on a runner, and like I said, you youll have one or more jobs, and those jobs by default run in parallel. There's no dependencies between them, they're just jobs. If you have like five or ten jobs, they could all try to kick off right at the same time, unless you have some explicit dependencies between them or you have some needs between them. Now, each job is using to run on its own runner and what a runner is, and I'll talk about those in a little bit more depth. It's like a vm or a docker container, something that is spun up for the purposes of running your job. Now, if it's using a GitHub hosted runner, which I'll show you some documentation about, it comes with a ton of tools. If it's a self hosted runner that you built and are running yourself, it is going to have whatever tools you put on it. So let's get into some demos and just see what we're talking about. This is where all of my demos live out on GitHub. But I wanted to talk a little bit first about those events, those actions. So in GitHub we talked about events that trigger workflows. So like I said, there's a workflow, there's a ton of different events that can trigger stuff like someone tried to push something against main or they created, we can go right here, they created a branch or a tag. You can see what happens if someone forks your code. That can be a trigger. So triggers are not just limited or the events are not just limited to things like pushing or pulling code. It can go far, far beyond that. Like someone created a project or moved a card or made a comment, or you want to do it on a schedule. So you want like a nightly build. This one is one of my favorites. It's super important. Workflow dispatch out of the box. There's no manual way to trigger a workflow. You have to add workflow dispatch if you want to manually trigger a workflow. So let's take a look at one. So I've got up here in my GitHub actions demos, you can come here to actions and you can see I have a whole bunch of different workflows listed here. So if we just start on this first one, I can take a look at it and I can open up what a yaml file looks like. In this particular case, this workflow. So we've got a name and you can see I've got on. So these are those events that are going to trigger the workflow to execute. So on a push, on a pull request, and on workflow dispatch, so I can manually trigger it. And then I'm going to list out my jobs. So I'm going to have one or more jobs. And in this case I'm going to name my job build. So I have a build job that's indented under jobs and I have to tell you what it runs on. It needs an image for this job to run on. So this one is going to run on Ubuntu latest, and I've given it some steps, like it's going to check out the code base and it's going to run an echo command. Well, maybe I want to build net or maybe I want to build Java or go or something. Do I need to go and set up all those tools and configure them? Well, I don't, because like I said, they already exist on a lot of the runners. So if I just do another quick search for hosted runners, again, great documentation comes right to it. And I can see kind of an overview of what I'm getting. I've got my workflow, it's running my jobs. This is giving you an example of different ways to specify if you want to run on Windows or Linux, but out of the box you pretty much get like a two core cpu, seven gigs of ram, some space, and there's lots of different tags for different versions, even Mac if you need to do like a mobile app or a Mac application. But trying to understand what's installed on it, I can look at my runner history and I can see what's on that machine, or I can also come here and see what's available. So on my run, if I look at my build, I can see the log for it and I can expand out setting up the job. I can see the operating system, it was an Ubuntu 20 machine, and I have a runner image and there is a link right here for my build on the included software. Now this has so much stuff, it's kind of funny how many languages and tools and packages are available out of the box like Docker and Kubectl NPM. We have CLI tools for pretty much every cloud out there. So right out of the box, using one of these hosted agents, I can run and do lots of things very quickly across a wide range of languages, and I don't have to do a lot of configuration to do it and I can get status. So let's go back to our actions. We had a basic trigger. I showed that we might need the workflow dispatch event, so if I want to run this, I can say run. And that enables the button here so I can manually run my workflow whenever I want to. So I'm triggering this run and what we'll see momentarily. There we go. It's manually run. It's queued up right now. I can go in and see kind of in real time as it executes, so I can get real time logs, I can get the output of individual commands. Cool. Well what about doing something a little bit more complex? Well I do have a more complex job, but let's say I don't want to build it on every push. I can have filters, I can do limitations, so I only want it to build on the main branch when I push on the main branch, or if I use a pattern of release something so the starstar will do things under that path. Maybe I want to only build it on v two tags or using star. I only want to build it when I touch Javascript files. So I have a lot of different options for controlling when things run. I youll do a schedule like a cron job, so I can specify cron syntax, so I have a lot of different capabilities. And just like with all the others, I can come out here and look at GitHub action workflow syntax and this is going to give me a detailed breakdown of all of the structures of the workflow. Yaml so I can see on pull request on branches, including branches, excluding branches using the not symbol. It's all very well documented out on the GitHub documentation page, so we have a lot of different options to build and record. Let's see what else we got. So what about multiple jobs? And I will go ahead and kick this one off and we'll take a look at it when it triggers. So I mentioned a workflow is made up of one or more jobs. I also said they all kind of run in parallel. So here I've got not one but two, three jobs, and job three finished first, so we had no real idea what was going to go when they all ran in parallel. And what that looks like if we're looking at a job, is I've got job one and it runs on Ubuntu and I've got job two and it runs on Ubuntu. So I have this list of jobs that I've built out, but what if I want some sort of dependency between them? What if I don't want job three to finish before job one? Maybe I want job one to always run first and then I don't care if job two or three finish first. That could be I want to do a build and then I want to run some acceptance tests or some unit tests. We can structure our jobs in any way we want. GitHub actions and the workflows are an amazing toolkit that lets us kind of automate and trigger our workflows as need be to match our working style or our requirements. And the way we can set up these dependencies in our workflows is using the needs keyword. So job two needs job one. So we can establish that dependency very easily. Let's keep going, let's see what else we got. So steps, there are lots of different steps. And I'll go ahead and run this guy and we'll go into what steps look like. So with steps they can be, like I said, commands tasks. Once this job loads we should see all the different steps. So we got all sorts of stuff. So I'm running, if we look at the setup job, I'm running on an Ubuntu machine, so it's Linux and I can just say echo my path. So I'm doing a bash variable environment, variable reference and it's spitting out my path. And try doing CMD. But CMD won't work because CMD is a Windows specific thing, but out of the box. The Linux machines come with Powershell because Powershell is cross platform, but not Powershell like the Windows 5.1 version. I can also specify random shells, like I want to use Python, or I want to do something with Perl. Or I can specify a checkout action. Like I want to use the checkout action. I want it to be locked to a specific hash, a specific shaw like commit. For this checkout action. I can also do different versions, like I want to do version two or three based on tag or based on a branch. I can check out things. So how do I know about this hash, this version? Well, actions besides my simple commands that I want to run. If we scroll all the way to the top of the GitHub site, we'll see the marketplace. So the GitHub marketplace is where we have all of those great actions that are built by the community, by people, by companies to help do things. In this case we had the checkout command. So if I look it up, we've got a blue check mark to show it's creator verified by GitHub. We've got stars. We can see version information about the individual actions. And the latest version we have tags on if it's built and test. Any action in the marketplace needs to be in a public open source repo or a public repo. Anyone pretty much can go and see issues or do pull requests. For now, almost every well written action you'll find has really good documentation around the usage of it. The options that you can pass into it, scenarios like they might give you scenarios on how to leverage it, to do certain things that you're trying to do. So looking for an action in the marketplace is a great thing to look for. And we can go right to the code base so I can click on the action and it's going to take me to the repo that has that action, so I can look at how somebody implemented something or how something was done. I can also go right to the open issues. So if I'm having a problem, I can go look at the issues that are affecting that particular version or that action. And so we have a lot of ways to look for and find capability that people have already implemented and consumed that now, something that you might have noticed and I kind of skipped over, I was passing information into the action. Well, we also can do environment variables, and these all kind of flow together into what are called contexts, and we'll talk about those momentarily. But let's say I want to pass information, I want to have some variables I want to pass something down into an action into my job should trigger it already ran. I can run a script and say I have environment variables that I'm going to call, and you can see I passed in my environment greeting hello name world. So I can pretty quickly, pretty easily pass information from my top section in my workflow. I can pass the name of a service or do some sort of dynamicism like string manipulation, and pass it down into lower level jobs and have those picked up. Now, something else that I think is really important to cover when we're dealing with variables is there is a hierarchy that exists. So when we have workflow level variables, or we have things like job level variables, or even step level variables, they can override each other. Like if you invoke the same environment key like environment name, this is saying hello from location step, and I've passed in information. But if I go and I look at my workflow, you'll see what's happening. I've defined at my workflow level a location workflow. But as I start executing on the specific job, the job overrides the workflow value. As I get down to an individual step, the step overrides the job level value. And so when I'm running my command hello world from step, I'm getting information from my workflow, from my local step in my environment. So understanding how youll can leverage values defined higher up in the hierarchy and how you can override them lower down in the hierarchy is important. Now we're dealing with variables. We're starting to pass stuff. Let's get into conditionals. So conditionals are a fun thing where we can add a job, we can conditionally execute jobs or steps based on values, and those values can be everything from the branch we're on or the environment we're in. So here youll notice run goodbye did not execute, but run hello did, and we can see why that is when we look inside the file. So we can do stuff like if, so if this expression is true, we can run this step, we can do things around jobs like maybe we don't want to do our deployment job if we're not on the branch main, if we're not in an environment like production. So we can have some conditional control of our different workflows, of our different steps. And conditionals are really a form of expression. So let me just show you what some expressions look like where we can do string formatting and we can do string interpolation. We can put values together and we can build up things. So if we want patterns and naming, we can leverage that. We can check if stuff ends with or contains strings. We can do array manipulation, so we can give it like a JSON String and make an object out of a JSON string. And then if it's an array, we can check to see if something contains an entry in the array, not just a string, but the whole array. And then we can do stuff like joining and concatenating, and we can print those out. So those all work very much like you would expect. We get some basic dynamicism and we can play around with string manipulation in some of our workflows, and some of our actions and expressions are essentially a type of context. So I've got a couple of contexts here, and I will show like the GitHub Context gives you a lot of information about the repo and the check ins and the commits and the users. So there's a ton of information there about the repository and the, you have stuff about individual jobs, like if they've run steps. So if we look at our GitHub workflow context, contexts are something that comes up quite a bit when we're building out things. We've got our environment. So if we look at our environment, we can see all of the different values that have been defined. If we're doing a job, we can find out information about the container and the services, if we list it out, so we can define different things. So contexts are going to be an object that we reference as we're doing some more complex interactions with the different workflows. And then secrets are a type of context. So secrets exist in GitHub under settings. So if we come over here to settings in my repository, you'll see down here under security, we've got secrets and we have actions. And you'll notice I have environment secrets which are listed here in this top section. And then down below I have repository secrets, which are for the repository. And just like we had a variable hierarchy, there is a secret hierarchy. Like you can have level secrets and repository level secrets and environment level secrets. So you can have different secrets that can override each other. I can edit this, which will change the value, or I can remove it. And the way you access, and you can see I've got some environment secrets here, I'll show those in a moment. But the way you access your secrets is relatively simple when you're looking at the workflow. So if we're in our workflow and we want to get to a secret, we do the dollar kind of handlebars secrets name. And if this did not exist as a secret, it will just come back as an empty string. It doesn't want to give you an error that that secret doesn't exist. That could kind of give people a way to hunt around to see if you can find the secret. Now, notice I'm passing it two different ways. I a lot of times will pass it by environment variable over passing it in the command line, just in case something is like listing processes on the system. Though I know there's arguments about passing secrets via environment variables. It's a little bit of a debate. Whichever way you want to do it, GitHub actually tries to protect you a little bit and tries to not print out things. It knows our secrets. So if I go into the logging and I look at these two things where I've passed it by context, it knows that was a secret and doesn't print it out. Same with environment, but with the environment, it knows I'm pulling the environment name, it just doesn't print out the value. So this can give you a little bit better information. If you're troubleshooting things, this might give you a little bit less, but there's multiple ways to do that. Now, I mentioned that the secrets come from the environment or from the individual actions. So environments are listed here on the left. So we have under code and automation, environments and environments give you a couple of different things. You can just hit new environment and make a new environment. So I can do something like this, and I've created an environment. Now I can ask for approvals. I mentioned that earlier with the CD part, and I can say I want someone to be a reviewer before you deploy into that environment. I don't have to have secrets or resources, it's just a tag. But if my job says it's using this environment comp 42 environment, it'll ask for approvals. I can also have protection on this environment, so I can say I can only deploy to this environment if it comes from my main branch. My main branch is the only one that's allowed to deploy into comp and maybe have secrets that are specific to this environment. Now, a lot of times what I'll do is I will have multiple environments with the exact same secrets. It makes it much easier for me to reuse my jobs or to copy and paste them. I'm still leveraging all of the same secret values. I'm just referencing them from a different environment. So that's how I can interact with my environments. If I have multiple environments like this, maybe they have multiple secrets. And that's okay, I've got secrets and I access them the exact same way. So when I'm accessing my secrets, it doesn't matter if I'm accessing a secret from my repo, from my, or from my environment. And I know Linux environment, shell environment versus environment, a little bit of reuse and I'll show some end to end examples in just a few moments. I want to talk about matrixes, matrices for a second. So matrices are a really cool feature that is available in workflows where maybe you're a library builder or you're building an application and youll want to be able to test it on multiple versions of software or multiple OSS. Well I can define a matrix of I'm building a python library or a node library and I want to test it on node 1214 and 16. And I need to make sure it works on Windows and Linux. So I can define and you can see I've kicked off a bunch of node builds and a bunch of windows builds, but I can go in and say I'm building node and I have a strategy, it's my matrix. So here are my node versions and here are my windows oss, and I'm leveraging these just like I do secrets and other contexts, Matrix OS and matrix node version. So I can access the same values because it's part of my matrix strategy. This is going to be my matrix context and I can access the thing. I'm only building out the job template essentially one time, but this gets copied three times, two, six times for the full matrix. So I think that's a really cool feature that some people need to leverage. Not everybody does, but it's a good to have. So let's look at some end to end stuff. I've got three workflows left and I just want to walk through a couple of examples. So like net, for instance, I do similar patterns to what I've done in other pipeline CI CD platforms, which is I'm a big fan of separating out my build and my deploy jobs stages. I like to have a build that compiles tests and then gives me an artifact. And I like that because I can get really fast feedback on it and it gives me an artifact that I can redeploy later. I can youll back and forward pretty easily and just redeploy that one task without having to rebuild everything. Because I know I had a good binary. I usually also do infrastructure as code. So you'll notice I upload two artifacts here one is my web app and one is my infrastructure as code. So it's IAC. So I'm uploading both infrastructure's code and my compiled application so I can deploy it out. Now I said environments can have approvals. Well I set up an approval here on this environment. And before I approve it, if we go take a look at the Yaml file, I'm doing some interesting things where I'm only going to run this workflow if I like no paths. So remove all the paths and only add in the. Net sample. So only do it when I touch that folder. And I'm doing some builds, I'm setting up net and giving it the specific version I want. And these are cool actions that you can look at. Anytime you look around on GitHub and you find someone using an interesting action, you can come out here and you can look up the action, you can see use cases and how that interacts. So I poke around a lot of open source projects to see how they're doing things. Cache defines one. It provides you with this type of syntax for compiling nuget packages and. Net packages a little bit more quickly that cache things. But I'm just doing individual commands to restore, build, test, publish and upload artifacts. Now this is where it kind of gets cool on my deploy, it needs build. So my deploy has to wait till the build finishes and I'm using an environment. So this is where I have an environment called net and this is where it's asking me do I have an approval to deploy into my net environment. I'm going to download infrastructure as code and I'm going to log into Azure because I want to deploy an arm template or a bicep file and I deploy out my infrastructure as code. There is an id here which I'll touch on momentarily. And then we get the web app and it deploys to the output of my deploy step. So because I gave this task an id of deploy, I can go steps deploy and I have an arm template here that has outputs so I can do outputs. Web app name I don't know what the name of this web app will be at the time I write it. Like my dev environment might give a different name than my prod environment because it's driven by a secret. So I don't know what to deploy to. So I need to get the dynamic output of this step right here in order to pass it to this step right here to deploying out my code. So let me go ahead and approve this so I can give an approval. I can leave a comment. This will now start kicking off the job and it'll spin up infrastructure, get the dynamic name, pass that into my deploy and deploy software onto that web service that got created. You can see it's pulling down the actions it needs and it's going to do that task. Now this is using the older style of Azure login, using the credentials with a service principle. They've got a newer style that's really nice called Openid Connect. They actually link to it right there in the conversation or in the log. And what's awesome about OpenID connect is you don't have to pass a secret, so you don't need a password like Azure and GitHub trust each other. And I might show this if I have a few minutes, but really look into OpenID connect. It's pretty neat, but yeah, I'm running this deploy. I don't know the web app name, but it's creating my deployment and then when that's done it's going to download the web app and deploy that out there and we can see kind of what that looks like in an older run that I did a little while ago. So if we look at our deploy here, it's getting the deploying and because I had a secret as part of it, it's obscuring it, but it gives you the URL to access the deployment and passed it in and it worked. Well, what about containers? What if I want to deploy a container? Well, containers are very easy to work with. In GitHub actions. We can log into Docker Hub. So if we want to log into Docker hub or ACR for Azure, the Azure container registry, you can use the Docker login action for both of those. If you want to do a build and push or you want to use build to compile a container, I'm giving it a folder of container example and I'm saying I want youll to push this code with these tags so you can have multiple tags on a container. Now I do have another sample out on GitHub that you can look at if you're interested in. That one is kind of more slated directly to containers. Anyways, I will find that in a minute. Could have sworn I had it right on my main page. But that one's another fun one where you can see a couple of steps like I want to do trivia scanning, I want to scan my container, maybe I don't push it when I first build my container and then I scan it and then I push it up. GitHub also provides a superlinter, and a superlinter will pretty much look at all of your code base in pretty much every language you can think of. It's really neat for just trying to standardize types along your code, just some weird stuff. Best practices. It has so many linters, it supports so many languages, and there's some config files you can check in that will help standardize how you want to do it. So if you want to turn on certain ones or turn off other linters, you have full control over how those different linters work. Now you might say, these actions are awesome. How do I get started? Well, you can just go to actions and say new workflow. And what this will do is it'll look in your code base and try to give you an idea. It'll scan your code and try to give you a starting point, like it's a docker image or net app or a grunt app. And you can search for these workflows. If I was doing like, oh, maybe I want to do NPM, well, there's a whole bunch here about oh, I want to do a node JS package and I can hit configure. And what this will do is it'll already set up your repo GitHub workflows. It'll give you this and it'll start you out with a reasonable place to start from. You can also search the marketplace right there. So I said I'm doing Azure. I can click an Azure button and add in an action potentially, so I can find a version and I can copy it and I can paste it over here and then I can delete the stuff I don't need. So it gives me a starting point. Another thing that I think is really interesting is the paths up here. Not everybody. It's interesting that it might have a whole bunch of stuff there. You can name it whatever you want, it'll automatically pick up those actions. Another really cool thing, GitHub has build essentially a training platform based on GitHub actions. So skills. GitHub is a full training thing built on this. Remember I said there were events around what happens if you fork it, or what happens if you comment or push? Well, when you use this template and you make a fork, it starts doing stuff and then it starts prompting you via pull requests, issues, comments. And if you come in here and you're curious how it works, look, they have some scripts and they have workflows. So this is going to unpush to main, it's going to check on a conditional is this GitHub a template, so you can see some cool tricks based on how things are working in the actions. And you can look across GitHub. So if you're looking at like GitHub workflows, you can go see what other projects are doing, doing codeql analysis, checking for spelling. So you can go take a look and see what people are doing. Oh, they're using a c spell action. That's cool. I didn't know I could do that. That's all I had. Please feel free to, like I said, follow some of my repositories and take a look. Feel free to clone some of these repos and play around. That's the best thing you can do, play around with actions, try them out. It works really cool, really well. And there's even a way, if you're really interested, to test some of these locally. You can see here, I'm doing a build, I'm doing a scan. So there's a tool out there called act that I like to recommend to people. If you're playing around, it is super cool. It runs in a container, but essentially this will let you run your GitHub actions locally, like you pretty much say act, and it'll pull in your workflows and it'll run them inside these containers. So you can kind of test things locally while you're trying to get stuff working. I've used this many times and it's a really cool repo, it's a really cool project. And it's something that I think if you do all these a lot, if you make a lot of workflows, that this is something that is a helpful tool. And I did not get into some of the reusable workflows or the templates, but again, search for the documentation and it'll work out. You'll find it. The documentation is incredible, so you'll be able to find that pretty quickly. And so with that, I'd like to thank everybody for joining me today, and happy deploying and playing around with GitHub actions. Thank you.

Chris Ayers

Senior Customer Engineer @ Microsoft

Chris Ayers's LinkedIn account Chris Ayers's twitter account

Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways