Skip to content
Open
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
146 changes: 146 additions & 0 deletions .github/scripts/requires-update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail

# --preview: print what would be created (PRs and issues) without touching git or GitHub.
# Usage: bash requires-update.sh --preview <json-file>
# bash requires-update.sh <json-file>
PREVIEW=false
if [[ "${1:-}" == "--preview" ]]; then
PREVIEW=true
shift
fi

JSON_FILE="${1:?Usage: $0 [--preview] <requires-update-json>}"

if [ ! -f "$JSON_FILE" ]; then
echo "No changes found (JSON report not written) — exiting."
exit 0
fi

team_count=$(jq 'length' "$JSON_FILE")
if [ "$team_count" -eq 0 ]; then
echo "No changes to commit — exiting."
exit 0
fi

generate_pr_body() {
local team_entry="$1"
echo "## Packages updated"
echo ""
jq -r '
.packages[] |
"### `\(.name)`\n" +
(if (.applied | length) > 0 then
([.applied[] | "- **\(.package)** (`\(.kind)`): `\(.current)` → `\(.proposed)`"] | join("\n"))
else "" end) +
(if (.skipped | length) > 0 then
"\n" + ([.skipped[] | "- ⚠️ **\(.package)** skipped: \(.warning)"] | join("\n"))
else "" end) +
"\n"
' <<< "$team_entry"
}

generate_issue_body() {
local team_entry="$1"
local slug="$2"
echo "The following packages have dependency updates available but could not be applied automatically."
echo ""
jq -r '
.packages[] | select((.skipped | length) > 0) |
"### `\(.name)`\n" +
([.skipped[] | "- **\(.package)**: \(.warning)"] | join("\n")) +
"\n"
' <<< "$team_entry"
echo "/cc @elastic/${slug}"
}

while IFS= read -r team_entry; do
slug=$(jq -r '.slug' <<< "$team_entry")
files=$(jq -r '[.packages[].files[]] | unique | .[]' <<< "$team_entry")

# No files written: all proposals were skipped — open an issue instead of a PR.
if [ -z "$files" ]; then
issue_title="[automation] Package version updates blocked for @elastic/${slug}"

if $PREVIEW; then
echo "======================================== ISSUE"
echo "Team: @elastic/${slug}"
echo ""
echo "Issue title: ${issue_title}"
echo ""
echo "Issue body:"
generate_issue_body "$team_entry" "$slug"
echo "========================================"
echo ""
continue
fi

# Update the existing open issue if one exists, otherwise create it.
existing_issue=$(gh issue list \
--state open \
--search "${issue_title} in:title" \
--json number,title \
--jq ".[] | select(.title == \"${issue_title}\") | .number" \
2>/dev/null | head -1)

if [ -n "$existing_issue" ]; then
gh issue edit "$existing_issue" --body "$(generate_issue_body "$team_entry" "$slug")"
echo "Updated existing issue #${existing_issue} for team ${slug}."
else
gh issue create \
--title "$issue_title" \
--body "$(generate_issue_body "$team_entry" "$slug")"
echo "Created issue for team ${slug}."
fi
continue
fi

branch="automated/requires-update-${slug}"

if $PREVIEW; then
echo "======================================== PR"
echo "Team: @elastic/${slug}"
echo "Branch: ${branch}"
echo "Files:"
echo "$files" | sed 's/^/ /'
echo ""
echo "PR title: [automation] Update required package versions for @elastic/${slug}"
echo ""
echo "PR body:"
generate_pr_body "$team_entry"
echo "========================================"
echo ""
continue
fi

# Reset HEAD to main without discarding the dirty working tree.
# "checkout -B" moves HEAD but does not touch untracked/modified files,
# so all other teams' dirty files survive subsequent iterations.
git checkout -B "$branch" origin/main

echo "$files" | xargs git add --
git commit -m "[automation] Update required package versions"
git push --force-with-lease origin "$branch"

# Get or create PR; capture PR number for changelog link fixup.
pr_url=$(gh pr list --head "$branch" --state open --json url -q '.[0].url' 2>/dev/null)
if [ -z "$pr_url" ]; then
pr_url=$(gh pr create \
--base main \
--head "$branch" \
--title "[automation] Update required package versions for @elastic/${slug}" \
--body "$(generate_pr_body "$team_entry")")
fi
pr_number="${pr_url##*/}"

# Fixup pull/0 placeholder in this team's changelog files.
changelog_files=$(jq -r '.packages[].files[] | select(endswith("changelog.yml"))' <<< "$team_entry")
if [ -n "$changelog_files" ] && [ -n "$pr_number" ]; then
echo "$changelog_files" | xargs sed -i'' "s|pull/0|pull/${pr_number}|g"
echo "$changelog_files" | xargs git add --
git diff --cached --quiet || {
git commit -m "Fix changelog PR links"
git push origin "$branch"
}
fi
done < <(jq -c '.[]' "$JSON_FILE")
95 changes: 95 additions & 0 deletions .github/scripts/testdata/requires-update.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
[
{
"slug": "obs-infraobs-integrations",
"packages": [
{
"name": "nginx_integration_otel",
"files": [
"packages/nginx_integration_otel/manifest.yml",
"packages/nginx_integration_otel/changelog.yml"
],
"applied": [
{
"kind": "input",
"package": "nginx_input_otel",
"current": "0.2.0",
"proposed": "0.3.0",
"kibana_constraint": "",
"warning": ""
}
],
"skipped": []
},
{
"name": "apache_otel",
"files": [
"packages/apache_otel/manifest.yml",
"packages/apache_otel/changelog.yml"
],
"applied": [
{
"kind": "input",
"package": "apache_input_otel",
"current": "1.0.0",
"proposed": "2.0.0",
"kibana_constraint": "",
"warning": ""
}
],
"skipped": [
{
"kind": "input",
"package": "apache_logs_otel",
"current": "0.5.0",
"proposed": "",
"kibana_constraint": "^9.6.0",
"warning": "apache_logs_otel 0.6.0 is available but requires kibana ^9.6.0 which is incompatible with this package's kibana constraint ^8.13.0"
}
]
}
]
},
{
"slug": "security-service-integrations",
"packages": [
{
"name": "okta",
"files": [
"packages/okta/manifest.yml",
"packages/okta/changelog.yml"
],
"applied": [
{
"kind": "input",
"package": "okta_input",
"current": "0.9.0",
"proposed": "0.10.0",
"kibana_constraint": "",
"warning": ""
}
],
"skipped": []
}
]
},
{
"slug": "ecosystem",
"packages": [
{
"name": "some_integration",
"files": [],
"applied": [],
"skipped": [
{
"kind": "input",
"package": "some_input",
"current": "1.2.0",
"proposed": "",
"kibana_constraint": "^9.0.0",
"warning": "some_input 1.3.0 is available but requires kibana ^9.0.0 which is incompatible with this package's kibana constraint ^8.0.0"
}
]
}
]
}
]
54 changes: 54 additions & 0 deletions .github/workflows/requires-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: Update required package versions

on:
workflow_dispatch:
schedule:
- cron: '0 9 * * 1'

permissions:
contents: read

jobs:
requires-update:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: .go-version

- name: Install mage
run: go install github.com/magefile/mage

- name: Install elastic-package
run: |
# TODO: remove this pin once `requires update --format json` ships in a released elastic-package version
go install github.com/elastic/elastic-package@4e3065472c950dfe2c98adf936a7c71ba9e0a762
echo "ELASTIC_PACKAGE_BIN=$(go env GOPATH)/bin/elastic-package" >> $GITHUB_ENV

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Fetch remote branch states
run: git fetch origin

- name: Run RequiresUpdate
env:
REQUIRES_UPDATE_JSON_OUT: /tmp/requires-update.json
run: |
mage RequiresUpdate 2>&1 | tee /tmp/requires-update-summary.txt
continue-on-error: true

- name: Group changes and open PRs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash .github/scripts/requires-update.sh /tmp/requires-update.json
27 changes: 21 additions & 6 deletions dev/citools/packagemanifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,34 @@ type conditions struct {
Elastic elasticConditions `config:"elastic" json:"elastic" yaml:"elastic"`
}

type requiresEntry struct {
Package string `config:"package" json:"package" yaml:"package"`
Version string `config:"version" json:"version" yaml:"version"`
}

type requiresBlock struct {
Input []requiresEntry `config:"input" json:"input" yaml:"input"`
Content []requiresEntry `config:"content" json:"content" yaml:"content"`
}

type packageManifest struct {
FormatVersion string `config:"format_version" json:"format_version" yaml:"format_version"`
Name string `config:"name" json:"name" yaml:"name"`
Type string `config:"type" json:"type" yaml:"type"`
Version string `config:"version" json:"version" yaml:"version"`
License string `config:"license" json:"license" yaml:"license"`
Conditions conditions `config:"conditions" json:"conditions" yaml:"conditions"`
FormatVersion string `config:"format_version" json:"format_version" yaml:"format_version"`
Name string `config:"name" json:"name" yaml:"name"`
Type string `config:"type" json:"type" yaml:"type"`
Version string `config:"version" json:"version" yaml:"version"`
License string `config:"license" json:"license" yaml:"license"`
Conditions conditions `config:"conditions" json:"conditions" yaml:"conditions"`
Requires *requiresBlock `config:"requires" json:"requires,omitempty" yaml:"requires,omitempty"`
}

func (m *packageManifest) IsValid() bool {
return m.FormatVersion != "" && m.Name != "" && m.Type != "" && m.Version != ""
}

func (m *packageManifest) HasRequires() bool {
return m.Requires != nil && (len(m.Requires.Input) > 0 || len(m.Requires.Content) > 0)
}

func ReadPackageManifest(path string) (*packageManifest, error) {
cfg, err := yaml.NewConfigWithFile(path, ucfg.PathSep("."))
if err != nil {
Expand Down
Loading
Loading