Thuta Learning
BasicDevOps & Toolsbeginner

Using Actions from the Marketplace

What you'll walk away with

  • Explain the core ideas behind Using Actions from the Marketplace
  • Read the diagram and trace how the pipeline flows and what gates each stage
  • Read the workflow file and predict which jobs run, in what order, and what gates them

Build the mental model

An action is a packaged, reusable step: a repository containing an action.yml plus code - JavaScript, a Docker image, or a composite list of other steps. When you write uses: actions/checkout@v4, Actions finds github.com/actions/checkout, resolves the ref v4, and executes that code on your runner. That last clause is the whole security story: you are letting somebody else's code run inside a machine that holds your repository token and any secrets that job can see.

The ref after @ can be a branch, a tag, or a commit SHA. @v4 looks like a version number, but it is only a git tag, and tags can be moved. The actions/* organisation deliberately advances the v4 tag as it releases v4.1.7, v4.2.0. That is a genuine benefit: you get bug fixes and security patches without touching your YAML. It is also the risk: the same unchanged line of YAML can run different code tomorrow. If a maintainer account is compromised, which has happened more than once, an attacker repoints the tag at a malicious commit and every repository using it runs that code on its next build.

So the practical guidance splits by trust. Pinning first-party actions/* to a major tag is widely accepted. Third-party actions should be pinned to a full 40-character commit SHA, because a SHA names content and cannot be re-pointed. A trailing comment recording which tag that SHA was handles the readability cost, and Dependabot can bump those pins. Before adopting a new action, ask three questions: when was it last committed to, how many maintainers stand behind it, and do you actually need it - many marketplace actions replace a single run: line.

text
HOW A USES REFERENCE RESOLVES, AND WHERE IT CAN MOVE
----------------------------------------------------
  uses: actions/checkout@v4
         |        |       |
       owner     repo    ref
                          |
                          v
                  +---------------+
                  |  git tag v4   |   <-- a MOVING pointer
                  +---------------+
                          |
                     resolves to
                          v
          +-------------------------------+
          | commit 1a2b3c4... (today)     |
          +-------------------------------+

  the owner (or whoever steals the account) can re-point it:

                  +---------------+
                  |  git tag v4   | - - - - +
                  +---------------+         |
                                            v
          +-------------------------------+ |
          | commit 1a2b3c4... (yesterday) | |
          +-------------------------------+ |
          | commit deadbee... (malicious)  |<+
          +-------------------------------+

  your YAML line never changed. The code it runs did.

  uses: some-owner/some-action@<40-char-sha>   # cannot move

Connect it to a real scenario

The workflow below shows both halves of the practice. actions/checkout, actions/setup-node, actions/cache, actions/upload-artifact and pnpm/action-setup are pinned to major tags. These are maintained by GitHub itself and by the pnpm project, and tag pinning is the widely accepted trade-off there.

Study the cache step. Its key contains ${{ runner.os }} and ${{ hashFiles('pnpm-lock.yaml') }}. hashFiles produces a hash of that file's contents, so the moment dependencies change the key changes and a fresh cache is written instead of a stale one being reused - which is exactly the bug you get from a hand-written constant key. restore-keys gives a prefix fallback so a slightly changed lockfile still starts from a mostly-warm cache.

At the bottom, note the commented SHA-pin pattern. When you adopt a third-party action, open its repository, find the release you want, copy the full 40-character commit SHA of that tag, put it after the @, and record the tag in a trailing comment so a human reading the file still knows which version it is. Then add .github/dependabot.yml with package-ecosystem: "github-actions" so Dependabot opens pull requests to move those pins forward. That combination gives you immutability by default and updates on a schedule you control, rather than updates that arrive silently mid-week.

Try the working example

yaml
name: Pinned Actions

on:
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: First-party action, pinned to a major tag
        uses: actions/checkout@v4

      - name: Set up pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

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

      - name: Cache the framework build output
        uses: actions/cache@v4
        with:
          path: .next/cache
          key: next-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            next-${{ runner.os }}-

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Upload the build output
        uses: actions/upload-artifact@v4
        with:
          name: web-build
          path: .next/

      # A third-party action should be pinned by full commit SHA, like this:
      #
      #   - name: Publish the test report
      #     uses: some-owner/some-action@<40-character-commit-sha>  # v1.9.1
      #
      # Copy the SHA from the action's release page, and keep the tag it
      # matched in the trailing comment so humans can still read the file.
You should see
Every pull request targeting main runs a single build job. Its steps execute in order: check out the repository, install pnpm 9, install Node 20 with the pnpm store cached, restore .next/cache under a key derived from the lockfile hash, install dependencies with a frozen lockfile, build, and upload .next/ as an artifact named web-build. If the lockfile is unchanged the cache key matches and the cache is restored as a hit; if it changed, the key is new, restore-keys falls back to the closest prefix match, and a fresh cache is saved at the end of the run. Any step exiting non-zero fails the job and reports a failed check on the pull request. The commented block at the end runs nothing - it documents the SHA-pinning form for third-party actions.

5-minute try-it

List every uses: line in the workflows of a repository you actually work on. For each one that is not under actions/, check when it was last committed to, how many stars it has, and how many people maintain it. Then pick one and convert it from a tag pin to a SHA pin: copy the full commit SHA from its release page, put it after the @, and add a trailing comment naming the tag. Finally add .github/dependabot.yml with the github-actions ecosystem enabled, push, and see what update pull requests Dependabot opens for you.

One important caution

Writing uses: some-owner/some-action@main. Every commit the maintainer pushes to that branch runs inside your pipeline immediately, with no review and no version to roll back to.

Handing secrets to a third-party action without ever reading its source. It executes on your runner with access to whatever that job can see - trusting it is the same decision as installing an unaudited dependency.

GitHub Docs - Security hardening for GitHub ActionsCI/CD with GitHub Actions

Easy traps

  • Writing uses: some-owner/some-action@main. Every commit the maintainer pushes to that branch runs inside your pipeline immediately, with no review and no version to roll back to.
  • Handing secrets to a third-party action without ever reading its source. It executes on your runner with access to whatever that job can see - trusting it is the same decision as installing an unaudited dependency.
  • Validate a workflow on a branch or test repository before pointing it at a production deployment.

Exercise

List every uses: line in the workflows of a repository you actually work on. For each one that is not under actions/, check when it was last committed to, how many stars it has, and how many people maintain it. Then pick one and convert it from a tag pin to a SHA pin: copy the full commit SHA from its release page, put it after the @, and add a trailing comment naming the tag. Finally add .github/dependabot.yml with the github-actions ecosystem enabled, push, and see what update pull requests Dependabot opens for you.

You'll know it worked when: Every pull request targeting main runs a single build job. Its steps execute in order: check out the repository, install pnpm 9, install Node 20 with the pnpm store cached, restore .next/cache under a key derived from the lockfile hash, install dependencies with a frozen lockfile, build, and upload .next/ as an artifact named web-build. If the lockfile is unchanged the cache key matches and the cache is restored as a hit; if it changed, the key is new, restore-keys falls back to the closest prefix match, and a fresh cache is saved at the end of the run. Any step exiting non-zero fails the job and reports a failed check on the pull request. The commented block at the end runs nothing - it documents the SHA-pinning form for third-party actions.

Using Actions from the Marketplace | Thuta Learning