From abandoned sandbox to Drupal.org contrib: what it actually takes to publish a module
Authored on
I have a confession. For years I had a Drupal module sitting in a sandbox on Drupal.org that I never properly published. It worked — it shipped on a client site, the client was happy, I moved on. The sandbox just sat there, frozen in Drupal 8 time, quietly collecting dust while the ecosystem moved forward without it.
Sound familiar?
I finally finished it. And the process of getting from "abandoned sandbox" to a real, tagged, Composer-installable release on Drupal.org taught me more about what "done" actually means in open source than building the thing did in the first place.
Why it got abandoned
The original module was called ng2_entity — named after Angular 2, which tells you when it was built. It let you render Angular components in Drupal entity view displays using the PDB framework. It solved a real problem on a real project. After the project shipped, there was no obvious reason to publish it properly. It wasn't hurting anyone sitting in a sandbox. I'd get to it eventually.
Eventually took a few years.
What changed wasn't a sudden burst of motivation. It was more that I kept bumping into the same pattern — interesting Angular + Drupal integration ideas with no good contrib home. The module I'd built already solved the hard part of the data bridge: entity fields resolved server-side, handed off to Angular via drupalSettings. It was worth finishing. And honestly, having something sitting almost-done in a sandbox was starting to bother me more than the work of finishing it.
Starting fresh instead of patching
The first decision was whether to patch the old sandbox or start clean. I went back and read the code. Angular 2-era bootstrapping, three deprecated Drupal 8 APIs, a dependency on pdb_ng2 which was itself still a sandbox with no Drupal 10 path. The module name had a version number baked into it. Patching that would have meant untangling old assumptions at every layer.
Starting fresh was the right call. New module, new name, Drupal 10/11 from the ground up, @angular/elements instead of the old bootstrapping approach. The legacy sandbox stays as historical reference. The successor is pdb_angular_entity.
If you want the full technical write-up of what the upgrade actually involved — the architecture decisions, the live-test bugs, the config schema rabbit holes — I covered all of that in the previous article. This one is about everything that happens after the code works.
The work nobody talks about
Getting the code right was the part I knew how to do. The part that actually surprised me was everything else — the Drupal.org project infrastructure, the release tagging, the Composer packaging, the git remote setup. None of it is hard exactly, but none of it is obvious either, and there's no single place that explains all of it together.
A few things worth knowing if you're going through this:
The short name matters permanently. The machine name you pick for your Drupal.org project is permanent. I went through two wrong names before landing on pdb_angular_entity — first pdb_angular (already taken by an abandoned project), then pdb_angular_elements (too implementation-specific). The final name describes what the module actually does. Think about it before you create the project page, not after.
Drupal.org only recognizes stable releases from tags, not branches. You can create a 1.0.x-dev release that tracks a branch, but to publish a stable 1.0.0 you need to push an actual git tag. That's git tag 1.0.0 && git push drupal 1.0.0 — not glamorous, but easy to miss if you're used to GitHub's release UI doing more of the work for you.
Port 22 to git.drupalcode.org may not work. It timed out on two different networks for me. Drupal.org doesn't offer SSH over port 443 the way GitHub does. The solution is HTTPS with a Personal Access Token scoped to write_repository, cached via osxkeychain. Also: if you use Cursor, push from Terminal.app — Cursor's integrated terminal intercepts HTTPS git credentials before they reach osxkeychain.
The "Install with Composer" widget takes time to appear. Brand-new projects don't get the sidebar box immediately. The packaging system needs time to register. The composer require command works before the widget shows up — it's cosmetic lag, not a real problem.
What "stable" means when you're the only user
I tagged 1.0.0 after the module passed coding standards (phpcs --standard=Drupal — 0 errors), config schema was clean, and a full round-trip was validated on my own site. Angular component renders in an entity view display, block plugin places via Block UI, drupalSettings populated correctly, development mode console logging confirmed.
But I'm still the only person who has actually run it in production. That's a strange thing to call stable.
My answer to that is: stable means the feature set is complete and intentional, not that it's battle-tested at scale. The 1.0.x branch covers entity view displays — that's the promise of 1.0.0. The block plugin and development mode are on 1.x-dev because they're real but newer. The distinction matters to other developers evaluating whether to take a dependency on your module. Being honest about it is more useful than inflating the stable branch.
Contrib isn't done when the code ships
Here's the thing I didn't fully appreciate before going through this: publishing the module is the start of the conversation, not the end of the work.
The module uses presentation: angular as a PDB component type. PDB's own hook_library_info_build() has a hardcoded reference to a pdb_angular/angular library — a remnant from an older contrib module that never made it to Drupal 10. Every presentation: angular component on a site without that old module installed triggers a missing extension warning. I fixed it with a hook_library_info_alter() guard in pdb_angular_entity.module, but the cleaner long-term fix is upstream in PDB. That only happens through the issue queue and the maintainer relationship — not through code I can ship unilaterally.
That's what "part of the ecosystem" actually means. Not just that your module works alongside others, but that the rough edges get smoothed out through community conversation rather than workarounds.
The next piece: Angular AI
The reason I wanted pdb_angular_entity properly published before starting the next phase is that the next phase is more ambitious. I've opened an RFC on the issue queue for a sub-module called pdb_angular_entity_ai.
The pitch: Angular's resource() API and signals-based reactivity make AI-powered components a natural fit. A PHP controller in Drupal acts as an LLM proxy — keeps API keys server-side, uses Drupal's access system for authorization, streams responses back via Server-Sent Events. The Angular component consumes the stream via resource(), manages loading and error state via signals, and receives its prompt context through the same drupalSettings bridge that already carries entity field values. No new discovery mechanism needed. The existing presentation: angular component model just works.
There are open questions — whether to integrate with the Drupal AI module for provider abstraction, whether Genkit belongs in a contrib context, how Angular's resource() handles SSE in practice. The RFC is where that gets figured out. If you have opinions, that's the right place to put them.
The sandbox that sat dormant for years became a published contrib module. The contrib module becomes the foundation for something more interesting. That's how these things are supposed to work — you just have to actually finish the first part.
Happy coding!