├── .DS_Store ├── .dockerignore ├── .github └── workflows │ └── cd-workflow.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── __test__ └── game.test.js ├── babel.config.js ├── index.html ├── package.json ├── public ├── .DS_Store └── index.css └── src ├── game.js ├── index.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githubtraining/github-actions-for-packages-demo/bb737d4f0903432b79b54df24814a6f6259b07b1/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __test__ 2 | node_modules 3 | .dockerignore 4 | .gitignore 5 | .github 6 | babel.config.js 7 | Dockerfile 8 | LICENSE 9 | package-lock.json 10 | package.json 11 | README.md -------------------------------------------------------------------------------- /.github/workflows/cd-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Docker CD 2 | 3 | on: 4 | push: 5 | # branches-ignore: 6 | # - "ci-workflow" 7 | # - "docker-workflow" 8 | paths: 9 | - "**Dockerfile**" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: npm install and build webpack 17 | run: | 18 | npm install 19 | npm run build 20 | - uses: actions/upload-artifact@master 21 | with: 22 | name: webpack artifacts 23 | path: public/ 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | needs: build 28 | strategy: 29 | matrix: 30 | os: [ubuntu-lastest, windows-2016] 31 | node-version: [8.x, 10.x] 32 | 33 | steps: 34 | - uses: actions/checkout@v1 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - uses: actions/download-artifact@master 40 | with: 41 | name: webpack artifacts 42 | path: public 43 | - name: npm install, and test 44 | run: | 45 | npm install 46 | npm test 47 | env: 48 | CI: true 49 | 50 | Build-and-Push-Docker-Image: 51 | runs-on: ubuntu-latest 52 | needs: test 53 | name: Docker Build, Tag, Push 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v1 58 | - name: Download built artifact 59 | uses: actions/download-artifact@master 60 | with: 61 | name: webpack artifacts 62 | path: public 63 | - name: Build, Tag, Push 64 | uses: mattdavis0351/actions/docker-gpr@v1 65 | with: 66 | repo-token: ${{ secrets.GITHUB_TOKEN }} 67 | image-name: tic-tac-toe-for-packages # this image-name is changed for the demo 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | public/*.js 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile contents 2 | FROM nginx:1.17 3 | COPY . /usr/share/nginx/html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 The GitHub Training Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tic Tac Toe Game 2 | 3 | Let's learn about CD while using GitHub Actions and the GitHub Package Registry! 4 | 5 | 6 | Head over to your first [Pull Request](../../pull/1) to get started -------------------------------------------------------------------------------- /__test__/game.test.js: -------------------------------------------------------------------------------- 1 | const Game = require('../src/game').default 2 | 3 | describe('Game', () => { 4 | let game, p1, p2 5 | beforeEach(() => { 6 | p1 = 'Salem' 7 | p2 = 'Nate' 8 | game = new Game(p1, p2) 9 | }) 10 | 11 | describe('Game', () => { 12 | it('Initializes with two players', async () => { 13 | expect(game.p1).toBe('Salem') 14 | expect(game.p2).toBe('Nate') 15 | }) 16 | 17 | it('Initializes with an empty board', async () => { 18 | for (let r = 0; r < game.board.length; r++) { 19 | for (let c = 0; c < game.board[r].lenght; c++) { 20 | expect(game.board[r][c]).toBeUndefined() 21 | } 22 | } 23 | }) 24 | 25 | it('Starts the game with a random player', async () => { 26 | Math.random = () => 0.4 27 | expect(new Game(p1, p2).player).toBe('Salem') 28 | 29 | Math.random = () => 0.6 30 | expect(new Game(p1, p2).player).toBe('Nate') 31 | }) 32 | }) 33 | 34 | describe('turn', () => { 35 | it("Inserts an 'X' into the top center", async () => { 36 | game.turn(0, 1) 37 | expect(game.board[0][1]).toBe('X') 38 | }) 39 | 40 | it("Inserts an 'X' into the top left", async () => { 41 | game.turn(0) 42 | expect(game.board[0][0]).toBe('X') 43 | }) 44 | }) 45 | 46 | describe('nextPlayer', () => { 47 | it('Sets the current player to be whoever it is not', async () => { 48 | Math.random = () => 0.4 49 | const game = new Game(p1, p2) 50 | expect(game.player).toBe('Salem') 51 | game.nextPlayer() 52 | expect(game.player).toBe('Nate') 53 | }) 54 | }) 55 | 56 | describe('hasWinner', () => { 57 | it('Wins if any row is filled', async () => { 58 | for (let r = 0; r < game.board.length; r++) { 59 | for (let c = 0; c < game.board[r].length; c++) { 60 | game.board[r][c] = 'X' 61 | } 62 | expect(game.hasWinner()).toBe(true) 63 | 64 | for (let c = 0; c < game.board[r].length; c++) { 65 | game.board[r][c] = null 66 | } 67 | } 68 | }) 69 | 70 | it('Wins if any column is filled', async () => { 71 | for (let r = 0; r < game.board.length; r++) { 72 | for (let c = 0; c < game.board[r].length; c++) { 73 | game.board[c][r] = 'X' 74 | } 75 | expect(game.hasWinner()).toBe(true) 76 | 77 | for (let c = 0; c < game.board[r].length; c++) { 78 | game.board[c][r] = null 79 | } 80 | } 81 | }) 82 | 83 | it('Wins if down-left diagonal is filled', async () => { 84 | for (let r = 0; r < game.board.length; r++) { 85 | game.board[r][r] = 'X' 86 | } 87 | expect(game.hasWinner()).toBe(true) 88 | }) 89 | 90 | it('Wins if up-right diagonal is filled', async () => { 91 | for (let r = 0; r < game.board.length; r++) { 92 | game.board[2 - r][r] = 'X' 93 | } 94 | expect(game.hasWinner()).toBe(true) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.0.0", 27 | "babel-jest": "^24.9.0", 28 | "babel-loader": "^8.0.5", 29 | "html-webpack-plugin": "^3.2.0", 30 | "script-ext-html-webpack-plugin": "^2.1.3", 31 | "standard": "^14.3.1", 32 | "webpack": "^4.28.4", 33 | "webpack-cli": "^3.2.1", 34 | "webpack-dev-server": "^3.1.14" 35 | }, 36 | "dependencies": {} 37 | } 38 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githubtraining/github-actions-for-packages-demo/bb737d4f0903432b79b54df24814a6f6259b07b1/public/.DS_Store -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | #game { display: flex; } 2 | 3 | #tictactoe { 4 | border-collapse: collapse; 5 | } 6 | 7 | #tictactoe td { 8 | min-height: 100px; 9 | min-width: 100px; 10 | height: 100px; 11 | width: 100px; 12 | text-align: center; 13 | 14 | font-size: 4em; 15 | border: thin solid black; 16 | } 17 | 18 | #tictactoe td:hover { 19 | background: #CCCCCC; 20 | } 21 | 22 | #tictactoe tr { 23 | width: 300px; 24 | height: 100px; 25 | } 26 | 27 | #score { 28 | margin-left: 5%; 29 | } 30 | 31 | #score table { 32 | border-collapse: collapse; 33 | } 34 | 35 | #score tr > * { 36 | padding: 5px; 37 | text-align: center; 38 | } 39 | 40 | #score th { 41 | border-bottom: thin solid black; 42 | } 43 | 44 | #score h2 { margin-top: 0; } 45 | 46 | 47 | #app { 48 | margin: auto; 49 | width: 60%; 50 | } 51 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | export default class Game { 2 | constructor (p1, p2) { 3 | this.p1 = p1 4 | this.p2 = p2 5 | this.board = [[null, null, null], [null, null, null], [null, null, null]] 6 | this.player = Math.random() < 0.5 ? this.p1 : this.p2 7 | this.sym = 'X' 8 | } 9 | 10 | turn (row, col) { 11 | col = col || row 12 | this.board[row][col] = this.sym 13 | } 14 | 15 | nextPlayer () { 16 | this.player = this.player === this.p1 ? this.p2 : this.p1 17 | this.sym = this.sym === 'X' ? 'O' : 'X' 18 | } 19 | 20 | hasWinner () { 21 | return this.rowWin() || this.colWin() || this.diagWin() 22 | } 23 | 24 | rowWin () { 25 | let win = false 26 | for (let r = 0; r < 3; r++) { 27 | const row = this.board[r] 28 | if (row[0] === null) { continue } 29 | win = win || (row[0] === row[1] && row[0] === row[2]) 30 | } 31 | 32 | return win 33 | } 34 | 35 | colWin () { 36 | let win = false 37 | for (let c = 0; c < 3; c++) { 38 | const col = this.board 39 | if (col[0][c] === null) { continue } 40 | win = win || (col[0][c] === col[1][c] && col[0][c] === col[2][c]) 41 | } 42 | 43 | return win 44 | } 45 | 46 | diagWin () { 47 | const b = this.board 48 | return ((b[0][0] !== null && b[0][0] === b[1][1] && b[0][0] === b[2][2]) || 49 | (b[0][2] !== null && b[0][2] === b[1][1] && b[0][2] === b[2][0])) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Game from './game.js' 2 | 3 | let p1, p2 4 | while (!p1) { 5 | p1 = window.prompt('Enter player 1 name:') 6 | } 7 | 8 | while (!p2 && p1 !== p2) { 9 | p2 = window.prompt(p1 === p2 10 | ? `Please enter a different name than ${p1}.` 11 | : 'Enter player 2 name:') 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) { document.getElementById('p1Score').innerText = ++score1 } else { document.getElementById('p2Score').innerText = ++score2 } 46 | 47 | document.getElementById('newGame').style.display = 'inline' 48 | document.getElementById('newGame').onclick = () => playGame(p1, p2) 49 | 50 | document.querySelectorAll('td').forEach(el => { 51 | el.onclick = undefined 52 | }) 53 | } else { 54 | game.nextPlayer() 55 | player.innerText = game.player 56 | } 57 | } 58 | }) 59 | })(p1, p2) 60 | } 61 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------