Build a CI/CD Deployment Gate

Build a GitHub Actions pipeline that blocks failed GitHub Pages releases.

Introduction

30 Second Summary

A website update can reach visitors before anyone notices that a required file is missing. The page is already live by the time the mistake becomes obvious.

In this project, you will build a release-status website with a CI/CD pipeline in GitHub Actions. A controlled failure will prove that your deployment gate protects the live site published through GitHub Pages.

What You'll Build

You'll demo a live release-status page whose workflow blocks an incomplete release while the current production page stays online.

By the end of this project, you'll have:

  • A live release-status page that shows visitors which approved release is currently in production.
  • A workflow history showing red validation, a skipped deployment, and a successful gated release.
  • A protected release path that leaves the current production page online when release-ready.txt is missing.
  • Secret Mission: A browser-controlled manual redeploy that launches the same gated workflow without creating another commit.

Are there any prerequisites?

You only need a Mac, internet access, and a free GitHub personal account. The project uses GitHub's browser editor, so no local installation is required.

Before We Start

Your CI/CD pipeline needs one clear release rule before any hands-on work begins. A successful check for release-ready.txt must happen before deployment starts.

Create Your GitHub Workspace

Your deployment gate needs one hosted workspace for its files. That workspace keeps the project accessible without local software.

GitHub provides the repository through your browser. GitHub Actions will run the pipeline later.

GitHub Pages will publish the release-status page from the same workspace. This approach keeps your attention on the deployment gate.

In this step, get ready to:
  • Create a verified GitHub personal account.
  • Create a public repository initialized with README.md.
  • Confirm the project navigation tools in your browser.
Create and verify your GitHub account

A verified personal account gives you access to repository creation. GitHub also uses your verified email for account communication.

Keep Credentials Private

Passwords are private credentials. Access tokens are private credentials too.

Neither belongs in this public project. GitHub's signup page is the only place in this step that needs your account details.

  • Visit the GitHub signup page.
  • Complete the account details requested on the signup page.
  • Finish the email verification GitHub sends to your inbox.
  • Confirm GitHub shows you as signed in.

Your verified account is now the identity behind every project change you make in the browser.

Having Trouble Verifying Your Account?

  • Check the inbox for the email address you entered during signup.
  • Check your spam folder if the verification message is missing.
  • Return to GitHub after completing the verification link.

Help me finish my GitHub account verification.

Create the project repository

A repository stores the website files plus the workflow that deploys them. Initializing it with a README gives you an immediate file to verify.

  • Select New repository from GitHub's repository menu.
  • Enter cicd-deployment-gate as the repository name.
  • Select Public as the visibility.
  • Enable Add README.
  • Click Create repository.

You should see the repository on its default main branch. The file list should contain README.md.

That's your hosted workspace in place. Every project file can now live under one repository name.

Why Use a Public Repository?

GitHub Pages on GitHub Free uses a public repository. The published website is also available on the internet.

GitHub Actions is free for public repositories using standard GitHub-hosted runners. This keeps the main project path at no cost.

Repository Missing or Incomplete?

  • Confirm your account email is verified if repository creation is unavailable.
  • Start repository creation again with Add README enabled if README.md is missing.
  • Check that the repository name is exactly cicd-deployment-gate.

Help me create the GitHub repository correctly.

Confirm the browser-only workspace

The repository page contains everything you need for this project. Its navigation separates files from workflow history and repository settings.

  • Find Code in the repository navigation.
  • Find Actions in the repository navigation.
  • Select Actions to confirm the workflow area is available.
  • Return to the repository by selecting Code.
  • Find Settings in the repository navigation.
  • Select Add file above the file list.
  • Confirm Create new file appears in the menu.

How Browser Editing Works

Create new file opens GitHub's browser editor. You will use it to add the website files plus the workflow.

Commit changes saves each edit to the repository. This replaces the local editor workflow for this project.

✔️ Awesome, I've got everything!

Great. Your public cicd-deployment-gate repository is ready in the browser.

ⓧ I'd like to double check the full code

README.md was created by GitHub when initializing the public cicd-deployment-gate repository with the Add README option enabled.

Before your final check, do you expect both the starter file and the workflow area to be available from the repository page?

  • Select Code in the repository navigation.
  • Confirm README.md is visible in the file list.
  • Confirm the Actions tab is available in the repository navigation.

You should see README.md on the main branch. The Actions tab should be ready for the workflow you add later.

Your GitHub workspace is ready in the browser. Next up, you'll publish the first visible release through GitHub Pages.

Publish the First Release

Your browser-only workspace is ready. The next milestone is a release that anyone can open in a browser.

A GitHub Actions workflow will carry your page from the main branch into a deployment.

GitHub Pages will host the result. This visible CI/CD baseline gives you something concrete to protect when validation arrives.

In this step, get ready to:
  • Create the first release page plus its approval marker.
  • Build a deploy-only workflow for pushes to main.
  • Publish Release 1: Live through GitHub Pages.
Create the release files

The HTML file supplies the page that visitors see. A small approval file establishes the release contract that your pipeline will validate later.

  • Keep passwords out of every file in this public repository.
  • Switch back to the repository's Code tab from earlier.
  • Click Add file above the file list.
  • Select Create new file.
  • Enter index.html in the filename box.
  • Add the first release page by pasting this code into the editor:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CI/CD Deployment Gate</title>
</head>
<body>
  <h1>Release 1: Live</h1>
</body>
</html>

What does this page do?

  • The <!doctype html> declaration tells the browser to render a modern HTML document.
  • The metadata keeps the page readable across browser window sizes.
  • The <title>CI/CD Deployment Gate</title> element names the browser tab.
  • The <h1>Release 1: Live</h1> element makes the deployed release state visible.
  • Click Commit changes to open the commit dialog.
  • Click Commit changes in the dialog to save index.html on main.

GitHub returns you to the repository. You should see index.html beside README.md in the file list.

Don't see index.html?

Confirm the filename is exactly index.html. It must sit at the top level of cicd-deployment-gate.

Return to the browser editor if the commit dialog is still open. Complete the commit before continuing.

Help me find why index.html was not created in my GitHub repository.

✔️ Awesome, I've got everything!

Your first release page is saved on main.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CI/CD Deployment Gate</title>
</head>
<body>
  <h1>Release 1: Live</h1>
</body>
</html>
  • Click Add file above the file list.
  • Select Create new file.
  • Enter release-ready.txt in the filename box.
  • Create the approval marker by pasting this exact content into the editor:
approved

What does this marker do?

The release-ready.txt file represents evidence that a release is approved. Its approved content gives the contract a clear human-readable meaning.

The validation job in the next step will require this file. Removing it will give you a deterministic failed release.

  • Click Commit changes to open the commit dialog.
  • Click Commit changes in the dialog to save release-ready.txt on main.

You should now see README.md, index.html, plus release-ready.txt in the repository.

Marker missing or misspelled?

Check that the filename uses hyphens exactly as shown in release-ready.txt. Confirm its content is the lowercase value approved.

Help me correct my release marker file.

✔️ Awesome, I've got everything!

Your page plus its approval marker are ready for deployment.

ⓧ I'd like to double check the full code

approved

Good progress. Your first release now has a visible page plus the approval evidence that later becomes its safety check.

Add the deploy-only workflow

A workflow trigger starts automation when a matching repository event occurs. This workflow listens for pushes to main.

Why GitHub Actions Instead of Branch Publishing?

Branch publishing can place static files online. GitHub Actions exposes the runner plus every deployment step in one workflow history.

That visible job graph is essential for the deployment gate you will build. You will be able to prove which job succeeded before production changed.

  • Switch back to the repository's Code tab.
  • Click Add file above the file list.
  • Select Create new file.
  • Enter .github/workflows/deploy.yml in the filename box.
  • Define the workflow name plus its trigger by pasting this first section into the editor:
name: CI/CD Deployment Gate

on:
  push:
    branches:
      - main

permissions:
  contents: read

What does this workflow setup do?

  • The name value identifies the workflow in the Actions tab.
  • The push trigger starts the workflow when repository changes are committed.
  • The branches filter limits that trigger to main.
  • The global contents: read permission gives the workflow read-only repository access by default.
  • Add one blank line after contents: read.
  • Define the deploy job by pasting this section below that blank line:
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v6.0.0
      - name: Upload GitHub Pages artifact
        uses: actions/upload-pages-artifact@v5.0.0
        with:
          path: '.'
      - name: Deploy GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5.0.1

What does the deploy job do?

  • The deploy job runs on a hosted runner selected by runs-on: ubuntu-latest.
  • The job's least-privilege permissions allow repository reads plus Pages publishing.
  • The github-pages deployment environment records the URL produced by the deployment step.
  • The actions/checkout@v7.0.1 action places the repository files in the runner workspace.
  • The actions/configure-pages@v6.0.0 action prepares the Pages metadata.
  • The actions/upload-pages-artifact@v5.0.0 action packages the repository as a deployable artifact.
  • The actions/deploy-pages@v5.0.1 action publishes the uploaded artifact.

✔️ Awesome, I've got everything!

Your browser editor now contains the complete deploy-only workflow.

ⓧ I'd like to double check the full code

name: CI/CD Deployment Gate

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v6.0.0
      - name: Upload GitHub Pages artifact
        uses: actions/upload-pages-artifact@v5.0.0
        with:
          path: '.'
      - name: Deploy GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5.0.1

Workflow path or indentation off?

Confirm the complete filename is .github/workflows/deploy.yml. The slashes create the required folders automatically.

YAML uses spaces to show nesting. Compare the indentation under jobs: with the full-code tab before committing.

Help me check my deploy-only workflow structure.

Publish with GitHub Pages

The repository's Pages source decides which publishing method controls the site. Selecting GitHub Actions connects the site to the custom workflow waiting in your editor.

  • Hold Cmd (macOS) or Ctrl (Windows) while clicking Settings to open it in a new browser tab.
  • Select Pages in the repository settings sidebar.
  • Open the Source menu under Build and deployment.
  • Select GitHub Actions as the publishing source.

The source now shows GitHub Actions. Your custom workflow can create the site's deployment.

Keep Public Files Safe

This GitHub Pages site is publicly available on the internet. Keep credentials plus private information out of every committed file.

  • Switch back to the workflow editor tab from earlier.
  • Click Commit changes to open the commit dialog.
  • Click Commit changes in the dialog to create .github/workflows/deploy.yml on main.
  • Select the repository's Actions tab.
  • Select the CI/CD Deployment Gate workflow.
  • Select the newest workflow run.
  • Select the deploy job to inspect its steps.

The first Pages publication can take up to 10 minutes. The deploy job may finish before the public page updates.

  • Wait for the deploy job to finish.

You should see a successful deploy job. Its checkout, configuration, artifact upload, plus deployment steps should all be green.

Before you open the site, which release heading do you expect to see?

  • Switch back to the repository settings tab.
  • Select Pages in the sidebar.
  • Click Visit site to open the deployed page.

You should see Release 1: Live as the page heading. That is your first automated deployment working from commit to public site.

Workflow failed or page missing?

If no workflow run appears, confirm .github/workflows/deploy.yml exists on main under the .github/workflows path.

If deployment fails, return to Settings plus Pages. Confirm the Source value is GitHub Actions.

If the job is green but the page is unavailable, wait up to 10 minutes before refreshing it.

Help me troubleshoot my first GitHub Pages deployment.

Your first release is live through an automated deployment. Next up, you will add a validation job and expose why independent CI plus CD jobs do not protect production.

Expose the Missing Deployment Gate

Your first release proved that GitHub Actions can publish a live page through GitHub Pages. Now you need to discover whether adding a validation job automatically protects that page.

A CI/CD check becomes a deployment gate only when its result controls the deployment path. This step tests two independent jobs against a deliberately incomplete release.

In this step, get ready to:
  • Add an independent validation job.
  • Create an unverified release candidate.
  • Compare the workflow outcomes with the live site.
Add an independent validation job

The new validate job turns release-ready.txt into a release contract. An artifact upload checks whether that evidence exists before marking validation as successful.

  • Return to the Code area of the cicd-deployment-gate repository.
  • Select the .github folder.
  • Select the workflows folder.
  • Select deploy.yml.
  • Choose the pencil-shaped edit control in the file toolbar.
  • Find the jobs: line.
  • Add the validation job above deploy: by inserting this code:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Verify release marker
        uses: actions/upload-artifact@v7.0.1
        with:
          name: release-ready-marker
          path: release-ready.txt
          if-no-files-found: error
          retention-days: 1

What does this job do?

  • The validate job runs on a hosted ubuntu-latest runner.
  • The checkout step makes the repository files available to the validation job.
  • The artifact step looks for release-ready.txt under the name release-ready-marker.
  • The if-no-files-found: error setting fails the job when the marker is missing.
  • The retention-days: 1 setting keeps a successful marker artifact for one day.
  • Select Commit changes.
  • Enter Add independent validation job in the commit message field.
  • Select Commit changes in the dialog.
  • Select the Actions tab.
  • Select the CI/CD Deployment Gate workflow.
  • Select the newest workflow run.

You should see both validate and deploy complete successfully. The existing marker proves the new validation job works when release evidence is present.

Don't see two successful jobs?

Check that validate: lines up with deploy: beneath jobs:. Confirm that release-ready.txt still appears in the repository.

Compare every space in the validation job with the reference below. Help me troubleshoot my validation job.

✔️ Awesome, I've got everything!

Great. Your workflow now contains two independent jobs that succeed while the release marker exists.

ⓧ I'd like to double check the full code

name: CI/CD Deployment Gate

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Verify release marker
        uses: actions/upload-artifact@v7.0.1
        with:
          name: release-ready-marker
          path: release-ready.txt
          if-no-files-found: error
          retention-days: 1

  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v6.0.0
      - name: Upload GitHub Pages artifact
        uses: actions/upload-pages-artifact@v5.0.0
        with:
          path: '.'
      - name: Deploy GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5.0.1
Commit an unverified release

A meaningful validation check needs a release that breaks its contract. You will remove the marker before changing the page heading to identify this candidate as unverified.

Deleting the marker can feel risky. You are removing one small project file while the repository and current live site remain available.

  • Return to the repository's Code area.
  • Select release-ready.txt.
  • Choose Delete file from the file toolbar.
  • Enter Remove release marker in the commit message field.
  • Select Commit changes.

The repository file list no longer includes release-ready.txt. Your release contract is now deliberately incomplete.

  • Select index.html from the repository's Code area.
  • Choose the pencil-shaped edit control in the file toolbar.
  • Find the heading text Release 1: Live.
  • Replace the heading text with Release 2: Unverified.
  • Select Commit changes.
  • Enter Publish unverified release in the commit message field.
  • Select Commit changes in the dialog.

The index.html file now contains the Release 2: Unverified heading. That commit starts a new workflow run against the missing marker.

Still see the marker or old heading?

Return to the repository's Code area. Confirm that release-ready.txt is absent from the top-level file list.

Open index.html to confirm that its heading contains Release 2: Unverified. Help me check my unverified release commit.

Observe the unsafe outcome

Before you inspect the run, do you think the failed validation check stopped the deployment job?

  • Select the Actions tab in the repository.
  • Select the CI/CD Deployment Gate workflow.
  • Select the latest run triggered by Publish unverified release.
  • Inspect the validate job to identify its result.
  • Return to the workflow run summary.
  • Inspect the deploy job to identify its result.

You will see the validate job in red because the marker is absent. You will see the deploy job in green because it ran independently.

Why did deployment continue?

GitHub Actions runs independent jobs in parallel by default. A failed job does not automatically control another job with no declared dependency.

Your workflow contains a validation check. Its job graph still allows unverified files to reach production.

GitHub Pages can take up to 10 minutes to publish a change. A short delay means the deployment is still propagating.

Before you refresh the site, which release do you think reached production?

  • Return to the public GitHub Pages site from the previous step.
  • Refresh the published page.

You will see Release 2: Unverified on the live page. The failed validation check did not protect production.

You have caught the pipeline flaw in action. A red validation job still allowed the unverified release onto the public site.

Still seeing Release 1: Live?

Confirm that the latest deploy job is green. Open that job's deployment URL to bypass an older browser tab.

Wait for the Pages publication window before refreshing again. Help me verify my GitHub Pages deployment.

Your workflow history now proves that a check alone does not create a gate. Next, you will connect the jobs so failed validation blocks deployment.

Block Failed Releases

The last workflow run exposed the unsafe path in your CI/CD pipeline. GitHub Actions published Release 2 even though validation failed.

An explicit job dependency turns validation into a deployment gate. This gate protects the current GitHub Pages release from a broken candidate.

In this step, get ready to:
  • Add a validation dependency to the deployment job.
  • Commit a blocked candidate while the release marker remains absent.
  • Prove that failed validation preserves the live release by skipping deployment.
Connect deployment to validation

Jobs run in parallel until a dependency connects them. The needs key makes one job wait for another job to succeed.

  • Return to the Code area of the cicd-deployment-gate repository.
  • Select the .github folder.
  • Select the workflows folder.
  • Select deploy.yml.
  • Select the file editor control in the upper-right corner.
  • Find this current deploy header:
  deploy:
    runs-on: ubuntu-latest

What does the current code mean?

The deploy job has no dependency here. GitHub Actions can start it without waiting for validate.

  • Insert needs: validate between the current lines so the deploy header reads:
  deploy:
    needs: validate
    runs-on: ubuntu-latest

How does the gate work?

The needs: validate line makes deploy dependent on validate. A failed validation now causes deployment to be skipped.

Committing this workflow edit starts a new run because the push trigger watches main.

  • Select Commit changes.
  • Enter Add deployment gate as the commit message.
  • Select Commit changes in the commit dialog.

Before you inspect this run, which job do you expect to stop after validation fails?

  • Select the Actions tab.
  • Select the CI/CD Deployment Gate workflow.
  • Select the newest workflow run.
  • Wait for the run to reach its final status.

You will see a red validate job. The deploy job is skipped because its dependency failed.

That relationship is now active. A failed marker check holds deployment behind the gate.

Is the deploy job still running?

  • Return to the workflow editor for .github/workflows/deploy.yml.
  • Check that needs: validate sits directly beneath deploy:.
  • Align needs: validate with runs-on: ubuntu-latest.
  • Confirm that release-ready.txt remains absent from the repository file list.

Help me fix the dependency between my validate and deploy jobs.

✔️ Awesome, I've got everything!

The workflow now contains the dependency that turns validation into a deployment gate.

ⓧ I'd like to double check the full code

name: CI/CD Deployment Gate

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Verify release marker
        uses: actions/upload-artifact@v7.0.1
        with:
          name: release-ready-marker
          path: release-ready.txt
          if-no-files-found: error
          retention-days: 1

  deploy:
    needs: validate
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v6.0.0
      - name: Upload GitHub Pages artifact
        uses: actions/upload-pages-artifact@v5.0.0
        with:
          path: '.'
      - name: Deploy GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5.0.1

What should the dependency look like?

The full workflow keeps the existing validation steps intact. The only new line is needs: validate inside the deploy job.

Create a blocked candidate

A deployment gate proves its value when a real candidate reaches it. The missing release-ready.txt marker gives this candidate a deterministic validation failure.

  • Return to the repository's Code area.
  • Select index.html.
  • Select the file editor control in the upper-right corner.
  • Find this current heading:
  <h1>Release 2: Unverified</h1>

What does the current heading represent?

This heading belongs to the unverified release that reached the live site before the gate existed.

  • Replace the current heading with this candidate heading:
  <h1>Release 3: Blocked Candidate</h1>

What does this candidate test?

The new heading makes the candidate visible if deployment runs. The absent marker should prevent that heading from reaching the public site.

This commit starts another workflow run with the gate active. The release marker remains absent.

  • Select Commit changes.
  • Enter Test blocked candidate as the commit message.
  • Select Commit changes in the commit dialog.

✔️ Awesome, I've got everything!

The repository now contains the blocked candidate heading while the release marker remains absent.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CI/CD Deployment Gate</title>
</head>
<body>
  <h1>Release 3: Blocked Candidate</h1>
</body>
</html>

What changed in the page?

The page keeps its existing structure. Its heading now identifies Release 3 as the blocked candidate.

Confirm production was protected

The candidate commit now tests the complete dependency path. Workflow history shows whether validation prevented deployment from starting.

Before you inspect the result, do you expect the candidate heading to replace the live release?

  • Select the Actions tab.
  • Select the CI/CD Deployment Gate workflow.
  • Select the newest run for Test blocked candidate.
  • Wait for the run to reach its final status.
  • Inspect the job graph on the run summary.

You will see the validate job in red. You will see the deploy job marked as skipped.

  • Return to the repository's Settings area.
  • Select Pages.
  • Select Visit site.
  • Refresh the published page.

You will still see Release 2: Unverified on the live page. The Release 3: Blocked Candidate heading remains only in the repository.

You have closed the unsafe deployment path. Failed validation now protects the existing release.

Did the blocked candidate reach the site?

  • Confirm that the newest run belongs to the Test blocked candidate commit.
  • Check that the run summary marks deploy as skipped.
  • Return to .github/workflows/deploy.yml if the deploy job ran.
  • Compare the deploy header with the full workflow above.

Help me find why my blocked candidate was deployed.

Your gate now preserves production when release evidence is missing. Next, you will restore the release contract so the approved page can pass through the same gate.

Ship an Approved Release

Your deployment gate stopped the blocked candidate. The live GitHub Pages site still serves Release 2: Unverified.

An approved release needs the missing marker before deployment can begin. In this step, you will restore that evidence and watch GitHub Actions complete the gated path.

In this step, get ready to:
  • Restore the required release marker.
  • Replace the blocked candidate with the approved page.
  • Prove that validation completes before deployment.
Restore the release contract

The missing release-ready.txt file caused the previous validation failure. Restoring it gives the pipeline the evidence required to approve the release.

The github.dev editor keeps both file changes together until you commit them. This creates one push containing the marker and the approved page.

  • Return to the cicd-deployment-gate repository's Code area.
  • Press . to switch to the github.dev editor.
  • Confirm that the bottom status bar shows main.
  • Select the new-file icon in the Explorer sidebar.
  • Enter release-ready.txt as the file name.
  • Add the approval marker by pasting:
approved

What does this marker do?

The validation job searches for release-ready.txt. Its approved content records the release decision in the repository.

The upload step succeeds when the file exists. That success unlocks the dependent deployment job.

  • Select index.html in the Explorer sidebar.
  • Select its existing content with Cmd+A (macOS) or Ctrl+A (Windows).
  • Delete the selected content.
  • Paste the document foundation as the first section:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CI/CD Deployment Gate</title>
  <style>
    :root {
      color-scheme: dark;
      font-family: system-ui, sans-serif;
      background: #0f172a;
      color: #e2e8f0;
    }

    body {
      min-height: 100vh;
      margin: 0;
      display: grid;
      place-items: center;
      background: linear-gradient(135deg, #0f172a, #1e293b);
    }

How does the foundation work?

  • The document metadata gives the page its title and responsive viewport.
  • The :root selector establishes the dark colour palette.
  • The body selector centres the release card in the viewport.
  • Append the card and heading styles below the document foundation:
    .card {
      width: min(680px, calc(100% - 3rem));
      padding: 2rem;
      border: 1px solid #334155;
      border-radius: 1rem;
      background: #111827;
      box-shadow: 0 24px 60px rgb(0 0 0 / 35%);
    }

    .eyebrow {
      color: #38bdf8;
      font-weight: 700;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    h1 {
      margin: 0.4rem 0 1rem;
      font-size: clamp(2rem, 7vw, 4rem);
    }

What styles the release card?

  • The .card selector creates the bordered panel around the release details.
  • The .eyebrow selector turns the pipeline label into a compact heading.
  • The h1 selector makes the release state the visual focus.
  • Append the pipeline styles below the heading styles:
    .pipeline {
      display: grid;
      gap: 0.75rem;
      padding: 0;
      list-style: none;
    }

    .pipeline li,
    .status {
      padding: 1rem;
      border-radius: 0.75rem;
      background: #1e293b;
    }

    .pipeline span {
      display: inline-grid;
      width: 1.8rem;
      height: 1.8rem;
      margin-right: 0.6rem;
      place-items: center;
      border-radius: 999px;
      background: #0369a1;
    }

How is the pipeline displayed?

  • The .pipeline selector spaces the three release stages.
  • Each list item receives its own dark panel.
  • The numbered span elements become circular stage markers.
  • Append the production status styles below the pipeline styles:
    .status {
      margin-top: 1.25rem;
      color: #bbf7d0;
    }

    .dot {
      display: inline-block;
      width: 0.7rem;
      height: 0.7rem;
      margin-right: 0.5rem;
      border-radius: 999px;
      background: #22c55e;
    }
  </style>
</head>

What signals production status?

The .status selector colours the production message green. The .dot selector adds a matching circular indicator.

The closing tags finish the page metadata section. The next section supplies the visible content.

  • Append the approved release content below the closing head tag:
<body>
  <main class="card">
    <p class="eyebrow">GitHub Actions deployment gate</p>
    <h1>Release 3: Approved</h1>
    <p>This page reached production only after the required release marker passed validation.</p>
    <ol class="pipeline">
      <li><span>1</span>Change committed to main</li>
      <li><span>2</span>Release marker validated</li>
      <li><span>3</span>GitHub Pages deployment completed</li>
    </ol>
    <p class="status" role="status"><span class="dot" aria-hidden="true"></span>Production is serving the gated release.</p>
  </main>
</body>
</html>

What will the page show?

  • The main heading identifies Release 3: Approved as the release state.
  • The ordered list shows the commit-to-deployment sequence.
  • The status message confirms that production serves the gated release.

✔️ Awesome, I've got everything!

Great. Your editor now contains the release marker and the final approved page.

ⓧ I'd like to double check the full code

approved
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CI/CD Deployment Gate</title>
  <style>
    :root {
      color-scheme: dark;
      font-family: system-ui, sans-serif;
      background: #0f172a;
      color: #e2e8f0;
    }

    body {
      min-height: 100vh;
      margin: 0;
      display: grid;
      place-items: center;
      background: linear-gradient(135deg, #0f172a, #1e293b);
    }

    .card {
      width: min(680px, calc(100% - 3rem));
      padding: 2rem;
      border: 1px solid #334155;
      border-radius: 1rem;
      background: #111827;
      box-shadow: 0 24px 60px rgb(0 0 0 / 35%);
    }

    .eyebrow {
      color: #38bdf8;
      font-weight: 700;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    h1 {
      margin: 0.4rem 0 1rem;
      font-size: clamp(2rem, 7vw, 4rem);
    }

    .pipeline {
      display: grid;
      gap: 0.75rem;
      padding: 0;
      list-style: none;
    }

    .pipeline li,
    .status {
      padding: 1rem;
      border-radius: 0.75rem;
      background: #1e293b;
    }

    .pipeline span {
      display: inline-grid;
      width: 1.8rem;
      height: 1.8rem;
      margin-right: 0.6rem;
      place-items: center;
      border-radius: 999px;
      background: #0369a1;
    }

    .status {
      margin-top: 1.25rem;
      color: #bbf7d0;
    }

    .dot {
      display: inline-block;
      width: 0.7rem;
      height: 0.7rem;
      margin-right: 0.5rem;
      border-radius: 999px;
      background: #22c55e;
    }
  </style>
</head>
<body>
  <main class="card">
    <p class="eyebrow">GitHub Actions deployment gate</p>
    <h1>Release 3: Approved</h1>
    <p>This page reached production only after the required release marker passed validation.</p>
    <ol class="pipeline">
      <li><span>1</span>Change committed to main</li>
      <li><span>2</span>Release marker validated</li>
      <li><span>3</span>GitHub Pages deployment completed</li>
    </ol>
    <p class="status" role="status"><span class="dot" aria-hidden="true"></span>Production is serving the gated release.</p>
  </main>
</body>
</html>
name: CI/CD Deployment Gate

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Verify release marker
        uses: actions/upload-artifact@v7.0.1
        with:
          name: release-ready-marker
          path: release-ready.txt
          if-no-files-found: error
          retention-days: 1

  deploy:
    needs: validate
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v6.0.0
      - name: Upload GitHub Pages artifact
        uses: actions/upload-pages-artifact@v5.0.0
        with:
          path: '.'
      - name: Deploy GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5.0.1
  • Select Source Control in the left Activity Bar.
  • Confirm that release-ready.txt appears under Changes.
  • Confirm that index.html appears under Changes.

Seeing both files under Changes proves that the editor is ready to include them in one commit.

Missing one of the changed files?

Return to the Explorer sidebar. Check that release-ready.txt sits beside index.html at the top level of the repository.

If index.html is absent from the change list, confirm that its heading says Release 3: Approved.

Help me check why github.dev does not show both changed files.

Trigger the gated pipeline

A single commit sends both pieces of release evidence to main. The push starts the same workflow that blocked the previous candidate.

Before you commit, which job do you expect to finish before deployment can start?

  • Select the plus icon beside Changes to stage both files.
  • Enter Ship approved release in the commit message field.
  • Click Commit & Push.
  • Return to the repository's GitHub.com page.
  • Select the Actions tab.
  • Select the CI/CD Deployment Gate workflow.
  • Select the newest workflow run.

You should see validate complete in green first. The deploy job then starts through its declared dependency.

Did the approved run fail?

Open the failed job to identify the affected step. If validation failed, confirm that release-ready.txt is present on main.

Check that the marker contains only approved.

Help me troubleshoot my failed approved release workflow.

Inspect the release evidence

The completed run records more than a green result. Its jobs show that the release marker became an artifact before the deployment started.

  • Select the green validate job.
  • Open the Verify release marker step.
  • Confirm that the marker upload completed successfully.
  • Return to the workflow run summary.
  • Confirm that release-ready-marker is listed as an uploaded artifact.
  • Select the green deploy job.
  • Open the Deploy GitHub Pages step.
  • Find the Pages deployment URL in the completed step output.

Before you visit the site, which release heading do you expect production to show now?

  • Return to the repository's Settings area.
  • Select Pages.
  • Click Visit site.

You should see a dark release card headed Release 3: Approved. The page also shows the three gated pipeline stages.

You have now proved the complete gate. The approved page reached production only after validation succeeded.

Still see the unverified release?

GitHub Pages changes can take up to 10 minutes to publish. Give the deployment time to propagate before refreshing the site.

Confirm that both jobs are green in the latest workflow run. A green deployment job proves that the approved page has entered the publishing process.

Help me check why GitHub Pages still shows the previous release.

Secret mission

Add a Manual Redeploy Trigger

Production sometimes needs a redeploy even when the source files have not changed. Add a browser-controlled manual trigger that runs the existing validation gate before GitHub Pages deploys the approved release again.

Clean Up Your Resources

Clean Up Your Resources

This public-repository project costs $0 to keep using standard GitHub-hosted runners. Choose whether to keep everything active, unpublish the site for now, or delete the project entirely.

Resources you used:

  • One public GitHub repository named cicd-deployment-gate with its project files.
  • One public GitHub Pages site displaying Release 3: Approved.
  • GitHub Actions workflow history with short-lived artifacts.

Keep everything running

No action needed. This option fits while you are still experimenting with the deployment gate.

  • Leave the public cicd-deployment-gate repository unchanged.
  • Keep the Pages site published.
  • Retain the workflow history for your blocked release and approved release.

Pushes to main still start the gated workflow. The Run workflow button remains available for manual redeployments.

Pause - I'll come back to this later

Pausing removes the public deployment while preserving the repository. The gated workflow remains ready for your next release.

  • Return to the cicd-deployment-gate repository on GitHub.com.
  • Select Settings in the repository navigation.
  • Select Pages.
  • Select Unpublish site.

The current Pages deployment is removed. A later successful workflow run creates a new deployment.

Delete - I don't want to use this again

Deletion is for when you want a completely fresh start. Repository deletion is permanent.

This choice removes the project and its deployment.

  • Return to the cicd-deployment-gate repository on GitHub.com.
  • Select Settings in the repository navigation.
  • Select General in the sidebar.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Follow each warning confirmation after reading it.
  • Enter cicd-deployment-gate when GitHub asks for the repository name.
  • Confirm the repository deletion.

GitHub removes the repository and its Pages deployment. This closes out the workflow history and artifacts listed above.

Nice Work!

Nice Work!

You made it! Your CI/CD deployment gate now protects a live GitHub Pages release through GitHub Actions.

You've learned how to:

  • Publish a live release-status page from a browser-built workflow.
  • Expose an unsafe job graph by showing how failed validation can still allow an unverified deployment.
  • Protect production with a deployment gate that uses the required marker plus needs: validate.
  • Secret Mission: Added workflow_dispatch to launch the same gated pipeline manually without creating a new commit.

Ready to quiz yourself?