GitHub Actions Exercise

A Web game deployment

Step 0: Clone or fork the cardgame repository

  • ustp-cardgame-test

  • React: A JavaScript library for building user interfaces.
  • TypeScript: JavaScript with static typing.
  • Vite: A fast build tool and dev server for modern web apps.
  • Tailwind CSS: A utility-first CSS framework.
  • GitHub Spark UI Framework (@github/spark): GitHub’s design system / component library.

To build the project locally, one need to install node.js. At the time of testing Node.js (v24.11.1) and npm (11.6.2)

Error when executing npm -v: https://stackoverflow.com/questions/57673913/vsc-powershell-after-npm-updating-packages-ps1-cannot-be-loaded-because-runnin

Run the project locally

$ npm install # to add dependency
$ npm run build --if-present # build the project locally

Step 1: Add a GitHub action

  • Tasks:
    • Add a Github action that builds the application

in .github/workflows/build.yml

name: Node.js CI

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout a repo
      uses: actions/checkout@v4

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: 24
        cache: 'npm'

    - name: Install dependencies
      run: npm install

    - name: Build project
      run: npm run build --if-present

Step 2: Mark the new GitHub action as required in Pull Requests

  • Tasks:
    • The new GH action that just got added should be marked as required when submitting PRs. It needs to pass before you can merge the PR.
  • Create the ruleset called “need passing ci”
    • Add “Target branches” -> add target -> include Default branch
    • enable “Require status checks to pass”
      • click on “Add checks” -> search for “build” and add it
      • Click on “Any source” and choose “GitHub Actions”

Step 2.5: Disallow direct pushes without a Pull Request

  • Create the ruleset called “no push without pull request (pr)”
    • Add “Target branches” -> add target -> include Default branch
    • enable “Require a pull request before merging”

Step 3: Add unit tests and update the GH action to run the tests

  • Tasks:
    • Add unit tests (ask AI)
    • Update the existing GH action to run the tests and upload the test results
    • Make sure that PRs are only merge-able when the tests pass

in .github/workflows/build.yml

name: Node.js CI

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout a repo
      uses: actions/checkout@v4

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: 24
        cache: 'npm'

    - name: Install dependencies
      run: npm install

    - name: Run Tests # need to run test before build
      run: npm test # same as npm run test

    - name: Build project
      run: npm run build --if-present

in src/components/Component.test.tsx

// src/setupTests.ts
import { render, screen } from "@testing-library/react";
import MemoryCard from "./MemoryCard";

test("renders greeting", () => {
  render(<MemoryCard />);
  expect(screen.getByText("Hello")).toBeInTheDocument();
});

in src/setupTests.ts

// src/setupTests.ts
import '@testing-library/jest-dom'

in vitest.config.ts

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    setupFiles: "src/setupTests.ts",

    outputFile: "test-results/results.xml",
    reporters: ["default", "junit"],
  },
});

in vite.config.ts, add

export default defineConfig({
  test:{
    globals: true,
    environment: "jsdom",
    setupFiles: "./src/setupTests.ts",
  },
});

in package.json, add

{
    "scripts": {
        "test": "vitest run"
    },
    "devDependencies": {
        "@testing-library/jest-dom": "^6.9.1",
        "@testing-library/react": "^16.3.0",
        "jsdom": "^27.2.0",
        "vitest": "^4.0.14"
    },
}

Until this moment, running the project remotely is a pain, especially for updating dependency.

By dedefault, one will see the following after a succesful run JUNIT report written to /home/runner/work/ustp-cardgame-t/ustp-cardgame-t/test-results/results.xml

  • Upload the test results

in .github/workflows/build.yml, add

    - name: Upload Test Results
      if: always() # Run this even if tests fail, so we can see WHAT failed
      uses: actions/upload-artifact@v4
      with:
        name: test-results
        path: test-results/results.xml

Step 4: Add a deployment GitHub action

  • Tasks:
    • Add a deployment GH action that deploys the app to a GitHub page
    • The action shouldn’t run automatically, only when manually run via the “Actions” tab in the repository
    • Make sure that GitHub pages are enabled and authorized for the new deployment GH action
    • Post the link to the new GitHub page as a comment when completed.
  • Go to the repository on GitHub:

Settings -> Pages -> Build and deployment -> Source: GitHub Actions Settings -> Environment -> github-pages

in .github/workflows/deploy.yml, add

# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy GitHub Pages

on:
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build: # Build job

    runs-on: ubuntu-latest

    steps:
    - name: Checkout a repo
      uses: actions/checkout@v4

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: 24
        cache: 'npm'

    - name: Install dependencies
      run: npm install

    - name: Run Tests # need to run test before build
      run: npm test # same as npm run test

    - name: Upload Test Results
      if: always() # Run this even if tests fail, so we can see WHAT failed
      uses: actions/upload-artifact@v4
      with:
        name: test-results
        path: test-results/results.xml

    - name: Build project
      run: npm run build 

    - name: Setup Pages
      uses: actions/configure-pages@v5

    - name: Upload artifact
      uses: actions/upload-pages-artifact@v3
      with:
        path: './dist' # Vite builds to the 'dist' folder
        
  # Deployment job
  deploy:      
    environment:
      name: github-pages
      url: $

    runs-on: ubuntu-latest

    needs: build

    steps:

    - name: Deploy to GitHub Pages
      id: deployment
      uses: actions/deploy-pages@v4

in vitest.config.ts

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  base: "./",
});

Step 5: Create a release branch

  • Tasks:
    • Create a release branch with the following name: “release/1.0.0” from the latest code in main.

in .github/workflows/release.yml

name: Create Specific Release Branch

on:
  workflow_dispatch:
    inputs:
      branch_name:
        description: 'The full name of the release branch to create (e.g., release/1.0.0)'
        required: true
        default: 'release/1.0.0'

permissions: 
  contents: write # This grants the necessary permission to create/push branches

jobs:
  create_release_branch:

    name: Create Release

    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: 'master'
          # This line ensures the runner has write access to create a branch
          token: $

      - name: Set up Git
        run: |
          # Configure Git to use the GitHub Actions bot identity
          git config user.name github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com

      - name: Create and Push New Release Branch
        id: create_branch
        run: |
          BRANCH_NAME="$"
          echo "Attempting to create branch: $BRANCH_NAME"          

          # Create the new branch from the current HEAD (which is the latest 'main')
          git branch $BRANCH_NAME
          
          # Push the new branch to GitHub
          git push origin $BRANCH_NAME
          
          echo "Branch '$BRANCH_NAME' successfully created from main."         

Step 6: Add a release note for the release/1.0.0 branch

  • Tasks:
    • Add a release note in the repository
    • Automatically create a tag
    • Generate release notes
    • Generate a discussion so that others can comment on the new release

in .github/workflows/auto_release.yml

name: Automatic GitHub Release

on:
  push:
    # Trigger the workflow only when a commit is pushed to a release branch
    branches:
      - 'release/**' 

jobs:
  create_github_release:

    name: Auto Create Release

    runs-on: ubuntu-latest
    
    # Grant necessary permissions for the GITHUB_TOKEN to create the release/tag
    permissions:
      contents: write # Required to create the release, tag, and publish notes
      discussions: write # Required to optionally create a discussion

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Extract Version Tag
        id: get_version
        run: |
          # The branch name will be 'release/1.0.0'. We extract the '1.0.0' part.
          BRANCH_NAME="$"
          # Removes 'release/' prefix
          TAG_NAME=${BRANCH_NAME#release/}
          
          # Set the output variable 'tag' for use in the next step
          echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT

      - name: Create Release & Tag
        uses: softprops/action-gh-release@v2 # A popular, feature-rich action for releases
        with:
          # 1. Automatically create a tag
          tag_name: v$ # Use the extracted version, prepended with 'v'
          name: Release v$
          
          # 2. Generate release notes
          # Compares the current commit to the previous tag/release to generate a changelog
          generate_release_notes: true 
          
          # 3. Add a release note in the repository (This is the Release description itself)
          body: |
            ## Release Notes for v$
            
            This is an automated release created from the $ branch.
            
            **Highlights:**
            * [Generated Notes Here]
            
            ---
            
            **Full Changelog:**
            $ # Placeholder for generated notes
          
          # 4. Generate a discussion
          # Creates a new discussion for the release in the 'Announcements' category
          discussion_category_name: Announcements 
          
          # Optional: Make it a draft first if you want to review it
          # draft: true 
          
          # Optional: Set this if this is a pre-release version
          # prerelease: false
  • Since we create a new workflow, we can merge this change to builds/buildFeature to trigger the auto_release workflow

Step 7: Enable and configure dependabot

  • Tasks:
    • Enable dependabot
    • Configure it to run weekly
    • Exclude a dependency from getting updated (i.e. globals)
    • Merge the dependabot update PRs when CI is green
    • https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide

Step 8: Add an action to create a release

  • Tasks:
    • Use this action: https://github.com/softprops/action-gh-release
    • Add a release GH action that creates a release on your repository
    • The action should run when a new tag is pushed to the repository (i.e. v2.0.0)
    • Upload the built memory-card-game (the “binaries”) as a .zip file as part of the release

in .github/workflows/auto_release_new.yml

name: Automatic GitHub Release New

on:
  push:
    # Trigger the workflow when a new tag is pushed to the repository
    tags:
      - 'v*' 
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    
    # Grant necessary permissions for the GITHUB_TOKEN to create the release/tag
    permissions:
      contents: write # Required to create the release, tag, and publish notes
      discussions: write # Required to optionally create a discussion

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 'lts/*'

      - name: Install dependencies
        run: npm ci

      - name: Build project
        run: npm run build

      - name: Create zip archive
        run: |
          cd dist
          zip -r ../release.zip .
        shell: bash

      - name: Extract Version Tag
        id: get_version
        run: |
          # The tag will be 'v1.0.0'. We extract the version part without the 'v' prefix.
          TAG_NAME="$"
          # Removes 'v' prefix if present
          VERSION=${TAG_NAME#v}
          
          # Set the output variable 'tag' for use in the next step
          echo "tag=$VERSION" >> $GITHUB_OUTPUT

      - name: Create Release & Tag
        uses: softprops/action-gh-release@v2 # A popular, feature-rich action for releases
        with:
          # 1. Automatically create a tag
          tag_name: v$ # Use the extracted version, prepended with 'v'
          name: Release v$
          
          # 2. Generate release notes
          # Compares the current commit to the previous tag/release to generate a changelog
          generate_release_notes: true 
          
          # 3. Add a release note in the repository (This is the Release description itself)
          body: |
            ## Release Notes for v$
            
            This is an automated release created from a new tag push.
            
            **Highlights:**
            * Built and packaged binary included in assets
            
            ---
            
            **Full Changelog:**
            See the commits included in this release.
          
          # 4. Upload the built binary as an asset
          files: release.zip
          
          # 5. Generate a discussion
          # Creates a new discussion for the release in the 'Announcements' category
          discussion_category_name: Announcements 
          
          # Optional: Make it a draft first if you want to review it
          # draft: true 
          
          # Optional: Set this if this is a pre-release version
          # prerelease: false

Questions

  • Can we have a sample solution?
  • Why do we need ceate two seperate rulesets? What is the difference?
  • Why I can not use npm ci?
  • How does .github dependabot work here?
  • How to update package.json if I don’t run the project locally? At least I didn’t manage.
  • How to see the uploaded test results?

References