CI/CD with GitHub Actions and Jenkins · Lesson 2 of 9 · about 35 minutes

One pipeline, two tools

How a GitHub Actions workflow and a Jenkinsfile describe the same pipeline: triggers, jobs and stages, where the work runs, and the one default that's opposite in each. You'll start Jenkins in Docker, run the same small pipeline in both tools, then break it on purpose.

By the end you'll know

  • The parts every pipeline has, whatever the tool
  • How to read a GitHub Actions workflow and a declarative Jenkinsfile
  • Why every GitHub job starts by checking out the code, and Jenkins stages don't
  • The default that's opposite in each tool: running side by side, or in order
  • How the same ideas look in GitLab CI/CD

Where you are

Lesson 1 was the why: catch problems early, on a clean machine, through the same gates every time. This lesson opens the files that make it happen. You'll read GitHub Actions and Jenkins side by side, then run the same small pipeline in each: Jenkins in a Docker container on your own computer, and GitHub Actions in a throwaway repository.

You'll need Docker running, the GitHub CLI (gh) signed in, and about half an hour.

The parts every pipeline has

Strip away the tool names, and every pipeline file answers the same five questions:

  • When does it run? The trigger: a push, a pull request, a tag, a schedule, or someone pressing a button.
  • Where does it run? The machine: a fresh virtual machine from GitHub, or an agent you look after.
  • What does it do? The steps: shell commands and ready-made pieces, like "check out the code".
  • How are the steps grouped? Into jobs (GitHub) or stages (Jenkins), which show up as separate boxes in the tool's page.
  • In what order? Which groups wait for which, and which run side by side. This is where the gates from Lesson 1 live.

Hold on to those five, because the rest of this lesson is the same five answers written two ways.

Couriers or one van

Think of getting a day's errands done. One way is to hire couriers: each one starts fresh from the depot, does one errand, and goes home. They can all go at once, but nothing carries over: if the second courier needs the parcel the first one picked up, someone has to hand it over at the depot. The other way is one van and one driver doing the stops in order, with everything piling up in the back as the day goes on. You can hire extra vans for the stops that don't depend on each other, but by default it's one van, one route. GitHub Actions jobs are the couriers. Jenkins stages are the van.

That difference explains most of what looks strange when you first compare the two files, so keep it in mind as you read them.

A GitHub Actions workflow

A workflow is a YAML file in .github/workflows/ in your repository. Here's a small one, shaped like CivicGrid's real pipeline but with echo in place of the real work, so you can see the structure without the detail. It's the one you'll run later:

YAML
name: First pipeline

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  where-am-i:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Running on $(hostname) as $(whoami) in $(pwd)"

  lint:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Lint would run here"

  tests:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Tests would run here"

  helm:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Helm check would run here"

  build:
    needs: [lint, tests, helm]
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Would build an image for run ${{ github.run_number }}"

Reading it against the five questions:

  • on: is when. This one runs on every push to main, and workflow_dispatch adds a Run workflow button in the Actions tab, so you can start it by hand.
  • runs-on: is where, set on every job, because every job gets its own machine. ubuntu-24.04 asks for a GitHub runner with that version of Ubuntu.
  • steps: is what. Each run: is a shell command. The other kind of step, uses:, pulls in a ready-made action someone else wrote, which you'll see in a moment.
  • jobs: is the grouping. Each key under it, like lint or build, is a job, and its name is what you'll see in GitHub's page.
  • needs: is the order. Without it, jobs run side by side. lint, tests and helm each need where-am-i, so they wait for it, then run at the same time. build needs all three, so it's the gate.

${{ github.run_number }} is an expression: GitHub fills it in before the step runs. Here it's the run's number, which goes up by one each time the workflow runs.

The same parts in CivicGrid's real file

Here's how CivicGrid's ci.yml answers the same questions, trimmed to the lines that matter here:

YAML
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: '3.13'
      - run: pip install ruff
      - run: ruff check app/

  build-and-push:
    runs-on: ubuntu-24.04
    needs: [lint, test, helm-lint]

Two things to notice. The trigger has three parts: pushes to main, tags starting with v, and pull requests into main. And every job starts with uses: actions/checkout@v7. That's the courier problem: each job starts on an empty machine, so the first thing it does is fetch the code. @v7 pins the action to a version, the same idea as pinning a Docker image tag in the Docker track.

Each trigger leads to different results, and it's easy to lose track of which. Pick an event and see what the real file does with it:

Try it: what does CivicGrid's ci.yml do when you…

  • lintRuns
  • testRuns
  • helm-lintRuns
  • build-and-pushBuilds, no push

Image pushed to GHCRNo

Tagspr-42sha-1a2b3c4 (worked out, not pushed)

The workflow listens for pull_request events on main, so all four jobs run. The image is built to prove it still builds, but the push step is turned off for pull requests, so nothing from an unmerged branch reaches the registry. The tags are worked out, but never used.

In a Jenkinsfile: A multibranch pipeline finds the pull request and runs it as its own job. Mark the push stage with when { branch 'main' } so it is skipped here.

A Jenkinsfile

A Jenkinsfile describes the same pipeline in Jenkins. It's written in a declarative syntax built on Groovy, a programming language that runs on Java, but you don't need to know Groovy to read or write it: it's mostly nested blocks in curly braces. Here's the same pipeline:

GROOVY
pipeline {
  agent any
  stages {
    stage('Where am I') {
      steps {
        sh 'echo "Running on $(hostname) as $(whoami) in $(pwd)"'
      }
    }
    stage('Checks') {
      parallel {
        stage('Lint') {
          steps { echo 'Lint would run here' }
        }
        stage('Tests') {
          steps { echo 'Tests would run here' }
        }
        stage('Helm') {
          steps { echo 'Helm check would run here' }
        }
      }
    }
    stage('Build') {
      steps {
        echo "Would build an image for build ${env.BUILD_NUMBER}"
      }
    }
  }
  post {
    success { echo 'Every stage passed' }
    failure { echo 'Something failed, so later stages were skipped' }
  }
}

The same five questions:

  • When: not in this file at all. Jenkins can't see your repository by itself; something has to tell it a commit happened. In real setups that's a webhook from GitHub, or Jenkins checking the repository on a timer, or a multibranch pipeline that finds every branch and pull request on its own. For this lesson you'll press Build Now.
  • Where: agent any means "on any machine Jenkins has available". It's set once, at the top, for the whole pipeline: one van.
  • What: steps holds the commands. sh runs a shell command and echo prints a message.
  • Grouping: each stage('…') is a group with a name, which shows as a column in Jenkins' page.
  • Order: stages run one after another, top to bottom. To run some side by side, you wrap them in a parallel block, which is exactly what Checks does.

post is something the GitHub file doesn't have: steps that run after everything else, depending on how it went. success runs only if everything passed, failure only if something failed. There's also always, which runs either way and is the usual place for cleanup.

Notice the strings, too. 'single quotes' are taken literally, while "double quotes" let Jenkins fill in values like ${env.BUILD_NUMBER}, the build's number. In the sh step, the outer single quotes mean Jenkins leaves $(hostname) alone, so the shell gets to run it. Mixing these up is one of the most common Jenkinsfile mistakes.

The default that's opposite

Put the two files next to each other and one difference stands out:

  • In GitHub Actions, jobs run side by side unless needs: says to wait.
  • In Jenkins, stages run in order unless a parallel block says otherwise.

Both pipelines above end up doing the same thing, but they get there from opposite ends. That matters when you move a pipeline from one tool to the other: forget a needs: when translating from Jenkins, and your build no longer waits for your tests. That's the "deploy that didn't wait" from Lesson 1's Spot the problem.

The other difference is the couriers and the van. GitHub jobs each start on a fresh machine, which is why each one checks out the code, and why handing a file from one job to the next takes an explicit upload and download, called an artifact. Jenkins stages share one workspace, a folder on the agent, so a file made in one stage is still there in the next. That's handy, and it's also how leftovers from one build can leak into the next if nothing cleans up, which is the "works on my machine" problem coming back in through the side door. Lesson 9 covers keeping agents clean.

Start Jenkins in Docker

Time to run both. Jenkins first. On your laptop, with Docker running:

BASH
docker run -d --name jenkins -p 127.0.0.1:8080:8080 -v jenkins_home:/var/jenkins_home --restart=on-failure jenkins/jenkins:lts-jdk21
  • jenkins/jenkins:lts-jdk21 is the official image: the long-term support (LTS) release, running on Java 21.
  • -v jenkins_home:/var/jenkins_home keeps everything Jenkins stores, its settings, jobs and plugins, in a Docker volume, so it survives the container being removed.
  • -p 127.0.0.1:8080:8080 publishes the web page on port 8080, but only on your own computer. Without the 127.0.0.1: part, anyone on your Wi-Fi could reach it. That's the firewall habit from the Networking track: don't open more than you need.

Jenkins' own instructions also publish port 50000, which is for agents on other machines to connect in. You don't have any yet, so it stays closed.

If the port is already taken

When I first ran this, Docker refused:

OUTPUT
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint jenkins (03237f25d3127cff7508419bda290091048de71b00b2405d96ecef8097b5db3b): Bind for 0.0.0.0:8080 failed: port is already allocated

Only one thing at a time can listen on a port, and something on my laptop already had 8080. Two commands find out what:

BASH
docker ps --format '{{.Names}}\t{{.Ports}}' | grep 8080
lsof -nP -iTCP:8080 -sTCP:LISTEN

The first lists running containers and their ports; the second asks the operating system which program is listening on 8080. For me it was a WordPress container from another project, published as 0.0.0.0:8080->80, and lsof showed Docker holding the port on every network (*:8080). That second detail was its own small finding: anyone on the same Wi-Fi could have reached that site. I didn't need the project any more, so I removed it, which freed the port. If you need whatever is on 8080, adding 127.0.0.1: to its port line closes it to your network, and you can give Jenkins another port instead.

Either way, Docker had still created the jenkins container, just without starting it, so remove it before trying again. To use a different port, change only the number on your side; inside the container it's still 8080, and only the door into it changes:

BASH
docker rm jenkins
docker run -d --name jenkins -p 127.0.0.1:8090:8080 -v jenkins_home:/var/jenkins_home --restart=on-failure jenkins/jenkins:lts-jdk21

If you do this, use http://localhost:8090 everywhere this lesson says 8080.

Jenkins takes a minute to start the first time. It makes a one-time password to unlock the setup page, which you can read from inside the container:

BASH
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Treat that like any password: paste it into the setup page and nowhere else. Then open http://localhost:8080 and:

  1. Paste the password to unlock Jenkins.
  2. Choose Install suggested plugins, and wait while it installs them. These include the Pipeline plugins that read Jenkinsfiles, and Git.
  3. Create your admin user. Use a real password: this account can run any command inside the container.
  4. Keep the suggested Jenkins URL, http://localhost:8080/, and finish.

Run your first Jenkins pipeline

  1. On the dashboard, choose New Item.
  2. Name it first-pipeline, choose Pipeline, and select OK.
  3. Scroll down to the Pipeline section. Leave Definition on Pipeline script, and paste the Jenkinsfile from above into the Script box.
  4. Select Save, then Build Now.

A build appears under Build History. Open it, then Console Output, to see everything it printed. The part to look for is the Where am I stage:

Here's the start and end of mine, with the stages in between left out:

OUTPUT
Started by user Kwami
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins
 in /var/jenkins_home/workspace/first-pipeline
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Where am I)
[Pipeline] sh
+ hostname
+ whoami
+ pwd
+ echo Running on 77b2592d84f4 as jenkins in /var/jenkins_home/workspace/first-pipeline
Running on 77b2592d84f4 as jenkins in /var/jenkins_home/workspace/first-pipeline
OUTPUT
[Pipeline] { (Build)
[Pipeline] echo
Would build an image for build 1
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Declarative: Post Actions)
[Pipeline] echo
Every stage passed
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

Reading it from the top:

  • Running on Jenkins names the machine the build ran on. Jenkins here is the built-in node: the controller itself, doing the work. That's fine for a first try, but real setups don't build on the controller, because a build can then reach everything Jenkins stores. Lesson 9 moves the work onto a separate agent.
  • /var/jenkins_home/workspace/first-pipeline is the workspace: a folder named after the job, which every stage shares. That's the van.
  • The lines starting with + are the shell showing each command just before it runs. Jenkins turns this on for sh steps, and it's very handy when a step fails, because the last + line tells you which command it was on.
  • 77b2592d84f4 is the hostname, which inside a Docker container is the container's own short ID. jenkins is the user the container runs as.
  • Would build an image for build 1 shows the double-quoted string at work: Jenkins filled in ${env.BUILD_NUMBER}, and this was build 1.
  • Declarative: Post Actions is the post block, running its success section, and Finished: SUCCESS is the result for the whole build.

The part in between, which isn't shown, has the three checks starting under parallel as Branch: Lint, Branch: Tests and Branch: Helm, with their lines mixed together because they ran at the same time.

Pasting the script into the job is the quickest way to try something, but it isn't how real teams work: the pipeline lives in Jenkins, not in your repository, so it has no history and no review. In real setups you choose Pipeline script from SCM, short for source control management, and Jenkins reads the Jenkinsfile from the repository, just as GitHub reads .github/workflows. That's pipeline as code, and it's where Lesson 3 goes.

Run the same pipeline in GitHub Actions

Now the couriers. Make a throwaway private repository for this track's experiments, so nothing here goes near CivicGrid:

BASH
cd ~/Apps
gh repo create cicd-sandbox --private --clone
cd cicd-sandbox
mkdir -p .github/workflows

Now create the workflow file. This writes the workflow from above into .github/workflows/first.yml:

BASH
cat > .github/workflows/first.yml <<'EOF'
name: First pipeline

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  where-am-i:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Running on $(hostname) as $(whoami) in $(pwd)"

  lint:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Lint would run here"

  tests:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Tests would run here"

  helm:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Helm check would run here"

  build:
    needs: [lint, tests, helm]
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Would build an image for run ${{ github.run_number }}"
EOF

The quotes around 'EOF' stop your own shell from filling in $(hostname) and the others while writing the file, so they run later, on GitHub's machine. Check it with cat .github/workflows/first.yml, then commit and push it:

BASH
git add .github/workflows/first.yml
git commit -m "First pipeline"
git branch -M main
git push -u origin main

git branch -M main makes sure the branch is called main, whatever your Git calls new branches. The push starts the workflow. This pipeline is short, so give it about half a minute, then check on it and read what each job printed:

BASH
gh run list --limit 1
gh run view --log

gh run list shows the latest run and whether it's finished. gh run view --log asks which run you mean, then prints every job's log. If it says the run is still in progress, wait a few seconds and try again. I first tried gh run watch straight after the push, and it said there was nothing to watch: GitHub hadn't registered the run yet, and by the time I tried again it had already finished.

The log is long, because every job starts with a few dozen lines describing the machine it got. Here are the lines that matter, from my run:

OUTPUT
where-am-i	Set up job	2026-10-25T06:53:45.2069338Z Region: westus2
where-am-i	Run echo "Running on $(hostname) as $(whoami) in $(pwd)"	2026-10-25T06:53:45.6611033Z Running on runnervmtai0g as runner in /home/runner/work/cicd-sandbox/cicd-sandbox
tests	Set up job	2026-10-25T06:53:51.8227034Z Region: centralus
lint	Set up job	2026-10-25T06:53:52.1248238Z Region: westus3
helm	Set up job	2026-10-25T06:53:52.4089192Z Region: centralus
build	Run echo "Would build an image for run 1"	2026-10-25T06:53:59.1362800Z Would build an image for run 1

Each line starts with the job, then the step, then a timestamp in UTC. Compare it with Jenkins:

  • A different machine every time. Jenkins ran as jenkins, inside its container, in a workspace named after the job. GitHub ran where-am-i as the runner user on runnervmtai0g, a virtual machine in Microsoft's Azure cloud, in the westus2 region. In this run, tests and helm got machines in centralus and lint one in westus3. Each job set up its own machine, which is the couriers, made literal.
  • Side by side, then the gate. where-am-i finished at 06:53:45. tests, lint and helm all started within a second of each other, about six seconds later, which is the time it takes to hand each job a fresh machine. build started at 06:53:58, once all three had finished.
  • Expressions are filled in first. The build step's name already says run 1: GitHub replaced ${{ github.run_number }} before the step ran. In the where-am-i step's name, $(hostname) is still written out, because that's left for the shell on the runner. GitHub's ${{ }} and the shell's $( ) look alike, but happen at different times on different machines.

Two more lines in each job's setup are worth a look in your own log: shell: /usr/bin/bash -e {0}, which means each run: step stops at the first command that fails, and a block headed GITHUB_TOKEN Permissions, which for this run said Contents: read, Metadata: read and Packages: read. That token, and keeping it to the least it needs, is Lesson 5.

Same pipeline, different machinery.

Break it on purpose

Now watch the gates work in both tools, like the animation in Lesson 1.

In Jenkins, open first-pipeline, choose Configure, and change the Tests stage so it fails:

GROOVY
stage('Tests') {
  steps { sh 'exit 1' }
}

exit 1 ends the shell with a failure code, which is how a failing test run looks to Jenkins. Save, then Build Now.

Here's the part of mine from the parallel block onwards:

OUTPUT
[Pipeline] { (Checks)
[Pipeline] parallel
[Pipeline] { (Branch: Lint)
[Pipeline] { (Branch: Tests)
[Pipeline] { (Branch: Helm)
[Pipeline] stage
[Pipeline] { (Lint)
[Pipeline] stage
[Pipeline] { (Tests)
[Pipeline] stage
[Pipeline] { (Helm)
[Pipeline] echo
Lint would run here
[Pipeline] }
[Pipeline] sh
[Pipeline] echo
Helm check would run here
[Pipeline] }
[Pipeline] // stage
[Pipeline] // stage
[Pipeline] }
[Pipeline] }
+ exit 1
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
Failed in branch Tests
[Pipeline] // parallel
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Build)
Stage "Build" skipped due to earlier failure(s)
[Pipeline] getContext
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Declarative: Post Actions)
[Pipeline] echo
Something failed, so later stages were skipped
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE

The lines from the three branches are mixed together, because they really did run at the same time: Lint and Helm printed their messages while the Tests branch was still starting its sh step. Then, in order:

  • + exit 1 is the shell showing the command it ran, which is how you'd find the failing command in a real build.
  • Failed in branch Tests says which part of the parallel block failed. Lint and Helm still finished normally, because nothing told them to stop.
  • Stage "Build" skipped due to earlier failure(s) is the gate. Build was never run.
  • Something failed, so later stages were skipped is the post block's failure section, which only runs in a build like this one.
  • ERROR: script returned exit code 1 and Finished: FAILURE are the result. On the job page, this build shows in red.

In GitHub Actions, make the same change in first.yml:

YAML
  tests:
    needs: where-am-i
    runs-on: ubuntu-24.04
    steps:
      - run: exit 1
BASH
git commit -am "Make the tests fail"
git push
sleep 30
gh run view

gh run view asks which run you mean; pick the newest, then View all jobs in this run. Mine said:

OUTPUT
X main First pipeline · 40171041003
Triggered via push about 2 minutes ago

JOBS
✓ where-am-i in 3s (ID 121101742744)
X tests in 4s (ID 121101758135)
  ✓ Set up job
  X Run exit 1
  ✓ Complete job
✓ helm in 3s (ID 121101758136)
✓ lint in 3s (ID 121101758152)
- build in 0s (ID 121101778491)

ANNOTATIONS
X Process completed with exit code 1.
tests: .github#5


To see what failed, try: gh run view 40171041003 --log-failed

The same story as Jenkins, told GitHub's way. tests failed at the Run exit 1 step, helm and lint passed on their own machines, and build has a - instead of a tick or a cross: it was skipped, in 0 seconds, because one of the jobs it needs failed. The suggested --log-failed is worth remembering: it prints only the logs of the steps that failed, which on a real pipeline saves scrolling through hundreds of lines.

In both tools, the other two checks still finish, because they don't depend on the tests, but the build never starts. That's the gate. Put both back how they were before moving on, and leave Jenkins running if you're carrying straight on to Lesson 3. If you're stopping, docker stop jenkins pauses it, and docker start jenkins brings it back with everything you set up.

The same ideas in GitLab CI/CD

GitLab's pipelines live in one file, .gitlab-ci.yml, at the top of the repository. It's YAML like GitHub's, and the same five questions map across all three tools:

IdeaGitHub ActionsJenkinsGitLab CI/CD
Pipeline file.github/workflows/*.ymlJenkinsfile.gitlab-ci.yml
When it runson:Webhooks, polling, multibranchworkflow: and rules:
A group of stepsJobStageJob
Orderingneeds:Stages run in orderstages: and needs:
Side by sideThe defaultparallel blockJobs in the same stage
Where it runsruns-on: a runneragentA runner, picked with tags:
A shell commandrun:shscript:
A reusable pieceuses: an actionPlugins and shared librariesinclude:
Only on mainif: github.ref == 'refs/heads/main'when { branch 'main' }rules: with $CI_COMMIT_BRANCH

GitLab's model is a mix of the other two: jobs are grouped into named stages that run in order, like Jenkins, while the jobs inside one stage run side by side and each starts on a fresh machine, like GitHub. The last lesson in this track ends with CivicGrid's whole pipeline written as a .gitlab-ci.yml, so you can read one with confidence.

Spot the problem

Three real mistakes. Work out what's wrong before you read the explanation.

The missing file. A workflow has two jobs. build runs make and creates a file called app.tar.gz. upload has needs: build, and runs a command to upload app.tar.gz. The upload fails: no such file. Why? Couriers. upload ran on a brand new machine that never had the file. needs: only controls the order, not what carries over. The fix is to upload the file as an artifact at the end of build, and download it at the start of upload, or do both in one job.

The pipeline that never runs. A team copies a workflow from an older project. It has on: push: branches: [master]. They push to their repository several times, and nothing ever appears in the Actions tab. No errors either. What's going on? Their default branch is called main, so no push ever matches master, and GitHub has nothing to run. It's a quiet failure, which makes it easy to miss: always check the Actions tab after adding a workflow.

The translation that lost a gate. A team moves their pipeline from Jenkins to GitHub Actions. The Jenkinsfile had three stages in order: Test, Build and Deploy. They write three GitHub jobs with the same names and the same steps. A week later, a build with failing tests gets deployed. What did they miss? The opposite default. Jenkins ran the stages in order without being told; GitHub runs jobs side by side unless told otherwise. Without needs: test on Build and needs: build on Deploy, all three started at once.

Where you are now

You can read both files now. A GitHub workflow is YAML: on for when, runs-on for where, steps for what, and jobs that run side by side until needs says to wait, each on a fresh machine. A declarative Jenkinsfile is nested blocks: an agent, stages that run in order until a parallel block says otherwise, all sharing one workspace, and post for what happens after. You've run the same pipeline in both, and watched a failing test stop the build in each. Lesson 3 swaps the echo lines for real work: linting and tests against Postgres and Redis, in both tools.

Check yourself

  1. In GitHub Actions, two jobs have no needs: line. How do they run?

  2. In a declarative Jenkinsfile, how do stages run unless told otherwise?

  3. Why does every job in CivicGrid's ci.yml start with actions/checkout?

  4. What does a Jenkinsfile's post block with a failure section do?

  5. A workflow has on: push: branches: [master], but the repo's branch is main. What happens on a push?

  6. Why does the docker run command publish Jenkins on 127.0.0.1:8080, not just 8080?

Key terms

  • Trigger (event): what starts a pipeline, like a push, a pull request, a tag or a schedule.
  • Job: a group of steps in GitHub Actions, run on its own runner.
  • Stage: a named group of steps in Jenkins; stages run in order by default.
  • Step: one command or ready-made piece inside a job or stage.
  • Action: a reusable piece of a GitHub workflow, pulled in with uses:.
  • needs:: makes a GitHub job wait for other jobs, and skip if they fail.
  • parallel: a Jenkins block that runs the stages inside it side by side.
  • Agent: a machine that runs Jenkins builds; agent any means any available one.
  • Workspace: the folder on a Jenkins agent where a pipeline's files live during a build.
  • Artifact: a file saved from one job so a later job, or a person, can download it.
  • Webhook: a message one service sends another when something happens, like GitHub telling Jenkins about a push.
  • Pipeline as code: keeping the pipeline definition in the repository, with its history and review.

Go deeper