Technology Apr 28, 2026 · 6 min read

When GitHub Actions Goes Silent: The Pending-Forever Bug I Hit Shipping My MCP Server to npm

I have an open-source MCP server. I tag a release, push, GitHub Actions builds, npm publishes, MCP Registry updates. That's the contract. It worked for v2.7.6 through v2.8.4. Then v2.8.5 didn't publish. Neither did v2.8.6. Or v2.9.0. Or v2.9.1. Or v2.9.2. Or v2.9.3. Six releases stuck. Not failing...

DE
DEV Community
by אחיה כהן
When GitHub Actions Goes Silent: The Pending-Forever Bug I Hit Shipping My MCP Server to npm

I have an open-source MCP server. I tag a release, push, GitHub Actions builds, npm publishes, MCP Registry updates. That's the contract. It worked for v2.7.6 through v2.8.4.

Then v2.8.5 didn't publish. Neither did v2.8.6. Or v2.9.0. Or v2.9.1. Or v2.9.2. Or v2.9.3.

Six releases stuck. Not failing — stuck. Yellow dot. Forever.

Here's what was actually happening. And how I got the releases out without GitHub Actions.

The symptom that doesn't match any docs

Every release event triggered the workflow. Every workflow showed up in the runs list. None of them ever started a job.

$ gh run view 25001890100 --json status,conclusion,jobs
{
  "status": "queued",
  "conclusion": null,
  "jobs": []
}

No conclusion. No jobs. Empty pending_deployments. Not "waiting for approval". Not "in_progress". Not "failure". Just pending with no work scheduled — for 125 hours.

If you search "GitHub Actions stuck pending", you'll find a hundred forum posts. Every answer assumes one of:

  1. You hit the runner concurrency limit (3 for free-tier macos)
  2. You have a deployment environment requiring approval
  3. Your runs-on: label is unreachable
  4. You're using self-hosted runners with no online agents

None of those applied. My workflow was simple, no environments with required reviewers, runs-on: macos-latest, no self-hosted runners.

The thing GitHub doesn't tell you in the run UI

The runs list shows pending. The run detail page shows pending. The job list shows nothing. The "deployment" tab shows nothing.

But if you look at your billing dashboard, there's a different story:

Your account has used 100% of included macOS minutes for this billing period.

That's it. That's the entire diagnostic. There is no banner on the run page. The workflow doesn't fail with a clear error. It just sits in the queue forever — because the runner that would pick it up doesn't exist, and the queue doesn't time out events.

The minutes counter resets monthly. Until it does, every release event becomes another silent pending row.

Two facts that surprised me

Fact 1: macOS runners cost 10x more than Linux runners. Both runs-on: macos-latest and runs-on: macos-13 charge against your Actions minutes at a 10x multiplier. The free 2,000 minutes/month gets you 200 minutes of macOS — about 20 release builds if each takes 10 minutes.

Fact 2: Switching to Linux didn't fix it. I changed runs-on: macos-latest to runs-on: ubuntu-latest. Same symptom. 0 jobs queued, status "pending". Why?

The macOS minutes meter is one bucket. The Linux meter is another. When the macOS bucket emptied, my pending macOS runs were still in the queue, blocking new runs. Even after switching the workflow to ubuntu, the concurrency group in the YAML serialized everything:

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

So new ubuntu runs queued behind old stuck macOS runs and never started.

The two-part fix

Part 1: workflow_dispatch with tag input

Adding a manual trigger lets you re-publish a tag whose release-event run got stuck, without deleting and recreating the GitHub Release:

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag to publish (e.g. v2.9.3)"
        required: true
        type: string

In every step that needs the tag, fall back through both event types:

- uses: actions/checkout@v6
  with:
    ref: ${{ github.event.inputs.tag || github.ref_name }}

That alone isn't enough — if the runner pool is still empty, the dispatched run also stalls. But it gives you a clean re-trigger path the moment runners are back.

Part 2: portable runner OS

The workflow downloaded mcp-publisher_darwin_${ARCH}.tar.gz — hardcoded "darwin". Switching to ubuntu broke that step. Generalize:

- name: Download mcp-publisher
  run: |
    OS=$(uname -s | tr '[:upper:]' '[:lower:]')
    ARCH=$(uname -m)
    if [ "$ARCH" = "x86_64" ]; then ARCH=amd64; fi
    if [ "$ARCH" = "aarch64" ]; then ARCH=arm64; fi
    curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" -o mcp-publisher.tar.gz
    tar -xzf mcp-publisher.tar.gz mcp-publisher

Now the same step works on macOS-arm64, macOS-x86_64, ubuntu-x86_64, and any future runner.

The manual workaround that actually shipped the release

While the workflow stays stuck, here's how I got v2.9.3 to npm and the MCP Registry from my laptop:

npm: the easy part

git checkout v2.9.3
npm publish --provenance --access public

--provenance requires a valid OIDC token, which only works inside GitHub Actions. Skip it locally:

npm publish --access public

You lose the provenance attestation, but the package ships. Provenance is a nice-to-have, not a publish blocker.

MCP Registry: the trickier part

The MCP Registry's CLI authenticates interactively:

mcp-publisher login github
# Opens a browser, asks you to paste a code, etc.

That's fine for humans. For a script — or for a Claude session running headless — you need non-interactive auth. The mcp-publisher binary accepts -token:

GH_TOKEN=$(gh auth token)
mcp-publisher login github -token "$GH_TOKEN"
mcp-publisher publish

The gh CLI you already use for everything else? Its token works as your GitHub PAT for mcp-publisher. No browser, no copy-paste.

After running these, the MCP Registry's io.github.achiya-automation/safari-mcp v2.9.3 went from "stuck on v2.7.6 for 3 weeks" to isLatest: true in about 15 seconds.

What I'd tell past-me

  1. Check the billing dashboard *first* when an Actions run sits pending with no error. The run UI does not surface "you're out of minutes". The billing page does.
  2. Don't trust runs-on: ubuntu-latest to "just be cheaper" — it is, but if you've burned your macOS minutes on stalled runs, the queue can still serialize new ones behind dead ones via your concurrency: group.
  3. Keep a manual publish path documented. Both npm and the MCP Registry have non-interactive auth options. Write the bash one-liners somewhere your future self can find them at 2am.
  4. workflow_dispatch with a tag input is cheap insurance. It costs you 6 lines of YAML and saves you from needing to delete-and-recreate GitHub Releases when the release-event run gets corrupted.

FAQ

Why didn't a timeout-minutes: rescue me?
That's a job-level timeout. It applies once a job starts. A run that never starts a job has nothing to time out.

Couldn't I have used a self-hosted runner?
Yes — and that's the right answer for high-volume projects. For an OSS hobby project, self-hosted is operationally heavier than the manual publish path.

Doesn't --provenance matter for supply-chain security?
For widely-installed packages, yes. For an OSS project's own emergency-publish workaround, the trade-off is "ship the release without provenance" vs "ship nothing". Pick the first one and re-publish with provenance on the next clean release.

Could I have known about the billing limit before hitting it?
GitHub does send an email when you cross 75% of your minutes. The email goes to the address on your billing account, which may not be the address you watch. Worth setting up a filter.

What about Actions minutes for OSS public repos?
GitHub gives unlimited minutes to public repos using GitHub-hosted runners — but that's only for repos owned by organizations on the Free plan, with the runner type matching the included unlimited tier. For personal accounts and certain runner combinations, the standard quota applies. Check the actual numbers under Settings → Billing → Plans for your specific account type.

If you've hit a similar stuck-pending pattern with no error in the run UI — that's the bug. Check your minutes. Then ship from your laptop.

The repo (with the workflow that handles all this now) is safari-mcp on GitHub.

DE
Source

This article was originally published by DEV Community and written by אחיה כהן.

Read original article on DEV Community
Back to Discover

Reading List