Drone is a Docker-based CI system, and that container-first design goes further than just the runtime environment: every plugin in a CI pipeline also runs as its own Docker container. That makes plugin development unusually flexible. You are not locked into a single language—Python, PHP, Node.js, Bash, and plenty of others all work, as long as the container can run them.
There are already many plugins available, and there is even an official Telegram plugin. The problem I ran into was that it did not support a custom Telegram API endpoint. For deployments on servers in mainland China, where Telegram often has to be accessed through a reverse proxy, that limitation makes it awkward to use. Since the node-telegram-bot-api package supports a configurable API base URL, writing a small Telegram notification plugin in Node.js turned out to be a straightforward solution.
How a Drone plugin works
A Docker container can define a startup command, so when Drone launches a plugin container, it can immediately execute the plugin script. At the same time, Drone injects both build information and plugin configuration into the container as environment variables.
That is the key reason plugins can be written in almost any language: every language can read environment variables.
A few naming conventions matter here:
- Plugin settings are injected with a
PLUGIN_xxxprefix, such asPLUGIN_TOKEN. - Build-related values are injected with prefixes like
CI_xxxorDRONE_xxx, for exampleDRONE_COMMITandCI_COMMIT_AUTHOR_NAME.
Writing the plugin
The implementation itself is very small. The main thing to remember is that plugin options should be read from variables starting with PLUGIN_. In this case, Telegram needs values such as PLUGIN_TOKEN.
Here is the plugin code:
const render = require('drone-render');
const TelegramBot = require('node-telegram-bot-api');
const {
PLUGIN_TOKEN,
PLUGIN_TO,
TELEGRAM_TOKEN,
TELEGRAM_TO,
PLUGIN_LANG,
PLUGIN_MESSAGE,
PLUGIN_BASE_API_URL
} = process.env;
const TOKEN = PLUGIN_TOKEN || TELEGRAM_TOKEN;
const TO = PLUGIN_TO || TELEGRAM_TO;
if(PLUGIN_LANG) {
render.locale(PLUGIN_LANG);
}
const bot = new TelegramBot(TOKEN, {
baseApiUrl: PLUGIN_BASE_API_URL
});
bot.sendMessage(TO, render(PLUGIN_MESSAGE));
There is not much to it. The script reads the token and recipient from the environment, optionally switches the rendering locale, creates a Telegram bot instance with a custom baseApiUrl, and sends the rendered message.
Packaging it with Docker
Once the plugin script is ready, the next step is building an image with a Dockerfile.
There is a small but useful optimization here. Every instruction in a Dockerfile creates a layer. Dependencies installed with npm install usually change less often than the plugin script itself, so it makes sense to copy package.json and install dependencies first, then copy the frequently updated script later. That way, repeated builds and pushes only need to upload the layers that actually changed.
FROM mhart/alpine-node:8.9.3
WORKDIR /telegram-node
COPY package.json /telegram-node/package.json
RUN npm install
COPY index.js /telegram-node/index.js
ENTRYPOINT [ "node", "/telegram-node/index.js" ]
After that, the image can be built and pushed for reuse:
docker build lizheming/drone-telegram-node .
docker push lizheming/drone-telegram-node
Testing locally
Before wiring it into a CI pipeline, it is easy to test the image directly with Docker:
docker run --rm \
-e PLUGIN_TOKEN=xxxxxxx \
-e PLUGIN_TO=xxxxxxx \
-e PLUGIN_MESSAGE=test \
-e PLUGIN_BASE_API_URL=xxxx \
lizheming/drone-telegram-node
If the message is delivered, the plugin is working.
A couple of issues worth noting
Reading values from secrets
Sensitive data such as tokens often should not be written directly into .drone.yml, especially when that configuration is visible to other project users. In those cases, Drone secrets are the better choice.
One important detail is that the secret name becomes the final environment variable name injected by Drone, so the naming cannot be arbitrary if your plugin expects a certain variable.
For example:
pipline:
telegram:
image: lizheming/drone-telegram-node
secrets: [ telegram_token, telegram_to ]
message: hello
Inside the plugin, the corresponding secret can then be read through process.env.TELEGTAM_TOKEN.
Why the plugin worked locally but not inside Drone
One problem I hit during development came from the Dockerfile entrypoint. My first version looked like this:
FROM mhart/alpine-node:8.9.3
WORKDIR /telegram-node
COPY package.json /telegram-node/package.json
RUN npm install
COPY index.js /telegram-node/index.js
ENTRYPOINT [ "node", "index.js" ]
Under a normal docker run, this works fine because WORKDIR is set, so index.js resolves to ${WORKDIR}/index.js.
But Drone needs access to the project source code when it runs a plugin, so it overrides the image's WORKDIR. That meant the container itself was healthy, and local execution looked fine, but once it ran inside Drone, the entry file could no longer be found and the plugin never actually started.
The fix was simply to use an absolute path in ENTRYPOINT instead of relying on the working directory.
Why this version was worth building
The main advantage of this plugin over the official Telegram plugin is support for a custom Telegram API address. For environments that depend on a reverse proxy to reach Telegram, that makes the plugin much more practical.
If you need Telegram notifications from Drone on servers inside China, this approach fits that use case well.