All posts
github-actionsgitlab-cimigration

What GitLab CI Users Miss When Switching to GitHub Actions

Real pain points from developers who've made the switch — and workarounds for the biggest gaps.

Marc Campbell·

If you've used GitLab CI and recently switched to GitHub Actions, you've probably felt the friction. They're both CI/CD tools, but they're built on fundamentally different philosophies.

I recently asked the r/devops community about their biggest GitHub Actions frustrations, and the GitLab veterans had a lot to say. Here's what they miss most — and practical workarounds where they exist.

1. No Dynamic Pipeline Generation

The GitLab way: You can generate pipeline definitions at runtime. Fetch YAML from a URL, generate jobs based on what files changed, create child pipelines dynamically.

The GitHub way: Your workflow is static. What's in the YAML file is what runs.

Workaround: Use matrix strategies with fromJson() to dynamically generate jobs based on a JSON file or output from a previous step. It's not as elegant as GitLab's approach, but it works:

yaml
jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: |
          # Generate matrix based on changed files
          echo "matrix={...}" >> $GITHUB_OUTPUT

  build:
    needs: setup
    strategy:
      matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building ${{ matrix.component }}"

2. Self-Hosted Runner Concurrency

The GitLab way: Set concurrent = 4 in your runner config. One runner handles multiple jobs.

The GitHub way: One runner, one job. Want concurrency? Run multiple runners.

Workaround: Deploy multiple runner instances, or use a Kubernetes-based runner solution like Actions Runner Controller (ARC). It auto-scales runners as pods.

yaml
# ARC handles this at the infrastructure level
# Each job gets its own ephemeral pod

This is more operationally complex than GitLab's approach, but scales better for large workloads.

3. Container Working Directory Leaves Root-Owned Files

The problem: When you run jobs in a container, GitHub mounts the workspace into it. Files created as root inside the container stay root-owned. The next job run fails because the runner user can't clean up the workspace.

Workaround: Add a cleanup step at the start of your jobs:

yaml
- name: Fix permissions
  run: sudo chown -R $USER:$USER .

Or use Docker's --user flag in a custom container action to run as the correct UID.

4. No Scoped Secrets Without Organizations

The GitLab way: Group-level variables cascade down to projects. Perfect for shared credentials.

The GitHub way: Org-level secrets or repo-level secrets. Nothing in between for teams or project groups.

Workaround: Use organization secrets with repository access policies. Not as granular as GitLab groups, but you can at least limit which repos can access which secrets.

For cross-repo private dependencies, use a GitHub App with create-github-app-token:

yaml
- uses: actions/create-github-app-token@v1
  id: app-token
  with:
    app-id: ${{ secrets.APP_ID }}
    private-key: ${{ secrets.APP_PRIVATE_KEY }}
    repositories: "other-private-repo"

- uses: actions/checkout@v4
  with:
    repository: myorg/other-private-repo
    token: ${{ steps.app-token.outputs.token }}

This is more setup than GitLab's automatic CI_JOB_TOKEN, but it's more secure than long-lived PATs.

5. Opt-In Checkout (vs GitLab's Opt-Out)

The GitLab way: Your repo is automatically checked out. Disable it if you don't want it.

The GitHub way: Nothing happens unless you explicitly add actions/checkout.

The reality: This one's not really worse, just different. Explicit is arguably better — some jobs don't need the code at all. But if you're coming from GitLab, you'll forget the checkout step at least once.

yaml
steps:
  - uses: actions/checkout@v4  # Don't forget this

6. Manual Approvals Require Environments

The GitLab way: Add when: manual to any job.

The GitHub way: Use Environments with required reviewers. But environments are also used for deployments, which means approvers get notification spam.

Workaround: If you just need a "click to continue" gate without the deployment ceremony, consider using a workflow_dispatch with inputs instead:

yaml
on:
  workflow_dispatch:
    inputs:
      confirmed:
        description: 'Type YES to confirm'
        required: true

Not as elegant as a button in the UI, but avoids the environment baggage.

7. Job Output Display Truncation

The problem: Long job outputs get cut off. GitLab lets you download the full log.

Workaround: Upload logs as artifacts:

yaml
- name: Run long command
  run: ./build.sh 2>&1 | tee build.log

- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: build-log
    path: build.log

8. Monorepo Status Checks

The problem: Required status checks are repo-wide. In a monorepo, you can't require different checks for different paths.

This one hurts. There's no great workaround. Some teams use path filters to skip jobs entirely, then use if: always() on a final "gate" job that other checks can reference. It's hacky.

The best solution is often to split your monorepo into multiple repos, which... defeats the purpose.

The Philosophical Difference

One commenter put it perfectly:

"Calling Actions a CI or CD tool is a stretch. It's better to think of it as cloud functions which trigger on repository events."

That's not wrong. GitHub Actions is fundamentally an event-driven automation system that happens to be good at CI/CD. GitLab CI is a purpose-built CI/CD pipeline tool.

Neither is objectively better. But if you're coming from GitLab expecting the same mental model, you're going to fight the tool instead of using it.

What Runless Does

Runless analyzes your GitHub Actions workflows and catches common issues — missing timeouts, unpinned actions, inefficient caching patterns. It won't fix the architectural differences between platforms, but it will help you avoid the footguns that make GitHub Actions slower and more expensive than it needs to be.

Try the free analyzer →

Want to optimize your workflows?

Paste your GitHub Actions YAML and get instant recommendations — no signup required.

Try the Analyzer