Deploy with GitHub Actions: GitHub Pages, Docker Images and Staged Releases

Testing is half of a pipeline; shipping is the other half. In this lesson you will deploy a site to GitHub Pages, publish a Playwright report, push a Docker image to GitHub Container Registry and build a staging-to-production pipeline with smoke tests and approvals.

📖 What You’ll Learn: CI vs CD, deploying to GitHub Pages, publishing a Playwright HTML report, building and pushing Docker images to ghcr.io on tags, a staged pipeline with environments, smoke tests and required reviewers, OIDC for cloud credentials, and rolling back.


🚀 From CI to CD

So far your workflows test code (Continuous Integration). Continuous Delivery/Deployment (CD) goes further: after the tests pass, the pipeline ships the result, a website, a Docker image, a package or a test report, automatically and the same way every time.

A typical pipeline:

push to main ──▶ build ──▶ test ──▶ deploy to staging ──▶ smoke tests ──▶ (approval) ──▶ deploy to production

This lesson shows three common deployment targets and the safety features around them. It builds on workflow syntax and secrets and environments.


🌐 Example 1: Deploy a Static Site to GitHub Pages

# .github/workflows/pages.yml
name: Deploy site to GitHub Pages
on:
  push:
    branches: [ main ]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npm run build                    # creates ./dist
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4

In Settings, Pages, set Source to GitHub Actions. More in GitHub Pages.

Bonus for testers: publish the Playwright HTML report

Replace the build steps with your test run and upload playwright-report/ as the Pages artifact. The team then opens the latest report at a stable URL instead of downloading artifacts.

      - run: npx playwright install --with-deps
      - run: npx playwright test
        continue-on-error: true               # publish the report even when tests fail
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./playwright-report

⚠️ Common mistake: A Pages site from a public repository is public. Do not publish reports that contain sensitive data, screenshots of internal systems or traces with tokens.


🐳 Example 2: Build and Push a Docker Image to GitHub Container Registry

# .github/workflows/docker.yml
name: Docker image
on:
  push:
    tags: [ 'v*' ]                            # release when a version tag is pushed

permissions:
  contents: read
  packages: write

jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/acme/test-runner:${{ github.ref_name }}
            ghcr.io/acme/test-runner:latest

Tagging a release (git tags) with git tag -a v1.2.0 -m "Release 1.2.0" and git push origin v1.2.0 triggers the build. Image names on ghcr.io must be lowercase. Running tests in containers is covered in Playwright with Docker and Kubernetes.


🖥️ Example 3: Staging and Production with Approvals

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [ main ]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false                   # never cancel a deployment halfway

jobs:
  test:
    uses: ./.github/workflows/playwright-tests.yml     # reusable test workflow
    with:
      base-url: https://qa.example.com
    secrets: inherit

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

  smoke-staging:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci && npx playwright install --with-deps chromium
      - run: npx playwright test --grep @smoke --project=chromium
        env:
          BASE_URL: https://staging.example.com

  deploy-production:
    needs: smoke-staging
    runs-on: ubuntu-latest
    environment:
      name: production                         # configured with required reviewers
      url: https://www.example.com
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh production
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

What makes this safe:

FeatureProtection
needs chainProduction only after tests, staging deployment and smoke tests pass
Environment production with required reviewersA person approves the production job
Environment deployment branchesOnly main can deploy to production
Environment secretsStaging and production use different credentials
concurrency without cancelDeployments run one at a time and are never cut off
Reusable workflow for testsOne test definition for every pipeline

The deploy script depends on your hosting (cloud CLI, SSH, Kubernetes, a hosting provider’s action). For cloud providers, prefer OpenID Connect (OIDC): permissions: id-token: write plus the provider’s official login action, so no long-lived cloud keys are stored as secrets.


↩️ Rolling Back

  • Redeploy the previous version: re-run the deployment workflow for an earlier tag or commit (keep workflow_dispatch with a ref or version input).
  • Or revert the bad commit on main with git revert and let the pipeline deploy again (undo changes).

📝 Assignment

  1. Deploy a simple static HTML page from a repository to GitHub Pages with the Pages workflow.
  2. Publish a Playwright HTML report to Pages from a practice (public, non-sensitive) project.
  3. Create staging and production environments; add yourself as required reviewer for production and restrict it to main.
  4. Build the staging → smoke test → production chain with echo deploy steps and approve the production job.
  5. Push a v0.1.0 tag and build a Docker image to ghcr.io (any small Dockerfile).

❓ FAQ

🤔 How do I deploy with GitHub Actions?

Add a workflow that runs after tests (needs), uses an environment for the target, and runs the deployment step (a Pages action, a Docker push, a cloud CLI or a script) with secrets or OIDC credentials.

🤔 How do I add manual approval before production deployment?

Create a production environment with required reviewers and reference it with environment: production in the deploy job.

🤔 How do I publish a Playwright report with GitHub Actions?

Upload playwright-report/ with actions/upload-pages-artifact and deploy with actions/deploy-pages, or upload it as a normal artifact.

🤔 How do I push a Docker image to GitHub Container Registry?

Log in with docker/login-action to ghcr.io using GITHUB_TOKEN with packages: write permission, then build and push with docker/build-push-action.

🤔 What is OIDC in GitHub Actions?

OpenID Connect lets workflows get short-lived cloud credentials directly from the cloud provider, so you do not store long-lived keys as secrets.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Actions Reusable Workflows and Custom (Composite) Actions

Copy-pasted YAML is hard to maintain. In this lesson you will create reusable workflows that other pipelines call as jobs, build a composite action for shared setup steps, and learn how to use third-party actions safely.

📖 What You’ll Learn: reusable workflows vs composite actions, defining workflow_call with inputs, secrets and outputs, calling workflows from the same and other repositories, composite actions and their rules, JavaScript and Docker actions, and safe use of third-party actions.


♻️ Why Reuse?

As repositories multiply, the same YAML gets copied everywhere: checkout, set up Node, install, run tests, upload the report. Copies drift apart and every fix must be made ten times. GitHub Actions has two ways to reuse:

Reusable workflowComposite action
ReusesWhole jobs (one or more)Steps inside a job
Defined withon: workflow_call in .github/workflows/action.yml with runs.using: composite
Called withjobs.<id>.uses:steps[*].uses:
Can choose its own runner✅❌ (runs in the caller’s job)
Can use secrets directlyPassed in by the callerVia inputs only
Best forStandard pipelines (test, deploy)Small shared step sequences (setup, notify)

📄 Reusable Workflows

1. Define the reusable workflow

# .github/workflows/playwright-tests.yml
name: Playwright tests (reusable)

on:
  workflow_call:
    inputs:
      project:
        type: string
        default: chromium
      base-url:
        type: string
        required: true
    secrets:
      TEST_PASSWORD:
        required: true
    outputs:
      result:
        description: Test result
        value: ${{ jobs.e2e.outputs.result }}

jobs:
  e2e:
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.run.outcome }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps ${{ inputs.project }}
      - id: run
        run: npx playwright test --project=${{ inputs.project }}
        env:
          BASE_URL: ${{ inputs.base-url }}
          TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

2. Call it

# .github/workflows/ci.yml
name: CI
on: [ pull_request ]

jobs:
  chromium:
    uses: ./.github/workflows/playwright-tests.yml          # same repository
    with:
      project: chromium
      base-url: https://staging.example.com
    secrets:
      TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

  firefox:
    uses: acme/ci-templates/.github/workflows/playwright-tests.yml@v1   # another repository
    with:
      project: firefox
      base-url: https://staging.example.com
    secrets: inherit                                         # pass all of the caller's secrets
RuleDetail
Reference./.github/workflows/file.yml (same repo) or owner/repo/.github/workflows/file.yml@ref
VersionPin @ref to a tag (@v1) or commit SHA for stability
InputsTyped: string, number, boolean
SecretsPassed explicitly or with secrets: inherit
AccessThe called workflow must be in a public repository, or in a private repository whose settings allow access
MatrixYou can call a reusable workflow inside a matrix with strategy on the calling job

🧱 Composite Actions

A composite action bundles steps. Create a folder with an action.yml:

# .github/actions/setup-playwright/action.yml
name: Setup Playwright
description: Install Node, dependencies and Playwright browsers
inputs:
  browsers:
    description: Browsers to install
    default: chromium
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
        cache: npm
    - run: npm ci
      shell: bash
    - run: npx playwright install --with-deps ${{ inputs.browsers }}
      shell: bash

📝 Note: Every run step in a composite action must declare a shell.

Use it in any job:

steps:
  - uses: actions/checkout@v4                       # checkout first: local actions live in the repo
  - uses: ./.github/actions/setup-playwright
    with:
      browsers: chromium firefox
  - run: npx playwright test

🧰 Other Kinds of Custom Actions

TypeWritten inUse when
CompositeYAML stepsCombining existing commands and actions
JavaScriptNode.js (runs.using: node20 or newer)Logic, API calls, fast startup
Docker containerAny language in a containerSpecific tools or environments (Linux runners only)

Publish actions from a public repository to the GitHub Marketplace by adding an action.yml in the root and creating a release.


🛡️ Using Third-Party Actions Safely

  • Prefer actions from verified creators (GitHub, Docker, Microsoft, AWS…).
  • Pin third-party actions to a full commit SHA (with a version comment) so a moved tag cannot change what runs: uses: some-org/some-action@<40-char-sha> # v2.1.0.
  • Let Dependabot update action versions with package-ecosystem: github-actions.
  • Limit permissions of GITHUB_TOKEN (secrets and environments).

📝 Assignment

  1. Create a reusable workflow test.yml with a project input and a required secret, and call it twice from ci.yml for two browsers.
  2. Return an output from the reusable workflow and print it in the caller.
  3. Create a composite action setup-playwright and use it in a job.
  4. Call a reusable workflow inside a matrix.
  5. Pin one third-party action to a commit SHA and add a Dependabot config for GitHub Actions updates.

❓ FAQ

🤔 What is a reusable workflow in GitHub Actions?

A workflow with the workflow_call trigger that other workflows can call as a job, passing inputs and secrets.

🤔 What is the difference between a reusable workflow and a composite action?

A reusable workflow reuses whole jobs and chooses its own runner. A composite action reuses a sequence of steps inside the caller’s job.

🤔 How do I pass secrets to a reusable workflow?

Under secrets: in the calling job, either one by one or with secrets: inherit.

🤔 How do I create a custom GitHub Action?

Create an action.yml describing inputs and how it runs (composite, JavaScript or Docker), in its own folder or repository, then reference it with uses:.

🤔 Should I pin GitHub Actions to a version?

Yes. Use a version tag for trusted publishers and a full commit SHA for third-party actions to avoid unexpected changes.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Actions Matrix Builds, Caching and Artifacts (with Examples)

Matrices, caches and artifacts make pipelines broader and faster. In this lesson you will run tests across browsers and operating systems in parallel, cache dependencies correctly, and keep and share reports with artifacts.

📖 What You’ll Learn: matrix strategy and job combinations, include and exclude, fail-fast and max-parallel, caching with setup actions and actions/cache, cache keys and restore keys, uploading and downloading artifacts, cache vs artifacts, and a complete cross-browser pipeline.


🧮 Three Tools for Faster, Broader Pipelines

FeatureSolves
MatrixRun the same job for many combinations (browsers, OS, versions) in parallel
CachingReuse downloaded dependencies between runs to save minutes
ArtifactsKeep files produced by a job (reports, screenshots, builds) and share them between jobs

🧮 Matrix Builds

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false                 # keep other combinations running if one fails
      max-parallel: 4
      matrix:
        os: [ ubuntu-latest, windows-latest ]
        browser: [ chromium, firefox, webkit ]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}

This creates 6 jobs (2 operating systems × 3 browsers) that run in parallel. Each appears separately in the run, e.g. “test (windows-latest, firefox)”.

include and exclude

strategy:
  matrix:
    os: [ ubuntu-latest, windows-latest ]
    browser: [ chromium, firefox, webkit ]
    exclude:
      - os: windows-latest
        browser: webkit                # skip this combination
    include:
      - os: macos-latest
        browser: webkit                # add one extra combination
      - os: ubuntu-latest
        browser: chromium
        smoke: true                    # add a variable to an existing combination
KeyEffect
fail-fastDefault true cancels all jobs when one fails; set false for test suites so you see every failure
max-parallelLimit simultaneous jobs
excludeRemove combinations
includeAdd combinations or extra values

The matrix is also how you shard a large suite across machines: see Playwright in GitHub Actions.


⚡ Caching Dependencies

The easy way: setup actions

Most language setup actions cache package downloads with one line:

- uses: actions/setup-node@v4
  with:
    node-version: lts/*
    cache: npm                 # also: yarn, pnpm

- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: pip

The flexible way: actions/cache

- name: Cache Maven packages
  uses: actions/cache@v4
  with:
    path: ~/.m2/repository
    key: maven-${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
    restore-keys: |
      maven-${{ runner.os }}-
FieldMeaning
pathFolder(s) to save and restore
keyExact cache name; including hashFiles(...) of the lock file creates a new cache when dependencies change
restore-keysFallback prefixes when there is no exact match

📝 Note: Caches are scoped to the branch and its base branch, are evicted when unused for about a week, and have a total size limit per repository. Never cache secrets.

💡 Tip: For Playwright, caching browsers rarely saves time because install --with-deps still installs system packages. Caching npm downloads (cache: npm) is the reliable win.


📦 Artifacts

Upload

- name: Upload Playwright report
  if: ${{ !cancelled() }}                 # also upload when tests fail
  uses: actions/upload-artifact@v4
  with:
    name: playwright-report-${{ matrix.browser }}   # unique name per matrix job
    path: playwright-report/
    retention-days: 14

⚠️ Common mistake: With upload-artifact@v4, artifact names must be unique within a run. In a matrix, include the matrix values in the name, otherwise uploads fail.

Download in a later job

publish:
  needs: test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/download-artifact@v4
      with:
        pattern: playwright-report-*
        path: all-reports
        merge-multiple: false
    - run: ls -R all-reports

Artifacts also appear at the bottom of the run’s summary page for download by humans.

CacheArtifact
PurposeSpeed up future runsKeep outputs of a run
Typical contentDependenciesReports, screenshots, builds, logs
Shared betweenRunsJobs in the same run, and people
LifetimeEvicted when unusedRetention days (configurable)

🧪 Putting It Together: Cross-Browser Test Pipeline

name: Cross-browser tests
on: [ pull_request ]

jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [ chromium, firefox, webkit ]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 7

📝 Assignment

  1. Create a matrix job that prints os and browser for 2 × 3 combinations.
  2. Exclude one combination and include one extra; check the job list.
  3. Set fail-fast: false and make one combination fail on purpose.
  4. Add cache: npm to setup-node and compare the install time of the first and second run.
  5. Upload a file as an artifact with a matrix-specific name and download it in a later job.
  6. Bonus: build the cross-browser pipeline above for your Playwright project.

❓ FAQ

🤔 What is a matrix in GitHub Actions?

A strategy that runs the same job for every combination of values you list, such as operating systems or browsers, in parallel.

🤔 How do I cache npm dependencies in GitHub Actions?

Use actions/setup-node with cache: npm, or actions/cache with a key based on hashFiles('**/package-lock.json').

🤔 What is the difference between cache and artifacts?

Caches speed up future runs by reusing dependencies. Artifacts store outputs of a run, such as reports, for download or for later jobs.

🤔 Why does upload-artifact fail in my matrix?

Artifact names must be unique per run in v4; include matrix values in the name.

🤔 How do I upload test reports even when tests fail?

Add if: ${{ !cancelled() }} (or always()) to the upload step.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Actions Variables, Secrets and Environments Explained

Pipelines need URLs, credentials and settings that do not belong in code. In this lesson you will use environment variables, configuration variables and secrets in GitHub Actions, control GITHUB_TOKEN permissions and protect deployments with environments.

📖 What You’ll Learn: env vs vars vs secrets, environment variables at each level, configuration variables, secrets and their rules, GITHUB_TOKEN permissions, environments with required reviewers and branch rules, organisation-level values, and running the same tests against several environments.


🔐 Configuration Without Hard-Coding

Pipelines need values that should not be written into code: URLs, user names, API keys, passwords. GitHub Actions offers three mechanisms:

MechanismForVisible in logsSet in
Environment variables (env)Values defined in the workflow fileYesThe YAML file
Configuration variables (vars)Non-secret settings (URLs, flags)YesSettings, Secrets and variables, Actions, Variables
Secrets (secrets)Passwords, tokens, keysNo (masked as ***)Settings, Secrets and variables, Actions, Secrets

🌱 Environment Variables in YAML

env:                               # workflow level
  CI: true

jobs:
  test:
    runs-on: ubuntu-latest
    env:                           # job level
      BROWSER: chromium
    steps:
      - name: Print
        env:                       # step level (most specific wins)
          GREETING: hello
        run: echo "$GREETING from $BROWSER"

Read them in your code with process.env.BROWSER (Node), os.environ["BROWSER"] (Python) or $BROWSER in the shell.


⚙️ Configuration Variables (vars)

Create Settings, Secrets and variables, Actions, Variables, New repository variable, e.g. BASE_URL = https://staging.example.com:

- run: npx playwright test
  env:
    BASE_URL: ${{ vars.BASE_URL }}

Use variables for anything you want to change without editing workflow files and that is not secret.


🗝️ Secrets

Create Settings, Secrets and variables, Actions, Secrets, New repository secret, e.g. TEST_PASSWORD:

- name: Run tests
  run: npx playwright test
  env:
    TEST_USER: ${{ vars.TEST_USER }}
    TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

Rules worth knowing:

  • Secret values are masked in logs; mask other dynamic values with echo "::add-mask::$VALUE".
  • Secrets are not passed to workflows triggered by pull requests from forks (to protect them from untrusted code).
  • You cannot read a secret back in the UI; you can only update or delete it.
  • Do not pass secrets on the command line of tools that print their arguments; prefer environment variables.
  • Secret names are case-insensitive and cannot start with GITHUB_.

The automatic GITHUB_TOKEN

Every run gets a temporary GITHUB_TOKEN for the repository, available as ${{ secrets.GITHUB_TOKEN }}. Grant only the permissions you need:

permissions:
  contents: read
  pull-requests: write     # e.g. to comment on PRs

For access to other repositories, use a fine-grained personal access token or a GitHub App, stored as a secret.


🌍 Environments

An environment (Settings, Environments) represents a deployment target such as qa, staging or production. Environments add:

FeatureWhat it does
Environment secrets and variablesDifferent values per environment, e.g. BASE_URL
Required reviewersA person must approve before the job runs
Wait timerDelay before deployment
Deployment branches and tagsOnly allowed branches (e.g. main) can deploy
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}      # the staging environment's secret

  smoke-test-staging:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx playwright install --with-deps chromium
      - run: npx playwright test --grep @smoke
        env:
          BASE_URL: ${{ vars.BASE_URL }}            # the staging URL

  deploy-production:
    needs: smoke-test-staging
    runs-on: ubuntu-latest
    environment: production                       # waits for a required reviewer
    steps:
      - run: ./deploy.sh

Each environment’s variables and secrets override repository-level ones with the same name. The Actions summary shows the deployment history per environment. Full pipelines are in Deploy with GitHub Actions.


🏢 Organisation Variables and Secrets

In an organisation, Settings, Secrets and variables, Actions lets admins create values shared with all or selected repositories, such as a common Slack webhook or test-environment URL.


🧪 Example: Same Tests, Different Environments

on:
  workflow_dispatch:
    inputs:
      target:
        type: environment
        description: Which environment to test

jobs:
  e2e:
    runs-on: ubuntu-latest
    environment: ${{ inputs.target }}
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx playwright install --with-deps
      - run: npx playwright test
        env:
          BASE_URL: ${{ vars.BASE_URL }}
          TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

The test code reads process.env.BASE_URL, as in environment variables with .env.


📝 Assignment

  1. Add a repository variable BASE_URL and a secret TEST_PASSWORD to a practice repository.
  2. Print the variable in a workflow and confirm the secret shows as *** in logs.
  3. Create two environments, qa and staging, each with its own BASE_URL.
  4. Add a required reviewer to staging and run a job that targets it; approve it.
  5. Set permissions: contents: read and observe what happens if a step tries to push.
  6. Build a manual workflow whose input chooses the environment to test.

❓ FAQ

🤔 What is the difference between vars and secrets in GitHub Actions?

vars are plain configuration values visible in logs; secrets are encrypted and masked. Use secrets for passwords, tokens and keys.

🤔 How do I use a secret in a GitHub Actions workflow?

Create it in Settings, Secrets and variables, Actions, then reference it with ${{ secrets.NAME }}, usually as an environment variable of a step.

🤔 What is GITHUB_TOKEN?

An automatically created, short-lived token for the repository, available in every workflow run; its permissions are set with the permissions key.

🤔 What are GitHub environments?

Named deployment targets with their own secrets and variables and optional protection rules such as required reviewers and allowed branches.

🤔 Why are my secrets empty in a pull request from a fork?

GitHub does not pass secrets to workflows triggered by forks, to protect them from untrusted code.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Actions Workflow Syntax: Events, Jobs, Steps, Runners and Expressions

Every GitHub Actions pipeline is a YAML file. In this lesson you will read a complete annotated workflow and learn each part: events, jobs, steps, runners, expressions, conditions, outputs, concurrency and permissions.

📖 What You’ll Learn: the building blocks of a workflow, a complete annotated example, events and filters, job keys like needs, if and timeout, steps with uses and run, expressions and contexts, status functions, passing outputs between steps and jobs, and runners and cost.


🧩 Anatomy of a Workflow

A workflow is a YAML file in .github/workflows/ that GitHub Actions runs when an event happens. The building blocks:

PartMeaning
WorkflowOne YAML file; a repository can have many
Event (on)What starts it: push, pull request, schedule, manual run…
JobA set of steps that runs on one runner (virtual machine); jobs run in parallel by default
StepOne command (run) or one reusable action (uses)
RunnerThe machine: GitHub-hosted (Ubuntu, Windows, macOS) or self-hosted
ActionA reusable unit from the Marketplace or your own repository

If you are completely new, read GitHub Actions Fundamentals for Beginners first.


📄 A Complete Annotated Workflow

name: CI                                   # shown in the Actions tab
run-name: CI for ${{ github.ref_name }} by @${{ github.actor }}

on:
  push:
    branches: [ main ]
    paths-ignore: [ '**.md' ]              # skip runs for documentation-only changes
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '30 20 * * *'                  # every day at 20:30 UTC
  workflow_dispatch:                       # manual "Run workflow" button
    inputs:
      browser:
        description: 'Browser project to run'
        type: choice
        options: [ chromium, firefox, webkit ]
        default: chromium

permissions:
  contents: read                           # least privilege for GITHUB_TOKEN

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true                 # cancel older runs of the same branch

env:
  NODE_VERSION: 22

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci
      - run: npm run lint

  test:
    needs: lint                            # runs after lint succeeds
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Run tests
        run: npx playwright test --project=${{ inputs.browser || 'chromium' }}
      - name: Upload report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

⚡ Events (on)

EventFires whenCommon filters
pushCommits are pushedbranches, tags, paths, paths-ignore
pull_requestA PR is opened, updated or reopenedbranches (the target branch), types
scheduleOn a cron timetable (UTC)cron expression
workflow_dispatchSomeone clicks Run workflow (or uses the API or gh workflow run)inputs
workflow_callAnother workflow calls this oneinputs, secrets (reusable workflows)
releaseA release is publishedtypes: [published]
workflow_runAnother workflow finishesworkflows, types

💡 Tip: Use paths / paths-ignore so a README change does not trigger a 20-minute test run.


🏗️ Jobs

KeyPurpose
runs-onRunner: ubuntu-latest, windows-latest, macos-latest, or labels of self-hosted runners
needsDependencies: needs: [lint, build]
ifRun the job only when a condition is true
timeout-minutesStop runaway jobs (default is 360)
strategy.matrixRun the job for several combinations (matrix)
environmentDeploy to a protected environment (environments)
outputsValues passed to later jobs
container / servicesRun in a Docker image, or start databases alongside the job

🪜 Steps

steps:
  - name: Check out code
    uses: actions/checkout@v4           # an action, with inputs under "with"
    with:
      fetch-depth: 0

  - name: Install
    run: npm ci                         # a shell command

  - name: Multi-line script
    working-directory: ./tests
    shell: bash
    run: |
      echo "Running on ${{ runner.os }}"
      ls -la

  - name: Optional step
    continue-on-error: true
    run: npm run audit

🧮 Expressions and Contexts

Expressions are written as ${{ ... }} and can read contexts:

ContextExamples
githubgithub.ref_name, github.sha, github.event_name, github.actor
env / vars / secretsEnvironment variables, configuration variables, secrets
inputsInputs from workflow_dispatch or workflow_call
matrixThe current matrix values
steps / needsOutputs of earlier steps or jobs
runnerrunner.os, runner.temp

Status functions for if: success() (default), failure(), always(), cancelled(). Other functions: contains(), startsWith(), format(), hashFiles(), toJSON().

- name: Notify on failure
  if: ${{ failure() && github.ref == 'refs/heads/main' }}
  run: echo "Main is broken!"

🔗 Passing Data Between Steps and Jobs

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      app_version: ${{ steps.read.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: read
        run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

  report:
    needs: version
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing version ${{ needs.version.outputs.app_version }}"

Other special files: $GITHUB_ENV (set environment variables for later steps) and $GITHUB_STEP_SUMMARY (Markdown shown on the run’s summary page).


💰 Runners and Cost

GitHub-hosted runners are free for public repositories. Private repositories get a monthly allowance of free minutes depending on the plan; Windows and macOS minutes count more than Linux. Use Linux runners, timeout-minutes, concurrency and paths filters to keep usage low. Self-hosted runners run on your own machines for special hardware or network access.


📝 Assignment

  1. In git-practice, create .github/workflows/ci.yml with push, pull_request and workflow_dispatch triggers.
  2. Add two jobs, lint and test (they can just echo), where test needs lint.
  3. Add a workflow_dispatch input and print it in a step.
  4. Add concurrency with cancel-in-progress and push twice quickly to see the first run cancelled.
  5. Pass a value from one job to another with outputs, and write a line to $GITHUB_STEP_SUMMARY.
  6. Add a nightly schedule and a paths-ignore for Markdown files.

❓ FAQ

🤔 Where do GitHub Actions workflow files go?

In the .github/workflows/ folder of the repository, as .yml or .yaml files.

🤔 What is the difference between a job and a step?

A job runs on its own runner and contains steps. Steps run in order inside the job; jobs run in parallel unless connected with needs.

🤔 How do I run a workflow manually?

Add the workflow_dispatch trigger, then click Run workflow on the Actions tab or use gh workflow run.

🤔 How do I run a step only when a previous step fails?

Use if: ${{ failure() }} on the step.

🤔 What time zone does schedule cron use?

UTC. Convert your local time, for example 02:00 IST is 20:30 UTC on the previous day.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Security: Secrets, Secret Scanning, Dependabot and Code Scanning

Leaked tokens and vulnerable dependencies are among the most common security problems in software projects. In this lesson you will use GitHub’s security features to keep secrets out of code, keep dependencies safe and scan code automatically.

📖 What You’ll Learn: the main repository security risks, encrypted secrets for GitHub Actions and their levels, secret scanning and push protection, Dependabot alerts, security updates and version updates, CodeQL code scanning, vulnerability reporting, and a security checklist.


🔐 Security on GitHub: The Big Picture

Most security incidents in code repositories come from three sources: leaked secrets, vulnerable dependencies and insecure code. GitHub has a tool for each:

RiskGitHub feature
Passwords, tokens and keys in codeSecrets for CI, secret scanning and push protection
Vulnerable librariesDependabot alerts, security updates and version updates
Insecure code patternsCode scanning with CodeQL
Reporting vulnerabilitiesSECURITY.md and private security advisories

Find them in your repository under Settings, Code security (also called “Advanced Security” or “Code security and analysis”).

📝 Note: For public repositories, Dependabot, secret scanning and code scanning are free. For private repositories, some features (such as code scanning and secret scanning) require a paid GitHub security product on an organisation plan. Dependabot alerts and updates are available for private repositories too.


🗝️ Secrets for GitHub Actions

Never put passwords or tokens in workflow files. Store them as encrypted secrets:

  1. Settings, Secrets and variables, Actions, New repository secret.
  2. Name it, e.g. TEST_PASSWORD, paste the value, save.

Use it in a workflow:

- name: Run Playwright tests
  run: npx playwright test
  env:
    BASE_URL: ${{ vars.BASE_URL }}
    TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
LevelShared with
Repository secretAll workflows in the repository
Environment secretJobs that target a specific environment (e.g. staging), optionally with required reviewers
Organisation secretSelected repositories in the organisation

Secrets are masked as *** in logs and are not passed to workflows triggered by pull requests from forks. Details in Variables, Secrets and Environments.


🕵️ Secret Scanning and Push Protection

Secret scanning detects known token formats (GitHub, AWS, Azure, Slack, Stripe and many more) in the repository and its history and raises alerts. Push protection goes further: it blocks the push when it detects a secret:

remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - Push cannot contain secrets
remote:   —— GitHub Personal Access Token ——
remote:    locations:
remote:      - commit: 9c7e1aa
remote:        path: tests/api.spec.ts:12

Remove the secret from the commit (amend or interactive rebase), move it to an environment variable or secret, and push again.

⚠️ Common mistake: If a real secret was ever pushed, revoke and rotate it first. Rewriting history afterwards does not help if someone already copied it (tokens).


📦 Dependabot

Dependabot alerts

GitHub reads your dependency files (package-lock.json, requirements.txt, pom.xml and others) and alerts you when a dependency has a known vulnerability, with severity and the fixed version. Enable Dependabot alerts in Settings, Code security.

Dependabot security updates

With security updates enabled, Dependabot opens pull requests that upgrade vulnerable dependencies to a safe version. Your CI runs on them like on any PR.

Dependabot version updates

To keep dependencies current (not just secure), add a config file:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      playwright:
        patterns:
          - "@playwright/*"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"

Dependabot then opens PRs such as “Bump @playwright/test from 1.x to 1.y”. Your test suite tells you whether the upgrade is safe, which is exactly why reliable automated tests matter.

💡 Tip: For Playwright projects, group Playwright packages together and remember that browser versions change with each upgrade: let CI run the full suite on Dependabot PRs before merging.


🔬 Code Scanning with CodeQL

Code scanning analyses source code for security vulnerabilities (SQL injection, cross-site scripting, unsafe deserialisation…) and shows results as alerts and as annotations on pull requests.

  1. Settings, Code security, Code scanning, Set up, Default.
  2. GitHub detects the languages and runs CodeQL on pushes and pull requests to the default branch, plus on a schedule.
  3. Use Advanced setup to customise it with a workflow file if needed.

Alerts appear under the Security tab; you can fix, dismiss with a reason, or create an issue.


📣 Reporting Vulnerabilities

  • Add a SECURITY.md file explaining how to report security issues privately.
  • Enable private vulnerability reporting so researchers can report through GitHub without a public issue.
  • Maintainers can draft security advisories and request a CVE.

✅ Repository Security Checklist

  • [ ] Two-factor authentication for all members (account setup)
  • [ ] No secrets in code; .env in .gitignore; CI secrets in GitHub Secrets
  • [ ] Secret scanning and push protection on
  • [ ] Dependabot alerts, security updates and a dependabot.yml
  • [ ] Code scanning with CodeQL
  • [ ] Branch protection with required reviews and checks
  • [ ] Workflow permissions: set to the minimum needed
  • [ ] SECURITY.md and private vulnerability reporting

📝 Assignment

  1. In a public practice repository, enable Dependabot alerts, security updates, secret scanning and push protection.
  2. Add a dependabot.yml for npm (or pip) and GitHub Actions, and look at the first Dependabot PRs.
  3. Add a repository secret TEST_PASSWORD and use it in a workflow step; check that the log shows ***.
  4. Enable CodeQL default setup and review the Security tab.
  5. Write a short SECURITY.md.
  6. Bonus: try pushing a fake token in the GitHub token format to a test branch and observe push protection (use a clearly fake value).

❓ FAQ

🤔 How do I store secrets in GitHub?

In Settings, Secrets and variables, Actions, as repository, environment or organisation secrets, and use them in workflows with the secrets context.

🤔 What is Dependabot?

A GitHub feature that alerts you about vulnerable dependencies and opens pull requests to update them, for security fixes or regular version updates.

🤔 What is push protection in GitHub?

Part of secret scanning that blocks a push when it contains a detected secret, before the secret reaches the repository.

🤔 What is CodeQL?

GitHub’s code analysis engine used by code scanning to find security vulnerabilities and code quality problems.

🤔 Are GitHub security features free?

For public repositories, Dependabot, secret scanning and code scanning are free. Some features for private repositories require a paid GitHub security product.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub CODEOWNERS, Issue Templates, Issue Forms and Pull Request Templates

Good repositories guide contributors automatically. In this lesson you will set up CODEOWNERS so the right people review every change, create issue templates and forms that produce complete bug reports, and add a pull request template.

📖 What You’ll Learn: the special .github files, CODEOWNERS syntax and precedence, enforcing code owner reviews, Markdown issue templates, YAML issue forms, the template chooser config, pull request templates, and organisation-wide defaults.


🧭 Why These Files?

As a repository grows, three questions come up again and again: Who should review this change? What information does a bug report need? What should every pull request description include? GitHub answers them with special files in the repository, usually in the .github/ folder:

FilePurpose
.github/CODEOWNERSAutomatically requests reviews from the owners of changed files
.github/ISSUE_TEMPLATE/*.md or *.ymlTemplates and forms for new issues
.github/ISSUE_TEMPLATE/config.ymlControls the “New issue” chooser
.github/pull_request_template.mdPre-fills every new pull request description

👥 CODEOWNERS

A CODEOWNERS file maps file patterns to people or teams. When a pull request changes matching files, GitHub automatically requests their review. Place it in .github/, the repository root or docs/.

# .github/CODEOWNERS
# Each line: <pattern> <owners...>. The LAST matching pattern wins.

# Default owners for everything
*                           @acme/qa-leads

# Test automation framework
/tests/                     @acme/qa-automation
/src/pages/                 @acme/qa-automation @asha-verma-qa

# CI and workflow changes need DevOps review
/.github/workflows/         @acme/devops

# Test data owned by one person
/test-data/*.json           @ravi-kumar

# Documentation
*.md                        @acme/docs
RuleDetail
Owners@username, @org/team-name or an email address; they need write access
PatternsMostly like .gitignore patterns
PrecedenceThe last matching line in the file wins, so put general rules first
EnforcementTurn on Require review from Code Owners in branch protection or rulesets to make owner approval mandatory

💡 Tip: GitHub shows a warning on the CODEOWNERS file page if a line has a syntax error or an owner without access. Check it after every change.


🐞 Issue Templates

Markdown templates

---
name: Bug report
about: Report a defect found in testing
title: "[Bug]: "
labels: bug, needs-triage
assignees: ''
---

### Steps to reproduce
1.
2.

### Expected result

### Actual result

### Environment
- Browser / device:
- Build / commit:

### Evidence
Screenshots, logs or Playwright traces.

Save as .github/ISSUE_TEMPLATE/bug_report.md. The block between --- lines (front matter) sets the name, default title, labels and assignees.

Issue forms (YAML)

Issue forms create a real form with required fields and dropdowns, which leads to more complete bug reports:

# .github/ISSUE_TEMPLATE/bug_report.yml
name: Bug report
description: Report a defect found in testing
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
  - type: textarea
    id: steps
    attributes:
      label: Steps to reproduce
      placeholder: "1. Log in as standard_user\n2. Add the backpack to the cart"
    validations:
      required: true
  - type: textarea
    id: expected-actual
    attributes:
      label: Expected vs actual result
    validations:
      required: true
  - type: dropdown
    id: environment
    attributes:
      label: Environment
      options:
        - QA
        - Staging
        - Production
    validations:
      required: true
  - type: dropdown
    id: severity
    attributes:
      label: Severity
      options: [Critical, Major, Minor, Trivial]

The template chooser

# .github/ISSUE_TEMPLATE/config.yml
blank_issues_enabled: false
contact_links:
  - name: Questions and discussion
    url: https://github.com/acme/qa-platform/discussions
    about: Ask questions here instead of opening an issue.

Issues are explained in GitHub Issues, Labels and Projects.


🔃 Pull Request Template

<!-- .github/pull_request_template.md -->
## What does this PR change?

## Why is it needed?
Closes #

## How was it tested?
- [ ] Unit tests
- [ ] Playwright tests pass locally (npx playwright test)
- [ ] Tested manually on: 

## Screenshots (UI changes)

## Checklist
- [ ] No secrets or debug code
- [ ] Docs updated if needed

Every new pull request starts with this text, which nudges authors to give reviewers what they need (pull requests). For several templates, create a .github/PULL_REQUEST_TEMPLATE/ folder; they are chosen with a ?template=name.md query parameter in the PR URL.


🏢 Organisation-Wide Defaults

Create a public repository named .github in your organisation (or account). Templates and community files there (issue templates, pull request template, CONTRIBUTING.md, SECURITY.md) act as defaults for repositories that do not have their own.


📝 Assignment

  1. Add .github/CODEOWNERS to git-practice that makes you the owner of everything and a teammate (or second account) the owner of tests/.
  2. Open a PR that changes a file in tests/ and confirm the review request is automatic.
  3. Add a Markdown bug report template and a YAML issue form; open an issue with each.
  4. Disable blank issues and add a contact link.
  5. Add a pull request template and open a new PR to see it pre-filled.
  6. Bonus: enable “Require review from Code Owners” in your ruleset.

❓ FAQ

🤔 What is a CODEOWNERS file?

A file in .github/, the root or docs/ that maps file patterns to users or teams who are automatically requested to review pull requests touching those files.

🤔 Where do I put the CODEOWNERS file?

In .github/CODEOWNERS, CODEOWNERS in the repository root, or docs/CODEOWNERS. GitHub uses the first one it finds in that order.

🤔 How do I create an issue template on GitHub?

Add Markdown or YAML files to .github/ISSUE_TEMPLATE/, or use Settings, Features, Issues, Set up templates.

🤔 How do I add a pull request template?

Create .github/pull_request_template.md; its content pre-fills every new pull request.

🤔 What is the difference between issue templates and issue forms?

Markdown templates pre-fill the text box. Issue forms (YAML) show structured fields such as dropdowns and required text areas.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Branch Protection Rules and Rulesets: Protect main Step by Step

Branch protection makes your team’s safe workflow mandatory: no direct pushes, required reviews and green CI before anything reaches main. In this lesson you will configure classic protection rules and modern rulesets, and connect required checks to GitHub Actions.

📖 What You’ll Learn: why protect branches, plan availability, every classic protection setting, rulesets and their advantages, a recommended ruleset for main, required status checks from GitHub Actions, rejected pushes, and protection by branch and tag.


🛡️ Why Protect Branches?

Without protection, anyone with write access can push directly to main, force-push over history or delete the branch. Branch protection makes the safe workflow mandatory:

  • Changes reach main only through pull requests.
  • Pull requests need approvals from reviewers (code review).
  • CI checks (tests, lint, build) must pass before merging.
  • Force pushes and deletions are blocked.

📝 Note: Branch protection and rulesets are available for public repositories on all plans. For private repositories, they require GitHub Pro, Team or Enterprise.

GitHub offers two systems: classic branch protection rules and newer rulesets. Both are covered below.


🧱 Classic Branch Protection Rules

Settings, Branches, Add branch protection rule (or Add classic branch protection rule). Enter the branch name pattern, e.g. main or release/*, and choose settings:

SettingWhat it enforces
Require a pull request before mergingNo direct pushes; changes come through PRs
Require approvals (e.g. 1 or 2)Minimum number of approving reviews
Dismiss stale approvals when new commits are pushedRe-review after changes
Require review from Code OwnersOwners of changed files must approve (CODEOWNERS)
Require status checks to passSelected CI jobs must succeed
Require branches to be up to date before mergingPR must include the latest main (tests run on the combined code)
Require conversation resolutionAll review comments resolved
Require signed commitsCommits must be verified (GPG, SSH or S/MIME)
Require linear historyNo merge commits (squash or rebase only)
Lock branchRead-only branch (e.g. old releases)
Do not allow bypassing the above settingsApplies rules to administrators too
Allow force pushes / Allow deletionsLeave off for main

📏 Rulesets (Recommended for New Setups)

Settings, Rules, Rulesets, New ruleset, New branch ruleset. Rulesets offer the same protections plus:

  • Several rulesets can apply at once and their rules are combined (the most restrictive wins).
  • An enforcement status: Active or Disabled (and Evaluate mode on some plans, to test a ruleset without blocking anyone).
  • A bypass list of roles, teams or apps allowed to bypass.
  • Targeting by branch patterns, the default branch or all branches, plus tag rulesets to protect release tags (tags).
  • Organisation-level rulesets on organisation plans, to apply one policy to many repositories.
  • Anyone with read access can see which rules apply, so contributors understand why a push was rejected.

A sensible ruleset for main

RuleSetting
Target branchesDefault branch
Restrict deletions✅
Block force pushes✅
Require a pull request before merging✅ 1 approval, dismiss stale approvals, require Code Owner review
Require status checks to pass✅ e.g. “playwright-tests”, “lint”
Require conversation resolution✅
Bypass listEmpty (or a release bot)

✅ Required Status Checks with GitHub Actions

Status checks are the job names from your workflows. A job must have run at least once (for example on a PR) before GitHub lets you pick it in the rule.

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
    branches: [ main ]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
  playwright-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test

Select lint and playwright-tests as required checks. A PR with failing tests then shows Merging is blocked. See Playwright in GitHub Actions.


🧪 What Happens When Someone Breaks the Rules?

$ git push origin main
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - Changes must be made through a pull request.
 ! [remote rejected] main -> main (push declined due to repository rule violations)

The fix is the normal workflow: push a branch and open a pull request.


💡 Recommended Protection by Branch

BranchSuggested protection
mainPR required, 1 to 2 approvals, required CI checks, no force push, no deletion
develop (Git Flow)PR required, required CI checks
release/*PR required, required checks, Code Owner review
Release tags v*Tag ruleset: restrict creation and deletion to maintainers
Feature branchesUsually unprotected

Match protections to your branching strategy.


📝 Assignment

  1. In your public git-practice repository, create a ruleset (or classic rule) for main: PR required, 1 approval, no force pushes, no deletions.
  2. Try git push origin main directly and read the rejection message.
  3. Add a simple GitHub Actions workflow with a job named lint (e.g. echo ok), run it on a PR, and make it a required status check.
  4. Make the job fail on purpose and observe that merging is blocked.
  5. Add a tag ruleset that protects tags matching v* from deletion.

❓ FAQ

🤔 What is branch protection in GitHub?

Rules that control how changes reach a branch, such as requiring pull requests, approvals and passing CI checks, and blocking force pushes and deletions.

🤔 What is the difference between branch protection rules and rulesets?

Both protect branches. Rulesets are newer: several can apply at once, they have enforcement statuses and bypass lists, can protect tags and can be defined at organisation level.

🤔 How do I require tests to pass before merging?

Add the CI job names as required status checks in a branch protection rule or ruleset; the jobs must have run at least once.

🤔 Why can’t I push to main?

The branch is protected and requires changes through a pull request. Push a feature branch and open a PR.

🤔 Is branch protection free?

For public repositories, yes. For private repositories it requires a paid plan such as GitHub Pro, Team or Enterprise.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

GitHub Personal Access Tokens: Create, Use and Secure Fine-Grained Tokens

GitHub no longer accepts passwords for Git and API access. In this lesson you will learn every way to authenticate, create a fine-grained personal access token with minimal permissions, use it safely with Git, the API and CI, and fix common token errors.

📖 What You’ll Learn: GitHub authentication methods, fine-grained vs classic tokens, creating a fine-grained token step by step, choosing permissions, using a token with Git over HTTPS and credential helpers, calling the API, tokens in CI and GITHUB_TOKEN, managing and revoking tokens, and troubleshooting.


🔑 How GitHub Authenticates Git and Tools

GitHub does not accept your account password for Git operations over HTTPS or for the API. Instead you use one of these:

MethodBest forLesson
Git Credential Manager (browser sign-in)Everyday HTTPS use on Windows and macOSConfigure Git
SSH keysDevelopers, Linux, many repositoriesSSH keys
Personal access token (PAT)Scripts, API calls, tools, HTTPS without a credential managerThis lesson
GITHUB_TOKENGitHub Actions workflows (automatic)Secrets and environments
GitHub CLI (gh auth login)Terminal work with GitHubGitHub CLI

A personal access token is a long, random string that acts like a password with limited permissions and an expiry date.


🧩 Fine-Grained vs Classic Tokens

Fine-grained tokenClassic token
Repository accessSelected repositories only (or all)All repositories you can access
PermissionsDetailed per area (Contents, Issues, Pull requests…)Broad scopes (repo, workflow…)
ExpiryRequiredOptional (can be “no expiration”)
Organisation approvalCan be required by the organisationNot available
Recommendation✅ Use by defaultOnly when a tool does not support fine-grained tokens yet

🛠️ Create a Fine-Grained Token

  1. On GitHub, open your profile picture menu, Settings, Developer settings, Personal access tokens, Fine-grained tokens.
  2. Click Generate new token.
  3. Token name: something descriptive, such as “Local test runner – git-practice”.
  4. Expiration: choose a short period (for example 30 or 90 days).
  5. Resource owner: you or your organisation.
  6. Repository access: Only select repositories, then choose the repositories.
  7. Permissions: grant only what is needed, for example:
TaskRepository permission
Clone, pull and push codeContents: Read and write
Read code onlyContents: Read-only
Create or comment on issuesIssues: Read and write
Open or update pull requestsPull requests: Read and write
Edit workflow files in .github/workflowsWorkflows: Read and write
  1. Click Generate token and copy it immediately: GitHub shows it only once.

⚠️ Common mistake: Treat a token exactly like a password. Never commit it, paste it into tickets or chat, or put it in a URL. If it leaks, revoke it immediately in the same settings page.


💻 Use a Token with Git over HTTPS

When Git asks for credentials:

Username: your-github-username
Password: <paste the token here>

A credential helper stores it so you are not asked every time:

git config --global credential.helper manager    # Git Credential Manager (Windows; also on macOS/Linux if installed)
git config --global credential.helper osxkeychain  # macOS Keychain
git config --global credential.helper store      # plain-text file (avoid on shared machines)

💡 Tip: If you use Git Credential Manager with browser sign-in, you usually do not need a token for Git at all. Tokens are mainly for scripts, API calls and tools.


🌐 Use a Token with the GitHub API

# Read the token from an environment variable, never type it in the command itself
curl -H "Authorization: Bearer $GITHUB_TOKEN" \
     -H "Accept: application/vnd.github+json" \
     https://api.github.com/user/repos

On Windows PowerShell set it for the session with $env:GITHUB_TOKEN = "<token>", or load it from a .env file in your scripts.


🤖 Tokens in CI and Automation

  • In GitHub Actions, prefer the built-in GITHUB_TOKEN: it is created for each run, scoped to the repository and expires automatically. Set its permissions with the permissions: key in the workflow.
  • When you need access to another repository (for example a private submodule), store a fine-grained PAT or a deploy key as an encrypted secret, never in the workflow file.
  • For organisation-wide automation, a GitHub App is often better than a personal token tied to one person.

🔄 Manage Tokens

  • Regenerate before expiry (GitHub emails a reminder).
  • Revoke tokens you no longer use.
  • For organisations with SAML single sign-on, authorise the token for the organisation (Configure SSO next to the token).
  • Check Settings, Security log for unexpected token use.

🐞 Troubleshooting

ErrorCause and fix
Support for password authentication was removedYou entered your account password; use a token, SSH or Git Credential Manager
403: Permission denied / Resource not accessibleToken lacks the permission or repository access; edit the token
refusing to allow a Personal Access Token to create or update workflowAdd the Workflows permission (or workflow scope for classic tokens)
Git keeps using an old tokenRemove the stored credential (Windows Credential Manager, macOS Keychain) and try again

📝 Assignment

  1. Create a fine-grained token with access only to git-practice, Contents read and write, expiring in 7 days.
  2. Clone git-practice over HTTPS in a new folder and push a commit using the token.
  3. Call https://api.github.com/user with the token from an environment variable.
  4. Revoke the token and confirm that pushing fails.
  5. Write down which authentication method you will use day to day, and why.

❓ FAQ

🤔 What is a GitHub personal access token?

A generated credential that replaces your password for Git over HTTPS, the GitHub API and tools, with limited permissions and an expiry date.

🤔 How do I create a personal access token in GitHub?

Go to Settings, Developer settings, Personal access tokens, Fine-grained tokens, Generate new token; choose repositories, permissions and an expiry, then copy the token.

🤔 Fine-grained or classic token: which should I use?

Fine-grained tokens, because they can be limited to specific repositories and permissions. Use classic tokens only when a tool requires them.

🤔 Where do I use the token when Git asks for a password?

Enter your GitHub username, then paste the token in the password field.

🤔 What should I do if my token is leaked?

Revoke it immediately in Developer settings, create a new one with minimal permissions, and check the security log.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Git Internals Explained: Objects, Trees, Commits, Refs and HEAD

What actually happens inside .git when you commit, branch or rebase? This lesson opens the hood: Git’s object database, how branches and HEAD work, and how everyday commands map to simple internal operations.

📖 What You’ll Learn: the .git folder, the four object types and content addressing, exploring objects with cat-file, ls-tree and hash-object, references, branches and HEAD, detached HEAD, how commands map to internals, and packfiles and garbage collection.


🔬 Why Learn Git Internals?

You can use Git for years without looking inside. But understanding how Git stores data makes every command make sense: why branches are instant, why rebase changes hashes, why “lost” commits can be recovered, and what HEAD really is. It also answers some of the toughest interview questions.


📁 Inside the .git Folder

ls .git
ItemWhat it stores
HEADWhich branch (or commit) you are on
configRepository settings (git config)
objects/All content: files, folders and commits, as compressed objects
refs/heads/Branches: one file per branch, containing a commit hash
refs/tags/Tags
refs/remotes/Remote-tracking branches such as origin/main
indexThe staging area (staging area)
logs/The reflog (git reflog)
hooks/Hook scripts (hooks)

🧱 The Four Object Types

Git is a content-addressable database: every object is stored under the hash of its content.

ObjectRepresentsContains
blobA file’s contentThe raw bytes (no file name!)
treeA directoryNames, modes and hashes of blobs and sub-trees
commitA snapshotA tree hash, parent commit hash(es), author, committer, message
tag (annotated)A named releaseThe tagged object, tagger, date, message (tags)
commit 8b1d4e2 ──▶ tree a1b2c3 ──▶ blob 5d6e7f  (README.md)
   │                     └──────▶ tree 9a8b7c  (tests/)
   │                                 └──────▶ blob 3c4d5e  (login.spec.ts)
   ▼
parent commit 3f2a91c

Hashes are SHA-1 by default (40 hex characters); newer Git versions can also create repositories that use SHA-256.


🔎 Explore Objects Yourself

git cat-file -t HEAD              # type of an object
git cat-file -p HEAD              # pretty-print the commit
tree a1b2c3d4e5f6...
parent 3f2a91c7d8e9...
author Asha Verma <asha.verma@example.com> 1793345000 +0530
committer Asha Verma <asha.verma@example.com> 1793345000 +0530

Add learning goals and initial test plan
git cat-file -p HEAD^{tree}       # list the root tree
git ls-tree -r HEAD               # every file in the commit with its blob hash
git cat-file -p <blob-hash>       # the file content
echo "hello" | git hash-object --stdin   # the hash Git would give this content

Because the hash depends on content, two identical files anywhere in history share one blob: Git stores each unique content only once.


🏷️ References: Branches Are Just Files

cat .git/HEAD
# ref: refs/heads/main

cat .git/refs/heads/main
# 8b1d4e2c7a9f0b1e2d3c4b5a69788f7e6d5c4b3a
  • A branch is a tiny file containing one commit hash. Creating a branch writes about 41 bytes, which is why it is instant.
  • Committing creates new objects and moves the current branch file to the new commit.
  • HEAD usually contains ref: refs/heads/<branch>. When it contains a hash directly, you are in detached HEAD (for example after checking out a tag or during bisect).
  • Packed refs may live in .git/packed-refs instead of individual files.

🔗 How the Commands Map to Internals

CommandWhat happens internally
git addCreates blobs and updates the index
git commitCreates trees and a commit object; moves the branch ref
git branch xCreates refs/heads/x pointing to the current commit
git switch xUpdates HEAD and the working directory from x’s tree
git mergeCreates a commit with two parents (or moves the ref for a fast-forward)
git rebaseCreates new commits (new parents, so new hashes) and moves the ref
git reset --hard XMoves the branch ref to X and updates index and files
git gcPacks objects into packfiles and removes unreachable ones after the reflog expires

📦 Packfiles and Storage

New objects start as individual compressed files (“loose objects”). git gc (run automatically from time to time) packs them into packfiles that store similar objects as deltas, which keeps repositories small.

git count-objects -vH       # number and size of loose and packed objects
git gc                      # pack and clean up (rarely needed manually)

📝 Assignment

  1. In git-practice, run cat .git/HEAD and cat .git/refs/heads/main (or look in packed-refs).
  2. Use git cat-file -p HEAD to follow your latest commit to its tree and to one blob’s content.
  3. Create a branch and confirm a new file appeared in .git/refs/heads/.
  4. Use git hash-object on a file and find the same hash in git ls-tree -r HEAD.
  5. Check out a tag or an old commit, then cat .git/HEAD to see detached HEAD.
  6. Run git count-objects -vH before and after git gc.

❓ FAQ

🤔 How does Git store data internally?

As objects in a content-addressable database: blobs for file content, trees for directories, commits for snapshots and tags, each identified by the hash of its content.

🤔 What is HEAD in Git?

A reference to what you have checked out. Usually it points to a branch (ref: refs/heads/main); in detached HEAD it points directly to a commit.

🤔 What is a blob in Git?

The object that stores a file’s content. It does not store the file name; names live in tree objects.

🤔 Why are Git branches so cheap?

A branch is just a small file containing a commit hash, so creating one does not copy any files.

🤔 Why does git rebase change commit hashes?

A commit’s hash includes its parent. Rebasing gives commits new parents, so Git creates new commit objects with new hashes.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in