├── .github ├── dependabot.yml ├── script │ └── initialize-repository.sh ├── steps │ ├── -step.txt │ ├── 0-welcome.md │ ├── 1-configure-label-based-job.md │ ├── 2-setup-azure-environment.md │ ├── 3-spinup-environment.md │ ├── 4-deploy-to-staging-environment.md │ ├── 5-deploy-to-prod-environment.md │ ├── 6-destroy-azure-environment.md │ └── X-finish.md └── workflows │ ├── 0-welcome.yml │ ├── 1-configure-label-based-job.yml │ ├── 2-setup-azure-environment.yml │ ├── 3-spinup-environment.yml │ ├── 4-deploy-to-staging-environment.yml │ ├── 5-deploy-to-prod-environment.yml │ └── 6-destroy-azure-environment.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── __test__ ├── __snapshots__ │ └── game.test.js.snap └── game.test.js ├── babel.config.js ├── handler.js ├── index.html ├── package-lock.json ├── package.json ├── public └── index.css └── src ├── game.js ├── index.js └── webpack.config.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/script/initialize-repository.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Make sure this file is executable 3 | # chmod a+x .github/script/initialize-repository.sh 4 | 5 | # USAGE: This should only be run once upon initial creation of the 6 | # learner's repository from the template repository. 7 | # Does a dry run by default, --dry-run=false to run live. 8 | 9 | # PURPOSE: This script establishes an initial related history for 10 | # all branches. It merges main into all other branches in this repository 11 | # while auto-resolving conflicts in favor of main. 12 | 13 | # BACKGROUND: This operation is required because when a repository is 14 | # created from a template repository with 'Include all branches', each 15 | # of the branches starts with only one initial commit and no related history. 16 | # 17 | # That state makes it impossible to create pull requests from the 18 | # step-specific branches into main as the learner progresses 19 | # through the course. 20 | 21 | # Setup committer identity 22 | git config user.name github-actions[bot] 23 | git config user.email github-actions[bot]@users.noreply.github.com 24 | 25 | # Fetch all remote branches 26 | git pull --all 27 | 28 | # Create list of all remote branches 29 | branches=$(git branch -r | grep -v main | sed -r 's/origin\///g' | paste -s -d ' ' -) 30 | 31 | # Merge main into each branch 32 | echo -e "Merge main into each branch\n---" 33 | for branch in $branches 34 | do 35 | # Dry run by default 36 | if [[ $1 = '--dry-run=false' ]] 37 | then 38 | git checkout "$branch" 39 | git pull origin main --no-rebase -X theirs --allow-unrelated-histories --no-edit 40 | git push origin "$branch" 41 | echo "---" 42 | else 43 | echo "plan: merge main into $branch" 44 | fi 45 | done 46 | -------------------------------------------------------------------------------- /.github/steps/-step.txt: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /.github/steps/0-welcome.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/steps/1-configure-label-based-job.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Step 1: Trigger a job based on labels 10 | 11 | _Welcome to the course :tada:_ 12 | 13 | ![Screen Shot 2022-06-07 at 4 01 43 PM](https://user-images.githubusercontent.com/6351798/172490466-00f27580-8906-471f-ae83-ef3b6370df7d.png) 14 | 15 | A lot of things go into delivering "continuously". These things can range from culture and behavior to specific automation. In this exercise, we're going to focus on the deployment part of our automation. 16 | 17 | In a GitHub Actions workflow, the `on` step defines what causes the workflow to run. In this case, we want the workflow to run different tasks when specific labels are applied to a pull request. 18 | 19 | We'll use labels as triggers for multiple tasks: 20 | 21 | - When someone applies a "spin up environment" label to a pull request, that'll tell GitHub Actions that we'd like to set up our resources on an Azure environment. 22 | - When someone applies a "stage" label to a pull request, that'll be our indicator that we'd like to deploy our application to a staging environment. 23 | - When someone applies a "destroy environment" label to a pull request, we'll tear down any resources that are running on our Azure account. 24 | 25 | ### :keyboard: Activity 1: Configure `GITHUB_TOKEN` permissions 26 | 27 | At the start of each workflow run, GitHub automatically creates a unique `GITHUB_TOKEN` secret to use in your workflow. We need to make sure this token has the permissions required for this course. 28 | 29 | 1. Open a new browser tab, and work on the steps in your second tab while you read the instructions in this tab. 30 | 1. Go to Settings > Actions > General. Ensure that the `GITHUB_TOKEN` also has **Read and write permissions** enabled under **Workflow permissions**. This is required for your workflow to be able to upload your image to the container registry. 31 | 32 | ### :keyboard: Activity 2: Configure a trigger based on labels 33 | 34 | For now, we'll focus on staging. We'll spin up and destroy our environment in a later step. 35 | 36 | 1. Go to the **Actions** tab. 37 | 1. Click **New workflow** 38 | 1. Search for "simple workflow" and click **Configure** 39 | 1. Name your workflow `deploy-staging.yml` 40 | 1. Edit the contents of this file and remove all triggers and jobs. 41 | 1. Edit the contents of the file to add a conditional that filters the `build` job when there is a label present called **stage**. Your resulting file should look like this: 42 | ```yaml 43 | name: Stage the app 44 | 45 | on: 46 | pull_request: 47 | types: [labeled] 48 | 49 | jobs: 50 | build: 51 | runs-on: ubuntu-latest 52 | 53 | if: contains(github.event.pull_request.labels.*.name, 'stage') 54 | ``` 55 | 1. Click **Start commit**, and choose to make a new branch named `staging-workflow`. 56 | 1. Click **Propose changes**. 57 | 1. Click **Create pull request**. 58 | 1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. 59 | -------------------------------------------------------------------------------- /.github/steps/2-setup-azure-environment.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Step 2: Set up an Azure environment 8 | 9 | _Good job getting started :gear:_ 10 | 11 | ### Nice work triggering a job on specific labels 12 | 13 | We won't be going into detail on the steps of this workflow, but it would be a good idea to become familiar with the actions we're using. They are: 14 | 15 | - [`actions/checkout`](https://github.com/actions/checkout) 16 | - [`actions/upload-artifact`](https://github.com/actions/upload-artifact) 17 | - [`actions/download-artifact`](https://github.com/actions/download-artifact) 18 | - [`docker/login-action`](https://github.com/docker/login-action) 19 | - [`docker/build-push-action`](https://github.com/docker/build-push-action) 20 | - [`azure/login`](https://github.com/Azure/login) 21 | - [`azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) 22 | 23 | ### :keyboard: Activity 1: Store your credentials in GitHub secrets and finish setting up your workflow 24 | 25 | 1. In a new tab, [create an Azure account](https://azure.microsoft.com/en-us/free/) if you don't already have one. If your Azure account is created through work, you may encounter issues accessing the necessary resources -- we recommend creating a new account for personal use and for this course. 26 | > **Note**: You may need a credit card to create an Azure account. If you're a student, you may also be able to take advantage of the [Student Developer Pack](https://education.github.com/pack) for access to Azure. If you'd like to continue with the course without an Azure account, Skills will still respond, but none of the deployments will work. 27 | 1. Create a [new subscription](https://docs.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription) in the Azure Portal. 28 | > **Note**: your subscription must be configured "Pay as you go" which will require you to enter billing information. This course will only use a few minutes from your free plan, but Azure requires the billing information. 29 | 1. Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) on your machine. 30 | 1. In your terminal, run: 31 | ```shell 32 | az login 33 | ``` 34 | 1. Select the subscription you just selected from the interactive authentication prompt. Copy the value of the subscription ID to a safe place. We'll call this `AZURE_SUBSCRIPTION_ID`. Here's an example of what it looks like: 35 | ```shell 36 | No Subscription name Subscription ID Tenant 37 | ----- ------------------- ------------------------------------ ----------------- 38 | [1] * some-subscription f****a09-****-4d1c-98**-f**********c Default Directory 39 | ``` 40 | 1. In your terminal, run the command below. 41 | 42 | ```shell 43 | az ad sp create-for-rbac --name "GitHub-Actions" --role contributor \ 44 | --scopes /subscriptions/{subscription-id} \ 45 | --sdk-auth 46 | 47 | # Replace {subscription-id} with the same id stored in AZURE_SUBSCRIPTION_ID. 48 | ``` 49 | 50 | > **Note**: The `\` character works as a line break on Unix based systems. If you are on a Windows based system the `\` character will cause this command to fail. Place this command on a single line if you are using Windows. 51 | 52 | 1. Copy the entire contents of the command's response, we'll call this `AZURE_CREDENTIALS`. Here's an example of what it looks like: 53 | ```shell 54 | { 55 | "clientId": "", 56 | "clientSecret": "", 57 | "subscriptionId": "", 58 | "tenantId": "", 59 | (...) 60 | } 61 | ``` 62 | 1. Back on GitHub, click on this repository's **Secrets and variables > Actions** in the Settings tab. 63 | 1. Click **New repository secret** 64 | 1. Name your new secret **AZURE_SUBSCRIPTION_ID** and paste the value from the `id:` field in the first command. 65 | 1. Click **Add secret**. 66 | 1. Click **New repository secret** again. 67 | 1. Name the second secret **AZURE_CREDENTIALS** and paste the entire contents from the second terminal command you entered. 68 | 1. Click **Add secret** 69 | 1. Go back to the Pull requests tab and in your pull request go to the **Files Changed** tab. Find and then edit the `.github/workflows/deploy-staging.yml` file to use some new actions. The full workflow file, should look like this: 70 | ```yaml 71 | name: Deploy to staging 72 | 73 | on: 74 | pull_request: 75 | types: [labeled] 76 | 77 | env: 78 | IMAGE_REGISTRY_URL: ghcr.io 79 | ############################################### 80 | ### Replace with GitHub username ### 81 | ############################################### 82 | DOCKER_IMAGE_NAME: -azure-ttt 83 | AZURE_WEBAPP_NAME: -ttt-app 84 | ############################################### 85 | 86 | jobs: 87 | build: 88 | if: contains(github.event.pull_request.labels.*.name, 'stage') 89 | 90 | runs-on: ubuntu-latest 91 | 92 | steps: 93 | - uses: actions/checkout@v4 94 | - uses: actions/setup-node@v4 95 | with: 96 | node-version: 16 97 | - name: npm install and build webpack 98 | run: | 99 | npm install 100 | npm run build 101 | - uses: actions/upload-artifact@v4 102 | with: 103 | name: webpack artifacts 104 | path: public/ 105 | 106 | Build-Docker-Image: 107 | runs-on: ubuntu-latest 108 | needs: build 109 | name: Build image and store in GitHub Container Registry 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@v4 113 | 114 | - name: Download built artifact 115 | uses: actions/download-artifact@v4 116 | with: 117 | name: webpack artifacts 118 | path: public 119 | 120 | - name: Log in to GHCR 121 | uses: docker/login-action@v3 122 | with: 123 | registry: ${{ env.IMAGE_REGISTRY_URL }} 124 | username: ${{ github.actor }} 125 | password: ${{ secrets.CR_PAT }} 126 | 127 | - name: Extract metadata (tags, labels) for Docker 128 | id: meta 129 | uses: docker/metadata-action@v5 130 | with: 131 | images: ${{env.IMAGE_REGISTRY_URL}}/${{ github.repository }}/${{env.DOCKER_IMAGE_NAME}} 132 | tags: | 133 | type=sha,format=long,prefix= 134 | 135 | - name: Build and push Docker image 136 | uses: docker/build-push-action@v5 137 | with: 138 | context: . 139 | push: true 140 | tags: ${{ steps.meta.outputs.tags }} 141 | labels: ${{ steps.meta.outputs.labels }} 142 | 143 | Deploy-to-Azure: 144 | runs-on: ubuntu-latest 145 | needs: Build-Docker-Image 146 | name: Deploy app container to Azure 147 | steps: 148 | - name: "Login via Azure CLI" 149 | uses: azure/login@v2 150 | with: 151 | creds: ${{ secrets.AZURE_CREDENTIALS }} 152 | 153 | - uses: azure/docker-login@v1 154 | with: 155 | login-server: ${{env.IMAGE_REGISTRY_URL}} 156 | username: ${{ github.actor }} 157 | password: ${{ secrets.CR_PAT }} 158 | 159 | - name: Deploy web app container 160 | uses: azure/webapps-deploy@v3 161 | with: 162 | app-name: ${{env.AZURE_WEBAPP_NAME}} 163 | images: ${{env.IMAGE_REGISTRY_URL}}/${{ github.repository }}/${{env.DOCKER_IMAGE_NAME}}:${{ github.sha }} 164 | 165 | - name: Azure logout via Azure CLI 166 | uses: azure/CLI@v2 167 | with: 168 | inlineScript: | 169 | az logout 170 | az cache purge 171 | az account clear 172 | ``` 173 | 1. After you've edited the file, click **Commit changes...** and commit to the `staging-workflow` branch. 174 | 1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. 175 | -------------------------------------------------------------------------------- /.github/steps/3-spinup-environment.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Step 3: Spin up an environment based on labels 8 | 9 | _Nicely done! :heart:_ 10 | 11 | GitHub Actions is cloud agnostic, so any cloud will work. We'll show how to deploy to Azure in this course. 12 | 13 | **What are _Azure resources_?** In Azure, a resource is an entity managed by Azure. We'll use the following Azure resources in this course: 14 | 15 | - A [web app](https://docs.microsoft.com/en-us/azure/app-service/overview) is how we'll be deploying our application to Azure. 16 | - A [resource group](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview#resource-groups) is a collection of resources, like web apps and virtual machines (VMs). 17 | - An [App Service plan](https://docs.microsoft.com/en-us/azure/app-service/overview-hosting-plans) is what runs our web app and manages the billing (our app should run for free). 18 | 19 | Through the power of GitHub Actions, we can create, configure, and destroy these resources through our workflow files. 20 | 21 | ### :keyboard: Activity 1: Set up a personal access token (PAT) 22 | 23 | Personal access tokens (PATs) are an alternative to using passwords for authentication to GitHub. We will use a PAT to allow your web app to pull the container image after your workflow pushes a newly built image to the registry. 24 | 25 | 1. Open a new browser tab, and work on the steps in your second tab while you read the instructions in this tab. 26 | 2. Create a personal access token with the `repo` and `write:packages` scopes. For more information, see ["Creating a personal access token."](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 27 | 3. Once you have generated the token we will need to store it in a secret so that it can be used within a workflow. Create a new repository secret named `CR_PAT` and paste the PAT token in as the value. 28 | 4. With this done we can move on to setting up our workflow. 29 | 30 | **Configuring your Azure environment** 31 | 32 | To deploy successfully to our Azure environment: 33 | 34 | 1. Create a new branch called `azure-configuration` by clicking on the branch dropdown on the top, left hand corner of the `Code` tab on your repository page. 35 | 2. Once you're in the new `azure-configuration` branch, go into the `.github/workflows` directory and create a new file titled `spinup-destroy.yml` by clicking **Add file**. Copy and paste the following into this new file: 36 | ```yaml 37 | name: Configure Azure environment 38 | 39 | on: 40 | pull_request: 41 | types: [labeled] 42 | 43 | env: 44 | IMAGE_REGISTRY_URL: ghcr.io 45 | AZURE_RESOURCE_GROUP: cd-with-actions 46 | AZURE_APP_PLAN: actions-ttt-deployment 47 | AZURE_LOCATION: '"East US"' 48 | ############################################### 49 | ### Replace with GitHub username ### 50 | ############################################### 51 | AZURE_WEBAPP_NAME: -ttt-app 52 | 53 | jobs: 54 | setup-up-azure-resources: 55 | runs-on: ubuntu-latest 56 | if: contains(github.event.pull_request.labels.*.name, 'spin up environment') 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v4 60 | 61 | - name: Azure login 62 | uses: azure/login@v2 63 | with: 64 | creds: ${{ secrets.AZURE_CREDENTIALS }} 65 | 66 | - name: Create Azure resource group 67 | if: success() 68 | run: | 69 | az group create --location ${{env.AZURE_LOCATION}} --name ${{env.AZURE_RESOURCE_GROUP}} --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}} 70 | 71 | - name: Create Azure app service plan 72 | if: success() 73 | run: | 74 | az appservice plan create --resource-group ${{env.AZURE_RESOURCE_GROUP}} --name ${{env.AZURE_APP_PLAN}} --is-linux --sku F1 --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}} 75 | 76 | - name: Create webapp resource 77 | if: success() 78 | run: | 79 | az webapp create --resource-group ${{ env.AZURE_RESOURCE_GROUP }} --plan ${{ env.AZURE_APP_PLAN }} --name ${{ env.AZURE_WEBAPP_NAME }} --deployment-container-image-name nginx --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}} 80 | 81 | - name: Configure webapp to use GHCR 82 | if: success() 83 | run: | 84 | az webapp config container set --docker-custom-image-name nginx --docker-registry-server-password ${{secrets.CR_PAT}} --docker-registry-server-url https://${{env.IMAGE_REGISTRY_URL}} --docker-registry-server-user ${{github.actor}} --name ${{ env.AZURE_WEBAPP_NAME }} --resource-group ${{ env.AZURE_RESOURCE_GROUP }} --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}} 85 | 86 | destroy-azure-resources: 87 | runs-on: ubuntu-latest 88 | 89 | if: contains(github.event.pull_request.labels.*.name, 'destroy environment') 90 | 91 | steps: 92 | - name: Checkout repository 93 | uses: actions/checkout@v4 94 | 95 | - name: Azure login 96 | uses: azure/login@v2 97 | with: 98 | creds: ${{ secrets.AZURE_CREDENTIALS }} 99 | 100 | - name: Destroy Azure environment 101 | if: success() 102 | run: | 103 | az group delete --name ${{env.AZURE_RESOURCE_GROUP}} --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}} --yes 104 | ``` 105 | 1. Click **Commit changes...** and select `Commit directly to the azure-configuration branch.` before clicking **Commit changes**. 106 | 1. Go to the Pull requests tab of the repository. 107 | 1. There should be a yellow banner with the `azure-configuration` branch where you can click **Compare & pull request**. 108 | 1. Set the title of the Pull request to: `Added spinup-destroy.yml workflow` and click `Create pull request`. 109 | 110 | We will cover the key functionality below and then put the workflow to use by applying a label to the pull request. 111 | 112 | This new workflow has two jobs: 113 | 114 | 1. **Set up Azure resources** will run if the pull request contains a label with the name "spin up environment". 115 | 2. **Destroy Azure resources** will run if the pull request contains a label with the name "destroy environment". 116 | 117 | In addition to each job, there's a few global environment variables: 118 | 119 | - `AZURE_RESOURCE_GROUP`, `AZURE_APP_PLAN`, and `AZURE_WEBAPP_NAME` are names for our resource group, app service plan, and web app, respectively, which we'll reference over multiple steps and workflows 120 | - `AZURE_LOCATION` lets us specify the [region](https://azure.microsoft.com/en-us/global-infrastructure/regions/) for the data centers, where our app will ultimately be deployed. 121 | 122 | **Setting up Azure resources** 123 | 124 | The first job sets up the Azure resources as follows: 125 | 126 | 1. Logs into your Azure account with the [`azure/login`](https://github.com/Azure/login) action. The `AZURE_CREDENTIALS` secret you created earlier is used for authentication. 127 | 1. Creates an [Azure resource group](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview#resource-groups) by running [`az group create`](https://docs.microsoft.com/en-us/cli/azure/group?view=azure-cli-latest#az-group-create) on the Azure CLI, which is [pre-installed on the GitHub-hosted runner](https://help.github.com/en/actions/reference/software-installed-on-github-hosted-runners). 128 | 1. Creates an [App Service plan](https://docs.microsoft.com/en-us/azure/app-service/overview-hosting-plans) by running [`az appservice plan create`](https://docs.microsoft.com/en-us/cli/azure/appservice/plan?view=azure-cli-latest#az-appservice-plan-create) on the Azure CLI. 129 | 1. Creates a [web app](https://docs.microsoft.com/en-us/azure/app-service/overview) by running [`az webapp create`](https://docs.microsoft.com/en-us/cli/azure/webapp?view=azure-cli-latest#az-webapp-create) on the Azure CLI. 130 | 1. Configures the newly created web app to use [GitHub Packages](https://help.github.com/en/packages/publishing-and-managing-packages/about-github-packages) by using [`az webapp config`](https://docs.microsoft.com/en-us/cli/azure/webapp/config?view=azure-cli-latest) on the Azure CLI. Azure can be configured to use its own [Azure Container Registry](https://docs.microsoft.com/en-us/azure/container-registry/), [DockerHub](https://docs.docker.com/docker-hub/), or a custom (private) registry. In this case, we'll configure GitHub Packages as a custom registry. 131 | 132 | **Destroying Azure resources** 133 | 134 | The second job destroys Azure resources so that you do not use your free minutes or incur billing. The job works as follows: 135 | 136 | 1. Logs into your Azure account with the [`azure/login`](https://github.com/Azure/login) action. The `AZURE_CREDENTIALS` secret you created earlier is used for authentication. 137 | 1. Deletes the resource group we created earlier using [`az group delete`](https://docs.microsoft.com/en-us/cli/azure/group?view=azure-cli-latest#az-group-delete) on the Azure CLI. 138 | 139 | ### :keyboard: Activity 2: Apply labels to create resources 140 | 141 | 1. Edit the `spinup-destroy.yml` file in your open pull request and replace any `` placeholders with your GitHub username. Commit this change directly to the `azure-configuration` branch. 142 | 1. Back in the Pull request, create and apply the `spin up environment` label to your open pull request 143 | 1. Wait for the GitHub Actions workflow to run and spin up your Azure environment. You can follow along in the Actions tab or in the pull request merge box. 144 | 1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. 145 | -------------------------------------------------------------------------------- /.github/steps/4-deploy-to-staging-environment.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Step 4: Deploy to a staging environment based on labels 8 | 9 | _Nicely done, you used a workflow to spin up your Azure environment :dancer:_ 10 | 11 | Now that the proper configuration and workflow files are present, let's test our actions! In this step, there's a small change to the game. Once you add the appropriate label to your pull request, you should be able to see the deployment! 12 | 13 | 1. Create a new branch named `staging-test` from `main` using the same steps as you did for the previous `azure-configuration` branch. 14 | 1. Edit the `.github/workflows/deploy-staging.yml` file, and replace every `` with your GitHub username. 15 | 1. Commit that change to the new `staging-test` branch. 16 | 1. Go to the Pull requests tab and there should be a yellow banner with the `staging-test` branch to `Compare & pull request`. Once the pull request is opened up, click `Create pull request`. 17 | 18 | ### :keyboard: Activity 1: Add the proper label to your pull request 19 | 20 | 1. Ensure that the `GITHUB_TOKEN` for this repository has read and write permissions under **Workflow permissions**. [Learn more](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token). This is required for your workflow to be able to upload your image to the container registry. 21 | 1. Create and apply the `stage` label to your open pull request 22 | 1. Wait for the GitHub Actions workflow to run and deploy the application to your Azure environment. You can follow along in the Actions tab or in the pull request merge box. The deployment may take a few moments but you've done the right thing. Once the deployment is successful, you'll see green check marks for each run, and you'll see a URL for your deployment. Play the game! 23 | 1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. 24 | -------------------------------------------------------------------------------- /.github/steps/5-deploy-to-prod-environment.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Step 5: Deploy to a production environment based on labels 8 | 9 | _Deployed! :ship:_ 10 | 11 | ### Nicely done 12 | 13 | As we've done before, create a new branch called `production-deployment-workflow` from `main`. In the `.github/workflows` directory, add a new file titled `deploy-prod.yml`. This new workflow deals specifically with commits to `main` and handles deployments to `prod`. 14 | 15 | **Continuous delivery** (CD) is a concept that contains many behaviors and other, more specific concepts. One of those concepts is **test in production**. That can mean different things to different projects and different companies, and isn't a strict rule that says you are or aren't "doing CD". 16 | 17 | In our case, we can match our production environment to be exactly like our staging environment. This minimizes opportunities for surprises once we deploy to production. 18 | 19 | ### :keyboard: Activity 1: Add triggers to production deployment workflow 20 | 21 | Copy and paste the following to your file, and replace any `` placeholders with your GitHub username. Note that not much has changed from our staging workflow, except for our trigger, and that we won't be filtering by labels. 22 | 23 | ```yaml 24 | name: Deploy to production 25 | 26 | on: 27 | push: 28 | branches: 29 | - main 30 | 31 | env: 32 | IMAGE_REGISTRY_URL: ghcr.io 33 | ############################################### 34 | ### Replace with GitHub username ### 35 | ############################################### 36 | DOCKER_IMAGE_NAME: -azure-ttt 37 | AZURE_WEBAPP_NAME: -ttt-app 38 | ############################################### 39 | 40 | jobs: 41 | build: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions/setup-node@v4 47 | with: 48 | node-version: 16 49 | - name: npm install and build webpack 50 | run: | 51 | npm install 52 | npm run build 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: webpack artifacts 56 | path: public/ 57 | 58 | Build-Docker-Image: 59 | runs-on: ubuntu-latest 60 | needs: build 61 | name: Build image and store in GitHub Container Registry 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Download built artifact 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: webpack artifacts 70 | path: public 71 | 72 | - name: Log in to GHCR 73 | uses: docker/login-action@v3 74 | with: 75 | registry: ${{ env.IMAGE_REGISTRY_URL }} 76 | username: ${{ github.actor }} 77 | password: ${{ secrets.CR_PAT }} 78 | 79 | - name: Extract metadata (tags, labels) for Docker 80 | id: meta 81 | uses: docker/metadata-action@v5 82 | with: 83 | images: ${{env.IMAGE_REGISTRY_URL}}/${{ github.repository }}/${{env.DOCKER_IMAGE_NAME}} 84 | tags: | 85 | type=sha,format=long,prefix= 86 | 87 | - name: Build and push Docker image 88 | uses: docker/build-push-action@v5 89 | with: 90 | context: . 91 | push: true 92 | tags: ${{ steps.meta.outputs.tags }} 93 | labels: ${{ steps.meta.outputs.labels }} 94 | 95 | Deploy-to-Azure: 96 | runs-on: ubuntu-latest 97 | needs: Build-Docker-Image 98 | name: Deploy app container to Azure 99 | steps: 100 | - name: "Login via Azure CLI" 101 | uses: azure/login@v2 102 | with: 103 | creds: ${{ secrets.AZURE_CREDENTIALS }} 104 | 105 | - uses: azure/docker-login@v1 106 | with: 107 | login-server: ${{env.IMAGE_REGISTRY_URL}} 108 | username: ${{ github.actor }} 109 | password: ${{ secrets.CR_PAT }} 110 | 111 | - name: Deploy web app container 112 | uses: azure/webapps-deploy@v3 113 | with: 114 | app-name: ${{env.AZURE_WEBAPP_NAME}} 115 | images: ${{env.IMAGE_REGISTRY_URL}}/${{ github.repository }}/${{env.DOCKER_IMAGE_NAME}}:${{github.sha}} 116 | 117 | - name: Azure logout via Azure CLI 118 | uses: azure/CLI@v2 119 | with: 120 | inlineScript: | 121 | az logout 122 | az cache purge 123 | az account clear 124 | ``` 125 | 126 | 1. Update every `` to your GitHub username. 127 | 1. Commit your changes to the `production-deployment-workflow` branch. 128 | 1. Go to the Pull requests tab and click **Compare & pull request** for the `production-deployment-workflow` branch and create a Pull request. 129 | 130 | Great! The syntax you used tells GitHub Actions to only run that workflow when a commit is made to the main branch. Now we can put this workflow into action to deploy to production! 131 | 132 | ### :keyboard: Activity 2: Merge your pull request 133 | 134 | 1. You can now [merge](https://docs.github.com/en/get-started/quickstart/github-glossary#merge) your pull request! 135 | 1. Click **Merge pull request** and leave this tab open as we will be applying a label to the closed pull request in the next step. 136 | 1. Now we just have to wait for the package to be published to GitHub Container Registry and the deployment to occur. 137 | 1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. 138 | -------------------------------------------------------------------------------- /.github/steps/6-destroy-azure-environment.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Step 6: Production deployment 8 | 9 | _Nice work! :sparkle:_ 10 | 11 | Great work, you've done it! You should be able to see your container image in the **Packages** section of your account on the main repository page. You can get the deployment URL in the Actions log, just like the staging URL. 12 | 13 | ### The cloud environment 14 | 15 | Throughout the course you've spun up resources that, if left unattended, could incur billing or consume your free minutes from the cloud provider. Once you have verified your application in production, let's tear down those environments so that you can keep your minutes for more learning! 16 | 17 | ### :keyboard: Activity 1: Destroy any running resources so you don't incur charges 18 | 19 | 1. Create and apply the `destroy environment` label to your merged `production-deployment-workflow` pull request. If you have already closed the tab with your pull request, you can open it again by clicking **Pull requests** and then clicking the **Closed** filter to view merged pull requests. 20 | 21 | Now that you've applied the proper label, let's wait for the GitHub Actions workflow to complete. When it's finished, you can confirm that your environment has been destroyed by visiting your app's URL, or by logging into the Azure portal to see it is not running. 22 | 23 | 2. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. 24 | -------------------------------------------------------------------------------- /.github/steps/X-finish.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Finish 7 | 8 | celebrate 9 | 10 | ### Congratulations, you've completed this course! 11 | 12 | Here's a recap of all the tasks you've accomplished in your repository: 13 | 14 | - Trigger a job based on labels 15 | - Set up the Azure environment 16 | - Spin up environment based on labels 17 | - Deploy to a staging environment based on labels 18 | - Deploy to a production environment based on labels 19 | - Destroy environment based on labels 20 | 21 | ### What's next? 22 | 23 | - [We'd love to hear what you thought of this course](https://github.com/orgs/skills/discussions/categories/deploy-to-azure). 24 | - [Take another GitHub Skills Course](https://github.com/skills). 25 | - [Read the GitHub Getting Started docs](https://docs.github.com/en/get-started). 26 | - To find projects to contribute to, check out [GitHub Explore](https://github.com/explore). 27 | -------------------------------------------------------------------------------- /.github/workflows/0-welcome.yml: -------------------------------------------------------------------------------- 1 | name: Step 0, Welcome 2 | 3 | # This step triggers after the learner creates a new repository from the template 4 | # This workflow updates from step 0 to step 1. 5 | 6 | # When creating a repository from a template, there is variability in the 7 | # order and timing of events that fire and when workflow triggers are registered. 8 | # Given that, these triggers are purposely broad to ensure this workflow is always triggered. 9 | # The conditions within the on_start job are to ensure it is only fully executed once. 10 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 11 | on: 12 | push: 13 | branches: 14 | - main 15 | workflow_dispatch: 16 | 17 | permissions: 18 | # Need `contents: read` to checkout the repository. 19 | # Need `contents: write` to update the step metadata. 20 | contents: write 21 | 22 | jobs: 23 | # Get the current step to only run the main job when the learner is on the same step. 24 | get_current_step: 25 | name: Check current step number 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - id: get_step 31 | run: | 32 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 33 | outputs: 34 | current_step: ${{ steps.get_step.outputs.current_step }} 35 | 36 | on_start: 37 | name: On start 38 | needs: get_current_step 39 | 40 | # We will only run this action when: 41 | # 1. This repository isn't the template repository. 42 | # 2. The step is currently 0. 43 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 44 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 45 | if: >- 46 | ${{ !github.event.repository.is_template 47 | && needs.get_current_step.outputs.current_step == 0 }} 48 | 49 | # We'll run Ubuntu for performance instead of Mac or Windows. 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | # We'll need to check out the repository so that we can edit the README. 54 | - name: Checkout 55 | uses: actions/checkout@v4 56 | with: 57 | fetch-depth: 0 # Let's get all the branches. 58 | 59 | # Update README from step 0 to step 1. 60 | - name: Update to step 1 61 | uses: skills/action-update-step@v2 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | from_step: 0 65 | to_step: 1 66 | 67 | # This is required to establish a related history for branches 68 | # after being created from the template repository. 69 | - name: Initialize repository 70 | run: ./.github/script/initialize-repository.sh --dry-run=false 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/1-configure-label-based-job.yml: -------------------------------------------------------------------------------- 1 | name: Step 1, Trigger a job based on labels 2 | 3 | # This step triggers after 0-start.yml. 4 | # This workflow updates from step 1 to step 2. 5 | 6 | # This will run every time we push to the deploy-staging.yml file on the staging-workflow branch. 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | push: 11 | branches: 12 | - staging-workflow 13 | paths: 14 | - .github/workflows/deploy-staging.yml 15 | 16 | permissions: 17 | # Need `contents: read` to checkout the repository. 18 | # Need `contents: write` to update the step metadata. 19 | contents: write 20 | 21 | jobs: 22 | # Get the current step to only run the main job when the learner is on the same step. 23 | get_current_step: 24 | name: Check current step number 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - id: get_step 30 | run: | 31 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 32 | outputs: 33 | current_step: ${{ steps.get_step.outputs.current_step }} 34 | 35 | on_staging_workflow_update: 36 | name: On staging workflow update 37 | needs: get_current_step 38 | 39 | # We will only run this action when: 40 | # 1. This repository isn't the template repository. 41 | # 2. The step is currently 1. 42 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 1 }} 47 | 48 | # We'll run Ubuntu for performance instead of Mac or Windows. 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | # We'll need to check out the repository so that we can edit the README. 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 0 # Let's get all the branches. 57 | 58 | # Verify the learner added the specific label triggers. 59 | - name: Verify workflow 60 | uses: skills/action-check-file@v1 61 | with: 62 | file: .github/workflows/deploy-staging.yml 63 | search: "github\\.event\\.pull_request\\.labels\\.\\*\\.name.*stage" 64 | 65 | # In README.md, switch step 1 for step 2. 66 | - name: Update to step 2 67 | uses: skills/action-update-step@v2 68 | with: 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | from_step: 1 71 | to_step: 2 72 | branch_name: staging-workflow 73 | -------------------------------------------------------------------------------- /.github/workflows/2-setup-azure-environment.yml: -------------------------------------------------------------------------------- 1 | name: Step 2, Set up the Azure environment 2 | 3 | # This workflow updates from step 2 to step 3. 4 | 5 | # This will run every time we push to the deploy-staging.yml file on the staging-workflow branch 6 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - staging-workflow 12 | paths: 13 | - .github/workflows/deploy-staging.yml 14 | 15 | permissions: 16 | # Need `contents: read` to checkout the repository. 17 | # Need `contents: write` to update the step metadata. 18 | contents: write 19 | pull-requests: write 20 | 21 | jobs: 22 | # Get the current step to only run the main job when the learner is on the same step. 23 | get_current_step: 24 | name: Check current step number 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - id: get_step 30 | run: | 31 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 32 | outputs: 33 | current_step: ${{ steps.get_step.outputs.current_step }} 34 | 35 | on_staging_workflow_update: 36 | name: On staging workflow update 37 | needs: get_current_step 38 | 39 | # We will only run this action when: 40 | # 1. This repository isn't the template repository. 41 | # 2. The step is currently 2. 42 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 2 }} 47 | 48 | # We'll run Ubuntu for performance instead of Mac or Windows. 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | # We'll need to check out the repository so that we can edit the README. 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 0 # Let's get all the branches. 57 | 58 | # Verify the learner configured Build-Docker-Image job. 59 | - name: Verify Build-Docker-Image job 60 | uses: skills/action-check-file@v1 61 | with: 62 | file: .github/workflows/deploy-staging.yml 63 | search: "Build-Docker-Image" 64 | 65 | - name: Verify Build-Docker-Image job 66 | uses: skills/action-check-file@v1 67 | with: 68 | file: .github/workflows/deploy-staging.yml 69 | search: "actions/download-artifact" 70 | 71 | - name: Verify Build-Docker-Image job 72 | uses: skills/action-check-file@v1 73 | with: 74 | file: .github/workflows/deploy-staging.yml 75 | search: "docker/login-action" 76 | 77 | - name: Verify Build-Docker-Image job 78 | uses: skills/action-check-file@v1 79 | with: 80 | file: .github/workflows/deploy-staging.yml 81 | search: "docker/build-push-action" 82 | 83 | # Verify the learner configured the Deploy-to-Azure job. 84 | - name: Verify Deploy-to-Azure job 85 | uses: skills/action-check-file@v1 86 | with: 87 | file: .github/workflows/deploy-staging.yml 88 | search: "Deploy-to-Azure" 89 | 90 | - name: Verify Deploy-to-Azure job 91 | uses: skills/action-check-file@v1 92 | with: 93 | file: .github/workflows/deploy-staging.yml 94 | search: "azure/login" 95 | 96 | - name: Verify Deploy-to-Azure job 97 | uses: skills/action-check-file@v1 98 | with: 99 | file: .github/workflows/deploy-staging.yml 100 | search: "azure/webapps-deploy" 101 | 102 | # Merge the pull open pull request. 103 | - name: Merge Pull Request 104 | run: gh pr merge --squash staging-workflow 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | 108 | # Pull main. 109 | - name: Pull main 110 | run: | 111 | chmod +x ./.github/script/initialize-repository.sh 112 | ./.github/script/initialize-repository.sh 113 | env: 114 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 115 | 116 | # In README.md, switch step 2 for step 3. 117 | - name: Update to step 3 118 | uses: skills/action-update-step@v2 119 | with: 120 | token: ${{ secrets.GITHUB_TOKEN }} 121 | from_step: 2 122 | to_step: 3 123 | -------------------------------------------------------------------------------- /.github/workflows/3-spinup-environment.yml: -------------------------------------------------------------------------------- 1 | name: Step 3, Spin up environment 2 | 3 | # This workflow updates from step 3 to step 4. 4 | 5 | # This will run after the "Configure Azure environment" workflow 6 | # completes on the azure-configuration branch. 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | workflow_run: 11 | workflows: ["Configure Azure environment"] 12 | types: [completed] 13 | branches: [azure-configuration] 14 | 15 | permissions: 16 | # Need `contents: read` to checkout the repository. 17 | # Need `contents: write` to update the step metadata. 18 | contents: write 19 | pull-requests: write 20 | 21 | jobs: 22 | # Get the current step to only run the main job when the learner is on the same step. 23 | get_current_step: 24 | name: Check current step number 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - id: get_step 30 | run: | 31 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 32 | outputs: 33 | current_step: ${{ steps.get_step.outputs.current_step }} 34 | 35 | on_push: 36 | name: On push to azure-configuration 37 | needs: get_current_step 38 | 39 | # We will only run this action when: 40 | # 1. This repository isn't the template repository. 41 | # 2. The step is currently 3. 42 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 3 }} 47 | 48 | # We'll run Ubuntu for performance instead of Mac or Windows. 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | # We'll need to check out the repository so that we can edit the README. 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 0 # Let's get all the branches. 57 | 58 | # Merge the pull open pull request. 59 | - name: Merge Pull Request 60 | run: gh pr merge --squash azure-configuration 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | # Pull main. 65 | - name: Pull main 66 | run: | 67 | chmod +x ./.github/script/initialize-repository.sh 68 | ./.github/script/initialize-repository.sh 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | 72 | # In README.md, switch step 3 for step 4. 73 | - name: Update to step 4 74 | uses: skills/action-update-step@v2 75 | with: 76 | token: ${{ secrets.GITHUB_TOKEN }} 77 | from_step: 3 78 | to_step: 4 79 | branch_name: azure-configuration 80 | -------------------------------------------------------------------------------- /.github/workflows/4-deploy-to-staging-environment.yml: -------------------------------------------------------------------------------- 1 | name: Step 4, Deploy to staging 2 | 3 | # This workflow updates from step 4 to step 5. 4 | 5 | # This will run after the "Deploy to staging" workflow 6 | # completes on the staging-test branch 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | workflow_run: 11 | workflows: ["Deploy to staging"] 12 | types: [completed] 13 | branches: [staging-test] 14 | 15 | permissions: 16 | # Need `contents: read` to checkout the repository. 17 | # Need `contents: write` to update the step metadata. 18 | contents: write 19 | pull-requests: write 20 | 21 | jobs: 22 | # Get the current step to only run the main job when the learner is on the same step. 23 | get_current_step: 24 | name: Check current step number 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - id: get_step 30 | run: | 31 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 32 | outputs: 33 | current_step: ${{ steps.get_step.outputs.current_step }} 34 | 35 | on_azure_environment_created: 36 | name: On Azure environment created 37 | needs: get_current_step 38 | runs-on: ubuntu-latest 39 | 40 | # We will only run this action when: 41 | # 1. This repository isn't the template repository. 42 | # 2. The step is currently 4. 43 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 44 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 45 | if: >- 46 | ${{ !github.event.repository.is_template 47 | && needs.get_current_step.outputs.current_step == 4 }} 48 | 49 | steps: 50 | # We'll need to check out the repository so that we can edit the README. 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | with: 54 | fetch-depth: 0 # Let's get all the branches. 55 | 56 | # Merge the pull open pull request 57 | - name: Merge Pull Request 58 | run: gh pr merge --squash staging-test 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | # Pull main. 63 | - name: Pull main 64 | run: | 65 | chmod +x ./.github/script/initialize-repository.sh 66 | ./.github/script/initialize-repository.sh 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | # In README.md, switch step 4 for step 5. 71 | - name: Update to step 5 72 | uses: skills/action-update-step@v2 73 | with: 74 | token: ${{ secrets.GITHUB_TOKEN }} 75 | from_step: 4 76 | to_step: 5 77 | branch_name: staging-test 78 | -------------------------------------------------------------------------------- /.github/workflows/5-deploy-to-prod-environment.yml: -------------------------------------------------------------------------------- 1 | name: Step 5, Test the deploy to staging workflow 2 | 3 | # This step triggers after a pull requst is merged to `main`. 4 | 5 | # This will run after the "Deploy to production"" workflow 6 | # completes on the production-deployment-workflow branch 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | workflow_run: 11 | workflows: ["Deploy to production"] 12 | types: [completed] 13 | branches: [main] 14 | 15 | permissions: 16 | # Need `contents: read` to checkout the repository. 17 | # Need `contents: write` to update the step metadata. 18 | contents: write 19 | 20 | jobs: 21 | # Get the current step to only run the main job when the learner is on the same step. 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: ${{ steps.get_step.outputs.current_step }} 33 | 34 | on_deploy_to_prod: 35 | name: On deploy to production 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository. 40 | # 2. The step is currently 5. 41 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 42 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 43 | if: >- 44 | ${{ !github.event.repository.is_template 45 | && needs.get_current_step.outputs.current_step == 5 }} 46 | 47 | # We'll run Ubuntu for performance instead of Mac or Windows. 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | # We'll need to check out the repository so that we can edit the README. 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | fetch-depth: 0 # Let's get all the branches. 56 | 57 | # In README.md, switch step 5 for step 6. 58 | - name: Update to step 6 59 | uses: skills/action-update-step@v2 60 | with: 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | from_step: 5 63 | to_step: 6 64 | -------------------------------------------------------------------------------- /.github/workflows/6-destroy-azure-environment.yml: -------------------------------------------------------------------------------- 1 | name: Step 6, Production deployment cleanup 2 | 3 | # This workflow updates from step 6 to step X. 4 | 5 | # This will run after the "Configure Azure environment" workflow 6 | # completes on the production-deployment-workflow branch 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | workflow_run: 11 | workflows: ["Configure Azure environment"] 12 | types: [completed] 13 | branches: [production-deployment-workflow] 14 | 15 | permissions: 16 | # Need `contents: read` to checkout the repository. 17 | # Need `contents: write` to update the step metadata. 18 | contents: write 19 | 20 | jobs: 21 | # Get the current step to only run the main job when the learner is on the same step. 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: ${{ steps.get_step.outputs.current_step }} 33 | 34 | on_destroy_completed: 35 | name: On destroy completed 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository. 40 | # 2. The step is currently 6. 41 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 42 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 43 | if: >- 44 | ${{ !github.event.repository.is_template 45 | && needs.get_current_step.outputs.current_step == 6 }} 46 | 47 | # We'll run Ubuntu for performance instead of Mac or Windows. 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | # We'll need to check out the repository so that we can edit the README. 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | fetch-depth: 0 # Let's get all the branches. 56 | 57 | # In README.md, switch step 6 for step X. 58 | - name: Update to step X 59 | uses: skills/action-update-step@v2 60 | with: 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | from_step: 6 63 | to_step: X 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | # Build # 40 | ######### 41 | node_modules/ 42 | public/*.js 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17 2 | COPY . /usr/share/nginx/html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) GitHub, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 9 | 10 | # Deploy to Azure 11 | 12 | _Create two deployment workflows using GitHub Actions and Microsoft Azure._ 13 | 14 |
15 | 16 | 21 | 22 | ## Welcome 23 | 24 | Create two deployment workflows using GitHub Actions and Microsoft Azure. 25 | 26 | - **Who is this for**: Developers, DevOps Engineers, new GitHub users, students, and teams. 27 | - **What you'll learn**: We'll learn how to create a workflow that enables Continuous Delivery using GitHub Actions and Microsoft Azure. 28 | - **What you'll build**: We will create two deployment workflows - the first workflow to deploy to staging based on a label and the second workflow to deploy to production based on merging to main. 29 | - **Prerequisites**: Before you start, you should be familiar with GitHub, GitHub Actions, and Continuous Integration with GitHub Actions. 30 | - **How long**: This course takes less than 2 hours to complete. 31 | 32 | In this course, you will: 33 | 34 | 1. Configure a job 35 | 2. Set up an Azure environment 36 | 3. Spin up the environment 37 | 4. Deploy to staging 38 | 5. Deploy to production 39 | 6. Destroy the environment 40 | 41 | ### How to start this course 42 | 43 | 53 | 54 | [![start-course](https://user-images.githubusercontent.com/1221423/235727646-4a590299-ffe5-480d-8cd5-8194ea184546.svg)](https://github.com/new?template_owner=skills&template_name=deploy-to-azure&owner=%40me&name=skills-deploy-to-azure&description=My+clone+repository&visibility=public) 55 | 56 | 1. Right-click **Start course** and open the link in a new tab. 57 | 2. In the new tab, most of the prompts will automatically fill in for you. 58 | - For owner, choose your personal account or an organization to host the repository. 59 | - We recommend creating a public repository, as private repositories will [use Actions minutes](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions). 60 | - Scroll down and click the **Create repository** button at the bottom of the form. 61 | 3. After your new repository is created, wait about 20 seconds, then refresh the page. Follow the step-by-step instructions in the new repository's README. 62 | 63 |
64 | 65 | 69 | 70 | --- 71 | 72 | Get help: [Post in our discussion board](https://github.com/orgs/skills/discussions/categories/deploy-to-azure) • [Review the GitHub status page](https://www.githubstatus.com/) 73 | 74 | © 2023 GitHub • [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md) • [MIT License](https://gh.io/mit) 75 | 76 |
77 | -------------------------------------------------------------------------------- /__test__/__snapshots__/game.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App Contains the compiled JavaScript 1`] = `"!function(e){var t={};function n(r){if(t[r])return t[r].exports;var l=t[r]={i:r,l:!1,exports:{}};return e[r].call(l.exports,l,l.exports,n),l.l=!0,l.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){\\"undefined\\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&\\"object\\"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,\\"default\\",{enumerable:!0,value:e}),2&t&&\\"string\\"!=typeof e)for(var l in e)n.d(r,l,function(t){return e[t]}.bind(null,l));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,\\"a\\",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=\\"\\",n(n.s=1)}([function(e,t,n){\\"use strict\\";n.r(t),n.d(t,\\"default\\",(function(){return r}));class r{constructor(e,t){this.p1=e,this.p2=t,this.board=[[null,null,null],[null,null,null],[null,null,null]],this.player=Math.random()<.5?this.p1:this.p2,this.sym=\\"X\\"}turn(e,t){t=t||e,this.board[e][t]=this.sym}nextPlayer(){this.player=this.player===this.p1?this.p2:this.p1,this.sym=\\"X\\"===this.sym?\\"O\\":\\"X\\"}hasWinner(){return this.rowWin()||this.colWin()||this.diagWin()}rowWin(){let e=!1;for(let t=0;t<3;t++){const n=this.board[t];null!==n[0]&&(e=e||n[0]===n[1]&&n[0]===n[2])}return e}colWin(){let e=!1;for(let t=0;t<3;t++){const n=this.board;null!==n[0][t]&&(e=e||n[0][t]===n[1][t]&&n[0][t]===n[2][t])}return e}diagWin(){const e=this.board;return null!==e[0][0]&&e[0][0]===e[1][1]&&e[0][0]===e[2][2]||null!==e[0][2]&&e[0][2]===e[1][1]&&e[0][2]===e[2][0]}}},function(e,t,n){n(2),e.exports=n(0)},function(e,t,n){\\"use strict\\";n.r(t);var r=n(0);let l,o;for(;!l;)l=window.prompt(\\"Who is player 1?\\");for(;!o&&l!==o;)o=window.prompt(l===o?\`Please enter a different name than \${l}.\`:\\"Who is player 2?\\");window.onload=()=>{document.getElementById(\\"p1Name\\").innerText=l,document.getElementById(\\"p2Name\\").innerText=o;let e=0,t=0;!function n(l,o){document.getElementById(\\"win\\").style.display=\\"none\\",document.getElementById(\\"turn\\").style.display=\\"inline\\",document.getElementById(\\"p1Score\\").innerText=e,document.getElementById(\\"p2Score\\").innerText=t;const i=new r.default(l,o),u=document.getElementById(\\"player\\");u.innerText=i.player,document.querySelectorAll(\\"#tictactoe td\\").forEach(r=>{r.innerText=\\"\\",r.onclick=c=>{r.onclick=void 0,c.target.innerText=i.sym,c.target.onclick=void 0;const[d,s]=c.target.classList;i.turn(d,s),i.hasWinner()?(document.getElementById(\\"winner\\").innerText=i.player,document.getElementById(\\"win\\").style.display=\\"inline\\",document.getElementById(\\"turn\\").style.display=\\"none\\",i.player===l?document.getElementById(\\"p1Score\\").innerText=++e:document.getElementById(\\"p2Score\\").innerText=++t,document.getElementById(\\"newGame\\").style.display=\\"inline\\",document.getElementById(\\"newGame\\").onclick=()=>n(l,o),document.querySelectorAll(\\"td\\").forEach(e=>{e.onclick=void 0})):(i.nextPlayer(),u.innerText=i.player)}})}(l,o)}}]);"`; 4 | -------------------------------------------------------------------------------- /__test__/game.test.js: -------------------------------------------------------------------------------- 1 | const Game = require("../src/game").default; 2 | const fs = require("fs"); 3 | 4 | describe("App", () => { 5 | it("Contains the compiled JavaScript", async (done) => { 6 | fs.readFile("./public/main.js", "utf8", (err, data) => { 7 | expect(err).toBe(null); 8 | expect(data).toMatchSnapshot(); 9 | done(); 10 | }); 11 | }); 12 | }); 13 | 14 | describe("Game", () => { 15 | let game, p1, p2; 16 | beforeEach(() => { 17 | p1 = "Salem"; 18 | p2 = "Nate"; 19 | game = new Game(p1, p2); 20 | }); 21 | 22 | describe("Game", () => { 23 | it("Initializes with two players", async () => { 24 | expect(game.p1).toBe("Salem"); 25 | expect(game.p2).toBe("Nate"); 26 | }); 27 | 28 | it("Initializes with an empty board", async () => { 29 | for (let r = 0; r < game.board.length; r++) { 30 | for (let c = 0; c < game.board[r].lenght; c++) { 31 | expect(game.board[r][c]).toBeUndefined(); 32 | } 33 | } 34 | }); 35 | 36 | it("Starts the game with a random player", async () => { 37 | Math.random = () => 0.4; 38 | expect(new Game(p1, p2).player).toBe("Salem"); 39 | 40 | Math.random = () => 0.6; 41 | expect(new Game(p1, p2).player).toBe("Nate"); 42 | }); 43 | }); 44 | 45 | describe("turn", () => { 46 | it("Inserts an 'X' into the top center", async () => { 47 | game.turn(0, 1); 48 | expect(game.board[0][1]).toBe("X"); 49 | }); 50 | 51 | it("Inserts an 'X' into the top left", async () => { 52 | game.turn(0); 53 | expect(game.board[0][0]).toBe("X"); 54 | }); 55 | }); 56 | 57 | describe("nextPlayer", () => { 58 | it("Sets the current player to be whoever it is not", async () => { 59 | Math.random = () => 0.4; 60 | const game = new Game(p1, p2); 61 | expect(game.player).toBe("Salem"); 62 | game.nextPlayer(); 63 | expect(game.player).toBe("Nate"); 64 | }); 65 | }); 66 | 67 | describe("hasWinner", () => { 68 | it("Wins if any row is filled", async () => { 69 | for (let r = 0; r < game.board.length; r++) { 70 | for (let c = 0; c < game.board[r].length; c++) { 71 | game.board[r][c] = "X"; 72 | } 73 | expect(game.hasWinner()).toBe(true); 74 | 75 | for (let c = 0; c < game.board[r].length; c++) { 76 | game.board[r][c] = null; 77 | } 78 | } 79 | }); 80 | 81 | it("Wins if any column is filled", async () => { 82 | for (let r = 0; r < game.board.length; r++) { 83 | for (let c = 0; c < game.board[r].length; c++) { 84 | game.board[c][r] = "X"; 85 | } 86 | expect(game.hasWinner()).toBe(true); 87 | 88 | for (let c = 0; c < game.board[r].length; c++) { 89 | game.board[c][r] = null; 90 | } 91 | } 92 | }); 93 | 94 | it("Wins if down-left diagonal is filled", async () => { 95 | for (let r = 0; r < game.board.length; r++) { 96 | game.board[r][r] = "X"; 97 | } 98 | expect(game.hasWinner()).toBe(true); 99 | }); 100 | 101 | it("Wins if up-right diagonal is filled", async () => { 102 | for (let r = 0; r < game.board.length; r++) { 103 | game.board[2 - r][r] = "X"; 104 | } 105 | expect(game.hasWinner()).toBe(true); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const files = { 5 | "/public/index.css": { 6 | content: fs.readFileSync( 7 | path.join(__dirname, "public", "index.css"), 8 | "utf8" 9 | ), 10 | type: "text/css", 11 | }, 12 | "/public/main.js": { 13 | content: fs.readFileSync(path.join(__dirname, "public", "main.js"), "utf8"), 14 | type: "text/javascript", 15 | }, 16 | "/": { 17 | content: fs.readFileSync(path.join(__dirname, "index.html"), "utf8"), 18 | type: "text/html", 19 | }, 20 | }; 21 | 22 | /** 23 | * 24 | * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format 25 | * @param {Object} event - API Gateway Lambda Proxy Input Format 26 | * 27 | * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 28 | * @param {Object} context 29 | * 30 | * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html 31 | * @returns {Object} object - API Gateway Lambda Proxy Output Format 32 | * 33 | */ 34 | exports.lambdaHandler = async (event, context) => { 35 | // This will either be /, /public/index.css, or /public/main.js 36 | const requestPath = event.path; 37 | const { content, type } = files[requestPath]; 38 | 39 | return { 40 | headers: { "content-type": type }, 41 | statusCode: 200, 42 | body: content, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TicTacToe 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

It's 's turn!

16 |

17 | wins! 18 | 19 |

20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |

Score

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
00
55 |
56 |
57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-template", 3 | "version": "1.0.0", 4 | "description": "Learn how to use GitHub Actions!", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "__test__", 8 | "src": "src" 9 | }, 10 | "scripts": { 11 | "build": "npx webpack --config ./src/webpack.config.js --mode production", 12 | "dev": "npx webpack --config ./src/webpack.config.js --mode development --watch", 13 | "test": "npx standard && npx jest" 14 | }, 15 | "author": "githubtraining", 16 | "license": "MIT", 17 | "standard": { 18 | "env": [ 19 | "jest", 20 | "browser" 21 | ] 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.2.2", 25 | "@babel/preset-env": "^7.2.3", 26 | "@babel/register": "^7.9.0", 27 | "babel-jest": "^29.7.0", 28 | "babel-loader": "^8.0.5", 29 | "jest": "^27.5.1", 30 | "standard": "^14.3.1", 31 | "webpack": "^5.92.0", 32 | "webpack-cli": "^5.1.4", 33 | "webpack-dev-server": "^5.0.4" 34 | }, 35 | "dependencies": { 36 | "kind-of": "^6.0.3", 37 | "node-forge": "^1.3.0" 38 | }, 39 | "jest": { 40 | "testRunner": "jest-jasmine2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | #game { 2 | display: flex; 3 | } 4 | 5 | #tictactoe { 6 | border-collapse: collapse; 7 | } 8 | 9 | #tictactoe td { 10 | min-height: 100px; 11 | min-width: 100px; 12 | height: 100px; 13 | width: 100px; 14 | text-align: center; 15 | 16 | font-size: 4em; 17 | border: thin solid black; 18 | } 19 | 20 | #tictactoe td:hover { 21 | background: #cccccc; 22 | } 23 | 24 | #tictactoe tr { 25 | width: 300px; 26 | height: 100px; 27 | } 28 | 29 | #score { 30 | margin-left: 5%; 31 | } 32 | 33 | #score table { 34 | border-collapse: collapse; 35 | } 36 | 37 | #score tr > * { 38 | padding: 5px; 39 | text-align: center; 40 | } 41 | 42 | #score th { 43 | border-bottom: thin solid black; 44 | } 45 | 46 | #score h2 { 47 | margin-top: 0; 48 | } 49 | 50 | #app { 51 | margin: auto; 52 | width: 60%; 53 | } 54 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | export default class Game { 2 | constructor(p1, p2) { 3 | this.p1 = p1; 4 | this.p2 = p2; 5 | this.board = [ 6 | [null, null, null], 7 | [null, null, null], 8 | [null, null, null], 9 | ]; 10 | this.player = Math.random() < 0.5 ? this.p1 : this.p2; 11 | this.sym = "X"; 12 | } 13 | 14 | turn(row, col) { 15 | col = col || row; 16 | this.board[row][col] = this.sym; 17 | } 18 | 19 | nextPlayer() { 20 | this.player = this.player === this.p1 ? this.p2 : this.p1; 21 | this.sym = this.sym === "X" ? "O" : "X"; 22 | } 23 | 24 | hasWinner() { 25 | return this.rowWin() || this.colWin() || this.diagWin(); 26 | } 27 | 28 | rowWin() { 29 | let win = false; 30 | for (let r = 0; r < 3; r++) { 31 | const row = this.board[r]; 32 | if (row[0] === null) { 33 | continue; 34 | } 35 | win = win || (row[0] === row[1] && row[0] === row[2]); 36 | } 37 | 38 | return win; 39 | } 40 | 41 | colWin() { 42 | let win = false; 43 | for (let c = 0; c < 3; c++) { 44 | const col = this.board; 45 | if (col[0][c] === null) { 46 | continue; 47 | } 48 | win = win || (col[0][c] === col[1][c] && col[0][c] === col[2][c]); 49 | } 50 | 51 | return win; 52 | } 53 | 54 | diagWin() { 55 | const b = this.board; 56 | return ( 57 | (b[0][0] !== null && b[0][0] === b[1][1] && b[0][0] === b[2][2]) || 58 | (b[0][2] !== null && b[0][2] === b[1][1] && b[0][2] === b[2][0]) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Game from "./game.js"; 2 | 3 | let p1, p2; 4 | while (!p1) { 5 | p1 = window.prompt("Who is player 1?"); 6 | } 7 | 8 | while (!p2 && p1 !== p2) { 9 | p2 = window.prompt( 10 | p1 === p2 ? `Please enter a different name than ${p1}.` : "Who is player 2?" 11 | ); 12 | } 13 | 14 | window.onload = () => { 15 | document.getElementById("p1Name").innerText = p1; 16 | document.getElementById("p2Name").innerText = p2; 17 | let score1 = 0; 18 | let score2 = 0; 19 | 20 | (function playGame(p1, p2) { 21 | document.getElementById("win").style.display = "none"; 22 | document.getElementById("turn").style.display = "inline"; 23 | document.getElementById("p1Score").innerText = score1; 24 | document.getElementById("p2Score").innerText = score2; 25 | 26 | const game = new Game(p1, p2); 27 | const player = document.getElementById("player"); 28 | player.innerText = game.player; 29 | 30 | document.querySelectorAll("#tictactoe td").forEach((el) => { 31 | el.innerText = ""; 32 | el.onclick = (evt) => { 33 | el.onclick = undefined; 34 | evt.target.innerText = game.sym; 35 | evt.target.onclick = undefined; 36 | 37 | const [row, col] = evt.target.classList; 38 | game.turn(row, col); 39 | 40 | if (game.hasWinner()) { 41 | document.getElementById("winner").innerText = game.player; 42 | document.getElementById("win").style.display = "inline"; 43 | document.getElementById("turn").style.display = "none"; 44 | 45 | if (game.player === p1) { 46 | document.getElementById("p1Score").innerText = ++score1; 47 | } else { 48 | document.getElementById("p2Score").innerText = ++score2; 49 | } 50 | 51 | document.getElementById("newGame").style.display = "inline"; 52 | document.getElementById("newGame").onclick = () => playGame(p1, p2); 53 | 54 | document.querySelectorAll("td").forEach((el) => { 55 | el.onclick = undefined; 56 | }); 57 | } else { 58 | game.nextPlayer(); 59 | player.innerText = game.player; 60 | } 61 | }; 62 | }); 63 | })(p1, p2); 64 | }; 65 | -------------------------------------------------------------------------------- /src/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: { 5 | "main.js": [ 6 | path.resolve(__dirname, "index.js"), 7 | path.resolve(__dirname, "game.js"), 8 | ], 9 | }, 10 | output: { 11 | filename: "main.js", 12 | path: path.resolve(__dirname, "../public"), 13 | }, 14 | }; 15 | --------------------------------------------------------------------------------