Continuous Integration and Continuous Delivery Systems

Disclaimer: The translated content of this article is generated by a large language model.

For a long time, I have always felt that Continuous Integration and Continuous Delivery (CI/CD) systems are often redundant for individuals; running things locally using the project's package system or a simple Makefile is enough to get the build artifacts. As I encountered more and more complex projects, I realized that these systems indeed attempt to save time and standardize product release and deployment processes in a software engineering way. However, they also habitually lock this entire process into their own platform infrastructure. The ultimate goal is to turn what originally belonged to the development process into an infrastructure product that can continuously charge fees, gradually raise prices, and make it difficult for users to migrate away.

 

What exactly is CI/CD doing?

Putting aside all the terminology, what the vast majority of CI/CD systems do is actually quite similar: they listen for some event in the code repository and then, according to the workflow file in the repository, hand over a series of steps to a runner or agent to execute. The trigger could be a push, pull request, tag, cron job, or a manual click; the execution content ranges from the most basic code pulling, dependency installation, and running tests, to image building, publishing releases, uploading artifacts, and deploying online services.

So no matter how differently their interfaces are packaged, these systems generally cannot do without a few components: a server that receives webhooks, a scheduling layer that parses workflow files, a runner or agent that actually runs the commands, and a storage layer that saves logs, secrets, and build statuses. You can also think of it as a remote automated build machine, except that this machine also happens to decide who you hand your secrets to, where you buy your computing power from, and which platform your logs and artifacts eventually land on.

CI/CD 101: GitHub Actions

If you use GitHub as your entry-level CI/CD platform, the advantages of GitHub Actions indeed barely need introduction: you can run it just by writing a YAML file, and it is tightly integrated with pull requests, releases, packages, and secrets. The onboarding experience is extremely smooth. On GitHub's current pricing page, the free tier for private repositories still offers 2,000 minutes per month, and the Team tier offers 3,000 minutes, which seems reasonably generous.

The problem lies precisely in this convenience. The more accustomed you are to handling code hosting, pipelines, artifact management, and deployment within GitHub, the higher the cost of migrating out. GitHub updated a GitHub Actions Pricing Adjustment Announcement on December 15, 2025. The page summary currently states that they are "postponing" the billing changes for self-hosted runners, while reducing the price of GitHub-hosted runners by up to 39% starting January 1, 2026. However, the original announcement text retained on the same page clearly stated: they intended to introduce a platform fee of $0.002 per minute for all Actions workflows, including self-hosted runners, which was originally scheduled to take effect on March 1, 2026.

While this announcement claims to have "heard the feedback" and is "lowering prices for customers," in reality, it lays bare the platform's monetization logic: control planes, scheduling services, reliability upgrades, and enterprise-level scaling—all of these will ultimately be reflected in the bill. The free tier has been relatively conservative in the past, and quality of service always prioritized the platform itself. When product direction aligns with financial report targets, the "generous allowance" given to individual developers often turns out to be just an early promotional rate.

Even more amusing is GitHub's split pricing for Copilot. According to the official Copilot pricing page at the time of writing this article, the individual version has been divided into Pro for $10/month, Pro+ for $39/month, and Max for $100/month, along with an additional concept of AI Credits quotas. At one point, new sign-ups for these paid personal plans were even "temporarily paused." A product that originally focused on autocompletion has eventually evolved into a tiered menu resembling a cloud bill, which in itself is quite a dark comedy.

As soon as GitHub announced this round of Actions billing, companies relying on the GitHub ecosystem quickly followed suit. Blacksmith's post The GitHub Actions control plane is no longer free is essentially explaining GitHub's new platform fees on one hand, while pitching their faster machines, persistent Docker layer cache, and 3,000 free minutes per month on the other. This is, of course, very reasonable, but in essence, they are no different from GitHub: they are not helping you escape GitHub Actions, but are rather trying to snatch some customers from the runner budget after GitHub started charging transit fees.

Woodpecker: Lightweight and Conservative Accompanying Service

Compared to all-in-one platforms like GitHub, tools like Woodpecker CI and Forgejo Actions are clearly much more restrained. They often resemble an accompanying feature of a specific code hosting platform, or a small service that can be deployed independently, rather than a commercial platform attempting to run the entire R&D pipeline. Their design goals are usually very direct: pull the code, run a few steps in a container, and return the results to the forge.

The advantage of these systems is that they are lightweight, and their concepts are relatively simple. The downside is that "lightweight" and "conservative" are often two sides of the same coin. You can hardly expect them to spoon-feed you various add-on services like GitHub does, but conversely, such tools at least do not package "convenience" into a dependency that is hard to migrate. For individuals and small teams, I actually consider this an advantage. The behavior of this tool is to act as a plugin or extension of the core service, providing support to maintainers who plan to host their core services.

Jenkins: Heavyweight Enterprise Products

I have never actually deployed or maintained Jenkins, so I can only speak of my impressions here. But even with a brief exposure to its surrounding ecosystem of controllers, agents, plugins, permissions, upgrades, and operations terminology, one can feel a clear atmosphere of an internal enterprise tool. You could say it is mature, classic, and has a massive ecosystem, or you could say it is heavy, carries a lot of historical baggage, and has a rather typical Java-esque bloat.

If a team already has dedicated personnel to maintain this system over the long term, Jenkins certainly has its place. But for someone like me who just wants to build a personally controlled pipeline, it looks more like upgrading "writing code" to "part-time maintenance of enterprise middleware."

Finally: Crow CI

What attracts me most about Crow CI is that it doesn't pretend to be another GitHub. The first few features listed in the official documentation, combined with an ability I personally care about, can be summarized as the following four points:

  1. Support for multiple code hosting platforms: GitHub, GitLab, Forgejo, Gitea, and Bitbucket can all be integrated.
  2. Container backends can choose from Docker, Podman, and also support Kubernetes.
  3. Running memory requirement is approximately 150 MB.
  4. Email notifications can be sent when a build fails.

To translate these points more plainly: it provides support for multiple forges, flexible runner backends, customizable machine specs, and meets the requirements of different deployment environments. This means the pipeline itself won't be locked down to a single platform.

Beyond these surface-level features, Crow has another key design aspect: it is a thoroughly containerized tool. The official documentation directly describes it as a container-only application, meaning the pipeline is natively centered around containers, rather than running tasks on the host machine first and treating containers as an afterthought. This is quite different from the "host-first" default mindset of GitHub Actions, GitLab Runner, and Jenkins. For individual self-hosting scenarios, this approach makes it easier to form a set of configurations with clear boundaries that are highly portable.

Crow Autoscaler and Some Deployment Experience

Crow's installation documentation splits the components very clearly: the server is responsible for UI, API, webhooks, and pipeline parsing; the agent is responsible for actually executing tasks; and the autoscaler is an optional component used to spin up or destroy cloud provider machines on demand. The documentation clearly explains the complex, optional, and resource-consuming components.

If you are just self-hosting on a single machine, the most straightforward way is to run crow-server and crow-agent separately in containers and let both sides share the same CROW_AGENT_SECRET. By default, you can start with SQLite for the database, but if you plan to run it long-term, PostgreSQL or MariaDB is more suitable. This approach is very similar to the choice I made in my previous Forgejo deployment post: while getting the service up and running is certainly important, as long as you plan to maintain it long-term, you should move state data to a place you are willing to look after as early as possible.

In my own experience, deploying Crow's server is actually not that difficult. The documentation is quite detailed for most of the process, it just doesn't hide the complexity like GitHub does. If you can't find something in the documentation, understanding some of the configuration in the source code with the help of AI is also feasible. If the agent restarts frequently, it's best to persist the configuration file; otherwise, it is prone to repeatedly registering as new agents. If the server and agent are placed on different machines, an intranet direct connection is certainly the easiest; if going over the public internet, you must properly configure TLS and a reverse proxy for the gRPC channel.

The official documentation also provides a separate Crow Autoscaler documentation. It mentions that a single server can connect to multiple autoscalers simultaneously, with each autoscaler having its own registration token, provider configuration, and scaling limits. In theory, you can piece together different cloud providers, regions, and machine architectures. Although I have already configured an autoscaler here, I haven't actually run it yet because I haven't connected it to a real cloud provider, so for now, I've just reserved the interface.

Technical Features, and Why We Need to Readjust

One great feature of Crow is that it allows you to combine configurations at different levels. Code hosting can be connected to different forges; the execution backend can be Docker, Kubernetes, or even experimental Podman; agents can be lightweight, always-on machines, or combined with the autoscaler to become on-demand larger machines. The autoscaler documentation even specifically outlines a hybrid setup: letting static agents handle lightweight tasks while autoscaled agents handle heavy tasks, routing different workflows using labels. This design is particularly friendly to personal projects, as it allows you to design for "good enough" and "affordable" first, rather than deploying Kubernetes or expensive AWS services right off the bat based on enterprise templates.

Extensibility mainly comes from the Plugin System. Crow treats plugins as predefined pipeline steps used to encapsulate complex operations, while also reducing the chance of exposing sensitive credentials directly in regular shell steps. It is compatible with Woodpecker plugins and partially compatible with Drone plugins, so the ecosystem is not starting from scratch.

Another interesting point is its support for using Jsonnet instead of YAML to write pipelines. Compared to YAML, where you can only copy and paste anchors and aliases, Jsonnet actually has variables, functions, conditional branches, and imports. This makes it much more suitable for extracting repetitive build matrices, image tags, and shared configurations across multiple repositories. Of course, this also means you have to understand the pipeline as a "configuration program" rather than continuing to treat it as a growing pile of YAML.

The cost is also very clear. Once you stop using GitHub Actions, you can no longer rely on that managed platform experience where "repositories, actions, the marketplace, and hosted runners are all ready for me." Crow is not just a reskinned copy of GitHub Actions; it requires you to re-understand concepts like webhooks, agents, labels, secrets, pipeline compilation, and scheduling. For those who just want to save mental effort, this is certainly a drawback; but for those who want to regain control, this is precisely the appeal.

An Example of a Completed Migration

Currently, I have migrated the build of my previous lightweight Git repository simple-git-server to Crow CI. The corresponding code repository is on Forgejo, and the pipeline page is already hosted on the Crow CI instance.

This proves at least one thing: for projects of my current scale, Crow CI is not just a "toy alternative" used only for writing blog posts, but can actually take over daily builds. As for whether to continue gradually migrating email configurations and production project automation, that will be a matter to tinker with slowly in the next phase.

评论系统尚未配置。请在 .env 中填写 giscus 所需的环境变量。