id: bump-pilot
namespace: company.team
description: |
BumpPilot checks requirements.txt once a week and opens a pull request
for whatever is outdated. It asks PyPI for the latest version of each
pinned package, skips anything that already has an open bump PR, and
stays quiet when everything is current.
inputs:
- id: repo_owner
type: STRING
description: who owns the repo, like kestra-io
- id: repo_name
type: STRING
description: the repo name, like kestra
- id: requirements_path
type: STRING
description: where the requirements file lives in the repo
defaults: requirements.txt
- id: base_branch
type: STRING
description: the branch the bump branch starts from
defaults: main
tasks:
- id: fetch_requirements
type: io.kestra.plugin.core.http.Request
description: asks GitHub for the requirements.txt file
method: GET
uri: https://api.github.com/repos/{{ inputs.repo_owner }}/{{ inputs.repo_name
}}/contents/{{ inputs.requirements_path }}?ref={{ inputs.base_branch }}
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: "application/vnd.github+json"
- id: get_base_sha
type: io.kestra.plugin.core.http.Request
description: asks GitHub for the latest commit on the base branch, new branches
start from here
method: GET
uri: https://api.github.com/repos/{{ inputs.repo_owner }}/{{ inputs.repo_name
}}/git/ref/heads/{{ inputs.base_branch }}
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: "application/vnd.github+json"
- id: list_open_prs
type: io.kestra.plugin.core.http.Request
description: asks GitHub for the open pull requests, so it never opens a
duplicate bump PR
method: GET
uri: https://api.github.com/repos/{{ inputs.repo_owner }}/{{ inputs.repo_name
}}/pulls?state=open&per_page=100
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: "application/vnd.github+json"
- id: find_outdated
type: io.kestra.plugin.scripts.python.Script
description: reads the file line by line, checks each pin against PyPI, and
returns what is outdated
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: python:3.11
dependencies:
- kestra
script: |
import base64
import json
import urllib.request
from datetime import datetime, timezone
from kestra import Kestra
def parse_version(v):
# compare versions as numbers, so 10.0 beats 9.0
parts = []
for piece in v.split('.'):
num = ''
for ch in piece:
if ch.isdigit():
num += ch
else:
break
parts.append(int(num) if num else 0)
return tuple(parts)
# GitHub sends the file base64-encoded, so decode it first
raw = base64.b64decode("""{{ outputs.fetch_requirements.body | jq(".content") | first }}""").decode()
# go line by line and keep the name and version pairs
pins = []
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith('#') or '==' not in line:
continue
name, version = line.split('==', 1)
pins.append((name.strip(), version.strip()))
# ask PyPI for the latest version of each pinned package
outdated = []
for name, current in pins:
try:
with urllib.request.urlopen(f'https://pypi.org/pypi/{name}/json', timeout=15) as r:
latest = json.load(r)['info']['version']
except Exception:
continue # not on PyPI, skip it quietly
if parse_version(latest) > parse_version(current):
outdated.append({'name': name, 'current': current, 'latest': latest})
# skip anything that already has an open bump PR
open_titles = json.loads("""{{ outputs.list_open_prs.body | jq("[.[].title]") | first }}""")
fresh = [o for o in outdated if not any(o['name'] in t for t in open_titles)]
# write the new requirements.txt with the bumped versions
new_lines = []
for line in raw.splitlines():
stripped = line.strip()
if '==' in stripped and not stripped.startswith('#'):
name = stripped.split('==', 1)[0].strip()
bump = next((o for o in fresh if o['name'] == name), None)
if bump:
line = f"{name}=={bump['latest']}"
new_lines.append(line)
new_content = base64.b64encode('\n'.join(new_lines).encode()).decode()
branch_name = datetime.now(timezone.utc).strftime('bump/deps-%Y%m%d-%H%M%S')
bump_names = ', '.join(o['name'] for o in fresh)
pr_lines = ['BumpPilot found outdated dependencies:', '']
for o in fresh:
pr_lines.append(f"- {o['name']}: {o['current']} -> {o['latest']}")
pr_lines += ['', '_Opened automatically by BumpPilot._']
pr_body = '\\n'.join(pr_lines)
Kestra.outputs({
'outdated': fresh,
'count': len(fresh),
'new_content': new_content,
'branch_name': branch_name,
'bump_names': bump_names,
'pr_body': pr_body,
})
- id: process_updates
type: io.kestra.plugin.core.flow.If
description: only does anything when something is actually outdated
condition: "{{ outputs.find_outdated.vars.count > 0 }}"
then:
- id: create_branch
type: io.kestra.plugin.core.http.Request
description: makes the bump branch from the base branch
method: POST
uri: https://api.github.com/repos/{{ inputs.repo_owner }}/{{ inputs.repo_name
}}/git/refs
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: "application/vnd.github+json"
contentType: application/json
body: |
{
"ref": "refs/heads/{{ outputs.find_outdated.vars.branch_name }}",
"sha": "{{ outputs.get_base_sha.body | jq('.object.sha') | first }}"
}
- id: update_file
type: io.kestra.plugin.core.http.Request
description: writes the bumped requirements.txt to the new branch
method: PUT
uri: https://api.github.com/repos/{{ inputs.repo_owner }}/{{ inputs.repo_name
}}/contents/{{ inputs.requirements_path }}
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: "application/vnd.github+json"
contentType: application/json
body: |
{
"message": "chore: bump dependencies",
"content": "{{ outputs.find_outdated.vars.new_content }}",
"sha": "{{ outputs.fetch_requirements.body | jq('.sha') | first }}",
"branch": "{{ outputs.find_outdated.vars.branch_name }}"
}
- id: open_pr
type: io.kestra.plugin.core.http.Request
description: opens the pull request with the bumped dependencies
method: POST
uri: https://api.github.com/repos/{{ inputs.repo_owner }}/{{ inputs.repo_name
}}/pulls
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: "application/vnd.github+json"
contentType: application/json
body: |
{
"title": "chore: bump {{ outputs.find_outdated.vars.bump_names }}",
"head": "{{ outputs.find_outdated.vars.branch_name }}",
"base": "{{ inputs.base_branch }}",
"body": "{{ outputs.find_outdated.vars.pr_body }}"
}
triggers:
- id: weekly
type: io.kestra.plugin.core.trigger.Schedule
cron: "0 9 * * 1"
timezone: UTC
inputs:
repo_owner: kestra-io
repo_name: kestra
requirements_path: requirements.txt
base_branch: main