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
32 changes: 32 additions & 0 deletions .github/workflows/upstream-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Upstream Version Check

on:
schedule:
# Run daily at 08:00 UTC.
- cron: '0 8 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Preview changes without creating an issue'
type: boolean
default: false

permissions:
issues: write

jobs:
check-upstream:
name: Check Upstream Releases
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install yq
uses: mikefarah/yq@v4

- name: Run upstream check
run: ci/scripts/upstream-check.sh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
31 changes: 31 additions & 0 deletions .upstream-monitor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Upstream Version Monitor Configuration
#
# Maps upstream product repositories to Helm chart artifacts.
# The ci/scripts/upstream-check.sh script reads this file to detect
# new upstream releases and open a GitHub issue when an update is available.
#
# Adding a new chart:
# 1. Add a new entry under `charts:` with the chart path.
# 2. List each upstream source with its GitHub repository.
# 3. Define targets — which files and YAML paths hold the current version.
#
# Fields:
# name — human-readable chart name (used in issue titles)
# path — path to the chart directory relative to repo root
# sources[].name — identifier for this upstream component
# sources[].github — GitHub owner/repo (used to query releases)
# sources[].strip_v_prefix — if true, strip leading "v" from the release
# tag before comparing (e.g. "v0.65.3" → "0.65.3")
# sources[].targets[].file — file holding current version, relative to chart path
# sources[].targets[].yaml_path — yq expression to the version field

charts:
- name: netbird
path: charts/netbird
sources:
- name: server
github: netbirdio/netbird
strip_v_prefix: true
targets:
- file: Chart.yaml
yaml_path: .appVersion
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

### Added

- **Automated upstream version tracking**: New scheduled GitHub Actions workflow
(`.github/workflows/upstream-check.yaml`) that runs daily to detect new
releases from upstream repositories and opens a GitHub issue when an update
is available. Currently tracks NetBird server (`netbirdio/netbird`).
- `.upstream-monitor.yaml` configuration file mapping upstream GitHub
repositories to Helm chart version fields. Add new charts or sources by
extending this file.
- `ci/scripts/upstream-check.sh` script that reads the monitor config, queries
the GitHub API for latest releases, compares with current chart versions,
and creates GitHub issues for available updates. Supports `DRY_RUN=true`
for preview mode.
- Workflow supports manual trigger via `workflow_dispatch` with optional
dry-run input.

## [0.1.1] — 2026-02-26

### Added
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,54 @@ See each chart's README for detailed configuration.
- [kubectl](https://kubernetes.io/docs/tasks/tools/) configured for your target cluster
- Kubernetes 1.24+ (1.28+ for SQLite PAT seeding)

## Automated Upstream Version Tracking

A scheduled GitHub Actions workflow checks upstream repositories daily for new
releases and opens a GitHub issue when a chart is behind upstream.

### How It Works

1. The workflow reads `.upstream-monitor.yaml` to discover which upstream repos
map to which chart version fields.
2. For each source, it queries the GitHub Releases API for the latest non-draft,
non-prerelease tag.
3. If the upstream version differs from what the chart currently references, a
GitHub issue is created with the current and latest versions, a link to the
upstream release, and a checklist of what needs to be done.

### Configuration

Edit `.upstream-monitor.yaml` to add new charts or upstream sources:

```yaml
charts:
- name: netbird
path: charts/netbird
sources:
- name: server
github: netbirdio/netbird
strip_v_prefix: true
targets:
- file: Chart.yaml
yaml_path: .appVersion
```
### Manual Trigger
Run the check on demand from the Actions tab → **Upstream Version Check** →
**Run workflow**. Enable the `dry_run` checkbox to preview changes without
creating an issue.

### Local Usage

```bash
# Preview what would change (no issue created)
DRY_RUN=true ./ci/scripts/upstream-check.sh
# Run for real (requires gh auth login)
./ci/scripts/upstream-check.sh
```

## Contributing

We welcome contributions! Please read our [Contributing Guide](CONTRIBUTING.md) before submitting pull requests.
Expand Down
206 changes: 206 additions & 0 deletions ci/scripts/upstream-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# upstream-check.sh — Detect new upstream releases and open GitHub issues.
#
# Reads .upstream-monitor.yaml, queries the GitHub API for latest releases,
# and creates a GitHub issue when a chart is behind upstream.
#
# Required tools: yq (v4+), gh (GitHub CLI), jq
#
# Environment variables:
# GH_TOKEN — GitHub token (set automatically in GitHub Actions)
# DRY_RUN — "true" to skip issue creation (default: "false")
#
# Usage:
# ./ci/scripts/upstream-check.sh # normal run
# DRY_RUN=true ./ci/scripts/upstream-check.sh # preview only

set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
CONFIG_FILE="${REPO_ROOT}/.upstream-monitor.yaml"
DRY_RUN="${DRY_RUN:-false}"

# ── Helpers ───────────────────────────────────────────────────────────────

log() { echo "==> $*"; }
info() { echo " $*"; }
warn() { echo " WARNING: $*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }

check_deps() {
local missing=()
for cmd in yq gh jq; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [[ ${#missing[@]} -gt 0 ]]; then
die "missing required tools: ${missing[*]}"
fi
}

# Fetch the latest non-prerelease, non-draft release tag from a GitHub repo.
get_latest_release_tag() {
local repo="$1"
gh api "repos/${repo}/releases/latest" --jq '.tag_name' 2>/dev/null || echo ""
}

# Fetch the URL of the latest release page.
get_latest_release_url() {
local repo="$1"
gh api "repos/${repo}/releases/latest" --jq '.html_url' 2>/dev/null || echo ""
}

# Read a YAML field from a file.
read_yaml() {
local file="$1" path="$2"
yq eval "$path" "$file"
}

# Check whether an open issue already exists with the given title.
issue_exists_with_title() {
local title="$1"
local count
count=$(gh issue list --state open --search "$title" --json number,title \
--jq "[.[] | select(.title == \"$title\")] | length" 2>/dev/null || echo "0")
[[ "$count" -gt 0 ]]
}

# ── Main ──────────────────────────────────────────────────────────────────

main() {
check_deps

if [[ ! -f "$CONFIG_FILE" ]]; then
die "config file not found: $CONFIG_FILE"
fi

local num_charts
num_charts=$(yq eval '.charts | length' "$CONFIG_FILE")

for ((i = 0; i < num_charts; i++)); do
local chart_name chart_rel_path chart_path
chart_name=$(yq eval ".charts[$i].name" "$CONFIG_FILE")
chart_rel_path=$(yq eval ".charts[$i].path" "$CONFIG_FILE")
chart_path="${REPO_ROOT}/${chart_rel_path}"

log "Chart: $chart_name ($chart_rel_path)"

if [[ ! -d "$chart_path" ]]; then
warn "chart directory not found: $chart_path — skipping"
continue
fi

local num_sources
num_sources=$(yq eval ".charts[$i].sources | length" "$CONFIG_FILE")

# Collect updates: each entry is "source_name|github_repo|file|yaml_path|old|new"
local updates=()

for ((j = 0; j < num_sources; j++)); do
local src_name github_repo strip_v
src_name=$(yq eval ".charts[$i].sources[$j].name" "$CONFIG_FILE")
github_repo=$(yq eval ".charts[$i].sources[$j].github" "$CONFIG_FILE")
strip_v=$(yq eval ".charts[$i].sources[$j].strip_v_prefix" "$CONFIG_FILE")

info "Source: $src_name ($github_repo)"

local latest_tag
latest_tag=$(get_latest_release_tag "$github_repo")
if [[ -z "$latest_tag" ]]; then
warn "could not fetch latest release for $github_repo — skipping"
continue
fi

local latest_version="$latest_tag"
if [[ "$strip_v" == "true" ]]; then
latest_version="${latest_tag#v}"
fi

local num_targets
num_targets=$(yq eval ".charts[$i].sources[$j].targets | length" "$CONFIG_FILE")

for ((k = 0; k < num_targets; k++)); do
local target_file yaml_path current_version
target_file=$(yq eval ".charts[$i].sources[$j].targets[$k].file" "$CONFIG_FILE")
yaml_path=$(yq eval ".charts[$i].sources[$j].targets[$k].yaml_path" "$CONFIG_FILE")
current_version=$(read_yaml "${chart_path}/${target_file}" "$yaml_path")

info " ${target_file} (${yaml_path}): current=${current_version} latest=${latest_version}"

if [[ "$current_version" != "$latest_version" ]]; then
info " UPDATE AVAILABLE: ${current_version} -> ${latest_version}"
updates+=("${src_name}|${github_repo}|${target_file}|${yaml_path}|${current_version}|${latest_version}")
else
info " up to date"
fi
done
done

# ── No updates needed ──────────────────────────────────────────────
if [[ ${#updates[@]} -eq 0 ]]; then
log "No updates needed for $chart_name"
echo ""
continue
fi

# ── Build issue title and body ─────────────────────────────────────
local title_parts=()
local issue_body
issue_body="## Upstream Version Update Available"$'\n\n'
issue_body+="The following upstream component(s) for the **${chart_name}** chart have new releases:"$'\n\n'
issue_body+="| Component | Current Version | Latest Version | File | Field |"$'\n'
issue_body+="| --------- | --------------- | -------------- | ---- | ----- |"$'\n'

for entry in "${updates[@]}"; do
IFS='|' read -r src repo file path old new <<< "$entry"
title_parts+=("${src} ${old}${new}")
local release_url
release_url=$(get_latest_release_url "$repo")
if [[ -n "$release_url" ]]; then
issue_body+="| ${src} | \`${old}\` | [\`${new}\`](${release_url}) | \`${file}\` | \`${path}\` |"$'\n'
else
issue_body+="| ${src} | \`${old}\` | \`${new}\` | \`${file}\` | \`${path}\` |"$'\n'
fi
done

issue_body+=$'\n'"### What needs to be done"$'\n\n'
issue_body+="1. Update the version references listed above."$'\n'
issue_body+="2. Update any hardcoded version strings in test assertions (\`charts/${chart_name}/tests/\`)."$'\n'
issue_body+="3. Bump the chart version in \`Chart.yaml\`."$'\n'
issue_body+="4. Review the upstream release notes for breaking changes."$'\n'
issue_body+="5. Run \`make test\` to verify lint and unit tests pass."$'\n'
issue_body+=$'\n'"---"$'\n'
issue_body+="*This issue was created automatically by the upstream version checker.*"$'\n'

local joined_parts
joined_parts=$(printf '%s, ' "${title_parts[@]}")
joined_parts="${joined_parts%, }"
local issue_title="chore(${chart_name}): upstream update available — ${joined_parts}"

# ── Dry run ────────────────────────────────────────────────────────
if [[ "$DRY_RUN" == "true" ]]; then
log "DRY RUN — would create issue:"
info "Title: $issue_title"
echo "$issue_body"
echo ""
continue
fi

# ── Check for existing issue ───────────────────────────────────────
if issue_exists_with_title "$issue_title"; then
log "Issue already exists with title: $issue_title — skipping"
echo ""
continue
fi

# ── Create issue ───────────────────────────────────────────────────
gh issue create \
--title "$issue_title" \
--body "$issue_body" \
--label "autorelease"

log "Issue created for $chart_name"
echo ""
done
}

main "$@"