Check an R Package Before Submitting to CRAN

What R CMD check verifies, how to read the ERROR/WARNING/NOTE output, and the current multi-platform workflow

Programming

A practical reference for checking an R package before you submit it to CRAN: what R CMD check / devtools::check() actually verifies, how to read the ERROR vs WARNING vs NOTE hierarchy (and which ones CRAN rejects for), and how to run the checks across platforms the current way — GitHub Actions, win-builder, and the macOS builder — plus a pre-submission checklist. Copy each recipe into your own package or CI.

Author
Published

July 18, 2026

Modified

July 18, 2026

TipWhat you’ll learn
  • What R CMD check (via devtools::check()) actually verifies about your package.
  • How to read the output — the ERROR / WARNING / NOTE hierarchy, and exactly which ones CRAN rejects for.
  • The current way to check across platforms — GitHub Actions, win-builder, and the macOS builder — and why the old single-call rhub recipe is retired.
  • A pre-submission checklist you can run before you hit submit.

Getting a package onto CRAN — the Comprehensive R Archive Network, R’s central package repository — means passing its automated checks. Those checks are the same R CMD check you can (and should) run yourself long before you submit. The trick is not just running the check but reading it: knowing which of the messages it prints will stop your submission cold, and which are routine.

This is a reference you can copy from. The workflow below talks to live services — GitHub Actions, win-builder, the macOS builder, the CRAN submission form — so nothing here executes in the page; copy each recipe into your own package or CI and run it there.

Note

Older tutorials — and an AI assistant drawing on them — often still suggest the retired rhub::check_for_cran() or the earlier single-call function names shown in the “stale vs. current” table below. The workflow here is the current one, and every load-bearing claim links to the primary docs. When in doubt, check what you’re told against those linked sources — they are the ground truth, and they change.

What R CMD check verifies

R CMD check is the command-line tool R ships for checking a package. It builds your package from source and then runs a long battery of tests: that the package installs, that the DESCRIPTION and NAMESPACE are well-formed, that every documented function matches its \usage, that examples run, that your tests/ pass, that vignettes build, that there are no undeclared dependencies, and much more. The full list lives in Writing R Extensions and the practical guide in R Packages.

The most convenient way to run it during development is devtools::check(), which builds a clean tarball and calls R CMD check on it:

devtools::check()

Every problem it finds is reported at one of three severity levels, from least to most serious: NOTE, WARNING, ERROR. Reading those correctly is the whole game.

Reading the output: ERROR, WARNING, NOTE

At the end of a run, R CMD check prints a summary like Status: 1 WARNING, 2 NOTEs. Here is what each level means and how CRAN treats it:

Status What it means Does CRAN reject for it? What to do
ERROR Something is genuinely broken — the package won’t install, an example or test failed, docs don’t match code. Yes — always. Fix it. A package with any error is not accepted.
WARNING A likely problem — a malformed DESCRIPTION, an undocumented argument, a non-portable construct. Yes. Fix it. Treat warnings as errors for CRAN.
NOTE Something worth a human’s attention — often benign (e.g. “New submission”), sometimes not. Sometimes. Eliminate what you can; justify the rest in cran-comments.md.

The bright line is at WARNING. The R Packages release chapter puts it plainly: “You must fix all ERRORs and WARNINGs. A package that contains any errors or warnings will not be accepted by CRAN.” CRAN’s own Repository Policy says packages “must pass R CMD check without warnings or significant notes to be admitted.”

NOTEs are the subtle part. Some are entirely routine — every first-time submission produces a New submission NOTE, which is expected. Others (an unexpected large installed size, a possibly-misspelled word, a URL that couldn’t be reached) may or may not block you. The guidance from R Packages is to “eliminate as many NOTEs as possible” because “each NOTE requires human oversight,” and CRAN’s policy explicitly lumps significant notes in with warnings. When you can’t remove a NOTE, CRAN’s policy tells you to “send an explanatory note … as a comment on the submission form” — which in practice means documenting it in your cran-comments.md (below).

The blanket advice from the R CMD check guide is the safest posture: “Our blanket advice is to eliminate all ERRORs, WARNINGs, and even NOTEs.” Aim for a completely clean Status: OK.

Check locally first

Before you check anywhere else, get a clean run on your own machine. devtools::check() is the everyday command; when you’re preparing an actual release, turn on the extra checks:

# Everyday check
devtools::check()

# Before a release: also check docs can build a PDF manual,
# and check against remote/CRAN-only conditions
devtools::check(remote = TRUE, manual = TRUE)

CRAN wants the final check run with the --as-cran flag, which turns on the stricter, CRAN-specific tests, and run against R-devel — the in-development version of R. (CRAN publishes three streams: R-release, the current stable R; R-devel, the next version under development; and R-oldrel, the previous minor version.) The Repository Policy is explicit: “Please ensure that R CMD check --as-cran has been run on the tarball to be uploaded before submission … with the current version of R-devel.” From the command line, that is:

R CMD check --as-cran yourpkg_0.1.0.tar.gz

A clean local check is necessary but not sufficient — your machine is one platform. CRAN builds on several, so you need to check on more than your own.

Check across platforms — the current way

CRAN tests submissions on Windows, macOS, and several Linux configurations, across R-release, R-devel, and R-oldrel. To catch platform- and version-specific problems before CRAN does, use three complementary services. Together they cover the matrix:

Platform / OS Tool R versions Notes
Linux, macOS, Windows GitHub Actions (r-lib/actions) release everywhere; devel and oldrel-1 on Linux Runs on every push/PR; the baseline.
Windows (official) win-builder devel, release, oldrel CRAN’s own Windows check machine.
macOS (Apple Silicon) macOS builder release (arm64) Same setup as CRAN’s M-series machine.

GitHub Actions (r-lib/actions)

The standard CI check is set up in one call with usethis, which writes a ready-made workflow file into your repository:

usethis::use_github_action("check-standard")

This drops in the maintained check-standard.yaml workflow from r-lib/actions (see the use_github_action() docs). Its build matrix is:

# from r-lib/actions check-standard.yaml (v2), abridged
matrix:
  config:
    - {os: macos-latest,   r: 'release'}
    - {os: windows-latest, r: 'release'}
    - {os: ubuntu-latest,  r: 'devel'}
    - {os: ubuntu-latest,  r: 'release'}
    - {os: ubuntu-latest,  r: 'oldrel-1'}

Notice that R-devel and R-oldrel are Linux-only in this baseline — Windows and macOS are checked on release only. That is exactly why win-builder and the macOS builder still matter: they fill the gaps this matrix leaves.

win-builder

win-builder is a free service run by the R project that checks your package on Windows. The devtools wrappers upload your source tarball to https://win-builder.r-project.org/ and email the results to the maintainer address in your DESCRIPTION (typically within ~30 minutes):

devtools::check_win_devel()      # R-devel   (the one CRAN wants)
devtools::check_win_release()    # R-release
devtools::check_win_oldrelease() # R-oldrel

See the check_win() docs. check_win_devel() is the important one — it matches the R-devel condition CRAN checks against.

macOS builder

For macOS on Apple Silicon, devtools::check_mac_release() submits your tarball to the R project’s macOS builder, which runs on the same arm64 configuration as CRAN’s Mac machine:

devtools::check_mac_release()

See the check_mac_release() docs. (There is a devel option on the web form; the packaged helper checks the release build.)

A note on rhub

For years the one-line recipe was rhub::check_for_cran(). That function is now deprecated and defunct — it no longer works. R-hub v2 (announced April 2024) was rebuilt around GitHub Actions: you run rhub::rhub_setup() once, then rhub::rhub_check() to launch checks on a wide set of platforms (or rhub::rc_submit() if your package isn’t on GitHub). See the rhub docs.

R-hub v2 is a perfectly good option — it shines when you need an unusual platform (a specific compiler, sanitizers, an exotic architecture). This reference centers the R project’s own services (GitHub Actions with r-lib/actions, win-builder, the macOS builder) because they cover the common matrix directly and are the toolchain most CRAN maintainers reach for first. Reach for R-hub v2 when you need the extra platforms — just don’t reach for the retired check_for_cran().

Here is the shift from the names you’ll still find in older material to the current ones:

Stale / retired Current
rhub::check_for_cran() (single call) rhub::rhub_setup() + rhub::rhub_check() (rhub v2, GitHub Actions)
usethis::use_github_actions() / use_github_action_check_standard() usethis::use_github_action("check-standard")
devtools::check_win() (generic) devtools::check_win_devel() / check_win_release() / check_win_oldrelease()
devtools::release() (interactive submit) usethis::use_release_issue() + devtools::submit_cran()
Hand-rolled R-CMD-check.yaml with a pinned container usethis::use_github_action("check-standard") (maintained r-lib/actions v2)

Pre-submission checklist

Once your checks are green across platforms, walk this list before submitting. Most of it is drawn from the R Packages release chapter:

  • Local check clean. devtools::check() reports Status: OK; before release, devtools::check(remote = TRUE, manual = TRUE).
  • Multi-platform check clean. GitHub Actions check-standard green, plus devtools::check_win_devel() and devtools::check_mac_release().
  • Spelling. devtools::spell_check() (a wrapper around the spelling package) — catch typos in docs before a CRAN reviewer does.
  • URLs. urlchecker::url_check() — CRAN rejects broken and redirecting URLs; see the URL check rules.
  • Reverse dependencies (only if other CRAN packages depend on yours). A reverse dependency is a package that imports or depends on yours; if you have any, run revdepcheck::revdep_check(num_workers = 4) to confirm your update doesn’t break them.
  • cran-comments.md. Scaffold it with usethis::use_cran_comments(), then document the environments you tested on and justify any remaining NOTEs — this is where you send CRAN the “explanatory note” the policy asks for.
  • Submit. Use devtools::submit_cran(). To drive the whole release as a tracked checklist, usethis::use_release_issue() opens a GitHub issue with every step (the older interactive devtools::release() is deprecated).

After you submit, CRAN runs its own automated incoming checks and then a human reviews the package. The Repository Policy notes that a pending version “may take a few days” — there is no published turnaround time, so plan for a few days and respond promptly if a reviewer emails you.

Common issues

The check passes locally but fails on win-builder or GitHub Actions. This is the single most common surprise, and it’s the whole reason to check on more than your own machine. The usual causes are platform-specific: a file-path or encoding assumption that only holds on your OS, a non-ASCII character in a data file, a URL that resolves from your network but not from the check server, or a dependency available on your machine but not in the clean CI environment. Read the specific failing platform’s log — the fix is almost always named right there in the NOTE or WARNING.

A NOTE about a “New submission” on your first upload. This is normal and expected — every package’s first submission produces it. You don’t need to remove it; just mention in cran-comments.md that this is a new submission. Don’t panic and don’t try to suppress it.

A NOTE about “possibly misspelled words” or an unreachable URL. These are the two NOTEs most likely to actually matter. Run devtools::spell_check() and urlchecker::url_check() locally and fix or whitelist each one before submitting, rather than leaving a reviewer to flag it.

Frequently asked questions

They are severity levels, from most to least serious. An ERROR means something is genuinely broken (the package won’t install, an example or test failed) — CRAN always rejects it. A WARNING flags a likely problem (a malformed DESCRIPTION, an undocumented argument) — CRAN rejects for it too. A NOTE is something worth a human’s attention; some are routine (a first submission), others (a significant note) can block you. The safe target is a completely clean Status: OK — eliminate every ERROR and WARNING, and as many NOTEs as you can.

The old rhub::check_for_cran() one-call recipe is deprecated and defunct — it no longer works. R-hub was rebuilt as rhub v2 (April 2024) around GitHub Actions: run rhub::rhub_setup() once, then rhub::rhub_check(). It’s still a good option, especially for unusual platforms (specific compilers, sanitizers, exotic architectures). For the common Windows/macOS/Linux matrix, most maintainers use GitHub Actions (r-lib/actions), win-builder, and the macOS builder directly.

There is no officially published turnaround time. CRAN runs automated incoming checks first, then a human reviews the package; the Repository Policy says a pending version “may take a few days.” Plan for a few days rather than hours, keep your submission clean to avoid a back-and-forth, and respond quickly if a reviewer emails you with a question.

No — you develop and check on whatever OS you have, then use the free R-project build services to check the platforms you don’t own: win-builder (devtools::check_win_devel()) checks Windows, and the macOS builder (devtools::check_mac_release()) checks Apple-Silicon macOS. GitHub Actions checks all three on every push. You never need to buy a second machine to satisfy CRAN’s cross-platform requirement.

Use three complementary services. Set up GitHub Actions with usethis::use_github_action("check-standard") — it checks Linux, macOS, and Windows on release, plus R-devel and R-oldrel on Linux. Fill the gaps with devtools::check_win_devel() (the R-devel Windows check CRAN wants) and devtools::check_mac_release() (Apple-Silicon macOS). Together they cover the matrix CRAN builds on.

Test your understanding

Your R CMD check run ends with:

Status: 1 WARNING, 2 NOTEs

The WARNING is “Undocumented arguments in documentation object ‘my_fun’”. One NOTE is “New submission”; the other is “Found the following (possibly) invalid URLs”.

Which of these must you fix before CRAN will accept the package, and how would you handle each?

Recall the bright line: where does CRAN draw the “must fix” boundary, and which NOTEs are routine versus the kind that require action?

  • The WARNING must be fixed. CRAN rejects any package with a WARNING. Document the missing arguments in the roxygen/.Rd for my_fun so the docs match the function signature, then re-check.
  • The “New submission” NOTE is routine — expected on a first upload. Leave it, and mention in cran-comments.md that this is a new submission.
  • The invalid-URL NOTE needs action. Run urlchecker::url_check(), then fix or update each flagged URL (broken and redirecting links both get flagged). If a URL is a genuine false positive, justify it in cran-comments.md.

Target Status: OK — one clean WARNING removed and both NOTEs resolved or explained.

Quick check. You’ve run usethis::use_github_action("check-standard") and it’s green. Do you still need devtools::check_win_devel()?

Yes. The check-standard matrix checks Windows on release only — R-devel and R-oldrel run on Linux. CRAN wants the --as-cran check on R-devel, and platform-specific problems can appear on Windows that Linux never sees. devtools::check_win_devel() covers exactly that gap.

Conclusion

Checking a package before CRAN is two skills: running the checks and reading them. Run devtools::check() locally with --as-cran conditions, then check across platforms the current way — GitHub Actions via use_github_action("check-standard"), plus check_win_devel() and check_mac_release() for the gaps that matrix leaves. Reading is the judgment part: fix every ERROR and WARNING, drive NOTEs to zero where you can, and justify the rest in cran-comments.md. Do that and CRAN’s automated checks hold no surprises — because you already ran them.

Citation

BibTeX citation:
@online{kassambara2026,
  author = {Kassambara, Alboukadel},
  title = {Check an {R} {Package} {Before} {Submitting} to {CRAN}},
  date = {2026-07-18},
  url = {https://www.datanovia.com/blog/check-r-package-before-cran},
  langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2026. “Check an R Package Before Submitting to CRAN.” July 18. https://www.datanovia.com/blog/check-r-package-before-cran.