| name | melpa-packaging |
| description | This skill should be used when the user prepares an Emacs Lisp package for MELPA submission, writes or reviews a MELPA recipe, decides which files ship in a package, resolves package-lint or checkdoc findings, aligns package headers (Package-Requires, Version, URL, Author, Maintainer), or handles the period between opening a MELPA pull request and acceptance. Trigger on mentions of MELPA, recipe, :files, package-build, package-lint, checkdoc, or *-pkg.el. |
| version | 2.0.0 |
Provide durable principles for shipping a MELPA-quality Emacs Lisp package: what the
build system does on your behalf, what belongs in source versus what is generated,
how to keep the recipe and the local lint/compile targets in agreement, and how to
triage the recurring review findings. The emphasis is on the reasoning ("why the rule
exists") so the guidance transfers to any package layout, followed by concrete examples.
<mental_model>
MELPA's package-build derives the distributable artifacts from your source; it does
not ship your working tree verbatim. It generates the package descriptor
(NAME-pkg.el) from headers in the main library, generates autoloads, and stamps a
version from the repository. Anything the build generates is something you should not
author or commit, because a committed copy either gets overwritten or, worse, ships
stale and conflicts with the generated one.
Treat the main library's comment headers as the single source of truth for package
metadata. Everything else about identity (descriptor, autoloads, version) flows
from there mechanically.
For the default (unstable) channel, package-build stamps the version from the date of
the latest commit that touched a file the recipe selects, formatted `%Y%m%d`. For the
stable channel it reads a matching SCM tag (parsed by `version-to-list`, adjustable
with `:version-regexp`). The `Version:` header you write is a floor/label, not the
published number.
Do not expect your `Version:` header to appear verbatim on MELPA unstable. Keep it
present and monotonic anyway — package.el and package-lint still read it, and tools
that consume the source directly rely on it.
The recipe's `:files` list is the exact boundary of what users receive. Anything you
exclude does not exist for an installed user, and anything you include ships whether or
not it is really part of the feature. A package that pulls in tests, fixtures, or build
scaffolding is larger and riskier than it needs to be.
Specify only what the feature needs to run. Then make your local byte-compile,
checkdoc, and package-lint targets operate on exactly that same set, so what you verify
locally is what users actually get.
<recipe_format>
A recipe is a single file in the archive's recipes/ directory, named exactly after the
package (no extension). Its contents are one Lisp form: (NAME :fetcher ... KEYWORDS...).
The filename must equal the package name and the main library's feature name.
(foo :fetcher github :repo "owner/foo")
The simplest correct recipe: default `:files`, latest commit drives the unstable version.
`:fetcher` may be `github`, `gitlab`, `codeberg`, `sourcehut`, or `git`/`hg` with an explicit `:url`.
(foo :fetcher github :repo "owner/foo"
:files ("src/*.el" (:exclude "src/foo-autoloads.el")))
Needed when sources live under a non-default directory such as `src/`.
Keep the trailing `(:exclude ...)` element to drop generated files; a bare list without it replaces (does not extend) the default exclusions.
(foo :fetcher github :repo "owner/foo"
:branch "main" :version-regexp "v?\\(.*\\)")
`:branch` overrides the default branch for the unstable build.
`:version-regexp` lets the stable channel parse tags like `v1.2.0` by capturing the numeric part; the captured group is passed to `version-to-list`.
Submission is a pull request that adds only the one recipe file. Reviewers commonly build
it locally (the archive provides a `make recipes/NAME` target and a sandboxed-install
target) before merging, so the recipe must build cleanly against the current repository
HEAD, not a local uncommitted tree.
Never commit a `NAME-pkg.el`. It is generated from the main library's metadata.
The descriptor's description, version, and requires are all read from the main
library's headers, so a hand-written descriptor is redundant at best and divergent at
worst.
Put package metadata in the comment headers of the main library (the file named after
the package): `Package-Requires`, `Version`, `URL`, `Keywords`, `Author`, `Maintainer`.
Headers must follow the `package.el` header format described in the Emacs Lisp manual's
Packaging section. `Package-Requires` is read literally to compute dependencies, so it
must list every dependency and a realistic minimum version, e.g.
`((emacs "29.1") (some-dep "1.2"))`.
;;; foo.el --- Do a useful thing -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Author Name
;; Author: Author Name <author@example.com>
;; Maintainer: Author Name <author@example.com>
;; URL: https://github.com/owner/foo
;; Version: 1.2.0
;; Keywords: convenience tools
;; Package-Requires: ((emacs "29.1") (dash "2.19"))
;; This file is NOT part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; ... (full GPLv3 notice) ...
;;; Commentary:
;;
;; One or more paragraphs describing what the package does.
;;; Code:
;; ... definitions ...
(provide 'foo)
;;; foo.el ends here
</code>
<notes>
<item>Summary line `;;; foo.el --- description` must stay under 80 characters, with the `lexical-binding` cookie in the `-*- -*-` block.</item>
<item>`Keywords` are space-separated and should be drawn from the standard finder keyword list (`M-x finder-list-keywords`); place them before `Package-Requires`.</item>
<item>End the file with `(provide 'foo)` followed by the `;;; foo.el ends here` footer line; both are checkdoc/package-lint expectations.</item>
</notes>
</example>
Every shipped `.el` file must begin with a `lexical-binding: t` file-local cookie.
The cookie belongs on the first line, in the `-*- ... -*-` block after the summary.
Understand the default `:files` set before overriding it. By default package-build
selects top-level and `lisp/` Elisp plus info/texinfo docs, and it already excludes
dotfiles and test files (`test.el`, `tests.el`, `*-test.el`, `*-tests.el`, and their
`lisp/` equivalents).
The default spec is roughly:
("*.el" "lisp/*.el"
"dir" "*.info" "*.texi" "*.texinfo"
"doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
"docs/dir" "docs/*.info" "docs/*.texi" "docs/*.texinfo"
(:exclude ".*.el" "lisp/.*.el"
"test.el" "tests.el" "*-test.el" "*-tests.el"
"lisp/test.el" "lisp/tests.el" "lisp/*-test.el" "lisp/*-tests.el"))
If your sources sit under `lisp/` and your tests match `*-test(s).el`, you often need
no `:files` at all. Add one only to change this set, and prefer the smallest override.
If source libraries live outside the default-selected locations (for example under
`src/`), the default file set will not pick them up. Declare `:files` explicitly, and
exclude generated files such as a committed `*-autoloads.el`.
Prefer a narrow inclusion with an explicit exclusion of generated files over a broad
wildcard, e.g. `:files ("src/*.el" (:exclude "src/NAME-autoloads.el"))`. Enumerate the
production modules if a wildcard would sweep in generated or non-shipping files.
Keep the set of files your Makefile (or equivalent) byte-compiles and lints identical
to the set the recipe ships. Point package-lint at the main file explicitly so
secondary modules are checked in the correct context.
Linting only the main library hides naming, custom-group, and docstring issues in
secondary modules. Run package-lint over every shipped file with the main file
designated (package-lint offers a main-file setting for exactly this). This convention
is a strong practice observed across well-maintained repos rather than an
automatically enforced MELPA rule.
Test files are not part of the package. They should not provide package features and
are excluded from the shipped set by default.
When tests need shared helpers, load helper files by path (for example via `load` under
`eval-and-compile`, making the load idempotent) instead of adding `provide`/`require`
feature coupling to test files. If an existing harness already relies on
feature-based `require` between test files, convert the loader to idempotent
path-based loading before removing those `provide` forms, or the test suite will fail
to load.
Until the recipe is actually merged and the package is live on MELPA, documentation
must not imply `package-install` works from MELPA.
During the pre-acceptance window, phrase availability conditionally ("Once available on
MELPA…") and document a working alternative such as `package-vc-install` or a
`use-package` `:vc` recipe. Apply the same care to changelog and section headings so
they do not assert submission or acceptance before it has happened.
<static_checks>
What the two required tools actually verify. Run both on every shipped file before
submitting. These are the concrete failures you will see, distinct from human-review
style requests.
The `lexical-binding: t` cookie is present on the first line.
The header summary line (`;;; NAME.el --- ...`) and the `;;; NAME.el ends here` footer exist and are well-formed.
`Package-Requires` is present and parseable when dependencies are used.
Each declared dependency is available from a configured package archive, and the pinned version actually exists (no non-existent or snapshot-only versions).
The declared `emacs` minimum matches the newest built-in symbol the code uses — package-lint knows which functions/variables were introduced in which Emacs version and will demand you raise the floor (or drop the call). This accuracy check is package-lint's central value.
Every defined symbol is namespaced with the package prefix; unprefixed `defun`/`defvar`/`defcustom`/`defface`/custom groups are flagged.
No reserved keybindings are bound (for example the user-reserved `C-c ` space).
`cl` is not `require`d directly; use `cl-lib`.
Designate the main file when linting a multi-file package (via `package-lint-main-file`, or `package-lint-batch-and-exit` with the main file first) so cross-file prefix and dependency checks resolve correctly.
Each docstring's first line is a single complete sentence in the imperative mood ("Toggle…", not "Toggles…" or "This toggles…").
Symbols referenced in docstrings are quoted as `` `symbol' ``; sentences end with two spaces before the next.
Interactive commands and public defuns/defvars have docstrings; the summary line stays within width.
The file has the expected section comments (`;;; Commentary:`, `;;; Code:`) and footer.
checkdoc governs docstring form; it does not enforce the predicate-naming or "not Global" phrasing below — those come from human reviewers applying Elisp conventions.
<review_findings>
Recurring items raised in MELPA human review and by package-lint/checkdoc. Split them
into blocking (the recipe will not be accepted, or the package misbehaves) and
deferrable (style or design calls that can wait). Do not silence a finding by weakening a
real design decision; record why it is deferred instead.
Missing `Maintainer:` header, or non-standard ad-hoc headers.
Add a `Maintainer:` header alongside `Author:`. Remove invented headers that are not
part of the package.el header convention; keep `Keywords` space-separated and place it
before `Package-Requires`.
`URL:`/homepage uses `http://` or points somewhere unreachable.
Use a canonical `https://` project URL.
License boilerplate is inconsistent across files, or a file claims "This file is part
of GNU Emacs."
Use "This file is NOT part of GNU Emacs." and keep the same GPL license block (and the
same GPL version line) across the main library and every shipped file. When tests
carry a header, match the main file's license version rather than mixing GPL-2+ and
GPL-3+.
`make-variable-buffer-local` used to declare a buffer-local variable.
Prefer `defvar-local` for variables intended to be buffer-local from the start; it is
the modern idiom reviewers expect.
Docstrings that checkdoc flags: non-imperative command summaries, mislabeled scope, wrong predicate phrasing.
Write command docstrings in the imperative mood ("Toggle…", not "Toggles…"). Do not
describe a buffer-local minor mode as "Global". For predicate functions (named with a
trailing `-p`/`p`), phrase the docstring as a question about what returns non-nil. Run
checkdoc and resolve every warning it emits on shipped files.
package-lint flags symbols that do not use the package's prefix, or reserved keybindings.
Give every defined symbol the package prefix. This is a genuine installability/coding
convention check, so treat prefix and keybinding warnings as blocking, not cosmetic.
Reviewer suggestions that touch behavior: redundant helper commands, cleanup-on-disable
semantics, calling the real `widen`, missing large-buffer performance tests.
Distinguish these from blocking findings. If a suggestion conflicts with an intended
design, keep the design and note the rationale in the pull request rather than changing
behavior to satisfy a non-blocking comment. Track genuine nice-to-haves separately so
they are not lost.
Pre-submission gate. Everything here should pass before opening the recipe pull request.
Main library headers present and correct: summary line with `lexical-binding: t`, `Author`, `Maintainer`, `URL` (https), `Version`, space-separated `Keywords`, then `Package-Requires` with every dependency and a realistic minimum.
No `NAME-pkg.el` committed; no committed generated autoloads shipped.
`Package-Requires` lists an `emacs` minimum that matches the APIs actually used.
Every shipped file carries the `lexical-binding` cookie, a matching `provide`, `Commentary`/`Code` sections, and consistent GPL boilerplate.
package-lint passes on every shipped file with the main file designated; checkdoc passes on every shipped file.
Package byte-compiles cleanly.
Recipe `:files` includes only what the feature needs; the default set is used unmodified where possible; tests and generated files are excluded.
Local compile/lint targets operate on the same file set the recipe ships.
Test files provide no package features; shared test helpers load by path, idempotently.
README/changelog make no MELPA-availability claim; a `package-vc`/`use-package :vc` install path is documented for the pre-acceptance window.
For stable-channel intent, an SCM tag exists in a form `version-to-list` accepts (add `:version-regexp` if the tag is prefixed).
<anti_patterns>
Checking in NAME-pkg.el or generated autoloads because "the build needs them".
Let package-build generate them. Keep only source; exclude any local generated copies via :files.
Using an over-broad `:files` that ships tests, fixtures, or generated files.
Start from the default set. Override only to reach non-default source locations, and pair inclusions with explicit `:exclude` of generated files.
Running package-lint/checkdoc on the main library alone and assuming the rest is clean.
Lint every shipped file with the main file designated, so secondary-module naming and docstring issues surface.
Documenting `package-install` from MELPA before the recipe is merged.
Phrase availability conditionally and document a VCS-based install until the package is live.
Changing real behavior only to make a non-blocking review comment go away.
Separate blocking from deferrable findings; keep intentional design and record the rationale.
Adding `provide`/feature coupling to test files so other tests can `require` them.
Load shared helpers by path idempotently; keep test files out of the package feature graph.
<decision_tree name="files_property">
Do you need a :files override at all?
No override — the default set already includes sources and excludes tests.
Add an explicit :files that reaches those sources and excludes generated files.
Extend :files to include the doc globs; keep them minimal.
Exclude it explicitly so it never ships.
</decision_tree>
Metadata lives in the main library headers; never commit a generated descriptor or autoloads.
Ship the minimal file set; keep local lint/compile targets aligned with the recipe.
package-lint and checkdoc must pass on every shipped file, with the main file designated.
Do not claim MELPA availability before the recipe is merged.
Prefer `defvar-local`, imperative command docstrings, `https` URLs, and consistent GPL boilerplate.
Keep test files out of the package feature graph.
Separate blocking review findings from deferrable design calls; record why anything is deferred.
<related_skills>
Emacs Lisp language, package.el, and editor-integration foundations
Macro-heavy source that must still satisfy package-lint prefix and docstring checks
README, Commentary, and changelog wording during the pre-acceptance window
Fetch current package-lint / package.el documentation when a check is ambiguous
</related_skills>
<related_agents>
Review package headers, docstrings, and lint findings against this guidance
Locate shipped-vs-generated files and recipe/Makefile drift across the repo
Align README/changelog availability claims with actual acceptance state
</related_agents>