Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions .opencode/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
name: release
description: Guide the release process for robotlb, including version bumping, Helm chart sync, changelog updates, and creating release branches.
---

## Purpose

Provide step-by-step instructions for releasing a new version of robotlb, ensuring proper versioning, Helm chart sync, changelog updates, and release branch management.

## When to use

Use this skill when asked to:
- Create a new release
- Bump the version
- Prepare a release PR
- Update the changelog for a release

## Prerequisites

Before starting a release:
1. Ensure you are on the `main` branch
2. Ensure the working tree is clean (no uncommitted changes)
3. Ensure local main is up to date with origin/main

## Version Decision Guide

Use Semantic Versioning (MAJOR.MINOR.PATCH). Determine the bump type by analyzing commits since the last release:

### Major Version (X.0.0)

Bump MAJOR when:
- Breaking changes to the CLI interface (removed/renamed commands or flags)
- Breaking changes to configuration format
- Breaking changes to public APIs
- Commit message contains `BREAKING CHANGE:` or `!` (e.g., `feat!: ...`)

### Minor Version (0.X.0)

Bump MINOR when:
- New features added (`feat:` commits)
- New CLI commands or flags
- New configuration options
- Backward-compatible enhancements

### Patch Version (0.0.X)

Bump PATCH when:
- Bug fixes (`fix:` commits)
- Documentation updates (`docs:` commits)
- Internal refactoring (`refactor:` commits)
- Performance improvements without API changes
- Dependency updates

### Decision Process

1. Run: `git log v$(sed -n 's/^version = "\(.*\)"/\1/p' ./Cargo.toml | head -n1)..HEAD --oneline`
2. Check commit messages for:
- `!` or `BREAKING CHANGE:` -> MAJOR
- `feat:` -> MINOR
- `fix:`, `docs:`, `refactor:`, etc. -> PATCH
3. If multiple types, use the highest precedence (MAJOR > MINOR > PATCH)

## Release Process

### Step 1: Verify Clean State

Ensure you're on main with no uncommitted changes and up to date with origin:

```bash
git checkout main
git pull origin main
git status # Should show "nothing to commit, working tree clean"
```

If there are local commits not on origin/main, they must be merged first.

### Step 2: Determine Version

1. Get current version:
```bash
cat Cargo.toml | grep '^version ='
```

2. Review commits since last release:
```bash
git log v<CURRENT_VERSION>..HEAD --oneline
```

3. Decide on MAJOR, MINOR, or PATCH bump based on the Version Decision Guide above.

### Step 3: Create Release Branch

Create a branch named `release/v{NEW_VERSION}`:

```bash
git checkout -b release/v<NEW_VERSION>
```

Example: `git checkout -b release/v0.3.0`

### Step 4: Update Version in Cargo.toml

Edit `Cargo.toml` and update the version field:

```toml
version = "<NEW_VERSION>"
```

The version is in the `[package]` section.

### Step 5: Update Cargo.lock

After changing Cargo.toml, update the lock file:

```bash
cargo update -p robotlb
```

### Step 6: Update Helm Chart Metadata

Keep the Helm chart version and appVersion in sync with the application release:

```bash
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' ./Cargo.toml | head -n1)
sed -i -E "s/^version: .*/version: ${VERSION}/" ./helm/Chart.yaml
sed -i -E "s/^appVersion: .*/appVersion: \"${VERSION}\"/" ./helm/Chart.yaml
```

### Step 7: Update Changelog

Generate the changelog using git-cliff:

```bash
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' ./Cargo.toml | head -n1)
git cliff -t ${VERSION} -u -p CHANGELOG.md
```

The changelog will be automatically updated with commits since the last release, grouped by type (Added, Fixed, Documentation, etc.).

### Step 8: Commit Changes

Stage and commit all changes:

```bash
git add .
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' ./Cargo.toml | head -n1)
git commit -m "release: Version $VERSION"
```

### Step 9: Push Branch and Create PR

Push the release branch:

```bash
git push -u origin release/v<NEW_VERSION>
```

Create a pull request to merge into main.

### Step 10: After Merge

After the PR is merged to main:
1. Create and push a tag: `git tag v<VERSION> && git push origin v<VERSION>`
2. The CI workflow automatically builds release artifacts and creates a GitHub Release

## Quick Reference

| Step | Command |
|------|---------|
| Check current version | `grep '^version =' Cargo.toml` |
| View recent commits | `git log v<CUR>..HEAD --oneline` |
| Create branch | `git checkout -b release/v<VER>` |
| Update lock file | `cargo update -p robotlb` |
| Update Helm chart | `sed -i -E "s/^version: .*/version: ${VERSION}/" ./helm/Chart.yaml` |
| Update changelog | `git cliff -t ${VERSION} -u -p CHANGELOG.md` |
| Commit | `git commit -m "release: Version <VER>"` |

## Checklist

- [ ] On main branch, clean working tree
- [ ] Pulled latest from origin/main
- [ ] Determined version bump type (MAJOR/MINOR/PATCH)
- [ ] Created release branch `release/v<VERSION>`
- [ ] Updated version in Cargo.toml
- [ ] Ran `cargo update -p robotlb`
- [ ] Updated Helm chart version and appVersion in `./helm/Chart.yaml`
- [ ] Ran `git cliff -t ${VERSION} -u -p CHANGELOG.md`
- [ ] Committed with message `release: Version <VERSION>`
- [ ] Pushed branch and created PR
- [ ] After merge: created tag `v<VERSION>`
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ After the chart is installed, you should be able to create `LoadBalancer` servic

### High availability

`robotlb` supports safe multi-replica deployment via Kubernetes Lease-based leader election.
Run at least 2 replicas and use pod anti-affinity so only one pod is active leader while the
other is standby.
`robotlb` supports safe multi-replica deployment via Kubernetes Lease-based leader election. Run at
least 2 replicas and use pod anti-affinity so only one pod is active leader while the other is
standby.

Example Helm values:

Expand Down Expand Up @@ -140,6 +140,9 @@ metadata:
# Requests specific IP address for the load balancer in the private network. If not specified,
# a random one is given. This parameter does nothing in case if network is not specified.
robotlb/lb-private-ip: "10.10.10.10"
# Whether to keep the public interface enabled. Set this to false for an internal-only load
# balancer. In that case, the private IP is announced in the Service status.
robotlb/lb-public-interface: "false"
# Node selector for the loadbalancer. This is only required if ROBOTLB_DYNAMIC_NODE_SELECTOR
# is set to false. If not specified then, all nodes will be selected as LB targets by default.
# This property helps you filter out nodes.
Expand All @@ -164,9 +167,9 @@ metadata:
# Location of the load balancer. This expects the code of one of Hetzner's available locations.
robotlb/lb-location: "hel1"
# Balancing algorithm. Can be either
# * least-connection
# * least-connections
# * round-robin
robotlb/lb-algorithm: "least-connection"
robotlb/lb-algorithm: "least-connections"
# Type of balancer.
robotlb/balancer-type: "lb11"
spec:
Expand Down
2 changes: 2 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub const LB_PROXY_MODE_ANNOTATION: &str = "robotlb/lb-proxy-mode";
pub const LB_NETWORK_ANNOTATION: &str = "robotlb/lb-network";
/// Annotation key for private IP mode.
pub const LB_PRIVATE_IP_ANNOTATION: &str = "robotlb/lb-private-ip";
/// Annotation key for enabling the public interface.
pub const LB_PUBLIC_INTERFACE_ANNOTATION: &str = "robotlb/lb-public-interface";

/// Annotation key for load balancer location.
pub const LB_LOCATION_ANNOTATION: &str = "robotlb/lb-location";
Expand Down
7 changes: 6 additions & 1 deletion src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ pub async fn reconcile_load_balancer(

let hcloud_lb = lb.reconcile().await?;

let ingress = build_ingress(&hcloud_lb, context.config.ipv6_ingress, lb.proxy_mode);
let ingress = build_ingress(
&hcloud_lb,
context.config.ipv6_ingress,
lb.proxy_mode,
lb.public_interface,
);
patch_ingress_status(&svc, &context, ingress).await?;

Ok(Action::requeue(Duration::from_secs(SUCCESS_REQUEUE_SECS)))
Expand Down
73 changes: 73 additions & 0 deletions src/controller/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ pub fn build_ingress(
hcloud_lb: &hcloud::models::LoadBalancer,
enable_ipv6: bool,
proxy_mode: bool,
public_interface: bool,
) -> Vec<Value> {
let ip_mode = if proxy_mode { "Proxy" } else { "VIP" };
let mut ingress = vec![];

if !public_interface {
for private_net in &hcloud_lb.private_net {
if let Some(ip) = &private_net.ip {
ingress.push(json!({
"ip": ip,
"ipMode": ip_mode
}));
}
}
return ingress;
}

let dns_ipv4 = hcloud_lb.public_net.ipv4.dns_ptr.clone().flatten();
let ipv4 = hcloud_lb.public_net.ipv4.ip.clone().flatten();
let dns_ipv6 = hcloud_lb.public_net.ipv6.dns_ptr.clone().flatten();
Expand Down Expand Up @@ -76,3 +89,63 @@ pub async fn patch_ingress_status(

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

fn test_load_balancer() -> hcloud::models::LoadBalancer {
hcloud::models::LoadBalancer {
public_net: Box::new(hcloud::models::LoadBalancerPublicNet {
ipv4: Box::new(hcloud::models::LoadBalancerPublicNetIpv4 {
ip: Some(Some("203.0.113.10".to_string())),
dns_ptr: Some(Some("public.example.com".to_string())),
}),
ipv6: Box::new(hcloud::models::LoadBalancerPublicNetIpv6 {
ip: Some(Some("2001:db8::10".to_string())),
dns_ptr: Some(Some("public-v6.example.com".to_string())),
}),
..Default::default()
}),
private_net: vec![hcloud::models::LoadBalancerPrivateNet {
ip: Some("10.10.0.5".to_string()),
..Default::default()
}],
..Default::default()
}
}

#[test]
fn build_ingress_uses_public_addresses_when_enabled() {
let ingress = build_ingress(&test_load_balancer(), true, false, true);

assert_eq!(
ingress,
vec![
json!({
"ip": "203.0.113.10",
"dns": "public.example.com",
"ipMode": "VIP"
}),
json!({
"ip": "2001:db8::10",
"dns": "public-v6.example.com",
"ipMode": "VIP"
})
]
);
}

#[test]
fn build_ingress_uses_private_address_when_public_interface_is_disabled() {
let ingress = build_ingress(&test_load_balancer(), true, false, false);

assert_eq!(
ingress,
vec![json!({
"ip": "10.10.0.5",
"ipMode": "VIP"
})]
);
}
}
3 changes: 2 additions & 1 deletion src/lb/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ pub async fn create_load_balancer(
location: &str,
balancer_type: &str,
algorithm: LoadBalancerAlgorithm,
public_interface: bool,
) -> RobotLBResult<hcloud::models::LoadBalancer> {
let response = hcloud::apis::load_balancers_api::create_load_balancer(
hcloud_config,
Expand All @@ -227,7 +228,7 @@ pub async fn create_load_balancer(
name: name.to_string(),
network: None,
network_zone: None,
public_interface: Some(true),
public_interface: Some(public_interface),
services: Some(vec![]),
targets: Some(vec![]),
},
Expand Down
Loading
Loading