From a1fecf3a87cabd56e99f4c0b3bb55e845307a1a4 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 12 Mar 2026 17:39:24 +0100 Subject: [PATCH 01/14] ci(ff): support feature freeze automation phase 1 --- go.mod | 18 ++ go.sum | 93 +++++++++ magefile.go | 540 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 651 insertions(+) diff --git a/go.mod b/go.mod index 3f6020c326..fb6c961b57 100644 --- a/go.mod +++ b/go.mod @@ -48,11 +48,15 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/elastic-transport-go/v8 v8.8.0 // indirect @@ -60,31 +64,44 @@ require ( github.com/elastic/go-sysinfo v1.15.1 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/elastic/gosigar v0.14.3 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.15.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-github/v68 v68.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shirou/gopsutil/v4 v4.24.7 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.elastic.co/ecszap v1.0.3 // indirect @@ -100,6 +117,7 @@ require ( golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.41.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5400b45c66..7920f5c528 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,12 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg= github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= @@ -10,12 +15,18 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -53,6 +64,8 @@ github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -61,6 +74,12 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -74,11 +93,18 @@ github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= +github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20230426061923-93006964c1fc h1:AGDHt781oIcL4EFk7cPnvBUYTwU8BEU6GDTO3ZMn1sE= github.com/google/pprof v0.0.0-20230426061923-93006964c1fc/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -93,14 +119,21 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -135,6 +168,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -158,12 +193,17 @@ github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk= github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -172,6 +212,7 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -183,8 +224,11 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.elastic.co/apm/module/apmchiv5/v2 v2.7.4 h1:X46uD2aE3cY4PU9Z7jV3id+4oHKhGz3Ej/bmbfD0JOU= @@ -228,19 +272,48 @@ go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -248,12 +321,29 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= @@ -264,10 +354,13 @@ google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhH google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD79kk= gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/magefile.go b/magefile.go index 06aecf9878..6b92365d3e 100644 --- a/magefile.go +++ b/magefile.go @@ -40,6 +40,11 @@ import ( "sync" "time" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/google/go-github/v68/github" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" "gopkg.in/yaml.v3" @@ -364,6 +369,9 @@ type Test mg.Namespace // Docker is the namespace for docker related tasks. type Docker mg.Namespace +// Release is the namespace for release automation. +type Release mg.Namespace + // envToBool reads the env var string s and parses it as a bool. func envToBool(s string) bool { v, ok := os.LookupEnv(s) @@ -2255,3 +2263,535 @@ func (Test) CloudE2ERun() error { err = errors.Join(err, os.WriteFile(filepath.Join("build", "test-cloude2e.out"), b.Bytes(), 0o644)) return err } + +// ============================================================================ +// Release Automation Functions +// ============================================================================ + +// ReleaseConfig holds configuration for release operations +type ReleaseConfig struct { + Version string + BaseBranch string + ReleaseBranch string + Owner string + Repo string + AuthorName string + AuthorEmail string +} + +// loadReleaseConfigFromEnv loads release configuration from environment variables +func loadReleaseConfigFromEnv() (*ReleaseConfig, error) { + version := os.Getenv("CURRENT_RELEASE") + if version == "" { + return nil, fmt.Errorf("CURRENT_RELEASE environment variable not set") + } + + baseBranch := os.Getenv("BASE_BRANCH") + if baseBranch == "" { + baseBranch = "main" + } + + // Extract major.minor for release branch (e.g., "9.4.0" -> "9.4") + parts := strings.Split(version, ".") + if len(parts) < 2 { + return nil, fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) + } + releaseBranch := fmt.Sprintf("%s.%s", parts[0], parts[1]) + + owner := os.Getenv("PROJECT_OWNER") + if owner == "" { + owner = "elastic" + } + + repo := os.Getenv("PROJECT_REPO") + if repo == "" { + repo = "fleet-server" + } + + authorName := os.Getenv("GITHUB_USERNAME") + if authorName == "" { + authorName = "elasticmachine" + } + + authorEmail := os.Getenv("GITHUB_EMAIL") + if authorEmail == "" { + authorEmail = "infra-root+elasticmachine@elastic.co" + } + + return &ReleaseConfig{ + Version: version, + BaseBranch: baseBranch, + ReleaseBranch: releaseBranch, + Owner: owner, + Repo: repo, + AuthorName: authorName, + AuthorEmail: authorEmail, + }, nil +} + +// UpdateVersion updates the version in version/version.go +func (Release) UpdateVersion(newVersion string) error { + versionFile := "version/version.go" + + content, err := os.ReadFile(versionFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", versionFile, err) + } + + // Replace the version string + // Pattern: const DefaultVersion = "X.Y.Z" + re := regexp.MustCompile(`(const\s+DefaultVersion\s*=\s*)"[^"]+"`) + newContent := re.ReplaceAllString(string(content), `${1}"`+newVersion+`"`) + + if newContent == string(content) { + return fmt.Errorf("version pattern not found in %s", versionFile) + } + + err = os.WriteFile(versionFile, []byte(newContent), 0644) + if err != nil { + return fmt.Errorf("failed to write %s: %w", versionFile, err) + } + + fmt.Printf("✓ Updated version to %s in %s\n", newVersion, versionFile) + return nil +} + +// UpdateMergify adds a new backport rule to .mergify.yml +func (Release) UpdateMergify(version string) error { + mergifyFile := ".mergify.yml" + + // Read the YAML file + content, err := os.ReadFile(mergifyFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", mergifyFile, err) + } + + // Extract major.minor from version (e.g., "9.4.0" -> "9.4") + parts := strings.Split(version, ".") + if len(parts) < 2 { + return fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) + } + branchVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) + + // Check if rule already exists + searchPattern := fmt.Sprintf("backport patches to %s branch", branchVersion) + if strings.Contains(string(content), searchPattern) { + fmt.Printf(" Backport rule for %s already exists\n", branchVersion) + return nil + } + + // Create new backport rule following the existing format + newRule := fmt.Sprintf(` - name: backport patches to %s branch + conditions: + - merged + - label=backport-%s + actions: + backport: + branches: + - "%s" +`, branchVersion, branchVersion, branchVersion) + + // Append the new rule to the end of the file + output := string(content) + newRule + + // Write back to file + err = os.WriteFile(mergifyFile, []byte(output), 0644) + if err != nil { + return fmt.Errorf("failed to write %s: %w", mergifyFile, err) + } + + fmt.Printf("✓ Added backport rule for %s to %s\n", branchVersion, mergifyFile) + return nil +} + +// GitRepo represents a Git repository with common operations +type GitRepo struct { + repo *git.Repository +} + +// openRepo opens the Git repository at the given path +func openRepo(path string) (*GitRepo, error) { + repo, err := git.PlainOpen(path) + if err != nil { + return nil, fmt.Errorf("failed to open git repo: %w", err) + } + return &GitRepo{repo: repo}, nil +} + +// createBranch creates and checks out a new branch +func (g *GitRepo) createBranch(branchName string) error { + w, err := g.repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) + } + + // Get current HEAD reference + headRef, err := g.repo.Head() + if err != nil { + return fmt.Errorf("failed to get HEAD: %w", err) + } + + // Create new branch reference + refName := plumbing.NewBranchReferenceName(branchName) + ref := plumbing.NewHashReference(refName, headRef.Hash()) + + err = g.repo.Storer.SetReference(ref) + if err != nil { + return fmt.Errorf("failed to create branch: %w", err) + } + + // Checkout the new branch + err = w.Checkout(&git.CheckoutOptions{ + Branch: refName, + }) + if err != nil { + return fmt.Errorf("failed to checkout branch: %w", err) + } + + fmt.Printf("✓ Created and checked out branch: %s\n", branchName) + return nil +} + +// commitAll commits all changes with the given message +func (g *GitRepo) commitAll(message, authorName, authorEmail string) error { + w, err := g.repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) + } + + // Add all changes + err = w.AddWithOptions(&git.AddOptions{ + All: true, + }) + if err != nil { + return fmt.Errorf("failed to add changes: %w", err) + } + + // Create commit + commit, err := w.Commit(message, &git.CommitOptions{ + Author: &object.Signature{ + Name: authorName, + Email: authorEmail, + When: time.Now(), + }, + }) + if err != nil { + return fmt.Errorf("failed to commit: %w", err) + } + + fmt.Printf("✓ Committed changes: %s\n", commit.String()[:7]) + return nil +} + +// push pushes the current branch to the remote +func (g *GitRepo) push(remoteName string, dryRun bool) error { + if dryRun { + fmt.Printf(" [DRY RUN] Would push to remote: %s\n", remoteName) + return nil + } + + err := g.repo.Push(&git.PushOptions{ + RemoteName: remoteName, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return fmt.Errorf("failed to push: %w", err) + } + + if errors.Is(err, git.NoErrAlreadyUpToDate) { + fmt.Println(" Branch already up to date on remote") + } else { + fmt.Printf("✓ Pushed to remote: %s\n", remoteName) + } + return nil +} + +// getCurrentBranch returns the name of the current branch +func (g *GitRepo) getCurrentBranch() (string, error) { + ref, err := g.repo.Head() + if err != nil { + return "", fmt.Errorf("failed to get HEAD: %w", err) + } + + if !ref.Name().IsBranch() { + return "", fmt.Errorf("HEAD is not a branch") + } + + return ref.Name().Short(), nil +} + +// setRemoteURL sets the URL for a remote +func (g *GitRepo) setRemoteURL(remoteName, url string) error { + _, err := g.repo.Remote(remoteName) + if errors.Is(err, git.ErrRemoteNotFound) { + // Remote doesn't exist, create it + _, err = g.repo.CreateRemote(&config.RemoteConfig{ + Name: remoteName, + URLs: []string{url}, + }) + if err != nil { + return fmt.Errorf("failed to create remote: %w", err) + } + fmt.Printf("✓ Created remote %s: %s\n", remoteName, url) + return nil + } else if err != nil { + return fmt.Errorf("failed to get remote: %w", err) + } + + // Update remote URL + err = g.repo.DeleteRemote(remoteName) + if err != nil { + return fmt.Errorf("failed to delete remote: %w", err) + } + + _, err = g.repo.CreateRemote(&config.RemoteConfig{ + Name: remoteName, + URLs: []string{url}, + }) + if err != nil { + return fmt.Errorf("failed to recreate remote: %w", err) + } + + fmt.Printf("✓ Updated remote %s: %s\n", remoteName, url) + return nil +} + +// newGitHubClient creates a new GitHub client from environment +func newGitHubClient() (*github.Client, error) { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return nil, fmt.Errorf("GITHUB_TOKEN environment variable not set") + } + + client := github.NewClient(nil).WithAuthToken(token) + return client, nil +} + +// createPR creates a pull request on GitHub +func createPR(client *github.Client, cfg *ReleaseConfig, title, body, head, base string, dryRun bool) (*github.PullRequest, error) { + if dryRun { + fmt.Printf(" [DRY RUN] Would create PR:\n") + fmt.Printf(" Title: %s\n", title) + fmt.Printf(" Head: %s\n", head) + fmt.Printf(" Base: %s\n", base) + return nil, nil + } + + pr, _, err := client.PullRequests.Create(context.Background(), cfg.Owner, cfg.Repo, &github.NewPullRequest{ + Title: github.String(title), + Head: github.String(head), + Base: github.String(base), + Body: github.String(body), + }) + if err != nil { + return nil, fmt.Errorf("failed to create PR: %w", err) + } + + fmt.Printf("✓ Created PR #%d: %s\n", *pr.Number, *pr.HTMLURL) + return pr, nil +} + +// PrepareMajorMinor prepares files for a major/minor release +func (Release) PrepareMajorMinor() error { + cfg, err := loadReleaseConfigFromEnv() + if err != nil { + return err + } + + fmt.Printf("Preparing major/minor release for version %s\n", cfg.Version) + + // Update version + r := Release{} + if err := r.UpdateVersion(cfg.Version); err != nil { + return err + } + + // Update mergify + if err := r.UpdateMergify(cfg.Version); err != nil { + return err + } + + fmt.Println("✓ Major/minor release preparation complete") + fmt.Printf(" Next steps:\n") + fmt.Printf(" 1. Review changes: git diff\n") + fmt.Printf(" 2. Create branch: mage release:createBranch\n") + fmt.Printf(" 3. Create PR: mage release:createPR\n") + + return nil +} + +// CreateBranch creates a release branch with all changes committed +func (Release) CreateBranch() error { + cfg, err := loadReleaseConfigFromEnv() + if err != nil { + return err + } + + dryRun := os.Getenv("DRY_RUN") == "true" + + repo, err := openRepo(".") + if err != nil { + return err + } + + branchName := cfg.ReleaseBranch + fmt.Printf("Creating release branch: %s\n", branchName) + + if !dryRun { + // Create and checkout branch + if err := repo.createBranch(branchName); err != nil { + return err + } + + // Commit all changes + commitMsg := fmt.Sprintf("[Release] Prepare %s release", cfg.Version) + if err := repo.commitAll(commitMsg, cfg.AuthorName, cfg.AuthorEmail); err != nil { + return err + } + } else { + fmt.Printf(" [DRY RUN] Would create branch %s and commit changes\n", branchName) + } + + // Push to remote + if err := repo.push("origin", dryRun); err != nil { + return err + } + + return nil +} + +// CreatePR creates a pull request for the release +func (Release) CreatePR() error { + cfg, err := loadReleaseConfigFromEnv() + if err != nil { + return err + } + + dryRun := os.Getenv("DRY_RUN") == "true" + + client, err := newGitHubClient() + if err != nil { + return err + } + + title := fmt.Sprintf("[Release] %s", cfg.Version) + body := fmt.Sprintf("## Release %s\n\nThis PR prepares the %s release.\n\n### Changes\n- Updated version to %s\n- Added mergify backport rule for %s\n", + cfg.Version, cfg.Version, cfg.Version, cfg.ReleaseBranch) + + _, err = createPR(client, cfg, title, body, cfg.ReleaseBranch, cfg.BaseBranch, dryRun) + return err +} + +// RunMajorMinor orchestrates the complete major/minor release workflow +func (Release) RunMajorMinor() error { + cfg, err := loadReleaseConfigFromEnv() + if err != nil { + return err + } + + dryRun := os.Getenv("DRY_RUN") == "true" + if dryRun { + fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") + } + + fmt.Printf("🚀 Starting major/minor release workflow for %s\n\n", cfg.Version) + + // Step 1: Prepare files + fmt.Println("Step 1: Preparing release files...") + r := Release{} + if err := r.PrepareMajorMinor(); err != nil { + return fmt.Errorf("failed to prepare release: %w", err) + } + + // Step 2: Create branch + fmt.Println("\nStep 2: Creating release branch...") + if err := r.CreateBranch(); err != nil { + return fmt.Errorf("failed to create branch: %w", err) + } + + // Step 3: Create PR + fmt.Println("\nStep 3: Creating pull request...") + if err := r.CreatePR(); err != nil { + return fmt.Errorf("failed to create PR: %w", err) + } + + fmt.Printf("\n✅ Major/minor release workflow complete for %s\n", cfg.Version) + return nil +} + +// PreparePatch prepares files for a patch release +func (Release) PreparePatch() error { + cfg, err := loadReleaseConfigFromEnv() + if err != nil { + return err + } + + fmt.Printf("Preparing patch release for version %s\n", cfg.Version) + + // Update version + r := Release{} + if err := r.UpdateVersion(cfg.Version); err != nil { + return err + } + + fmt.Println("✓ Patch release preparation complete") + return nil +} + +// RunPatch orchestrates the complete patch release workflow +func (Release) RunPatch() error { + cfg, err := loadReleaseConfigFromEnv() + if err != nil { + return err + } + + dryRun := os.Getenv("DRY_RUN") == "true" + if dryRun { + fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") + } + + fmt.Printf("🚀 Starting patch release workflow for %s\n\n", cfg.Version) + + // Prepare files + fmt.Println("Preparing patch release files...") + r := Release{} + if err := r.PreparePatch(); err != nil { + return fmt.Errorf("failed to prepare patch release: %w", err) + } + + fmt.Printf("\n✅ Patch release workflow complete for %s\n", cfg.Version) + fmt.Println(" Review changes and commit to the release branch") + return nil +} + +// PrepareNext prepares files for the next development cycle +func (Release) PrepareNext() error { + // Get current version + currentVersion := os.Getenv("CURRENT_RELEASE") + if currentVersion == "" { + return fmt.Errorf("CURRENT_RELEASE environment variable not set") + } + + // Calculate next version (increment minor) + parts := strings.Split(currentVersion, ".") + if len(parts) < 3 { + return fmt.Errorf("invalid version format: %s", currentVersion) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("invalid minor version: %s", parts[1]) + } + + nextVersion := fmt.Sprintf("%s.%d.0", parts[0], minor+1) + + fmt.Printf("Preparing next development cycle: %s -> %s\n", currentVersion, nextVersion) + + // Update version + r := Release{} + if err := r.UpdateVersion(nextVersion); err != nil { + return err + } + + fmt.Printf("✓ Next release preparation complete: %s\n", nextVersion) + return nil +} From 23422b96cf39cff68aa72809842cddf10959e519 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 12 Mar 2026 17:47:55 +0100 Subject: [PATCH 02/14] ci(ff): support feature freeze automation phase 2 --- RELEASE.md | 464 +++++++++++++++++++++++++++++++++++++++++++++++ magefile_test.go | 378 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 842 insertions(+) create mode 100644 RELEASE.md create mode 100644 magefile_test.go diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..c21df14b31 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,464 @@ +# Fleet-Server Release Automation + +This document describes the fleet-server release automation implemented using Mage, following the architecture patterns established in elastic-agent. + +## Quick Start + +### Prerequisites + +- Go 1.25+ +- Mage installed (`go install github.com/magefile/mage@latest`) +- GitHub token with repo permissions +- Clean git working directory + +### Major/Minor Release (e.g., 9.5.0) + +```bash +export CURRENT_RELEASE="9.5.0" +export GITHUB_TOKEN="ghp_your_token" +export DRY_RUN=true + +# Test first (updates files locally, no push/PR) +mage release:runMajorMinor + +# Review changes +git status +git diff + +# Run for real +export DRY_RUN=false +mage release:runMajorMinor +``` + +### Patch Release (e.g., 9.4.1) + +```bash +# Checkout base branch +git checkout 9.4 +git pull + +export CURRENT_RELEASE="9.4.1" +export BASE_BRANCH="9.4" +export GITHUB_TOKEN="ghp_your_token" + +mage release:runPatch +``` + +## Available Commands + +### Orchestration Commands (Recommended) + +These commands run the full release workflow: + +- `mage release:runMajorMinor` - Complete major/minor release workflow +- `mage release:runPatch` - Complete patch release workflow + +### Individual Commands + +For manual control or debugging: + +- `mage release:updateVersion ` - Update version in version/version.go +- `mage release:updateMergify ` - Add backport rule to .mergify.yml +- `mage release:prepareMajorMinor` - Prepare all files for major/minor release +- `mage release:preparePatch` - Prepare files for patch release +- `mage release:prepareNext` - Prepare next development cycle +- `mage release:createBranch` - Create and push release branch +- `mage release:createPR` - Create GitHub pull request + +## Environment Variables + +### Required + +| Variable | Description | Example | +|----------|-------------|---------| +| `CURRENT_RELEASE` | Version to release | `9.5.0` | +| `GITHUB_TOKEN` | GitHub API token | `ghp_xxx` | + +### Optional + +| Variable | Description | Default | +|----------|-------------|---------| +| `DRY_RUN` | Test mode (no push/PR) | `false` | +| `BASE_BRANCH` | Base branch for PR | `main` | +| `PROJECT_OWNER` | GitHub repository owner | `elastic` | +| `PROJECT_REPO` | GitHub repository name | `fleet-server` | +| `GITHUB_USERNAME` | Git commit author name | `elasticmachine` | +| `GITHUB_EMAIL` | Git commit author email | `infra-root+elasticmachine@elastic.co` | + +## Workflows + +### Major/Minor Release Workflow + +The `runMajorMinor` command performs these steps: + +1. **Prepare Files** + - Updates `version/version.go` with new version + - Adds backport rule to `.mergify.yml` + +2. **Create Branch** + - Creates release branch (e.g., `9.5`) + - Commits all changes + - Pushes to remote (unless DRY_RUN=true) + +3. **Create PR** + - Creates pull request from release branch to main + - Includes version update details + - (Skipped if DRY_RUN=true) + +### Patch Release Workflow + +The `runPatch` command performs these steps: + +1. **Prepare Files** + - Updates `version/version.go` with patch version + +2. **Commit & Review** + - Changes are prepared for manual review + - No automatic branch creation + - Commit to the current release branch manually + +### Next Development Cycle + +The `prepareNext` command: + +- Increments minor version (e.g., 9.4.0 → 9.5.0) +- Updates `version/version.go` +- Used after a release to bump version on main + +## DRY_RUN Mode + +DRY_RUN mode allows you to test the release process safely: + +```bash +export DRY_RUN=true +mage release:runMajorMinor +``` + +**What DRY_RUN does:** +- ✅ Updates local files (version/version.go, .mergify.yml) +- ✅ Shows what would be committed +- ❌ Does NOT create branches +- ❌ Does NOT push to remote +- ❌ Does NOT create pull requests + +**After DRY_RUN:** +- Review changes with `git diff` +- Discard changes with `git checkout .` +- Or proceed with `DRY_RUN=false` + +## Testing on a Fork + +Before using the release automation in production, it's **highly recommended** to test on your personal fork first. This allows you to verify the workflow end-to-end without affecting the official repository. + +### Step 1: Fork and Clone + +1. Fork the fleet-server repository on GitHub to your account +2. Clone your fork locally: + ```bash + git clone https://github.com/YOUR-USERNAME/fleet-server.git + cd fleet-server + ``` + +3. Ensure you're on a clean main branch: + ```bash + git checkout main + git pull origin main + ``` + +### Step 2: Configure Environment + +Set environment variables to point to your fork: + +```bash +# Point to your fork +export PROJECT_OWNER="your-username" +export PROJECT_REPO="fleet-server" + +# Use a test version to avoid confusion +export CURRENT_RELEASE="9.5.0-test" + +# Your GitHub personal access token +export GITHUB_TOKEN="ghp_your_token" + +# Important: Set to false to actually create branches and PRs +export DRY_RUN=false +``` + +### Step 3: Run the Workflow + +Execute the major/minor release workflow: + +```bash +mage release:runMajorMinor +``` + +This will: +- Update `version/version.go` to `9.5.0-test` +- Add backport rule for `9.5` to `.mergify.yml` +- Create branch `9.5` on your fork +- Push the branch to your fork +- Create a PR from `9.5` → `main` on your fork + +### Step 4: Verify the Results + +Check what was created: + +```bash +# View the created branch +git branch -a | grep 9.5 + +# View the created PR +gh pr list --repo YOUR-USERNAME/fleet-server +``` + +Or visit GitHub: `https://github.com/YOUR-USERNAME/fleet-server/pulls` + +### Step 5: Review and Clean Up + +1. Review the PR on your fork to ensure: + - Version was updated correctly + - Mergify rule was added properly + - Commit message is appropriate + +2. If satisfied, close the PR and delete the test branch: + ```bash + gh pr close BRANCH-NUMBER --repo YOUR-USERNAME/fleet-server --delete-branch + ``` + +3. Reset your fork's main branch: + ```bash + git checkout main + git reset --hard origin/main + ``` + +### Testing Different Scenarios + +#### Test Patch Release on Fork + +```bash +export PROJECT_OWNER="your-username" +export CURRENT_RELEASE="9.4.1-test" +export BASE_BRANCH="9.4" +export GITHUB_TOKEN="ghp_your_token" + +# Create the release branch first if it doesn't exist +git checkout -b 9.4 +git push -u origin 9.4 + +# Run patch workflow +mage release:runPatch +``` + +#### Test with DRY_RUN First + +For extra safety, test with DRY_RUN before creating real branches: + +```bash +export PROJECT_OWNER="your-username" +export DRY_RUN=true +mage release:runMajorMinor + +# Review local changes +git diff + +# If satisfied, run for real +export DRY_RUN=false +mage release:runMajorMinor +``` + +### Troubleshooting Fork Testing + +**Issue: "failed to push: authentication required"** +- Ensure your GITHUB_TOKEN has write access to your fork +- Verify: `gh auth status` + +**Issue: "branch already exists"** +- Delete the existing branch first: + ```bash + git branch -D 9.5 + git push origin --delete 9.5 + ``` + +**Issue: "PR already exists"** +- Close or merge the existing PR first +- Or use a different version number (e.g., `9.5.0-test2`) + +### Best Practices + +1. **Always test on fork first** before running in production +2. **Use test version numbers** (e.g., `X.Y.Z-test`) to distinguish from real releases +3. **Clean up test branches and PRs** after testing to keep your fork tidy +4. **Test both workflows**: major/minor AND patch releases +5. **Verify DRY_RUN mode** works correctly before relying on it + +## Troubleshooting + +### "CURRENT_RELEASE environment variable not set" + +Set the required environment variable: +```bash +export CURRENT_RELEASE="9.5.0" +``` + +### "GITHUB_TOKEN environment variable not set" + +Set your GitHub token: +```bash +export GITHUB_TOKEN=$(gh auth token) +``` + +Or create a personal access token at: https://github.com/settings/tokens + +### "failed to open git repo" + +Ensure you're in the fleet-server repository root: +```bash +cd /path/to/fleet-server +``` + +### "version pattern not found" + +The version file format has changed. Check that `version/version.go` contains: +```go +const DefaultVersion = "X.Y.Z" +``` + +### Uncommitted changes warning + +The workflow warns about uncommitted files but continues. To avoid confusion: +```bash +git status +git stash # or commit your changes +``` + +## Architecture + +Fleet-server follows the same architecture as elastic-agent but is simpler: + +**Key Differences from elastic-agent:** +- Fewer files to update (only 1: `version/version.go`) +- No docs/K8s manifest updates needed +- All code in `magefile.go` (no separate package) +- Simpler implementation (~540 lines vs ~2000+ in elastic-agent) + +**Libraries used:** +- `github.com/go-git/go-git/v5` - Git operations +- `github.com/google/go-github/v68` - GitHub API +- `gopkg.in/yaml.v3` - YAML parsing (already present) + +**No external tools:** +- ❌ No `hub` CLI +- ❌ No `gh` CLI +- ❌ No `sed` / `awk` +- ❌ No `yq` +- ✅ Pure Go implementation + +## Examples + +### Complete Major/Minor Release + +```bash +# 1. Start on main branch +git checkout main +git pull + +# 2. Set up environment +export CURRENT_RELEASE="9.5.0" +export GITHUB_TOKEN=$(gh auth token) + +# 3. Test with dry run +export DRY_RUN=true +mage release:runMajorMinor + +# 4. Review changes +git diff version/version.go +git diff .mergify.yml + +# 5. Run for real +export DRY_RUN=false +mage release:runMajorMinor + +# 6. Check the PR that was created +gh pr list +``` + +### Patch Release on Existing Branch + +```bash +# 1. Checkout release branch +git checkout 9.4 +git pull + +# 2. Set up environment +export CURRENT_RELEASE="9.4.1" +export BASE_BRANCH="9.4" +export GITHUB_TOKEN=$(gh auth token) + +# 3. Run patch workflow +mage release:runPatch + +# 4. Review and commit +git diff +git add version/version.go +git commit -m "Bump version to 9.4.1" +git push +``` + +### Bump Version After Release + +```bash +# 1. After 9.5.0 release, bump main to 9.6.0 +git checkout main + +# 2. Run next version prep +export CURRENT_RELEASE="9.5.0" +mage release:prepareNext + +# 3. Commit the change +git add version/version.go +git commit -m "Bump version to 9.6.0" +git push +``` + +## Testing + +Run unit tests for release functions: + +```bash +go test -v -tags=mage -run "TestUpdate|TestLoad|TestPrepare" . +``` + +Check test coverage: + +```bash +go test -v -tags=mage -coverprofile=coverage.out . +go tool cover -func=coverage.out | grep -E "(UpdateVersion|UpdateMergify)" +``` + +## Migration from Makefile + +This replaces the old Makefile-based release process: + +| Old (Makefile) | New (Mage) | +|----------------|------------| +| `make prepare-major-minor-release` | `mage release:runMajorMinor` | +| `make prepare-patch-release` | `mage release:runPatch` | +| `make update-version VERSION=9.5.0` | `mage release:updateVersion 9.5.0` | +| `make prepare-next-release` | `mage release:prepareNext` | + +**Benefits of Mage:** +- Pure Go (no sed, hub, Python dependencies) +- Type-safe with compile-time checks +- Built-in DRY_RUN mode +- Better error messages +- Easier to test and maintain + +## Support + +- **Issues**: https://github.com/elastic/fleet-server/issues +- **Documentation**: This file and migration plan +- **Code**: `magefile.go` (search for "Release Automation") + +--- + +**Last Updated**: 2026-03-12 diff --git a/magefile_test.go b/magefile_test.go new file mode 100644 index 0000000000..a468713400 --- /dev/null +++ b/magefile_test.go @@ -0,0 +1,378 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build mage + +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestUpdateVersion(t *testing.T) { + tests := []struct { + name string + initial string + newVersion string + wantErr bool + wantContent string + }{ + { + name: "update version successfully", + initial: `const DefaultVersion = "9.4.0"`, + newVersion: "9.5.0", + wantErr: false, + wantContent: `const DefaultVersion = "9.5.0"`, + }, + { + name: "update to snapshot version", + initial: `const DefaultVersion = "9.4.0"`, + newVersion: "9.5.0-SNAPSHOT", + wantErr: false, + wantContent: `const DefaultVersion = "9.5.0-SNAPSHOT"`, + }, + { + name: "update with different spacing", + initial: `const DefaultVersion = "9.4.0"`, + newVersion: "9.5.0", + wantErr: false, + wantContent: `const DefaultVersion = "9.5.0"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temp directory and file + tmpDir := t.TempDir() + versionDir := filepath.Join(tmpDir, "version") + err := os.Mkdir(versionDir, 0755) + if err != nil { + t.Fatalf("failed to create version dir: %v", err) + } + + versionFile := filepath.Join(versionDir, "version.go") + initialContent := `// Copyright header + +package version + +` + tt.initial + ` +` + err = os.WriteFile(versionFile, []byte(initialContent), 0644) + if err != nil { + t.Fatalf("failed to write initial file: %v", err) + } + + // Save current dir and change to temp dir + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + // Run UpdateVersion + r := Release{} + err = r.UpdateVersion(tt.newVersion) + + if (err != nil) != tt.wantErr { + t.Errorf("UpdateVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify file content + content, err := os.ReadFile(versionFile) + if err != nil { + t.Fatalf("failed to read updated file: %v", err) + } + + if !strings.Contains(string(content), tt.wantContent) { + t.Errorf("UpdateVersion() content = %s, want to contain %s", string(content), tt.wantContent) + } + } + }) + } +} + +func TestUpdateMergify(t *testing.T) { + tests := []struct { + name string + version string + wantErr bool + wantBranch string + wantLabel string + shouldAppend bool + }{ + { + name: "add new backport rule", + version: "9.5.0", + wantErr: false, + wantBranch: "9.5", + wantLabel: "backport-9.5", + shouldAppend: true, + }, + { + name: "version with patch", + version: "10.0.1", + wantErr: false, + wantBranch: "10.0", + wantLabel: "backport-10.0", + shouldAppend: true, + }, + { + name: "invalid version format", + version: "9", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temp directory and file + tmpDir := t.TempDir() + mergifyFile := filepath.Join(tmpDir, ".mergify.yml") + + initialContent := `pull_request_rules: + - name: backport patches to 9.3 branch + conditions: + - merged + - label=backport-9.3 + actions: + backport: + branches: + - "9.3" +` + err := os.WriteFile(mergifyFile, []byte(initialContent), 0644) + if err != nil { + t.Fatalf("failed to write initial file: %v", err) + } + + // Save current dir and change to temp dir + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + // Run UpdateMergify + r := Release{} + err = r.UpdateMergify(tt.version) + + if (err != nil) != tt.wantErr { + t.Errorf("UpdateMergify() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.shouldAppend { + // Verify file content + content, err := os.ReadFile(mergifyFile) + if err != nil { + t.Fatalf("failed to read updated file: %v", err) + } + + contentStr := string(content) + + // Check that the new rule was added + expectedName := "backport patches to " + tt.wantBranch + " branch" + if !strings.Contains(contentStr, expectedName) { + t.Errorf("UpdateMergify() missing rule name: %s", expectedName) + } + + if !strings.Contains(contentStr, tt.wantLabel) { + t.Errorf("UpdateMergify() missing label: %s", tt.wantLabel) + } + + if !strings.Contains(contentStr, `"`+tt.wantBranch+`"`) { + t.Errorf("UpdateMergify() missing branch: %s", tt.wantBranch) + } + + // Verify old rule is still there + if !strings.Contains(contentStr, "9.3") { + t.Error("UpdateMergify() removed existing rules") + } + } + }) + } +} + +func TestUpdateMergify_Idempotent(t *testing.T) { + // Create temp directory and file + tmpDir := t.TempDir() + mergifyFile := filepath.Join(tmpDir, ".mergify.yml") + + initialContent := `pull_request_rules: + - name: backport patches to 9.5 branch + conditions: + - merged + - label=backport-9.5 + actions: + backport: + branches: + - "9.5" +` + err := os.WriteFile(mergifyFile, []byte(initialContent), 0644) + if err != nil { + t.Fatalf("failed to write initial file: %v", err) + } + + // Save current dir and change to temp dir + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + // Run UpdateMergify twice with same version + r := Release{} + err = r.UpdateMergify("9.5.0") + if err != nil { + t.Fatalf("first UpdateMergify() failed: %v", err) + } + + // Get content after first run + content1, _ := os.ReadFile(mergifyFile) + + err = r.UpdateMergify("9.5.0") + if err != nil { + t.Fatalf("second UpdateMergify() failed: %v", err) + } + + // Get content after second run + content2, _ := os.ReadFile(mergifyFile) + + // Content should be identical (idempotent) + if string(content1) != string(content2) { + t.Error("UpdateMergify() is not idempotent - content changed on second run") + } + + // Should only have one rule for 9.5 + count := strings.Count(string(content2), "backport patches to 9.5 branch") + if count != 1 { + t.Errorf("UpdateMergify() created %d rules for 9.5, want 1", count) + } +} + +func TestLoadReleaseConfigFromEnv(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + wantErr bool + wantCheck func(*testing.T, *ReleaseConfig) + }{ + { + name: "all env vars set", + envVars: map[string]string{ + "CURRENT_RELEASE": "9.5.0", + "BASE_BRANCH": "main", + "PROJECT_OWNER": "elastic", + "PROJECT_REPO": "fleet-server", + }, + wantErr: false, + wantCheck: func(t *testing.T, cfg *ReleaseConfig) { + if cfg.Version != "9.5.0" { + t.Errorf("Version = %s, want 9.5.0", cfg.Version) + } + if cfg.ReleaseBranch != "9.5" { + t.Errorf("ReleaseBranch = %s, want 9.5", cfg.ReleaseBranch) + } + if cfg.BaseBranch != "main" { + t.Errorf("BaseBranch = %s, want main", cfg.BaseBranch) + } + }, + }, + { + name: "defaults applied", + envVars: map[string]string{ + "CURRENT_RELEASE": "10.0.0", + }, + wantErr: false, + wantCheck: func(t *testing.T, cfg *ReleaseConfig) { + if cfg.BaseBranch != "main" { + t.Errorf("BaseBranch = %s, want main (default)", cfg.BaseBranch) + } + if cfg.Owner != "elastic" { + t.Errorf("Owner = %s, want elastic (default)", cfg.Owner) + } + if cfg.Repo != "fleet-server" { + t.Errorf("Repo = %s, want fleet-server (default)", cfg.Repo) + } + }, + }, + { + name: "missing CURRENT_RELEASE", + envVars: map[string]string{}, + wantErr: true, + }, + { + name: "invalid version format", + envVars: map[string]string{ + "CURRENT_RELEASE": "9", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear env and set test values + os.Clearenv() + for k, v := range tt.envVars { + os.Setenv(k, v) + } + + cfg, err := loadReleaseConfigFromEnv() + + if (err != nil) != tt.wantErr { + t.Errorf("loadReleaseConfigFromEnv() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.wantCheck != nil { + tt.wantCheck(t, cfg) + } + }) + } +} + +func TestPrepareNext(t *testing.T) { + // Create temp directory and file + tmpDir := t.TempDir() + versionDir := filepath.Join(tmpDir, "version") + err := os.Mkdir(versionDir, 0755) + if err != nil { + t.Fatalf("failed to create version dir: %v", err) + } + + versionFile := filepath.Join(versionDir, "version.go") + initialContent := `package version + +const DefaultVersion = "9.4.0" +` + err = os.WriteFile(versionFile, []byte(initialContent), 0644) + if err != nil { + t.Fatalf("failed to write initial file: %v", err) + } + + // Save current dir and change to temp dir + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + // Set env var + os.Setenv("CURRENT_RELEASE", "9.4.0") + + // Run PrepareNext + r := Release{} + err = r.PrepareNext() + if err != nil { + t.Fatalf("PrepareNext() failed: %v", err) + } + + // Verify version was incremented + content, err := os.ReadFile(versionFile) + if err != nil { + t.Fatalf("failed to read updated file: %v", err) + } + + expectedVersion := "9.5.0" + if !strings.Contains(string(content), expectedVersion) { + t.Errorf("PrepareNext() = %s, want %s", string(content), expectedVersion) + } +} From a666488077ce4f04fdca8ed17ae73e8037a7181f Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 12 Mar 2026 18:21:30 +0100 Subject: [PATCH 03/14] update --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index fb6c961b57..14a0608c62 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,10 @@ require ( github.com/elastic/go-ucfg v0.9.1 github.com/fxamacker/cbor/v2 v2.9.0 github.com/go-chi/chi/v5 v5.2.5 + github.com/go-git/go-git/v5 v5.12.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/google/go-cmp v0.7.0 + github.com/google/go-github/v68 v68.0.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-version v1.8.0 github.com/hashicorp/golang-lru/v2 v2.0.7 @@ -68,12 +70,10 @@ require ( github.com/fatih/color v1.15.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-github/v68 v68.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect From eff7847ee8d4fec539d99a10ba1b26d9993a6645 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 12 Mar 2026 18:59:25 +0100 Subject: [PATCH 04/14] fix linting --- go.sum | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/go.sum b/go.sum index 7920f5c528..b83ba7e17d 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,14 @@ github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqC github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -64,6 +68,8 @@ github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -72,12 +78,16 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -160,6 +170,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/open-telemetry/opamp-go v0.23.0 h1:k7h7w/muprut9/DAhUC4anX4v7hIdgO02gIsSjV4uq0= github.com/open-telemetry/opamp-go v0.23.0/go.mod h1:DIIVdkLefdqPW5L+4I2twmAicVrTB0Bp5XJAfedZzAM= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -326,6 +338,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From de623a90155005e55155c0b9d2168d924f0caa4b Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 23 Mar 2026 16:08:18 +0100 Subject: [PATCH 05/14] go mod tidy --- go.sum | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/go.sum b/go.sum index af084b6efd..cf68cf87d3 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,6 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= -github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= @@ -291,14 +289,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -308,8 +302,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -339,8 +331,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -348,8 +338,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -357,8 +347,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -367,8 +355,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From bf17d6ffe9cf467142d90cbc44dbb1004cb3c079 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 9 Jul 2026 11:53:05 +0200 Subject: [PATCH 06/14] fix(ci): update magefile_test.go license header to Elastic License 2.0 --- magefile_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magefile_test.go b/magefile_test.go index a468713400..87c6e72a5c 100644 --- a/magefile_test.go +++ b/magefile_test.go @@ -1,6 +1,6 @@ // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License; -// you may not use this file except in compliance with the Elastic License. +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. //go:build mage From c67df45be48133d3e1da6dd2cdcf3ffd5695cdc0 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 9 Jul 2026 12:42:06 +0200 Subject: [PATCH 07/14] Make release automation idempotent on re-run Allow release workflows to be safely retriggered by no-oping when the target version is already applied, reusing existing branches and open PRs, and skipping empty commits. --- RELEASE.md | 21 +++++- magefile.go | 83 +++++++++++++++++++-- magefile_test.go | 188 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 7 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index c21df14b31..38a781d6c2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -146,6 +146,21 @@ mage release:runMajorMinor - Discard changes with `git checkout .` - Or proceed with `DRY_RUN=false` +## Idempotency + +All release automation steps are designed to be **idempotent** — safe to re-run after a partial failure or CI retry: + +| Step | Re-run behavior | +|------|-----------------| +| `updateVersion` | No-op if `DefaultVersion` already matches | +| `updateMergify` | No-op if backport rule already exists | +| `createBranch` | Checks out existing branch instead of failing | +| `commitAll` | Skips commit when there are no changes | +| `push` | Succeeds when remote is already up to date | +| `createPR` | Returns existing open PR for the same head/base | + +This means `mage release:runMajorMinor` and `mage release:runPatch` can be safely retried without manual cleanup when a step has already completed. + ## Testing on a Fork Before using the release automation in production, it's **highly recommended** to test on your personal fork first. This allows you to verify the workflow end-to-end without affecting the official repository. @@ -273,14 +288,16 @@ mage release:runMajorMinor - Verify: `gh auth status` **Issue: "branch already exists"** -- Delete the existing branch first: +- The automation checks out the existing branch automatically on re-run +- To start fresh, delete the branch manually: ```bash git branch -D 9.5 git push origin --delete 9.5 ``` **Issue: "PR already exists"** -- Close or merge the existing PR first +- The automation returns the existing open PR on re-run +- To create a new PR, close the existing one first - Or use a different version number (e.g., `9.5.0-test2`) ### Best Practices diff --git a/magefile.go b/magefile.go index 3047df6906..4f487d65c9 100644 --- a/magefile.go +++ b/magefile.go @@ -2370,9 +2370,16 @@ func (Release) UpdateVersion(newVersion string) error { // Replace the version string // Pattern: const DefaultVersion = "X.Y.Z" re := regexp.MustCompile(`(const\s+DefaultVersion\s*=\s*)"[^"]+"`) - newContent := re.ReplaceAllString(string(content), `${1}"`+newVersion+`"`) + contentStr := string(content) + alreadyAtVersion := regexp.MustCompile(`const\s+DefaultVersion\s*=\s*"` + regexp.QuoteMeta(newVersion) + `"`) + if alreadyAtVersion.MatchString(contentStr) { + fmt.Printf(" Version already set to %s in %s\n", newVersion, versionFile) + return nil + } + + newContent := re.ReplaceAllString(contentStr, `${1}"`+newVersion+`"`) - if newContent == string(content) { + if newContent == contentStr { return fmt.Errorf("version pattern not found in %s", versionFile) } @@ -2454,6 +2461,22 @@ func (g *GitRepo) createBranch(branchName string) error { return fmt.Errorf("failed to get worktree: %w", err) } + refName := plumbing.NewBranchReferenceName(branchName) + _, err = g.repo.Reference(refName, true) + if err == nil { + err = w.Checkout(&git.CheckoutOptions{ + Branch: refName, + }) + if err != nil { + return fmt.Errorf("failed to checkout existing branch: %w", err) + } + fmt.Printf("✓ Checked out existing branch: %s\n", branchName) + return nil + } + if !errors.Is(err, plumbing.ErrReferenceNotFound) { + return fmt.Errorf("failed to check branch: %w", err) + } + // Get current HEAD reference headRef, err := g.repo.Head() if err != nil { @@ -2461,7 +2484,6 @@ func (g *GitRepo) createBranch(branchName string) error { } // Create new branch reference - refName := plumbing.NewBranchReferenceName(branchName) ref := plumbing.NewHashReference(refName, headRef.Hash()) err = g.repo.Storer.SetReference(ref) @@ -2488,6 +2510,15 @@ func (g *GitRepo) commitAll(message, authorName, authorEmail string) error { return fmt.Errorf("failed to get worktree: %w", err) } + status, err := w.Status() + if err != nil { + return fmt.Errorf("failed to get worktree status: %w", err) + } + if status.IsClean() { + fmt.Println(" No changes to commit") + return nil + } + // Add all changes err = w.AddWithOptions(&git.AddOptions{ All: true, @@ -2550,7 +2581,7 @@ func (g *GitRepo) getCurrentBranch() (string, error) { // setRemoteURL sets the URL for a remote func (g *GitRepo) setRemoteURL(remoteName, url string) error { - _, err := g.repo.Remote(remoteName) + remote, err := g.repo.Remote(remoteName) if errors.Is(err, git.ErrRemoteNotFound) { // Remote doesn't exist, create it _, err = g.repo.CreateRemote(&config.RemoteConfig{ @@ -2566,6 +2597,10 @@ func (g *GitRepo) setRemoteURL(remoteName, url string) error { return fmt.Errorf("failed to get remote: %w", err) } + if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] == url { + return nil + } + // Update remote URL err = g.repo.DeleteRemote(remoteName) if err != nil { @@ -2595,6 +2630,23 @@ func newGitHubClient() (*github.Client, error) { return client, nil } +// findExistingPR returns an open pull request for the given head and base branches, if one exists. +func findExistingPR(ctx context.Context, client *github.Client, owner, repo, head, base string) (*github.PullRequest, error) { + opts := &github.PullRequestListOptions{ + State: "open", + Head: fmt.Sprintf("%s:%s", owner, head), + Base: base, + } + prs, _, err := client.PullRequests.List(ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("failed to list pull requests: %w", err) + } + if len(prs) > 0 { + return prs[0], nil + } + return nil, nil +} + // createPR creates a pull request on GitHub func createPR(client *github.Client, cfg *ReleaseConfig, title, body, head, base string, dryRun bool) (*github.PullRequest, error) { if dryRun { @@ -2605,13 +2657,34 @@ func createPR(client *github.Client, cfg *ReleaseConfig, title, body, head, base return nil, nil } - pr, _, err := client.PullRequests.Create(context.Background(), cfg.Owner, cfg.Repo, &github.NewPullRequest{ + ctx := context.Background() + existingPR, err := findExistingPR(ctx, client, cfg.Owner, cfg.Repo, head, base) + if err != nil { + return nil, err + } + if existingPR != nil { + fmt.Printf(" Pull request already exists: #%d %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) + return existingPR, nil + } + + pr, _, err := client.PullRequests.Create(ctx, cfg.Owner, cfg.Repo, &github.NewPullRequest{ Title: github.String(title), Head: github.String(head), Base: github.String(base), Body: github.String(body), }) if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == 422 { + existingPR, listErr := findExistingPR(ctx, client, cfg.Owner, cfg.Repo, head, base) + if listErr != nil { + return nil, fmt.Errorf("failed to create PR: %w", err) + } + if existingPR != nil { + fmt.Printf(" Pull request already exists: #%d %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) + return existingPR, nil + } + } return nil, fmt.Errorf("failed to create PR: %w", err) } diff --git a/magefile_test.go b/magefile_test.go index 87c6e72a5c..92fd6e2175 100644 --- a/magefile_test.go +++ b/magefile_test.go @@ -11,6 +11,9 @@ import ( "path/filepath" "strings" "testing" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" ) func TestUpdateVersion(t *testing.T) { @@ -95,6 +98,191 @@ package version } } +func TestUpdateVersion_Idempotent(t *testing.T) { + tmpDir := t.TempDir() + versionDir := filepath.Join(tmpDir, "version") + err := os.Mkdir(versionDir, 0755) + if err != nil { + t.Fatalf("failed to create version dir: %v", err) + } + + versionFile := filepath.Join(versionDir, "version.go") + initialContent := `package version + +const DefaultVersion = "9.4.0" +` + err = os.WriteFile(versionFile, []byte(initialContent), 0644) + if err != nil { + t.Fatalf("failed to write initial file: %v", err) + } + + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + r := Release{} + err = r.UpdateVersion("9.5.0") + if err != nil { + t.Fatalf("first UpdateVersion() failed: %v", err) + } + + content1, err := os.ReadFile(versionFile) + if err != nil { + t.Fatalf("failed to read file after first update: %v", err) + } + + err = r.UpdateVersion("9.5.0") + if err != nil { + t.Fatalf("second UpdateVersion() failed: %v", err) + } + + content2, err := os.ReadFile(versionFile) + if err != nil { + t.Fatalf("failed to read file after second update: %v", err) + } + + if string(content1) != string(content2) { + t.Error("UpdateVersion() is not idempotent - content changed on second run") + } +} + +func TestUpdateVersion_AlreadyAtVersion(t *testing.T) { + tmpDir := t.TempDir() + versionDir := filepath.Join(tmpDir, "version") + err := os.Mkdir(versionDir, 0755) + if err != nil { + t.Fatalf("failed to create version dir: %v", err) + } + + versionFile := filepath.Join(versionDir, "version.go") + initialContent := `package version + +const DefaultVersion = "9.5.0" +` + err = os.WriteFile(versionFile, []byte(initialContent), 0644) + if err != nil { + t.Fatalf("failed to write initial file: %v", err) + } + + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + r := Release{} + err = r.UpdateVersion("9.5.0") + if err != nil { + t.Fatalf("UpdateVersion() failed when version already set: %v", err) + } + + content, err := os.ReadFile(versionFile) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + if string(content) != initialContent { + t.Errorf("UpdateVersion() modified file when already at target version") + } +} + +func createTestGitRepo(t *testing.T) (*GitRepo, string) { + t.Helper() + + tmpDir := t.TempDir() + repo, err := git.PlainInit(tmpDir, false) + if err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + w, err := repo.Worktree() + if err != nil { + t.Fatalf("failed to get worktree: %v", err) + } + + testFile := filepath.Join(tmpDir, "README.md") + err = os.WriteFile(testFile, []byte("# Test Repo"), 0644) + if err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + _, err = w.Add("README.md") + if err != nil { + t.Fatalf("failed to add file: %v", err) + } + + _, err = w.Commit("Initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Test User", + Email: "test@example.com", + }, + }) + if err != nil { + t.Fatalf("failed to create initial commit: %v", err) + } + + return &GitRepo{repo: repo}, tmpDir +} + +func TestCreateBranch_Idempotent(t *testing.T) { + gitRepo, _ := createTestGitRepo(t) + + err := gitRepo.createBranch("9.5") + if err != nil { + t.Fatalf("first createBranch() failed: %v", err) + } + + branch, err := gitRepo.getCurrentBranch() + if err != nil { + t.Fatalf("getCurrentBranch() failed: %v", err) + } + if branch != "9.5" { + t.Fatalf("expected branch 9.5, got %s", branch) + } + + err = gitRepo.createBranch("9.5") + if err != nil { + t.Fatalf("second createBranch() failed: %v", err) + } + + branch, err = gitRepo.getCurrentBranch() + if err != nil { + t.Fatalf("getCurrentBranch() after second call failed: %v", err) + } + if branch != "9.5" { + t.Errorf("createBranch() is not idempotent - on branch %s, want 9.5", branch) + } +} + +func TestCommitAll_NoChanges(t *testing.T) { + gitRepo, _ := createTestGitRepo(t) + + err := gitRepo.commitAll("Empty commit", "Test Author", "test@example.com") + if err != nil { + t.Errorf("commitAll() with no changes error = %v", err) + } +} + +func TestSetRemoteURL_Idempotent(t *testing.T) { + gitRepo, _ := createTestGitRepo(t) + remoteURL := "https://github.com/test/repo.git" + + err := gitRepo.setRemoteURL("origin", remoteURL) + if err != nil { + t.Fatalf("first setRemoteURL() failed: %v", err) + } + + err = gitRepo.setRemoteURL("origin", remoteURL) + if err != nil { + t.Fatalf("second setRemoteURL() failed: %v", err) + } + + remote, err := gitRepo.repo.Remote("origin") + if err != nil { + t.Fatalf("failed to get remote: %v", err) + } + if len(remote.Config().URLs) == 0 || remote.Config().URLs[0] != remoteURL { + t.Errorf("setRemoteURL() URL = %v, want %s", remote.Config().URLs, remoteURL) + } +} + func TestUpdateMergify(t *testing.T) { tests := []struct { name string From 64eae34b55cbf3e6fe366fe02b70c87e56453658 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 9 Jul 2026 12:50:44 +0200 Subject: [PATCH 08/14] refactor(release): extract FF automation to dev-tools/mage/release Move release workflow logic into a dedicated package aligned with elastic-agent and beats, leaving thin mage release:* wrappers in magefile.go. --- RELEASE.md | 26 +- dev-tools/go.mod | 27 +- dev-tools/go.sum | 98 ++- dev-tools/mage/release/git.go | 188 ++++++ dev-tools/mage/release/git_test.go | 115 ++++ dev-tools/mage/release/github.go | 106 ++++ dev-tools/mage/release/release.go | 296 +++++++++ .../mage/release/release_test.go | 238 ++----- go.mod | 7 +- magefile.go | 586 +----------------- 10 files changed, 937 insertions(+), 750 deletions(-) create mode 100644 dev-tools/mage/release/git.go create mode 100644 dev-tools/mage/release/git_test.go create mode 100644 dev-tools/mage/release/github.go create mode 100644 dev-tools/mage/release/release.go rename magefile_test.go => dev-tools/mage/release/release_test.go (64%) diff --git a/RELEASE.md b/RELEASE.md index 38a781d6c2..7e70288da2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -350,13 +350,23 @@ git stash # or commit your changes ## Architecture -Fleet-server follows the same architecture as elastic-agent but is simpler: +Fleet-server follows the same architecture as elastic-agent and beats: **Key Differences from elastic-agent:** -- Fewer files to update (only 1: `version/version.go`) +- Fewer files to update (only `version/version.go` and `.mergify.yml`) - No docs/K8s manifest updates needed -- All code in `magefile.go` (no separate package) -- Simpler implementation (~540 lines vs ~2000+ in elastic-agent) + +**File structure:** +``` +dev-tools/mage/release/ +├── release.go # Version/mergify updates and workflow orchestration +├── git.go # Git operations (branch, commit, push) +├── github.go # GitHub API integration (PR creation) +├── release_test.go +└── git_test.go +``` + +`magefile.go` exposes thin `mage release:*` wrappers that delegate to this package. **Libraries used:** - `github.com/go-git/go-git/v5` - Git operations @@ -442,14 +452,14 @@ git push Run unit tests for release functions: ```bash -go test -v -tags=mage -run "TestUpdate|TestLoad|TestPrepare" . +cd dev-tools && go test ./mage/release/... -count=1 ``` Check test coverage: ```bash -go test -v -tags=mage -coverprofile=coverage.out . -go tool cover -func=coverage.out | grep -E "(UpdateVersion|UpdateMergify)" +cd dev-tools && go test ./mage/release/... -coverprofile=coverage.out +go tool cover -func=coverage.out ``` ## Migration from Makefile @@ -474,7 +484,7 @@ This replaces the old Makefile-based release process: - **Issues**: https://github.com/elastic/fleet-server/issues - **Documentation**: This file and migration plan -- **Code**: `magefile.go` (search for "Release Automation") +- **Code**: `dev-tools/mage/release/` and `magefile.go` release wrappers --- diff --git a/dev-tools/go.mod b/dev-tools/go.mod index 2f31faa140..63e91b1c12 100644 --- a/dev-tools/go.mod +++ b/dev-tools/go.mod @@ -13,33 +13,56 @@ tool ( ) require ( + github.com/go-git/go-git/v5 v5.12.0 + github.com/google/go-github/v68 v68.0.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/elastic/go-json-schema-generate v0.0.0-20220323152209-ec19b88f6b5e // indirect github.com/elastic/go-licenser v0.4.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/getkin/kin-openapi v0.132.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jstemmer/go-junit-report v1.0.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/sergi/go-diff v1.1.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect go.elastic.co/go-licence-detector v0.7.0 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.40.0 // indirect golang.org/x/perf v0.0.0-20250305200902-02a15fd477ba // indirect golang.org/x/sync v0.14.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.33.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/dev-tools/go.sum b/dev-tools/go.sum index 8355f27cc1..7a40c2b16a 100644 --- a/dev-tools/go.sum +++ b/dev-tools/go.sum @@ -1,8 +1,23 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -15,11 +30,25 @@ github.com/elastic/go-json-schema-generate v0.0.0-20220323152209-ec19b88f6b5e h1 github.com/elastic/go-json-schema-generate v0.0.0-20220323152209-ec19b88f6b5e/go.mod h1:w6t176CDaF2cZXwuQtFA5T+trYjvo5OYxLbBwAE7gxU= github.com/elastic/go-licenser v0.4.2 h1:bPbGm8bUd8rxzSswFOqvQh1dAkKGkgAmrPxbUi+Y9+A= github.com/elastic/go-licenser v0.4.2/go.mod h1:W8eH6FaZDR8fQGm+7FnVa7MxI1b/6dAqxz+zPB8nm5c= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -27,6 +56,8 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -40,18 +71,27 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= +github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0 h1:OggOMmdI0JLwg1FkOKH9S7fVHF0oEm8PX6S8kAdpOps= github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds= github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -82,22 +122,32 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -107,13 +157,24 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.elastic.co/go-licence-detector v0.7.0 h1:qC31sfyfNcNx/zMYcLABU0ac3MbGHZgksCAb5lMDUMg= go.elastic.co/go-licence-detector v0.7.0/go.mod h1:f5ty8pjynzQD8BcS+s0qtlOGKc35/HKQxCVi8SHhV5k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -121,8 +182,14 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/perf v0.0.0-20250305200902-02a15fd477ba h1:0iM7SkkyXtZKGBWfn+ka1Rj6VOCRyFMkEHaCWsaR7no= @@ -130,6 +197,8 @@ golang.org/x/perf v0.0.0-20250305200902-02a15fd477ba/go.mod h1:YEhzh+7vn0fUjydWE golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -137,28 +206,47 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -180,6 +268,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/dev-tools/mage/release/git.go b/dev-tools/mage/release/git.go new file mode 100644 index 0000000000..cd3e35e80b --- /dev/null +++ b/dev-tools/mage/release/git.go @@ -0,0 +1,188 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "errors" + "fmt" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +// GitRepo represents a Git repository with common operations. +type GitRepo struct { + repo *git.Repository +} + +// OpenRepo opens the Git repository at the given path. +func OpenRepo(path string) (*GitRepo, error) { + repo, err := git.PlainOpen(path) + if err != nil { + return nil, fmt.Errorf("failed to open git repo: %w", err) + } + return &GitRepo{repo: repo}, nil +} + +// CreateBranch creates and checks out a new branch, or checks out an existing one. +func (g *GitRepo) CreateBranch(branchName string) error { + w, err := g.repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) + } + + refName := plumbing.NewBranchReferenceName(branchName) + _, err = g.repo.Reference(refName, true) + if err == nil { + err = w.Checkout(&git.CheckoutOptions{ + Branch: refName, + }) + if err != nil { + return fmt.Errorf("failed to checkout existing branch: %w", err) + } + fmt.Printf("✓ Checked out existing branch: %s\n", branchName) + return nil + } + if !errors.Is(err, plumbing.ErrReferenceNotFound) { + return fmt.Errorf("failed to check branch: %w", err) + } + + headRef, err := g.repo.Head() + if err != nil { + return fmt.Errorf("failed to get HEAD: %w", err) + } + + ref := plumbing.NewHashReference(refName, headRef.Hash()) + + err = g.repo.Storer.SetReference(ref) + if err != nil { + return fmt.Errorf("failed to create branch: %w", err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Branch: refName, + }) + if err != nil { + return fmt.Errorf("failed to checkout branch: %w", err) + } + + fmt.Printf("✓ Created and checked out branch: %s\n", branchName) + return nil +} + +// CommitAll commits all changes with the given message. +func (g *GitRepo) CommitAll(message, authorName, authorEmail string) error { + w, err := g.repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) + } + + status, err := w.Status() + if err != nil { + return fmt.Errorf("failed to get worktree status: %w", err) + } + if status.IsClean() { + fmt.Println(" No changes to commit") + return nil + } + + err = w.AddWithOptions(&git.AddOptions{ + All: true, + }) + if err != nil { + return fmt.Errorf("failed to add changes: %w", err) + } + + commit, err := w.Commit(message, &git.CommitOptions{ + Author: &object.Signature{ + Name: authorName, + Email: authorEmail, + When: time.Now(), + }, + }) + if err != nil { + return fmt.Errorf("failed to commit: %w", err) + } + + fmt.Printf("✓ Committed changes: %s\n", commit.String()[:7]) + return nil +} + +// Push pushes the current branch to the remote. +func (g *GitRepo) Push(remoteName string, dryRun bool) error { + if dryRun { + fmt.Printf(" [DRY RUN] Would push to remote: %s\n", remoteName) + return nil + } + + err := g.repo.Push(&git.PushOptions{ + RemoteName: remoteName, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return fmt.Errorf("failed to push: %w", err) + } + + if errors.Is(err, git.NoErrAlreadyUpToDate) { + fmt.Println(" Branch already up to date on remote") + } else { + fmt.Printf("✓ Pushed to remote: %s\n", remoteName) + } + return nil +} + +// GetCurrentBranch returns the name of the current branch. +func (g *GitRepo) GetCurrentBranch() (string, error) { + ref, err := g.repo.Head() + if err != nil { + return "", fmt.Errorf("failed to get HEAD: %w", err) + } + + if !ref.Name().IsBranch() { + return "", fmt.Errorf("HEAD is not a branch") + } + + return ref.Name().Short(), nil +} + +// SetRemoteURL sets the URL for a remote. +func (g *GitRepo) SetRemoteURL(remoteName, url string) error { + remote, err := g.repo.Remote(remoteName) + if errors.Is(err, git.ErrRemoteNotFound) { + _, err = g.repo.CreateRemote(&config.RemoteConfig{ + Name: remoteName, + URLs: []string{url}, + }) + if err != nil { + return fmt.Errorf("failed to create remote: %w", err) + } + fmt.Printf("✓ Created remote %s: %s\n", remoteName, url) + return nil + } else if err != nil { + return fmt.Errorf("failed to get remote: %w", err) + } + + if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] == url { + return nil + } + + err = g.repo.DeleteRemote(remoteName) + if err != nil { + return fmt.Errorf("failed to delete remote: %w", err) + } + + _, err = g.repo.CreateRemote(&config.RemoteConfig{ + Name: remoteName, + URLs: []string{url}, + }) + if err != nil { + return fmt.Errorf("failed to recreate remote: %w", err) + } + + fmt.Printf("✓ Updated remote %s: %s\n", remoteName, url) + return nil +} diff --git a/dev-tools/mage/release/git_test.go b/dev-tools/mage/release/git_test.go new file mode 100644 index 0000000000..2f8d3398ed --- /dev/null +++ b/dev-tools/mage/release/git_test.go @@ -0,0 +1,115 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "os" + "path/filepath" + "testing" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func createTestGitRepo(t *testing.T) (*GitRepo, string) { + t.Helper() + + tmpDir := t.TempDir() + + repo, err := git.PlainInit(tmpDir, false) + if err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + w, err := repo.Worktree() + if err != nil { + t.Fatalf("failed to get worktree: %v", err) + } + + testFile := filepath.Join(tmpDir, "README.md") + err = os.WriteFile(testFile, []byte("# Test Repo"), 0o644) + if err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + _, err = w.Add("README.md") + if err != nil { + t.Fatalf("failed to add file: %v", err) + } + + _, err = w.Commit("Initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Test User", + Email: "test@example.com", + }, + }) + if err != nil { + t.Fatalf("failed to create initial commit: %v", err) + } + + return &GitRepo{repo: repo}, tmpDir +} + +func TestCreateBranchIdempotent(t *testing.T) { + gitRepo, _ := createTestGitRepo(t) + + err := gitRepo.CreateBranch("9.5") + if err != nil { + t.Fatalf("first CreateBranch() failed: %v", err) + } + + branch, err := gitRepo.GetCurrentBranch() + if err != nil { + t.Fatalf("GetCurrentBranch() failed: %v", err) + } + if branch != "9.5" { + t.Fatalf("expected branch 9.5, got %s", branch) + } + + err = gitRepo.CreateBranch("9.5") + if err != nil { + t.Fatalf("second CreateBranch() failed: %v", err) + } + + branch, err = gitRepo.GetCurrentBranch() + if err != nil { + t.Fatalf("GetCurrentBranch() after second call failed: %v", err) + } + if branch != "9.5" { + t.Errorf("CreateBranch() is not idempotent - on branch %s, want 9.5", branch) + } +} + +func TestCommitAllNoChanges(t *testing.T) { + gitRepo, _ := createTestGitRepo(t) + + err := gitRepo.CommitAll("Empty commit", "Test Author", "test@example.com") + if err != nil { + t.Errorf("CommitAll() with no changes error = %v", err) + } +} + +func TestSetRemoteURLIdempotent(t *testing.T) { + gitRepo, _ := createTestGitRepo(t) + remoteURL := "https://github.com/test/repo.git" + + err := gitRepo.SetRemoteURL("origin", remoteURL) + if err != nil { + t.Fatalf("first SetRemoteURL() failed: %v", err) + } + + err = gitRepo.SetRemoteURL("origin", remoteURL) + if err != nil { + t.Fatalf("second SetRemoteURL() failed: %v", err) + } + + remote, err := gitRepo.repo.Remote("origin") + if err != nil { + t.Fatalf("failed to get remote: %v", err) + } + if len(remote.Config().URLs) == 0 || remote.Config().URLs[0] != remoteURL { + t.Errorf("SetRemoteURL() URL = %v, want %s", remote.Config().URLs, remoteURL) + } +} diff --git a/dev-tools/mage/release/github.go b/dev-tools/mage/release/github.go new file mode 100644 index 0000000000..40b28b2061 --- /dev/null +++ b/dev-tools/mage/release/github.go @@ -0,0 +1,106 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/google/go-github/v68/github" +) + +// GitHubClient wraps the GitHub API client. +type GitHubClient struct { + client *github.Client + ctx context.Context +} + +// NewGitHubClient creates a new GitHub client with authentication. +func NewGitHubClient(token string) *GitHubClient { + return &GitHubClient{ + client: github.NewClient(nil).WithAuthToken(token), + ctx: context.Background(), + } +} + +// NewGitHubClientFromEnv creates a GitHub client using GITHUB_TOKEN env var. +func NewGitHubClientFromEnv() (*GitHubClient, error) { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return nil, fmt.Errorf("GITHUB_TOKEN environment variable not set") + } + return NewGitHubClient(token), nil +} + +// PROptions contains options for creating a pull request. +type PROptions struct { + Owner string + Repo string + Title string + Head string + Base string + Body string +} + +// FindOpenPR returns an open pull request matching head and base, or nil if none exists. +func (gh *GitHubClient) FindOpenPR(owner, repo, head, base string) (*github.PullRequest, error) { + headRef := fmt.Sprintf("%s:%s", owner, head) + prs, _, err := gh.client.PullRequests.List(gh.ctx, owner, repo, &github.PullRequestListOptions{ + State: "open", + Head: headRef, + Base: base, + }) + if err != nil { + return nil, fmt.Errorf("failed to list pull requests: %w", err) + } + if len(prs) == 0 { + return nil, nil + } + return prs[0], nil +} + +// CreatePR creates a new pull request. +// If an open pull request already exists for the same head and base, it is returned instead. +func (gh *GitHubClient) CreatePR(opts PROptions, dryRun bool) (*github.PullRequest, error) { + if dryRun { + fmt.Printf(" [DRY RUN] Would create PR:\n") + fmt.Printf(" Title: %s\n", opts.Title) + fmt.Printf(" Head: %s\n", opts.Head) + fmt.Printf(" Base: %s\n", opts.Base) + return nil, nil + } + + existing, err := gh.FindOpenPR(opts.Owner, opts.Repo, opts.Head, opts.Base) + if err != nil { + return nil, err + } + if existing != nil { + fmt.Printf(" Pull request already exists: #%d %s\n", existing.GetNumber(), existing.GetHTMLURL()) + return existing, nil + } + + pr, _, err := gh.client.PullRequests.Create(gh.ctx, opts.Owner, opts.Repo, &github.NewPullRequest{ + Title: github.String(opts.Title), + Head: github.String(opts.Head), + Base: github.String(opts.Base), + Body: github.String(opts.Body), + }) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == 422 { + existing, findErr := gh.FindOpenPR(opts.Owner, opts.Repo, opts.Head, opts.Base) + if findErr == nil && existing != nil { + fmt.Printf(" Pull request already exists: #%d %s\n", existing.GetNumber(), existing.GetHTMLURL()) + return existing, nil + } + } + return nil, fmt.Errorf("failed to create PR: %w", err) + } + + fmt.Printf("✓ Created PR #%d: %s\n", pr.GetNumber(), pr.GetHTMLURL()) + return pr, nil +} diff --git a/dev-tools/mage/release/release.go b/dev-tools/mage/release/release.go new file mode 100644 index 0000000000..414823b887 --- /dev/null +++ b/dev-tools/mage/release/release.go @@ -0,0 +1,296 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" +) + +// ReleaseConfig holds configuration for release operations. +type ReleaseConfig struct { + Version string + BaseBranch string + ReleaseBranch string + Owner string + Repo string + AuthorName string + AuthorEmail string +} + +// LoadReleaseConfigFromEnv loads release configuration from environment variables. +func LoadReleaseConfigFromEnv() (*ReleaseConfig, error) { + version := os.Getenv("CURRENT_RELEASE") + if version == "" { + return nil, fmt.Errorf("CURRENT_RELEASE environment variable not set") + } + + baseBranch := os.Getenv("BASE_BRANCH") + if baseBranch == "" { + baseBranch = "main" + } + + parts := strings.Split(version, ".") + if len(parts) < 2 { + return nil, fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) + } + releaseBranch := fmt.Sprintf("%s.%s", parts[0], parts[1]) + + owner := os.Getenv("PROJECT_OWNER") + if owner == "" { + owner = "elastic" + } + + repo := os.Getenv("PROJECT_REPO") + if repo == "" { + repo = "fleet-server" + } + + authorName := os.Getenv("GITHUB_USERNAME") + if authorName == "" { + authorName = "elasticmachine" + } + + authorEmail := os.Getenv("GITHUB_EMAIL") + if authorEmail == "" { + authorEmail = "infra-root+elasticmachine@elastic.co" + } + + return &ReleaseConfig{ + Version: version, + BaseBranch: baseBranch, + ReleaseBranch: releaseBranch, + Owner: owner, + Repo: repo, + AuthorName: authorName, + AuthorEmail: authorEmail, + }, nil +} + +// UpdateVersion updates the version in version/version.go. +func UpdateVersion(newVersion string) error { + versionFile := "version/version.go" + + content, err := os.ReadFile(versionFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", versionFile, err) + } + + re := regexp.MustCompile(`(const\s+DefaultVersion\s*=\s*)"[^"]+"`) + contentStr := string(content) + alreadyAtVersion := regexp.MustCompile(`const\s+DefaultVersion\s*=\s*"` + regexp.QuoteMeta(newVersion) + `"`) + if alreadyAtVersion.MatchString(contentStr) { + fmt.Printf(" Version already set to %s in %s\n", newVersion, versionFile) + return nil + } + + newContent := re.ReplaceAllString(contentStr, `${1}"`+newVersion+`"`) + + if newContent == contentStr { + return fmt.Errorf("version pattern not found in %s", versionFile) + } + + err = os.WriteFile(versionFile, []byte(newContent), 0o644) + if err != nil { + return fmt.Errorf("failed to write %s: %w", versionFile, err) + } + + fmt.Printf("✓ Updated version to %s in %s\n", newVersion, versionFile) + return nil +} + +// UpdateMergify adds a new backport rule to .mergify.yml. +func UpdateMergify(version string) error { + mergifyFile := ".mergify.yml" + + content, err := os.ReadFile(mergifyFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", mergifyFile, err) + } + + parts := strings.Split(version, ".") + if len(parts) < 2 { + return fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) + } + branchVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) + + searchPattern := fmt.Sprintf("backport patches to %s branch", branchVersion) + if strings.Contains(string(content), searchPattern) { + fmt.Printf(" Backport rule for %s already exists\n", branchVersion) + return nil + } + + newRule := fmt.Sprintf(` - name: backport patches to %s branch + conditions: + - merged + - label=backport-%s + actions: + backport: + branches: + - "%s" +`, branchVersion, branchVersion, branchVersion) + + output := string(content) + newRule + + err = os.WriteFile(mergifyFile, []byte(output), 0o644) + if err != nil { + return fmt.Errorf("failed to write %s: %w", mergifyFile, err) + } + + fmt.Printf("✓ Added backport rule for %s to %s\n", branchVersion, mergifyFile) + return nil +} + +// PrepareMajorMinorRelease prepares files for a major/minor release. +func PrepareMajorMinorRelease(cfg *ReleaseConfig) error { + fmt.Printf("Preparing major/minor release for version %s\n", cfg.Version) + + if err := UpdateVersion(cfg.Version); err != nil { + return err + } + + if err := UpdateMergify(cfg.Version); err != nil { + return err + } + + fmt.Println("✓ Major/minor release preparation complete") + fmt.Printf(" Next steps:\n") + fmt.Printf(" 1. Review changes: git diff\n") + fmt.Printf(" 2. Create branch: mage release:createBranch\n") + fmt.Printf(" 3. Create PR: mage release:createPR\n") + + return nil +} + +// CreateReleaseBranch creates a release branch with all changes committed. +func CreateReleaseBranch(cfg *ReleaseConfig, repoPath string, dryRun bool) error { + repo, err := OpenRepo(repoPath) + if err != nil { + return err + } + + branchName := cfg.ReleaseBranch + fmt.Printf("Creating release branch: %s\n", branchName) + + if !dryRun { + if err := repo.CreateBranch(branchName); err != nil { + return err + } + + commitMsg := fmt.Sprintf("[Release] Prepare %s release", cfg.Version) + if err := repo.CommitAll(commitMsg, cfg.AuthorName, cfg.AuthorEmail); err != nil { + return err + } + } else { + fmt.Printf(" [DRY RUN] Would create branch %s and commit changes\n", branchName) + } + + return repo.Push("origin", dryRun) +} + +// CreateReleasePR creates a pull request for the release. +func CreateReleasePR(cfg *ReleaseConfig, ghClient *GitHubClient, dryRun bool) error { + title := fmt.Sprintf("[Release] %s", cfg.Version) + body := fmt.Sprintf("## Release %s\n\nThis PR prepares the %s release.\n\n### Changes\n- Updated version to %s\n- Added mergify backport rule for %s\n", + cfg.Version, cfg.Version, cfg.Version, cfg.ReleaseBranch) + + _, err := ghClient.CreatePR(PROptions{ + Owner: cfg.Owner, + Repo: cfg.Repo, + Title: title, + Head: cfg.ReleaseBranch, + Base: cfg.BaseBranch, + Body: body, + }, dryRun) + return err +} + +// RunMajorMinorRelease orchestrates the complete major/minor release workflow. +func RunMajorMinorRelease(cfg *ReleaseConfig, dryRun bool) error { + if dryRun { + fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") + } + + fmt.Printf("🚀 Starting major/minor release workflow for %s\n\n", cfg.Version) + + fmt.Println("Step 1: Preparing release files...") + if err := PrepareMajorMinorRelease(cfg); err != nil { + return fmt.Errorf("failed to prepare release: %w", err) + } + + fmt.Println("\nStep 2: Creating release branch...") + if err := CreateReleaseBranch(cfg, ".", dryRun); err != nil { + return fmt.Errorf("failed to create branch: %w", err) + } + + fmt.Println("\nStep 3: Creating pull request...") + ghClient, err := NewGitHubClientFromEnv() + if err != nil { + return err + } + if err := CreateReleasePR(cfg, ghClient, dryRun); err != nil { + return fmt.Errorf("failed to create PR: %w", err) + } + + fmt.Printf("\n✅ Major/minor release workflow complete for %s\n", cfg.Version) + return nil +} + +// PreparePatchRelease prepares files for a patch release. +func PreparePatchRelease(cfg *ReleaseConfig) error { + fmt.Printf("Preparing patch release for version %s\n", cfg.Version) + + if err := UpdateVersion(cfg.Version); err != nil { + return err + } + + fmt.Println("✓ Patch release preparation complete") + return nil +} + +// RunPatchRelease orchestrates the complete patch release workflow. +func RunPatchRelease(cfg *ReleaseConfig, dryRun bool) error { + if dryRun { + fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") + } + + fmt.Printf("🚀 Starting patch release workflow for %s\n\n", cfg.Version) + + fmt.Println("Preparing patch release files...") + if err := PreparePatchRelease(cfg); err != nil { + return fmt.Errorf("failed to prepare patch release: %w", err) + } + + fmt.Printf("\n✅ Patch release workflow complete for %s\n", cfg.Version) + fmt.Println(" Review changes and commit to the release branch") + return nil +} + +// PrepareNextRelease prepares files for the next development cycle. +func PrepareNextRelease(currentVersion string) error { + parts := strings.Split(currentVersion, ".") + if len(parts) < 3 { + return fmt.Errorf("invalid version format: %s", currentVersion) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("invalid minor version: %s", parts[1]) + } + + nextVersion := fmt.Sprintf("%s.%d.0", parts[0], minor+1) + + fmt.Printf("Preparing next development cycle: %s -> %s\n", currentVersion, nextVersion) + + if err := UpdateVersion(nextVersion); err != nil { + return err + } + + fmt.Printf("✓ Next release preparation complete: %s\n", nextVersion) + return nil +} diff --git a/magefile_test.go b/dev-tools/mage/release/release_test.go similarity index 64% rename from magefile_test.go rename to dev-tools/mage/release/release_test.go index 92fd6e2175..d92d53e2fe 100644 --- a/magefile_test.go +++ b/dev-tools/mage/release/release_test.go @@ -2,18 +2,13 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -//go:build mage - -package main +package release import ( "os" "path/filepath" "strings" "testing" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" ) func TestUpdateVersion(t *testing.T) { @@ -49,10 +44,9 @@ func TestUpdateVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create temp directory and file tmpDir := t.TempDir() versionDir := filepath.Join(tmpDir, "version") - err := os.Mkdir(versionDir, 0755) + err := os.Mkdir(versionDir, 0o755) if err != nil { t.Fatalf("failed to create version dir: %v", err) } @@ -64,19 +58,20 @@ package version ` + tt.initial + ` ` - err = os.WriteFile(versionFile, []byte(initialContent), 0644) + err = os.WriteFile(versionFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } - // Save current dir and change to temp dir origDir, _ := os.Getwd() - defer os.Chdir(origDir) - os.Chdir(tmpDir) + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } - // Run UpdateVersion - r := Release{} - err = r.UpdateVersion(tt.newVersion) + err = UpdateVersion(tt.newVersion) if (err != nil) != tt.wantErr { t.Errorf("UpdateVersion() error = %v, wantErr %v", err, tt.wantErr) @@ -84,7 +79,6 @@ package version } if !tt.wantErr { - // Verify file content content, err := os.ReadFile(versionFile) if err != nil { t.Fatalf("failed to read updated file: %v", err) @@ -98,10 +92,10 @@ package version } } -func TestUpdateVersion_Idempotent(t *testing.T) { +func TestUpdateVersionIdempotent(t *testing.T) { tmpDir := t.TempDir() versionDir := filepath.Join(tmpDir, "version") - err := os.Mkdir(versionDir, 0755) + err := os.Mkdir(versionDir, 0o755) if err != nil { t.Fatalf("failed to create version dir: %v", err) } @@ -111,17 +105,20 @@ func TestUpdateVersion_Idempotent(t *testing.T) { const DefaultVersion = "9.4.0" ` - err = os.WriteFile(versionFile, []byte(initialContent), 0644) + err = os.WriteFile(versionFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } origDir, _ := os.Getwd() - defer os.Chdir(origDir) - os.Chdir(tmpDir) + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } - r := Release{} - err = r.UpdateVersion("9.5.0") + err = UpdateVersion("9.5.0") if err != nil { t.Fatalf("first UpdateVersion() failed: %v", err) } @@ -131,7 +128,7 @@ const DefaultVersion = "9.4.0" t.Fatalf("failed to read file after first update: %v", err) } - err = r.UpdateVersion("9.5.0") + err = UpdateVersion("9.5.0") if err != nil { t.Fatalf("second UpdateVersion() failed: %v", err) } @@ -146,10 +143,10 @@ const DefaultVersion = "9.4.0" } } -func TestUpdateVersion_AlreadyAtVersion(t *testing.T) { +func TestUpdateVersionAlreadyAtVersion(t *testing.T) { tmpDir := t.TempDir() versionDir := filepath.Join(tmpDir, "version") - err := os.Mkdir(versionDir, 0755) + err := os.Mkdir(versionDir, 0o755) if err != nil { t.Fatalf("failed to create version dir: %v", err) } @@ -159,17 +156,20 @@ func TestUpdateVersion_AlreadyAtVersion(t *testing.T) { const DefaultVersion = "9.5.0" ` - err = os.WriteFile(versionFile, []byte(initialContent), 0644) + err = os.WriteFile(versionFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } origDir, _ := os.Getwd() - defer os.Chdir(origDir) - os.Chdir(tmpDir) + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } - r := Release{} - err = r.UpdateVersion("9.5.0") + err = UpdateVersion("9.5.0") if err != nil { t.Fatalf("UpdateVersion() failed when version already set: %v", err) } @@ -183,106 +183,6 @@ const DefaultVersion = "9.5.0" } } -func createTestGitRepo(t *testing.T) (*GitRepo, string) { - t.Helper() - - tmpDir := t.TempDir() - repo, err := git.PlainInit(tmpDir, false) - if err != nil { - t.Fatalf("failed to init repo: %v", err) - } - - w, err := repo.Worktree() - if err != nil { - t.Fatalf("failed to get worktree: %v", err) - } - - testFile := filepath.Join(tmpDir, "README.md") - err = os.WriteFile(testFile, []byte("# Test Repo"), 0644) - if err != nil { - t.Fatalf("failed to write test file: %v", err) - } - - _, err = w.Add("README.md") - if err != nil { - t.Fatalf("failed to add file: %v", err) - } - - _, err = w.Commit("Initial commit", &git.CommitOptions{ - Author: &object.Signature{ - Name: "Test User", - Email: "test@example.com", - }, - }) - if err != nil { - t.Fatalf("failed to create initial commit: %v", err) - } - - return &GitRepo{repo: repo}, tmpDir -} - -func TestCreateBranch_Idempotent(t *testing.T) { - gitRepo, _ := createTestGitRepo(t) - - err := gitRepo.createBranch("9.5") - if err != nil { - t.Fatalf("first createBranch() failed: %v", err) - } - - branch, err := gitRepo.getCurrentBranch() - if err != nil { - t.Fatalf("getCurrentBranch() failed: %v", err) - } - if branch != "9.5" { - t.Fatalf("expected branch 9.5, got %s", branch) - } - - err = gitRepo.createBranch("9.5") - if err != nil { - t.Fatalf("second createBranch() failed: %v", err) - } - - branch, err = gitRepo.getCurrentBranch() - if err != nil { - t.Fatalf("getCurrentBranch() after second call failed: %v", err) - } - if branch != "9.5" { - t.Errorf("createBranch() is not idempotent - on branch %s, want 9.5", branch) - } -} - -func TestCommitAll_NoChanges(t *testing.T) { - gitRepo, _ := createTestGitRepo(t) - - err := gitRepo.commitAll("Empty commit", "Test Author", "test@example.com") - if err != nil { - t.Errorf("commitAll() with no changes error = %v", err) - } -} - -func TestSetRemoteURL_Idempotent(t *testing.T) { - gitRepo, _ := createTestGitRepo(t) - remoteURL := "https://github.com/test/repo.git" - - err := gitRepo.setRemoteURL("origin", remoteURL) - if err != nil { - t.Fatalf("first setRemoteURL() failed: %v", err) - } - - err = gitRepo.setRemoteURL("origin", remoteURL) - if err != nil { - t.Fatalf("second setRemoteURL() failed: %v", err) - } - - remote, err := gitRepo.repo.Remote("origin") - if err != nil { - t.Fatalf("failed to get remote: %v", err) - } - if len(remote.Config().URLs) == 0 || remote.Config().URLs[0] != remoteURL { - t.Errorf("setRemoteURL() URL = %v, want %s", remote.Config().URLs, remoteURL) - } -} - func TestUpdateMergify(t *testing.T) { tests := []struct { name string @@ -317,7 +217,6 @@ func TestUpdateMergify(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create temp directory and file tmpDir := t.TempDir() mergifyFile := filepath.Join(tmpDir, ".mergify.yml") @@ -331,19 +230,20 @@ func TestUpdateMergify(t *testing.T) { branches: - "9.3" ` - err := os.WriteFile(mergifyFile, []byte(initialContent), 0644) + err := os.WriteFile(mergifyFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } - // Save current dir and change to temp dir origDir, _ := os.Getwd() - defer os.Chdir(origDir) - os.Chdir(tmpDir) + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } - // Run UpdateMergify - r := Release{} - err = r.UpdateMergify(tt.version) + err = UpdateMergify(tt.version) if (err != nil) != tt.wantErr { t.Errorf("UpdateMergify() error = %v, wantErr %v", err, tt.wantErr) @@ -351,7 +251,6 @@ func TestUpdateMergify(t *testing.T) { } if !tt.wantErr && tt.shouldAppend { - // Verify file content content, err := os.ReadFile(mergifyFile) if err != nil { t.Fatalf("failed to read updated file: %v", err) @@ -359,7 +258,6 @@ func TestUpdateMergify(t *testing.T) { contentStr := string(content) - // Check that the new rule was added expectedName := "backport patches to " + tt.wantBranch + " branch" if !strings.Contains(contentStr, expectedName) { t.Errorf("UpdateMergify() missing rule name: %s", expectedName) @@ -373,7 +271,6 @@ func TestUpdateMergify(t *testing.T) { t.Errorf("UpdateMergify() missing branch: %s", tt.wantBranch) } - // Verify old rule is still there if !strings.Contains(contentStr, "9.3") { t.Error("UpdateMergify() removed existing rules") } @@ -382,8 +279,7 @@ func TestUpdateMergify(t *testing.T) { } } -func TestUpdateMergify_Idempotent(t *testing.T) { - // Create temp directory and file +func TestUpdateMergifyIdempotent(t *testing.T) { tmpDir := t.TempDir() mergifyFile := filepath.Join(tmpDir, ".mergify.yml") @@ -397,40 +293,37 @@ func TestUpdateMergify_Idempotent(t *testing.T) { branches: - "9.5" ` - err := os.WriteFile(mergifyFile, []byte(initialContent), 0644) + err := os.WriteFile(mergifyFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } - // Save current dir and change to temp dir origDir, _ := os.Getwd() - defer os.Chdir(origDir) - os.Chdir(tmpDir) + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } - // Run UpdateMergify twice with same version - r := Release{} - err = r.UpdateMergify("9.5.0") + err = UpdateMergify("9.5.0") if err != nil { t.Fatalf("first UpdateMergify() failed: %v", err) } - // Get content after first run content1, _ := os.ReadFile(mergifyFile) - err = r.UpdateMergify("9.5.0") + err = UpdateMergify("9.5.0") if err != nil { t.Fatalf("second UpdateMergify() failed: %v", err) } - // Get content after second run content2, _ := os.ReadFile(mergifyFile) - // Content should be identical (idempotent) if string(content1) != string(content2) { t.Error("UpdateMergify() is not idempotent - content changed on second run") } - // Should only have one rule for 9.5 count := strings.Count(string(content2), "backport patches to 9.5 branch") if count != 1 { t.Errorf("UpdateMergify() created %d rules for 9.5, want 1", count) @@ -499,16 +392,15 @@ func TestLoadReleaseConfigFromEnv(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Clear env and set test values os.Clearenv() for k, v := range tt.envVars { os.Setenv(k, v) } - cfg, err := loadReleaseConfigFromEnv() + cfg, err := LoadReleaseConfigFromEnv() if (err != nil) != tt.wantErr { - t.Errorf("loadReleaseConfigFromEnv() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("LoadReleaseConfigFromEnv() error = %v, wantErr %v", err, tt.wantErr) return } @@ -519,11 +411,10 @@ func TestLoadReleaseConfigFromEnv(t *testing.T) { } } -func TestPrepareNext(t *testing.T) { - // Create temp directory and file +func TestPrepareNextRelease(t *testing.T) { tmpDir := t.TempDir() versionDir := filepath.Join(tmpDir, "version") - err := os.Mkdir(versionDir, 0755) + err := os.Mkdir(versionDir, 0o755) if err != nil { t.Fatalf("failed to create version dir: %v", err) } @@ -533,27 +424,24 @@ func TestPrepareNext(t *testing.T) { const DefaultVersion = "9.4.0" ` - err = os.WriteFile(versionFile, []byte(initialContent), 0644) + err = os.WriteFile(versionFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } - // Save current dir and change to temp dir origDir, _ := os.Getwd() - defer os.Chdir(origDir) - os.Chdir(tmpDir) - - // Set env var - os.Setenv("CURRENT_RELEASE", "9.4.0") + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } - // Run PrepareNext - r := Release{} - err = r.PrepareNext() + err = PrepareNextRelease("9.4.0") if err != nil { - t.Fatalf("PrepareNext() failed: %v", err) + t.Fatalf("PrepareNextRelease() failed: %v", err) } - // Verify version was incremented content, err := os.ReadFile(versionFile) if err != nil { t.Fatalf("failed to read updated file: %v", err) @@ -561,6 +449,6 @@ const DefaultVersion = "9.4.0" expectedVersion := "9.5.0" if !strings.Contains(string(content), expectedVersion) { - t.Errorf("PrepareNext() = %s, want %s", string(content), expectedVersion) + t.Errorf("PrepareNextRelease() = %s, want %s", string(content), expectedVersion) } } diff --git a/go.mod b/go.mod index 708a51dcde..950ad1539b 100644 --- a/go.mod +++ b/go.mod @@ -10,14 +10,13 @@ require ( github.com/elastic/elastic-agent-client/v7 v7.18.1 github.com/elastic/elastic-agent-libs v0.44.0 github.com/elastic/elastic-agent-system-metrics v0.14.4 + github.com/elastic/fleet-server/dev-tools v0.0.0 github.com/elastic/go-elasticsearch/v8 v8.19.6 github.com/elastic/go-ucfg v0.9.1 github.com/fxamacker/cbor/v2 v2.9.2 github.com/go-chi/chi/v5 v5.3.0 - github.com/go-git/go-git/v5 v5.12.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-github/v68 v68.0.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-version v1.9.0 github.com/hashicorp/golang-lru/v2 v2.0.7 @@ -70,10 +69,12 @@ require ( github.com/fatih/color v1.15.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-github/v68 v68.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -119,3 +120,5 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.1 // indirect ) + +replace github.com/elastic/fleet-server/dev-tools => ./dev-tools diff --git a/magefile.go b/magefile.go index 4f487d65c9..3784891d16 100644 --- a/magefile.go +++ b/magefile.go @@ -40,17 +40,13 @@ import ( "sync" "time" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/google/go-github/v68/github" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" "gopkg.in/yaml.v3" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/transport/tlscommon" + "github.com/elastic/fleet-server/dev-tools/mage/release" "github.com/elastic/fleet-server/v7/version" ) @@ -2293,607 +2289,79 @@ func (Test) CloudE2ERun() error { return err } -// ============================================================================ -// Release Automation Functions -// ============================================================================ - -// ReleaseConfig holds configuration for release operations -type ReleaseConfig struct { - Version string - BaseBranch string - ReleaseBranch string - Owner string - Repo string - AuthorName string - AuthorEmail string +// UpdateVersion updates the version in version/version.go. +func (Release) UpdateVersion(version string) error { + return release.UpdateVersion(version) } -// loadReleaseConfigFromEnv loads release configuration from environment variables -func loadReleaseConfigFromEnv() (*ReleaseConfig, error) { - version := os.Getenv("CURRENT_RELEASE") - if version == "" { - return nil, fmt.Errorf("CURRENT_RELEASE environment variable not set") - } - - baseBranch := os.Getenv("BASE_BRANCH") - if baseBranch == "" { - baseBranch = "main" - } - - // Extract major.minor for release branch (e.g., "9.4.0" -> "9.4") - parts := strings.Split(version, ".") - if len(parts) < 2 { - return nil, fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) - } - releaseBranch := fmt.Sprintf("%s.%s", parts[0], parts[1]) - - owner := os.Getenv("PROJECT_OWNER") - if owner == "" { - owner = "elastic" - } - - repo := os.Getenv("PROJECT_REPO") - if repo == "" { - repo = "fleet-server" - } - - authorName := os.Getenv("GITHUB_USERNAME") - if authorName == "" { - authorName = "elasticmachine" - } - - authorEmail := os.Getenv("GITHUB_EMAIL") - if authorEmail == "" { - authorEmail = "infra-root+elasticmachine@elastic.co" - } - - return &ReleaseConfig{ - Version: version, - BaseBranch: baseBranch, - ReleaseBranch: releaseBranch, - Owner: owner, - Repo: repo, - AuthorName: authorName, - AuthorEmail: authorEmail, - }, nil -} - -// UpdateVersion updates the version in version/version.go -func (Release) UpdateVersion(newVersion string) error { - versionFile := "version/version.go" - - content, err := os.ReadFile(versionFile) - if err != nil { - return fmt.Errorf("failed to read %s: %w", versionFile, err) - } - - // Replace the version string - // Pattern: const DefaultVersion = "X.Y.Z" - re := regexp.MustCompile(`(const\s+DefaultVersion\s*=\s*)"[^"]+"`) - contentStr := string(content) - alreadyAtVersion := regexp.MustCompile(`const\s+DefaultVersion\s*=\s*"` + regexp.QuoteMeta(newVersion) + `"`) - if alreadyAtVersion.MatchString(contentStr) { - fmt.Printf(" Version already set to %s in %s\n", newVersion, versionFile) - return nil - } - - newContent := re.ReplaceAllString(contentStr, `${1}"`+newVersion+`"`) - - if newContent == contentStr { - return fmt.Errorf("version pattern not found in %s", versionFile) - } - - err = os.WriteFile(versionFile, []byte(newContent), 0644) - if err != nil { - return fmt.Errorf("failed to write %s: %w", versionFile, err) - } - - fmt.Printf("✓ Updated version to %s in %s\n", newVersion, versionFile) - return nil -} - -// UpdateMergify adds a new backport rule to .mergify.yml +// UpdateMergify adds a new backport rule to .mergify.yml. func (Release) UpdateMergify(version string) error { - mergifyFile := ".mergify.yml" - - // Read the YAML file - content, err := os.ReadFile(mergifyFile) - if err != nil { - return fmt.Errorf("failed to read %s: %w", mergifyFile, err) - } - - // Extract major.minor from version (e.g., "9.4.0" -> "9.4") - parts := strings.Split(version, ".") - if len(parts) < 2 { - return fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) - } - branchVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) - - // Check if rule already exists - searchPattern := fmt.Sprintf("backport patches to %s branch", branchVersion) - if strings.Contains(string(content), searchPattern) { - fmt.Printf(" Backport rule for %s already exists\n", branchVersion) - return nil - } - - // Create new backport rule following the existing format - newRule := fmt.Sprintf(` - name: backport patches to %s branch - conditions: - - merged - - label=backport-%s - actions: - backport: - branches: - - "%s" -`, branchVersion, branchVersion, branchVersion) - - // Append the new rule to the end of the file - output := string(content) + newRule - - // Write back to file - err = os.WriteFile(mergifyFile, []byte(output), 0644) - if err != nil { - return fmt.Errorf("failed to write %s: %w", mergifyFile, err) - } - - fmt.Printf("✓ Added backport rule for %s to %s\n", branchVersion, mergifyFile) - return nil -} - -// GitRepo represents a Git repository with common operations -type GitRepo struct { - repo *git.Repository -} - -// openRepo opens the Git repository at the given path -func openRepo(path string) (*GitRepo, error) { - repo, err := git.PlainOpen(path) - if err != nil { - return nil, fmt.Errorf("failed to open git repo: %w", err) - } - return &GitRepo{repo: repo}, nil -} - -// createBranch creates and checks out a new branch -func (g *GitRepo) createBranch(branchName string) error { - w, err := g.repo.Worktree() - if err != nil { - return fmt.Errorf("failed to get worktree: %w", err) - } - - refName := plumbing.NewBranchReferenceName(branchName) - _, err = g.repo.Reference(refName, true) - if err == nil { - err = w.Checkout(&git.CheckoutOptions{ - Branch: refName, - }) - if err != nil { - return fmt.Errorf("failed to checkout existing branch: %w", err) - } - fmt.Printf("✓ Checked out existing branch: %s\n", branchName) - return nil - } - if !errors.Is(err, plumbing.ErrReferenceNotFound) { - return fmt.Errorf("failed to check branch: %w", err) - } - - // Get current HEAD reference - headRef, err := g.repo.Head() - if err != nil { - return fmt.Errorf("failed to get HEAD: %w", err) - } - - // Create new branch reference - ref := plumbing.NewHashReference(refName, headRef.Hash()) - - err = g.repo.Storer.SetReference(ref) - if err != nil { - return fmt.Errorf("failed to create branch: %w", err) - } - - // Checkout the new branch - err = w.Checkout(&git.CheckoutOptions{ - Branch: refName, - }) - if err != nil { - return fmt.Errorf("failed to checkout branch: %w", err) - } - - fmt.Printf("✓ Created and checked out branch: %s\n", branchName) - return nil -} - -// commitAll commits all changes with the given message -func (g *GitRepo) commitAll(message, authorName, authorEmail string) error { - w, err := g.repo.Worktree() - if err != nil { - return fmt.Errorf("failed to get worktree: %w", err) - } - - status, err := w.Status() - if err != nil { - return fmt.Errorf("failed to get worktree status: %w", err) - } - if status.IsClean() { - fmt.Println(" No changes to commit") - return nil - } - - // Add all changes - err = w.AddWithOptions(&git.AddOptions{ - All: true, - }) - if err != nil { - return fmt.Errorf("failed to add changes: %w", err) - } - - // Create commit - commit, err := w.Commit(message, &git.CommitOptions{ - Author: &object.Signature{ - Name: authorName, - Email: authorEmail, - When: time.Now(), - }, - }) - if err != nil { - return fmt.Errorf("failed to commit: %w", err) - } - - fmt.Printf("✓ Committed changes: %s\n", commit.String()[:7]) - return nil -} - -// push pushes the current branch to the remote -func (g *GitRepo) push(remoteName string, dryRun bool) error { - if dryRun { - fmt.Printf(" [DRY RUN] Would push to remote: %s\n", remoteName) - return nil - } - - err := g.repo.Push(&git.PushOptions{ - RemoteName: remoteName, - }) - if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - return fmt.Errorf("failed to push: %w", err) - } - - if errors.Is(err, git.NoErrAlreadyUpToDate) { - fmt.Println(" Branch already up to date on remote") - } else { - fmt.Printf("✓ Pushed to remote: %s\n", remoteName) - } - return nil -} - -// getCurrentBranch returns the name of the current branch -func (g *GitRepo) getCurrentBranch() (string, error) { - ref, err := g.repo.Head() - if err != nil { - return "", fmt.Errorf("failed to get HEAD: %w", err) - } - - if !ref.Name().IsBranch() { - return "", fmt.Errorf("HEAD is not a branch") - } - - return ref.Name().Short(), nil -} - -// setRemoteURL sets the URL for a remote -func (g *GitRepo) setRemoteURL(remoteName, url string) error { - remote, err := g.repo.Remote(remoteName) - if errors.Is(err, git.ErrRemoteNotFound) { - // Remote doesn't exist, create it - _, err = g.repo.CreateRemote(&config.RemoteConfig{ - Name: remoteName, - URLs: []string{url}, - }) - if err != nil { - return fmt.Errorf("failed to create remote: %w", err) - } - fmt.Printf("✓ Created remote %s: %s\n", remoteName, url) - return nil - } else if err != nil { - return fmt.Errorf("failed to get remote: %w", err) - } - - if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] == url { - return nil - } - - // Update remote URL - err = g.repo.DeleteRemote(remoteName) - if err != nil { - return fmt.Errorf("failed to delete remote: %w", err) - } - - _, err = g.repo.CreateRemote(&config.RemoteConfig{ - Name: remoteName, - URLs: []string{url}, - }) - if err != nil { - return fmt.Errorf("failed to recreate remote: %w", err) - } - - fmt.Printf("✓ Updated remote %s: %s\n", remoteName, url) - return nil -} - -// newGitHubClient creates a new GitHub client from environment -func newGitHubClient() (*github.Client, error) { - token := os.Getenv("GITHUB_TOKEN") - if token == "" { - return nil, fmt.Errorf("GITHUB_TOKEN environment variable not set") - } - - client := github.NewClient(nil).WithAuthToken(token) - return client, nil -} - -// findExistingPR returns an open pull request for the given head and base branches, if one exists. -func findExistingPR(ctx context.Context, client *github.Client, owner, repo, head, base string) (*github.PullRequest, error) { - opts := &github.PullRequestListOptions{ - State: "open", - Head: fmt.Sprintf("%s:%s", owner, head), - Base: base, - } - prs, _, err := client.PullRequests.List(ctx, owner, repo, opts) - if err != nil { - return nil, fmt.Errorf("failed to list pull requests: %w", err) - } - if len(prs) > 0 { - return prs[0], nil - } - return nil, nil + return release.UpdateMergify(version) } -// createPR creates a pull request on GitHub -func createPR(client *github.Client, cfg *ReleaseConfig, title, body, head, base string, dryRun bool) (*github.PullRequest, error) { - if dryRun { - fmt.Printf(" [DRY RUN] Would create PR:\n") - fmt.Printf(" Title: %s\n", title) - fmt.Printf(" Head: %s\n", head) - fmt.Printf(" Base: %s\n", base) - return nil, nil - } - - ctx := context.Background() - existingPR, err := findExistingPR(ctx, client, cfg.Owner, cfg.Repo, head, base) - if err != nil { - return nil, err - } - if existingPR != nil { - fmt.Printf(" Pull request already exists: #%d %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) - return existingPR, nil - } - - pr, _, err := client.PullRequests.Create(ctx, cfg.Owner, cfg.Repo, &github.NewPullRequest{ - Title: github.String(title), - Head: github.String(head), - Base: github.String(base), - Body: github.String(body), - }) - if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) && ghErr.Response.StatusCode == 422 { - existingPR, listErr := findExistingPR(ctx, client, cfg.Owner, cfg.Repo, head, base) - if listErr != nil { - return nil, fmt.Errorf("failed to create PR: %w", err) - } - if existingPR != nil { - fmt.Printf(" Pull request already exists: #%d %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) - return existingPR, nil - } - } - return nil, fmt.Errorf("failed to create PR: %w", err) - } - - fmt.Printf("✓ Created PR #%d: %s\n", *pr.Number, *pr.HTMLURL) - return pr, nil -} - -// PrepareMajorMinor prepares files for a major/minor release +// PrepareMajorMinor prepares files for a major/minor release. func (Release) PrepareMajorMinor() error { - cfg, err := loadReleaseConfigFromEnv() + cfg, err := release.LoadReleaseConfigFromEnv() if err != nil { return err } - - fmt.Printf("Preparing major/minor release for version %s\n", cfg.Version) - - // Update version - r := Release{} - if err := r.UpdateVersion(cfg.Version); err != nil { - return err - } - - // Update mergify - if err := r.UpdateMergify(cfg.Version); err != nil { - return err - } - - fmt.Println("✓ Major/minor release preparation complete") - fmt.Printf(" Next steps:\n") - fmt.Printf(" 1. Review changes: git diff\n") - fmt.Printf(" 2. Create branch: mage release:createBranch\n") - fmt.Printf(" 3. Create PR: mage release:createPR\n") - - return nil + return release.PrepareMajorMinorRelease(cfg) } -// CreateBranch creates a release branch with all changes committed +// CreateBranch creates a release branch with all changes committed. func (Release) CreateBranch() error { - cfg, err := loadReleaseConfigFromEnv() - if err != nil { - return err - } - - dryRun := os.Getenv("DRY_RUN") == "true" - - repo, err := openRepo(".") + cfg, err := release.LoadReleaseConfigFromEnv() if err != nil { return err } - - branchName := cfg.ReleaseBranch - fmt.Printf("Creating release branch: %s\n", branchName) - - if !dryRun { - // Create and checkout branch - if err := repo.createBranch(branchName); err != nil { - return err - } - - // Commit all changes - commitMsg := fmt.Sprintf("[Release] Prepare %s release", cfg.Version) - if err := repo.commitAll(commitMsg, cfg.AuthorName, cfg.AuthorEmail); err != nil { - return err - } - } else { - fmt.Printf(" [DRY RUN] Would create branch %s and commit changes\n", branchName) - } - - // Push to remote - if err := repo.push("origin", dryRun); err != nil { - return err - } - - return nil + return release.CreateReleaseBranch(cfg, ".", os.Getenv("DRY_RUN") == "true") } -// CreatePR creates a pull request for the release +// CreatePR creates a pull request for the release. func (Release) CreatePR() error { - cfg, err := loadReleaseConfigFromEnv() + cfg, err := release.LoadReleaseConfigFromEnv() if err != nil { return err } - - dryRun := os.Getenv("DRY_RUN") == "true" - - client, err := newGitHubClient() + ghClient, err := release.NewGitHubClientFromEnv() if err != nil { return err } - - title := fmt.Sprintf("[Release] %s", cfg.Version) - body := fmt.Sprintf("## Release %s\n\nThis PR prepares the %s release.\n\n### Changes\n- Updated version to %s\n- Added mergify backport rule for %s\n", - cfg.Version, cfg.Version, cfg.Version, cfg.ReleaseBranch) - - _, err = createPR(client, cfg, title, body, cfg.ReleaseBranch, cfg.BaseBranch, dryRun) - return err + return release.CreateReleasePR(cfg, ghClient, os.Getenv("DRY_RUN") == "true") } -// RunMajorMinor orchestrates the complete major/minor release workflow +// RunMajorMinor orchestrates the complete major/minor release workflow. func (Release) RunMajorMinor() error { - cfg, err := loadReleaseConfigFromEnv() + cfg, err := release.LoadReleaseConfigFromEnv() if err != nil { return err } - - dryRun := os.Getenv("DRY_RUN") == "true" - if dryRun { - fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") - } - - fmt.Printf("🚀 Starting major/minor release workflow for %s\n\n", cfg.Version) - - // Step 1: Prepare files - fmt.Println("Step 1: Preparing release files...") - r := Release{} - if err := r.PrepareMajorMinor(); err != nil { - return fmt.Errorf("failed to prepare release: %w", err) - } - - // Step 2: Create branch - fmt.Println("\nStep 2: Creating release branch...") - if err := r.CreateBranch(); err != nil { - return fmt.Errorf("failed to create branch: %w", err) - } - - // Step 3: Create PR - fmt.Println("\nStep 3: Creating pull request...") - if err := r.CreatePR(); err != nil { - return fmt.Errorf("failed to create PR: %w", err) - } - - fmt.Printf("\n✅ Major/minor release workflow complete for %s\n", cfg.Version) - return nil + return release.RunMajorMinorRelease(cfg, os.Getenv("DRY_RUN") == "true") } -// PreparePatch prepares files for a patch release +// PreparePatch prepares files for a patch release. func (Release) PreparePatch() error { - cfg, err := loadReleaseConfigFromEnv() + cfg, err := release.LoadReleaseConfigFromEnv() if err != nil { return err } - - fmt.Printf("Preparing patch release for version %s\n", cfg.Version) - - // Update version - r := Release{} - if err := r.UpdateVersion(cfg.Version); err != nil { - return err - } - - fmt.Println("✓ Patch release preparation complete") - return nil + return release.PreparePatchRelease(cfg) } -// RunPatch orchestrates the complete patch release workflow +// RunPatch orchestrates the complete patch release workflow. func (Release) RunPatch() error { - cfg, err := loadReleaseConfigFromEnv() + cfg, err := release.LoadReleaseConfigFromEnv() if err != nil { return err } - - dryRun := os.Getenv("DRY_RUN") == "true" - if dryRun { - fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") - } - - fmt.Printf("🚀 Starting patch release workflow for %s\n\n", cfg.Version) - - // Prepare files - fmt.Println("Preparing patch release files...") - r := Release{} - if err := r.PreparePatch(); err != nil { - return fmt.Errorf("failed to prepare patch release: %w", err) - } - - fmt.Printf("\n✅ Patch release workflow complete for %s\n", cfg.Version) - fmt.Println(" Review changes and commit to the release branch") - return nil + return release.RunPatchRelease(cfg, os.Getenv("DRY_RUN") == "true") } -// PrepareNext prepares files for the next development cycle +// PrepareNext prepares files for the next development cycle. func (Release) PrepareNext() error { - // Get current version currentVersion := os.Getenv("CURRENT_RELEASE") if currentVersion == "" { return fmt.Errorf("CURRENT_RELEASE environment variable not set") } - - // Calculate next version (increment minor) - parts := strings.Split(currentVersion, ".") - if len(parts) < 3 { - return fmt.Errorf("invalid version format: %s", currentVersion) - } - - minor, err := strconv.Atoi(parts[1]) - if err != nil { - return fmt.Errorf("invalid minor version: %s", parts[1]) - } - - nextVersion := fmt.Sprintf("%s.%d.0", parts[0], minor+1) - - fmt.Printf("Preparing next development cycle: %s -> %s\n", currentVersion, nextVersion) - - // Update version - r := Release{} - if err := r.UpdateVersion(nextVersion); err != nil { - return err - } - - fmt.Printf("✓ Next release preparation complete: %s\n", nextVersion) - return nil + return release.PrepareNextRelease(currentVersion) } From 815c688a7404ee9a01a36c2f4a2cbe27c18c95a5 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 9 Jul 2026 13:15:31 +0200 Subject: [PATCH 09/14] fix(ci): allow local go.mod replace for dev-tools module Enable gomoddirectives replace-local so the dev-tools submodule replace required by mage release imports passes golangci-lint. --- .golangci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 104cf923ce..ce610eb8be 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -91,8 +91,8 @@ linters: min-occurrences: 2 gomoddirectives: - # Allow local `replace` directives. Default is false. - replace-local: false + # Allow local `replace` directives (e.g. dev-tools submodule in go.mod). + replace-local: true gomodguard: blocked: # List of blocked modules. From 457a0a78ea1a6dcb00ce467ae96739a7e30ee91f Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 9 Jul 2026 13:29:39 +0200 Subject: [PATCH 10/14] fix(docker): copy dev-tools go.mod before go mod download The root go.mod replaces dev-tools with a local path. Docker layer caching must include dev-tools/go.mod and go.sum so go mod download can resolve the replaced module. --- Dockerfile | 1 + Dockerfile.build | 1 + Dockerfile.fips | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 04ef6931f8..ea683ba431 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /fleet-server # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change COPY go.mod go.sum ./ +COPY dev-tools/go.mod dev-tools/go.sum ./dev-tools/ RUN go mod download && go mod verify RUN go install github.com/magefile/mage # Uses version from go.mod implicitly ENV PATH="$PATH:/go/bin" diff --git a/Dockerfile.build b/Dockerfile.build index 4cb027053d..00ac2a8935 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -13,6 +13,7 @@ USER fleet-server WORKDIR /fleet-server/ # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change COPY go.mod go.sum ./ +COPY dev-tools/go.mod dev-tools/go.sum ./dev-tools/ RUN go mod download && go mod verify RUN go install github.com/magefile/mage # uses version in go.mod diff --git a/Dockerfile.fips b/Dockerfile.fips index 8adae79231..a773061d6e 100644 --- a/Dockerfile.fips +++ b/Dockerfile.fips @@ -10,6 +10,7 @@ WORKDIR /fleet-server/ # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change COPY go.mod go.sum ./ +COPY dev-tools/go.mod dev-tools/go.sum ./dev-tools/ # GOFIPS140 must be set before go mod download so that the golang.org/fips140 module is cached during image build. # Without this, the module would be fetched at build time by the host user who lacks write access to the module cache. ENV GOFIPS140=certified @@ -32,6 +33,7 @@ FROM docker.elastic.co/beats-dev/golang-crossbuild:${GO_VERSION}-${SUFFIX} AS bu WORKDIR /fleet-server ENV PATH="$PATH:/go/bin" COPY go.mod go.sum ./ +COPY dev-tools/go.mod dev-tools/go.sum ./dev-tools/ RUN go mod download && go mod verify RUN go install github.com/magefile/mage # uses version in go.mod From f3e69497583d9b0ff16ead0d7d900e14a29e68c1 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Tue, 14 Jul 2026 12:16:11 +0200 Subject: [PATCH 11/14] Align fleet-server release automation with ingest-dev fleet-server.mak Reorganize dev-tools/mage/release into config, workflows, mergify, git, and github packages matching beats and elastic-agent. Workflows now produce the same branches, PRs, labels, and file updates as the former Makefile-based release process. --- RELEASE.md | 512 +++---------------------- dev-tools/mage/release/README.md | 66 ++++ dev-tools/mage/release/config.go | 192 ++++++++++ dev-tools/mage/release/config_test.go | 130 +++++++ dev-tools/mage/release/git.go | 252 +++++++++--- dev-tools/mage/release/git_test.go | 85 +++- dev-tools/mage/release/github.go | 146 ++++--- dev-tools/mage/release/mergify.go | 85 ++++ dev-tools/mage/release/release.go | 276 ++----------- dev-tools/mage/release/release_test.go | 267 +------------ dev-tools/mage/release/workflows.go | 375 ++++++++++++++++++ magefile.go | 58 +-- 12 files changed, 1328 insertions(+), 1116 deletions(-) create mode 100644 dev-tools/mage/release/README.md create mode 100644 dev-tools/mage/release/config.go create mode 100644 dev-tools/mage/release/config_test.go create mode 100644 dev-tools/mage/release/mergify.go create mode 100644 dev-tools/mage/release/workflows.go diff --git a/RELEASE.md b/RELEASE.md index 7e70288da2..12c7aa9227 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,491 +1,95 @@ # Fleet-Server Release Automation -This document describes the fleet-server release automation implemented using Mage, following the architecture patterns established in elastic-agent. +Mage-based release workflows for Fleet Server, replacing the former ingest-dev +[`fleet-server.mak`](https://github.com/elastic/ingest-dev/blob/main/release_scripts/fleet-server.mak) +Makefile process. Package layout matches beats and elastic-agent under +`dev-tools/mage/release/`. -## Quick Start - -### Prerequisites - -- Go 1.25+ -- Mage installed (`go install github.com/magefile/mage@latest`) -- GitHub token with repo permissions -- Clean git working directory - -### Major/Minor Release (e.g., 9.5.0) - -```bash -export CURRENT_RELEASE="9.5.0" -export GITHUB_TOKEN="ghp_your_token" -export DRY_RUN=true - -# Test first (updates files locally, no push/PR) -mage release:runMajorMinor - -# Review changes -git status -git diff - -# Run for real -export DRY_RUN=false -mage release:runMajorMinor -``` - -### Patch Release (e.g., 9.4.1) - -```bash -# Checkout base branch -git checkout 9.4 -git pull - -export CURRENT_RELEASE="9.4.1" -export BASE_BRANCH="9.4" -export GITHUB_TOKEN="ghp_your_token" - -mage release:runPatch -``` - -## Available Commands - -### Orchestration Commands (Recommended) - -These commands run the full release workflow: - -- `mage release:runMajorMinor` - Complete major/minor release workflow -- `mage release:runPatch` - Complete patch release workflow - -### Individual Commands - -For manual control or debugging: - -- `mage release:updateVersion ` - Update version in version/version.go -- `mage release:updateMergify ` - Add backport rule to .mergify.yml -- `mage release:prepareMajorMinor` - Prepare all files for major/minor release -- `mage release:preparePatch` - Prepare files for patch release -- `mage release:prepareNext` - Prepare next development cycle -- `mage release:createBranch` - Create and push release branch -- `mage release:createPR` - Create GitHub pull request - -## Environment Variables - -### Required - -| Variable | Description | Example | -|----------|-------------|---------| -| `CURRENT_RELEASE` | Version to release | `9.5.0` | -| `GITHUB_TOKEN` | GitHub API token | `ghp_xxx` | - -### Optional - -| Variable | Description | Default | -|----------|-------------|---------| -| `DRY_RUN` | Test mode (no push/PR) | `false` | -| `BASE_BRANCH` | Base branch for PR | `main` | -| `PROJECT_OWNER` | GitHub repository owner | `elastic` | -| `PROJECT_REPO` | GitHub repository name | `fleet-server` | -| `GITHUB_USERNAME` | Git commit author name | `elasticmachine` | -| `GITHUB_EMAIL` | Git commit author email | `infra-root+elasticmachine@elastic.co` | - -## Workflows - -### Major/Minor Release Workflow - -The `runMajorMinor` command performs these steps: - -1. **Prepare Files** - - Updates `version/version.go` with new version - - Adds backport rule to `.mergify.yml` - -2. **Create Branch** - - Creates release branch (e.g., `9.5`) - - Commits all changes - - Pushes to remote (unless DRY_RUN=true) - -3. **Create PR** - - Creates pull request from release branch to main - - Includes version update details - - (Skipped if DRY_RUN=true) - -### Patch Release Workflow - -The `runPatch` command performs these steps: - -1. **Prepare Files** - - Updates `version/version.go` with patch version - -2. **Commit & Review** - - Changes are prepared for manual review - - No automatic branch creation - - Commit to the current release branch manually - -### Next Development Cycle - -The `prepareNext` command: - -- Increments minor version (e.g., 9.4.0 → 9.5.0) -- Updates `version/version.go` -- Used after a release to bump version on main - -## DRY_RUN Mode - -DRY_RUN mode allows you to test the release process safely: +## Quick start ```bash +export PROJECT_OWNER="your-user" +export CURRENT_RELEASE="9.6.0" +export GITHUB_TOKEN=$(gh auth token) export DRY_RUN=true -mage release:runMajorMinor -``` - -**What DRY_RUN does:** -- ✅ Updates local files (version/version.go, .mergify.yml) -- ✅ Shows what would be committed -- ❌ Does NOT create branches -- ❌ Does NOT push to remote -- ❌ Does NOT create pull requests - -**After DRY_RUN:** -- Review changes with `git diff` -- Discard changes with `git checkout .` -- Or proceed with `DRY_RUN=false` - -## Idempotency - -All release automation steps are designed to be **idempotent** — safe to re-run after a partial failure or CI retry: - -| Step | Re-run behavior | -|------|-----------------| -| `updateVersion` | No-op if `DefaultVersion` already matches | -| `updateMergify` | No-op if backport rule already exists | -| `createBranch` | Checks out existing branch instead of failing | -| `commitAll` | Skips commit when there are no changes | -| `push` | Succeeds when remote is already up to date | -| `createPR` | Returns existing open PR for the same head/base | - -This means `mage release:runMajorMinor` and `mage release:runPatch` can be safely retried without manual cleanup when a step has already completed. - -## Testing on a Fork - -Before using the release automation in production, it's **highly recommended** to test on your personal fork first. This allows you to verify the workflow end-to-end without affecting the official repository. - -### Step 1: Fork and Clone - -1. Fork the fleet-server repository on GitHub to your account -2. Clone your fork locally: - ```bash - git clone https://github.com/YOUR-USERNAME/fleet-server.git - cd fleet-server - ``` - -3. Ensure you're on a clean main branch: - ```bash - git checkout main - git pull origin main - ``` -### Step 2: Configure Environment - -Set environment variables to point to your fork: - -```bash -# Point to your fork -export PROJECT_OWNER="your-username" -export PROJECT_REPO="fleet-server" - -# Use a test version to avoid confusion -export CURRENT_RELEASE="9.5.0-test" - -# Your GitHub personal access token -export GITHUB_TOKEN="ghp_your_token" - -# Important: Set to false to actually create branches and PRs -export DRY_RUN=false -``` - -### Step 3: Run the Workflow - -Execute the major/minor release workflow: - -```bash +# Major/minor after feature freeze mage release:runMajorMinor -``` - -This will: -- Update `version/version.go` to `9.5.0-test` -- Add backport rule for `9.5` to `.mergify.yml` -- Create branch `9.5` on your fork -- Push the branch to your fork -- Create a PR from `9.5` → `main` on your fork - -### Step 4: Verify the Results - -Check what was created: - -```bash -# View the created branch -git branch -a | grep 9.5 - -# View the created PR -gh pr list --repo YOUR-USERNAME/fleet-server -``` - -Or visit GitHub: `https://github.com/YOUR-USERNAME/fleet-server/pulls` -### Step 5: Review and Clean Up +# Next patch + backport PRs (run after runMajorMinor) +mage release:runNextRelease -1. Review the PR on your fork to ensure: - - Version was updated correctly - - Mergify rule was added properly - - Commit message is appropriate - -2. If satisfied, close the PR and delete the test branch: - ```bash - gh pr close BRANCH-NUMBER --repo YOUR-USERNAME/fleet-server --delete-branch - ``` - -3. Reset your fork's main branch: - ```bash - git checkout main - git reset --hard origin/main - ``` - -### Testing Different Scenarios - -#### Test Patch Release on Fork - -```bash -export PROJECT_OWNER="your-username" -export CURRENT_RELEASE="9.4.1-test" -export BASE_BRANCH="9.4" -export GITHUB_TOKEN="ghp_your_token" - -# Create the release branch first if it doesn't exist -git checkout -b 9.4 -git push -u origin 9.4 - -# Run patch workflow +# Patch release on an existing release branch +export RELEASE_BRANCH="9.6" +export CURRENT_RELEASE="9.6.2" mage release:runPatch -``` - -#### Test with DRY_RUN First - -For extra safety, test with DRY_RUN before creating real branches: -```bash -export PROJECT_OWNER="your-username" -export DRY_RUN=true -mage release:runMajorMinor - -# Review local changes git diff - -# If satisfied, run for real -export DRY_RUN=false -mage release:runMajorMinor -``` - -### Troubleshooting Fork Testing - -**Issue: "failed to push: authentication required"** -- Ensure your GITHUB_TOKEN has write access to your fork -- Verify: `gh auth status` - -**Issue: "branch already exists"** -- The automation checks out the existing branch automatically on re-run -- To start fresh, delete the branch manually: - ```bash - git branch -D 9.5 - git push origin --delete 9.5 - ``` - -**Issue: "PR already exists"** -- The automation returns the existing open PR on re-run -- To create a new PR, close the existing one first -- Or use a different version number (e.g., `9.5.0-test2`) - -### Best Practices - -1. **Always test on fork first** before running in production -2. **Use test version numbers** (e.g., `X.Y.Z-test`) to distinguish from real releases -3. **Clean up test branches and PRs** after testing to keep your fork tidy -4. **Test both workflows**: major/minor AND patch releases -5. **Verify DRY_RUN mode** works correctly before relying on it - -## Troubleshooting - -### "CURRENT_RELEASE environment variable not set" - -Set the required environment variable: -```bash -export CURRENT_RELEASE="9.5.0" +go test ./dev-tools/mage/release/... -count=1 ``` -### "GITHUB_TOKEN environment variable not set" +Use plain `X.Y.Z` semver for `CURRENT_RELEASE` (no `-test` or `-SNAPSHOT` suffixes). -Set your GitHub token: -```bash -export GITHUB_TOKEN=$(gh auth token) -``` +## Workflow alignment -Or create a personal access token at: https://github.com/settings/tokens +| Former (ingest-dev) | Mage command | PRs produced | +|---|---|---| +| `prepare-major-minor-release` + `create-branch-major-minor-release` | `mage release:runMajorMinor` | Push release branch; PR `bump-version-` → `main` | +| `prepare-next-release` + `create-prs-next-release` | `mage release:runNextRelease` | PR `update-version-next-` → release branch; PR `add-backport-next-` → `main` | +| `prepare-patch-release` + `create-prs-patch-release` | `mage release:runPatch` | PR `update-docs-version-` → release branch | -### "failed to open git repo" +## Environment variables -Ensure you're in the fleet-server repository root: -```bash -cd /path/to/fleet-server -``` - -### "version pattern not found" +| Variable | Required | Default | Description | +|---|---|---|---| +| `CURRENT_RELEASE` | yes | — | Version to release (`9.6.0`) | +| `GITHUB_TOKEN` | yes (unless dry run) | — | GitHub API token | +| `DRY_RUN` | no | `false` | Prepare branches locally without push/PR | +| `BASE_BRANCH` | no | `main` | Base branch for major/minor | +| `RELEASE_BRANCH` | no | inferred | Release branch (e.g. `9.6`) | +| `NEXT_RELEASE` | no | patch+1 | Next patch on release branch | +| `NEXT_PROJECT_MINOR_VERSION` | no | minor+1 | Next minor for main bump | +| `PROJECT_OWNER` | no | `elastic` | GitHub owner | +| `PROJECT_REPO` | no | `fleet-server` | GitHub repository | +| `PROJECT_REVIEWERS` | no | `elastic/elastic-agent-control-plane` | PR reviewers | -The version file format has changed. Check that `version/version.go` contains: -```go -const DefaultVersion = "X.Y.Z" -``` +## Files updated -### Uncommitted changes warning +- `version/version.go` — `DefaultVersion` +- `.mergify.yml` — backport rule (next-release workflow) -The workflow warns about uncommitted files but continues. To avoid confusion: -```bash -git status -git stash # or commit your changes -``` +## Idempotency -## Architecture +Release steps are safe to re-run after partial failure or CI retry: -Fleet-server follows the same architecture as elastic-agent and beats: +| Step | Re-run behavior | +|---|---| +| `UpdateVersion` | No-op when version already matches | +| `UpdateMergify` | No-op when backport rule already exists | +| Branch creation | Reuses existing branch | +| `CommitAll` | Skips when worktree is clean | +| `Push` | Succeeds when remote is up to date | +| `CreatePR` | Returns existing open PR for same head/base | -**Key Differences from elastic-agent:** -- Fewer files to update (only `version/version.go` and `.mergify.yml`) -- No docs/K8s manifest updates needed +## Package layout -**File structure:** ``` dev-tools/mage/release/ -├── release.go # Version/mergify updates and workflow orchestration -├── git.go # Git operations (branch, commit, push) -├── github.go # GitHub API integration (PR creation) -├── release_test.go -└── git_test.go +├── config.go # Env loading and version inference +├── release.go # UpdateVersion +├── mergify.go # UpdateMergify +├── workflows.go # RunMajorMinor, RunNextRelease, RunPatch +├── git.go # Branch, commit, push helpers +├── github.go # PR creation and labels +└── README.md # Maintainer reference ``` -`magefile.go` exposes thin `mage release:*` wrappers that delegate to this package. - -**Libraries used:** -- `github.com/go-git/go-git/v5` - Git operations -- `github.com/google/go-github/v68` - GitHub API -- `gopkg.in/yaml.v3` - YAML parsing (already present) - -**No external tools:** -- ❌ No `hub` CLI -- ❌ No `gh` CLI -- ❌ No `sed` / `awk` -- ❌ No `yq` -- ✅ Pure Go implementation - -## Examples - -### Complete Major/Minor Release - -```bash -# 1. Start on main branch -git checkout main -git pull - -# 2. Set up environment -export CURRENT_RELEASE="9.5.0" -export GITHUB_TOKEN=$(gh auth token) - -# 3. Test with dry run -export DRY_RUN=true -mage release:runMajorMinor - -# 4. Review changes -git diff version/version.go -git diff .mergify.yml - -# 5. Run for real -export DRY_RUN=false -mage release:runMajorMinor - -# 6. Check the PR that was created -gh pr list -``` - -### Patch Release on Existing Branch - -```bash -# 1. Checkout release branch -git checkout 9.4 -git pull - -# 2. Set up environment -export CURRENT_RELEASE="9.4.1" -export BASE_BRANCH="9.4" -export GITHUB_TOKEN=$(gh auth token) - -# 3. Run patch workflow -mage release:runPatch - -# 4. Review and commit -git diff -git add version/version.go -git commit -m "Bump version to 9.4.1" -git push -``` - -### Bump Version After Release - -```bash -# 1. After 9.5.0 release, bump main to 9.6.0 -git checkout main - -# 2. Run next version prep -export CURRENT_RELEASE="9.5.0" -mage release:prepareNext - -# 3. Commit the change -git add version/version.go -git commit -m "Bump version to 9.6.0" -git push -``` +See `dev-tools/mage/release/README.md` for detailed command mapping. ## Testing -Run unit tests for release functions: - ```bash cd dev-tools && go test ./mage/release/... -count=1 ``` -Check test coverage: - -```bash -cd dev-tools && go test ./mage/release/... -coverprofile=coverage.out -go tool cover -func=coverage.out -``` - -## Migration from Makefile - -This replaces the old Makefile-based release process: - -| Old (Makefile) | New (Mage) | -|----------------|------------| -| `make prepare-major-minor-release` | `mage release:runMajorMinor` | -| `make prepare-patch-release` | `mage release:runPatch` | -| `make update-version VERSION=9.5.0` | `mage release:updateVersion 9.5.0` | -| `make prepare-next-release` | `mage release:prepareNext` | - -**Benefits of Mage:** -- Pure Go (no sed, hub, Python dependencies) -- Type-safe with compile-time checks -- Built-in DRY_RUN mode -- Better error messages -- Easier to test and maintain - -## Support - -- **Issues**: https://github.com/elastic/fleet-server/issues -- **Documentation**: This file and migration plan -- **Code**: `dev-tools/mage/release/` and `magefile.go` release wrappers - ---- - -**Last Updated**: 2026-03-12 +Discard local workflow changes after review with `git reset --hard HEAD`. diff --git a/dev-tools/mage/release/README.md b/dev-tools/mage/release/README.md new file mode 100644 index 0000000000..7e645ecf67 --- /dev/null +++ b/dev-tools/mage/release/README.md @@ -0,0 +1,66 @@ +# Fleet Server release automation + +Mage-based release workflows for Fleet Server, aligned with the former +[`release_scripts/fleet-server.mak`](https://github.com/elastic/ingest-dev/blob/main/release_scripts/fleet-server.mak) +process and the package layout used in beats and elastic-agent. + +## Commands + +| Mage command | Former Makefile target | Description | +|---|---|---| +| `mage release:runMajorMinor` | `prepare-major-minor-release` + `create-branch-major-minor-release` | Push release branch; open bump-main PR | +| `mage release:runNextRelease` | `prepare-next-release` + `create-prs-next-release` | Open next patch + backport PRs | +| `mage release:runPatch` | `prepare-patch-release` + `create-prs-patch-release` | Open patch version PR into release branch | +| `mage release:updateVersion ` | `update-version` | Update `version/version.go` only | +| `mage release:updateMergify ` | `update-mergify` | Append backport rule to `.mergify.yml` | + +## Environment variables + +| Variable | Required | Default | Description | +|---|---|---|---| +| `CURRENT_RELEASE` | yes | — | Release version (`X.Y.Z`) | +| `GITHUB_TOKEN` | yes (unless `DRY_RUN=true`) | — | Token for push and PR creation | +| `DRY_RUN` | no | `false` | Prepare branches locally without push/PR | +| `BASE_BRANCH` | no | `main` | Base branch for major/minor | +| `RELEASE_BRANCH` | no | inferred from `CURRENT_RELEASE` | Release branch (e.g. `9.6`) | +| `NEXT_RELEASE` | no | inferred patch+1 | Next patch version on release branch | +| `NEXT_PROJECT_MINOR_VERSION` | no | inferred minor+1 | Next minor version for main bump | +| `LATEST_RELEASE` | no | inferred patch-1 | Previous patch (patch workflow) | +| `PROJECT_OWNER` | no | `elastic` | GitHub owner | +| `PROJECT_REPO` | no | `fleet-server` | GitHub repository | +| `PROJECT_REVIEWERS` | no | `elastic/elastic-agent-control-plane` | PR reviewers | + +## Files updated + +- `version/version.go` — `DefaultVersion` +- `.mergify.yml` — backport rule (next-release workflow only) + +Fleet Server has no K8s/docs manifest updates in the former Makefile process. + +## Local testing + +```bash +export PROJECT_OWNER="your-user" +export CURRENT_RELEASE="9.6.0" +export GITHUB_TOKEN=$(gh auth token) +export DRY_RUN=true + +mage release:runMajorMinor +git diff + +mage release:runNextRelease +git diff + +export RELEASE_BRANCH="9.6" +export CURRENT_RELEASE="9.6.2" +mage release:runPatch +git diff + +go test ./dev-tools/mage/release/... -count=1 +``` + +Discard local changes after review with `git reset --hard HEAD`. + +## Idempotency + +Workflow steps are safe to re-run: existing branches are reused, empty commits are skipped, open PRs are rediscovered, and file updates no-op when already applied. diff --git a/dev-tools/mage/release/config.go b/dev-tools/mage/release/config.go new file mode 100644 index 0000000000..13cabdece3 --- /dev/null +++ b/dev-tools/mage/release/config.go @@ -0,0 +1,192 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +// ReleaseConfig holds the configuration for release operations. +type ReleaseConfig struct { + CurrentRelease string + LatestRelease string + NextRelease string + NextProjectMinorVersion string + NextProjectMinorOnly string + + BaseBranch string + ReleaseBranch string + + ProjectOwner string + ProjectRepo string + GitHubToken string + ProjectReviewers []string + + GitAuthorName string + GitAuthorEmail string + + DryRun bool +} + +// LoadConfigFromEnv loads release configuration from environment variables. +func LoadConfigFromEnv() (*ReleaseConfig, error) { + currentRelease := os.Getenv("CURRENT_RELEASE") + if currentRelease == "" { + return nil, fmt.Errorf("CURRENT_RELEASE environment variable is required") + } + + latestRelease, err := inferLatestRelease(currentRelease) + if err != nil { + return nil, fmt.Errorf("failed to infer LATEST_RELEASE: %w", err) + } + + nextRelease, err := inferNextRelease(currentRelease) + if err != nil { + return nil, fmt.Errorf("failed to infer NEXT_RELEASE: %w", err) + } + + nextProjectMinorVersion, err := inferNextProjectMinorVersion(currentRelease) + if err != nil { + return nil, fmt.Errorf("failed to infer next project minor version: %w", err) + } + + releaseBranch := inferReleaseBranch(currentRelease) + + if envLatest := os.Getenv("LATEST_RELEASE"); envLatest != "" { + latestRelease = envLatest + } + if envNext := os.Getenv("NEXT_RELEASE"); envNext != "" { + nextRelease = envNext + } + if envBranch := os.Getenv("RELEASE_BRANCH"); envBranch != "" { + releaseBranch = envBranch + } + if envNextMinor := os.Getenv("NEXT_PROJECT_MINOR_VERSION"); envNextMinor != "" { + nextProjectMinorVersion = envNextMinor + } + + nextProjectMinorOnly := inferNextProjectMinorOnly(nextProjectMinorVersion) + + cfg := &ReleaseConfig{ + CurrentRelease: currentRelease, + LatestRelease: latestRelease, + NextRelease: nextRelease, + NextProjectMinorVersion: nextProjectMinorVersion, + NextProjectMinorOnly: nextProjectMinorOnly, + BaseBranch: getEnvOrDefault("BASE_BRANCH", "main"), + ReleaseBranch: releaseBranch, + ProjectOwner: getEnvOrDefault("PROJECT_OWNER", "elastic"), + ProjectRepo: getEnvOrDefault("PROJECT_REPO", "fleet-server"), + GitHubToken: os.Getenv("GITHUB_TOKEN"), + GitAuthorName: getEnvOrDefault("GITHUB_USERNAME", getEnvOrDefault("GIT_AUTHOR_NAME", "elasticmachine")), + GitAuthorEmail: getEnvOrDefault("GITHUB_EMAIL", getEnvOrDefault("GIT_AUTHOR_EMAIL", "infra-root+elasticmachine@elastic.co")), + DryRun: parseDryRun(os.Getenv("DRY_RUN")), + } + + reviewers := getEnvOrDefault("PROJECT_REVIEWERS", "elastic/elastic-agent-control-plane") + cfg.ProjectReviewers = strings.Split(reviewers, ",") + + return cfg, nil +} + +// LoadReleaseConfigFromEnv is a deprecated alias for LoadConfigFromEnv. +func LoadReleaseConfigFromEnv() (*ReleaseConfig, error) { + return LoadConfigFromEnv() +} + +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func parseDryRun(value string) bool { + if value == "" { + return false + } + if parsed, err := strconv.ParseBool(value); err == nil { + return parsed + } + return strings.EqualFold(value, "true") +} + +func inferLatestRelease(currentRelease string) (string, error) { + parts := strings.Split(currentRelease, ".") + if len(parts) < 3 { + return "", fmt.Errorf("invalid version format: %s (expected major.minor.patch)", currentRelease) + } + + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return "", fmt.Errorf("invalid patch version: %s", parts[2]) + } + + if patch == 0 { + return "", nil + } + + return fmt.Sprintf("%s.%s.%d", parts[0], parts[1], patch-1), nil +} + +func inferNextRelease(currentRelease string) (string, error) { + parts := strings.Split(currentRelease, ".") + if len(parts) < 3 { + return "", fmt.Errorf("invalid version format: %s (expected major.minor.patch)", currentRelease) + } + + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return "", fmt.Errorf("invalid patch version: %s", parts[2]) + } + + return fmt.Sprintf("%s.%s.%d", parts[0], parts[1], patch+1), nil +} + +func inferNextProjectMinorVersion(currentRelease string) (string, error) { + parts := strings.Split(currentRelease, ".") + if len(parts) < 3 { + return "", fmt.Errorf("invalid version format: %s (expected major.minor.patch)", currentRelease) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return "", fmt.Errorf("invalid minor version: %s", parts[1]) + } + + return fmt.Sprintf("%s.%d.0", parts[0], minor+1), nil +} + +func inferNextProjectMinorOnly(nextProjectMinorVersion string) string { + parts := strings.Split(nextProjectMinorVersion, ".") + if len(parts) >= 2 { + return parts[0] + "." + parts[1] + } + return nextProjectMinorVersion +} + +func inferReleaseBranch(currentRelease string) string { + parts := strings.Split(currentRelease, ".") + if len(parts) >= 2 { + return parts[0] + "." + parts[1] + } + return "" +} + +// Validate checks if the configuration is valid. +func (c *ReleaseConfig) Validate() error { + if c.CurrentRelease == "" { + return fmt.Errorf("CurrentRelease is required") + } + + if !c.DryRun && c.GitHubToken == "" { + return fmt.Errorf("GITHUB_TOKEN is required when not in dry-run mode") + } + + return nil +} diff --git a/dev-tools/mage/release/config_test.go b/dev-tools/mage/release/config_test.go new file mode 100644 index 0000000000..e9df19b426 --- /dev/null +++ b/dev-tools/mage/release/config_test.go @@ -0,0 +1,130 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "os" + "testing" +) + +func TestInferLatestRelease(t *testing.T) { + tests := []struct { + name string + current string + want string + wantErr bool + }{ + {name: "patch release", current: "9.6.2", want: "9.6.1"}, + {name: "x.y.0 returns empty", current: "9.6.0", want: ""}, + {name: "invalid version", current: "9.6", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := inferLatestRelease(tt.current) + if (err != nil) != tt.wantErr { + t.Fatalf("inferLatestRelease() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("inferLatestRelease() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestInferNextRelease(t *testing.T) { + got, err := inferNextRelease("9.6.0") + if err != nil { + t.Fatalf("inferNextRelease() failed: %v", err) + } + if got != "9.6.1" { + t.Fatalf("inferNextRelease() = %q, want 9.6.1", got) + } +} + +func TestInferNextProjectMinorVersion(t *testing.T) { + got, err := inferNextProjectMinorVersion("9.6.0") + if err != nil { + t.Fatalf("inferNextProjectMinorVersion() failed: %v", err) + } + if got != "9.7.0" { + t.Fatalf("inferNextProjectMinorVersion() = %q, want 9.7.0", got) + } +} + +func TestLoadConfigFromEnv(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + wantErr bool + wantCheck func(*testing.T, *ReleaseConfig) + }{ + { + name: "defaults applied", + envVars: map[string]string{ + "CURRENT_RELEASE": "9.6.0", + }, + wantErr: false, + wantCheck: func(t *testing.T, cfg *ReleaseConfig) { + if cfg.ReleaseBranch != "9.6" { + t.Errorf("ReleaseBranch = %s, want 9.6", cfg.ReleaseBranch) + } + if cfg.NextRelease != "9.6.1" { + t.Errorf("NextRelease = %s, want 9.6.1", cfg.NextRelease) + } + if cfg.NextProjectMinorVersion != "9.7.0" { + t.Errorf("NextProjectMinorVersion = %s, want 9.7.0", cfg.NextProjectMinorVersion) + } + if cfg.ProjectOwner != "elastic" { + t.Errorf("ProjectOwner = %s, want elastic", cfg.ProjectOwner) + } + if cfg.ProjectRepo != "fleet-server" { + t.Errorf("ProjectRepo = %s, want fleet-server", cfg.ProjectRepo) + } + }, + }, + { + name: "missing CURRENT_RELEASE", + envVars: map[string]string{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Clearenv() + for k, v := range tt.envVars { + os.Setenv(k, v) + } + + cfg, err := LoadConfigFromEnv() + if (err != nil) != tt.wantErr { + t.Fatalf("LoadConfigFromEnv() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && tt.wantCheck != nil { + tt.wantCheck(t, cfg) + } + }) + } +} + +func TestParseDryRun(t *testing.T) { + tests := []struct { + input string + want bool + }{ + {input: "", want: false}, + {input: "true", want: true}, + {input: "TRUE", want: true}, + {input: "false", want: false}, + {input: "1", want: true}, + } + + for _, tt := range tests { + if got := parseDryRun(tt.input); got != tt.want { + t.Errorf("parseDryRun(%q) = %v, want %v", tt.input, got, tt.want) + } + } +} diff --git a/dev-tools/mage/release/git.go b/dev-tools/mage/release/git.go index cd3e35e80b..a212e4a2cc 100644 --- a/dev-tools/mage/release/git.go +++ b/dev-tools/mage/release/git.go @@ -7,95 +7,168 @@ package release import ( "errors" "fmt" + "os" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" + "github.com/go-git/go-git/v5/plumbing/transport/http" ) -// GitRepo represents a Git repository with common operations. +// GitRepo wraps go-git Repository with helper methods. type GitRepo struct { repo *git.Repository + path string } -// OpenRepo opens the Git repository at the given path. +// OpenRepo opens a git repository at the specified path. func OpenRepo(path string) (*GitRepo, error) { repo, err := git.PlainOpen(path) if err != nil { - return nil, fmt.Errorf("failed to open git repo: %w", err) + return nil, fmt.Errorf("failed to open repository: %w", err) } - return &GitRepo{repo: repo}, nil + + return &GitRepo{ + repo: repo, + path: path, + }, nil } -// CreateBranch creates and checks out a new branch, or checks out an existing one. +// BranchExists reports whether a local branch exists. +func (g *GitRepo) BranchExists(branchName string) (bool, error) { + _, err := g.repo.Reference(plumbing.NewBranchReferenceName(branchName), true) + if err == nil { + return true, nil + } + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return false, nil + } + return false, fmt.Errorf("failed to check branch %s: %w", branchName, err) +} + +// CreateBranch creates a new branch from the current HEAD. func (g *GitRepo) CreateBranch(branchName string) error { - w, err := g.repo.Worktree() + exists, err := g.BranchExists(branchName) if err != nil { - return fmt.Errorf("failed to get worktree: %w", err) + return err } - - refName := plumbing.NewBranchReferenceName(branchName) - _, err = g.repo.Reference(refName, true) - if err == nil { - err = w.Checkout(&git.CheckoutOptions{ - Branch: refName, - }) - if err != nil { - return fmt.Errorf("failed to checkout existing branch: %w", err) - } - fmt.Printf("✓ Checked out existing branch: %s\n", branchName) + if exists { + fmt.Printf("Branch already exists: %s\n", branchName) return nil } - if !errors.Is(err, plumbing.ErrReferenceNotFound) { - return fmt.Errorf("failed to check branch: %w", err) - } headRef, err := g.repo.Head() if err != nil { return fmt.Errorf("failed to get HEAD: %w", err) } + refName := plumbing.NewBranchReferenceName(branchName) ref := plumbing.NewHashReference(refName, headRef.Hash()) err = g.repo.Storer.SetReference(ref) if err != nil { - return fmt.Errorf("failed to create branch: %w", err) + return fmt.Errorf("failed to create branch %s: %w", branchName, err) + } + + fmt.Printf("Created branch: %s\n", branchName) + return nil +} + +// EnsureBranchFrom checks out baseBranch and creates or checks out branchName from that point. +func (g *GitRepo) EnsureBranchFrom(baseBranch, branchName string) error { + if err := g.CheckoutBranch(baseBranch); err != nil { + return fmt.Errorf("failed to checkout base branch %s: %w", baseBranch, err) + } + + exists, err := g.BranchExists(branchName) + if err != nil { + return err + } + if exists { + return g.CheckoutBranch(branchName) + } + + if err := g.CreateBranch(branchName); err != nil { + return err + } + return g.CheckoutBranch(branchName) +} + +// EnsureBranch checks out an existing local or remote branch, or creates it from HEAD. +func (g *GitRepo) EnsureBranch(branchName string) error { + exists, err := g.BranchExists(branchName) + if err != nil { + return err + } + if exists { + return g.CheckoutBranch(branchName) + } + + remoteRef, err := g.repo.Reference(plumbing.NewRemoteReferenceName("origin", branchName), true) + if err == nil { + localRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName(branchName), remoteRef.Hash()) + if err := g.repo.Storer.SetReference(localRef); err != nil { + return fmt.Errorf("failed to create local branch %s from origin: %w", branchName, err) + } + fmt.Printf("Created local branch from origin: %s\n", branchName) + return g.CheckoutBranch(branchName) + } + if !errors.Is(err, plumbing.ErrReferenceNotFound) { + return fmt.Errorf("failed to check remote branch %s: %w", branchName, err) + } + + if err := g.CreateBranch(branchName); err != nil { + return err + } + return g.CheckoutBranch(branchName) +} + +// CheckoutBranch checks out an existing branch. +func (g *GitRepo) CheckoutBranch(branchName string) error { + currentBranch, err := g.GetCurrentBranch() + if err == nil && currentBranch == branchName { + fmt.Printf("Already on branch: %s\n", branchName) + return nil + } + + w, err := g.repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) } err = w.Checkout(&git.CheckoutOptions{ - Branch: refName, + Branch: plumbing.NewBranchReferenceName(branchName), }) if err != nil { - return fmt.Errorf("failed to checkout branch: %w", err) + return fmt.Errorf("failed to checkout branch %s: %w", branchName, err) } - fmt.Printf("✓ Created and checked out branch: %s\n", branchName) + fmt.Printf("Checked out branch: %s\n", branchName) return nil } -// CommitAll commits all changes with the given message. -func (g *GitRepo) CommitAll(message, authorName, authorEmail string) error { +// CommitAll stages all changes and creates a commit. +func (g *GitRepo) CommitAll(message, authorName, authorEmail string) (bool, error) { w, err := g.repo.Worktree() if err != nil { - return fmt.Errorf("failed to get worktree: %w", err) + return false, fmt.Errorf("failed to get worktree: %w", err) } status, err := w.Status() if err != nil { - return fmt.Errorf("failed to get worktree status: %w", err) + return false, fmt.Errorf("failed to get status: %w", err) } if status.IsClean() { - fmt.Println(" No changes to commit") - return nil + fmt.Println("No changes to commit") + return false, nil } - err = w.AddWithOptions(&git.AddOptions{ - All: true, - }) + err = w.AddGlob(".") if err != nil { - return fmt.Errorf("failed to add changes: %w", err) + return false, fmt.Errorf("failed to stage changes: %w", err) } commit, err := w.Commit(message, &git.CommitOptions{ @@ -106,52 +179,119 @@ func (g *GitRepo) CommitAll(message, authorName, authorEmail string) error { }, }) if err != nil { - return fmt.Errorf("failed to commit: %w", err) + return false, fmt.Errorf("failed to commit: %w", err) } - fmt.Printf("✓ Committed changes: %s\n", commit.String()[:7]) - return nil + fmt.Printf("Created commit: %s\n", commit.String()) + return true, nil } -// Push pushes the current branch to the remote. -func (g *GitRepo) Push(remoteName string, dryRun bool) error { - if dryRun { - fmt.Printf(" [DRY RUN] Would push to remote: %s\n", remoteName) +// HasCommitsAheadOf reports whether HEAD has commits not reachable from baseBranch. +func (g *GitRepo) HasCommitsAheadOf(baseBranch string) (bool, error) { + headRef, err := g.repo.Head() + if err != nil { + return false, fmt.Errorf("failed to get HEAD: %w", err) + } + + baseRef, err := g.repo.Reference(plumbing.NewBranchReferenceName(baseBranch), true) + if err != nil { + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return true, nil + } + return false, fmt.Errorf("failed to get base branch %s: %w", baseBranch, err) + } + + if headRef.Hash() == baseRef.Hash() { + return false, nil + } + + commitIter, err := g.repo.Log(&git.LogOptions{From: headRef.Hash()}) + if err != nil { + return false, fmt.Errorf("failed to walk commits: %w", err) + } + defer commitIter.Close() + + foundBase := false + err = commitIter.ForEach(func(c *object.Commit) error { + if c.Hash == baseRef.Hash() { + foundBase = true + return storer.ErrStop + } return nil + }) + if err != nil && !errors.Is(err, storer.ErrStop) { + return false, fmt.Errorf("failed to compare commits with %s: %w", baseBranch, err) + } + + if foundBase { + return true, nil + } + + return headRef.Hash() != baseRef.Hash(), nil +} + +// Push pushes the current branch to the remote. +func (g *GitRepo) Push(remoteName string) error { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return fmt.Errorf("GITHUB_TOKEN environment variable is required for pushing") + } + + currentBranch, err := g.GetCurrentBranch() + if err != nil { + return err } - err := g.repo.Push(&git.PushOptions{ + refSpec := config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", currentBranch, currentBranch)) + + err = g.repo.Push(&git.PushOptions{ RemoteName: remoteName, + RefSpecs: []config.RefSpec{refSpec}, + Auth: &http.BasicAuth{ + Username: "git", + Password: token, + }, }) if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { return fmt.Errorf("failed to push: %w", err) } - if errors.Is(err, git.NoErrAlreadyUpToDate) { - fmt.Println(" Branch already up to date on remote") - } else { - fmt.Printf("✓ Pushed to remote: %s\n", remoteName) - } + fmt.Printf("Pushed branch %s to remote: %s\n", currentBranch, remoteName) return nil } // GetCurrentBranch returns the name of the current branch. func (g *GitRepo) GetCurrentBranch() (string, error) { - ref, err := g.repo.Head() + headRef, err := g.repo.Head() if err != nil { return "", fmt.Errorf("failed to get HEAD: %w", err) } - if !ref.Name().IsBranch() { + if !headRef.Name().IsBranch() { return "", fmt.Errorf("HEAD is not a branch") } - return ref.Name().Short(), nil + return headRef.Name().Short(), nil +} + +// IsClean checks if the working directory is clean. +func (g *GitRepo) IsClean() (bool, error) { + w, err := g.repo.Worktree() + if err != nil { + return false, fmt.Errorf("failed to get worktree: %w", err) + } + + status, err := w.Status() + if err != nil { + return false, fmt.Errorf("failed to get status: %w", err) + } + + return status.IsClean(), nil } // SetRemoteURL sets the URL for a remote. func (g *GitRepo) SetRemoteURL(remoteName, url string) error { - remote, err := g.repo.Remote(remoteName) + _, err := g.repo.Remote(remoteName) if errors.Is(err, git.ErrRemoteNotFound) { _, err = g.repo.CreateRemote(&config.RemoteConfig{ Name: remoteName, @@ -160,16 +300,12 @@ func (g *GitRepo) SetRemoteURL(remoteName, url string) error { if err != nil { return fmt.Errorf("failed to create remote: %w", err) } - fmt.Printf("✓ Created remote %s: %s\n", remoteName, url) + fmt.Printf("Created remote %s: %s\n", remoteName, url) return nil } else if err != nil { return fmt.Errorf("failed to get remote: %w", err) } - if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] == url { - return nil - } - err = g.repo.DeleteRemote(remoteName) if err != nil { return fmt.Errorf("failed to delete remote: %w", err) @@ -183,6 +319,6 @@ func (g *GitRepo) SetRemoteURL(remoteName, url string) error { return fmt.Errorf("failed to recreate remote: %w", err) } - fmt.Printf("✓ Updated remote %s: %s\n", remoteName, url) + fmt.Printf("Updated remote %s: %s\n", remoteName, url) return nil } diff --git a/dev-tools/mage/release/git_test.go b/dev-tools/mage/release/git_test.go index 2f8d3398ed..fc101a967e 100644 --- a/dev-tools/mage/release/git_test.go +++ b/dev-tools/mage/release/git_test.go @@ -7,6 +7,7 @@ package release import ( "os" "path/filepath" + "strings" "testing" "github.com/go-git/go-git/v5" @@ -49,46 +50,47 @@ func createTestGitRepo(t *testing.T) (*GitRepo, string) { t.Fatalf("failed to create initial commit: %v", err) } - return &GitRepo{repo: repo}, tmpDir + return &GitRepo{repo: repo, path: tmpDir}, tmpDir } -func TestCreateBranchIdempotent(t *testing.T) { - gitRepo, _ := createTestGitRepo(t) +func TestEnsureBranchFrom(t *testing.T) { + gitRepo, tmpDir := createTestGitRepo(t) - err := gitRepo.CreateBranch("9.5") - if err != nil { - t.Fatalf("first CreateBranch() failed: %v", err) + origDir, _ := os.Getwd() + defer func() { + _ = os.Chdir(origDir) + }() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + + if err := gitRepo.EnsureBranchFrom("master", "9.6"); err != nil { + t.Fatalf("EnsureBranchFrom() failed: %v", err) } branch, err := gitRepo.GetCurrentBranch() if err != nil { t.Fatalf("GetCurrentBranch() failed: %v", err) } - if branch != "9.5" { - t.Fatalf("expected branch 9.5, got %s", branch) - } - - err = gitRepo.CreateBranch("9.5") - if err != nil { - t.Fatalf("second CreateBranch() failed: %v", err) + if branch != "9.6" { + t.Fatalf("expected branch 9.6, got %s", branch) } - branch, err = gitRepo.GetCurrentBranch() - if err != nil { - t.Fatalf("GetCurrentBranch() after second call failed: %v", err) - } - if branch != "9.5" { - t.Errorf("CreateBranch() is not idempotent - on branch %s, want 9.5", branch) + if err := gitRepo.EnsureBranchFrom("master", "9.6"); err != nil { + t.Fatalf("second EnsureBranchFrom() failed: %v", err) } } func TestCommitAllNoChanges(t *testing.T) { gitRepo, _ := createTestGitRepo(t) - err := gitRepo.CommitAll("Empty commit", "Test Author", "test@example.com") + committed, err := gitRepo.CommitAll("Empty commit", "Test Author", "test@example.com") if err != nil { t.Errorf("CommitAll() with no changes error = %v", err) } + if committed { + t.Error("CommitAll() should not commit when there are no changes") + } } func TestSetRemoteURLIdempotent(t *testing.T) { @@ -113,3 +115,46 @@ func TestSetRemoteURLIdempotent(t *testing.T) { t.Errorf("SetRemoteURL() URL = %v, want %s", remote.Config().URLs, remoteURL) } } + +func TestBranchNameHelpers(t *testing.T) { + if got := bumpVersionBranchName("9.7.0"); got != "bump-version-9.7.0" { + t.Fatalf("bumpVersionBranchName() = %q", got) + } + if got := nextVersionBranchName("9.6.1"); got != "update-version-next-9.6.1" { + t.Fatalf("nextVersionBranchName() = %q", got) + } + if got := backportNextBranchName("9.7.0"); got != "add-backport-next-9.7.0" { + t.Fatalf("backportNextBranchName() = %q", got) + } + if got := patchDocsBranchName("9.6.2"); got != "update-docs-version-9.6.2" { + t.Fatalf("patchDocsBranchName() = %q", got) + } +} + +func TestIsReleaseWritablePath(t *testing.T) { + if !isReleaseWritablePath("version/version.go") { + t.Fatal("version/version.go should be writable") + } + if isReleaseWritablePath("main.go") { + t.Fatal("main.go should not be writable") + } +} + +func TestBranchNameHelpersUnique(t *testing.T) { + names := []string{ + bumpVersionBranchName("9.7.0"), + nextVersionBranchName("9.6.1"), + backportNextBranchName("9.7.0"), + patchDocsBranchName("9.6.2"), + } + seen := make(map[string]struct{}, len(names)) + for _, name := range names { + if _, ok := seen[name]; ok { + t.Fatalf("duplicate branch helper name: %s", name) + } + seen[name] = struct{}{} + if strings.TrimSpace(name) == "" { + t.Fatal("branch helper returned empty name") + } + } +} diff --git a/dev-tools/mage/release/github.go b/dev-tools/mage/release/github.go index 40b28b2061..5a6a8058e2 100644 --- a/dev-tools/mage/release/github.go +++ b/dev-tools/mage/release/github.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/google/go-github/v68/github" ) @@ -19,7 +20,20 @@ type GitHubClient struct { ctx context.Context } -// NewGitHubClient creates a new GitHub client with authentication. +// PROptions holds options for creating a pull request. +type PROptions struct { + Owner string + Repo string + Title string + Head string + Base string + Body string + Draft bool + Reviewers []string + Labels []string +} + +// NewGitHubClient creates a new GitHub API client. func NewGitHubClient(token string) *GitHubClient { return &GitHubClient{ client: github.NewClient(nil).WithAuthToken(token), @@ -36,71 +50,111 @@ func NewGitHubClientFromEnv() (*GitHubClient, error) { return NewGitHubClient(token), nil } -// PROptions contains options for creating a pull request. -type PROptions struct { - Owner string - Repo string - Title string - Head string - Base string - Body string +// CreatePR creates a pull request, or returns an existing open PR with the same head and base. +func (gh *GitHubClient) CreatePR(opts PROptions) (*github.PullRequest, error) { + existingPR, found, err := gh.FindOpenPR(opts.Owner, opts.Repo, opts.Head, opts.Base) + if err != nil { + return nil, err + } + if found { + fmt.Printf("Open PR already exists #%d: %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) + gh.ensurePRLabels(opts.Owner, opts.Repo, existingPR.GetNumber(), opts.Labels) + return existingPR, nil + } + + newPR := &github.NewPullRequest{ + Title: github.Ptr(opts.Title), + Head: github.Ptr(opts.Head), + Base: github.Ptr(opts.Base), + Body: github.Ptr(opts.Body), + Draft: github.Ptr(opts.Draft), + } + + pr, _, err := gh.client.PullRequests.Create(gh.ctx, opts.Owner, opts.Repo, newPR) + if err != nil { + return nil, fmt.Errorf("failed to create PR: %w", err) + } + + if len(opts.Reviewers) > 0 { + reviewersReq := github.ReviewersRequest{ + Reviewers: opts.Reviewers, + } + _, _, err = gh.client.PullRequests.RequestReviewers(gh.ctx, opts.Owner, opts.Repo, pr.GetNumber(), reviewersReq) + if err != nil { + fmt.Printf("Warning: failed to add reviewers: %v\n", err) + } + } + + gh.ensurePRLabels(opts.Owner, opts.Repo, pr.GetNumber(), opts.Labels) + + fmt.Printf("Created PR #%d: %s\n", pr.GetNumber(), pr.GetHTMLURL()) + return pr, nil } -// FindOpenPR returns an open pull request matching head and base, or nil if none exists. -func (gh *GitHubClient) FindOpenPR(owner, repo, head, base string) (*github.PullRequest, error) { - headRef := fmt.Sprintf("%s:%s", owner, head) +// FindOpenPR returns an open pull request for the given head and base branches, if one exists. +func (gh *GitHubClient) FindOpenPR(owner, repo, head, base string) (*github.PullRequest, bool, error) { + headQuery := head + if !strings.Contains(head, ":") { + headQuery = fmt.Sprintf("%s:%s", owner, head) + } + prs, _, err := gh.client.PullRequests.List(gh.ctx, owner, repo, &github.PullRequestListOptions{ State: "open", - Head: headRef, + Head: headQuery, Base: base, + ListOptions: github.ListOptions{ + PerPage: 1, + }, }) if err != nil { - return nil, fmt.Errorf("failed to list pull requests: %w", err) + return nil, false, fmt.Errorf("failed to list pull requests: %w", err) } if len(prs) == 0 { - return nil, nil + return nil, false, nil } - return prs[0], nil + + return prs[0], true, nil } -// CreatePR creates a new pull request. -// If an open pull request already exists for the same head and base, it is returned instead. -func (gh *GitHubClient) CreatePR(opts PROptions, dryRun bool) (*github.PullRequest, error) { - if dryRun { - fmt.Printf(" [DRY RUN] Would create PR:\n") - fmt.Printf(" Title: %s\n", opts.Title) - fmt.Printf(" Head: %s\n", opts.Head) - fmt.Printf(" Base: %s\n", opts.Base) - return nil, nil +func (gh *GitHubClient) ensurePRLabels(owner, repo string, number int, labels []string) { + if len(labels) == 0 { + return } + if err := gh.AddLabels(owner, repo, number, labels); err != nil { + fmt.Printf("Warning: failed to add labels: %v\n", err) + } +} - existing, err := gh.FindOpenPR(opts.Owner, opts.Repo, opts.Head, opts.Base) +// AddLabels adds labels to a pull request or issue. +func (gh *GitHubClient) AddLabels(owner, repo string, number int, labels []string) error { + _, _, err := gh.client.Issues.AddLabelsToIssue(gh.ctx, owner, repo, number, labels) if err != nil { - return nil, err + return fmt.Errorf("failed to add labels: %w", err) } - if existing != nil { - fmt.Printf(" Pull request already exists: #%d %s\n", existing.GetNumber(), existing.GetHTMLURL()) - return existing, nil + + fmt.Printf("Added labels to #%d: %v\n", number, labels) + return nil +} + +// GetDefaultBranch gets the default branch for a repository. +func (gh *GitHubClient) GetDefaultBranch(owner, repo string) (string, error) { + repository, _, err := gh.client.Repositories.Get(gh.ctx, owner, repo) + if err != nil { + return "", fmt.Errorf("failed to get repository: %w", err) } - pr, _, err := gh.client.PullRequests.Create(gh.ctx, opts.Owner, opts.Repo, &github.NewPullRequest{ - Title: github.String(opts.Title), - Head: github.String(opts.Head), - Base: github.String(opts.Base), - Body: github.String(opts.Body), - }) + return repository.GetDefaultBranch(), nil +} + +// BranchExists checks if a branch exists in the remote repository. +func (gh *GitHubClient) BranchExists(owner, repo, branch string) (bool, error) { + _, _, err := gh.client.Repositories.GetBranch(gh.ctx, owner, repo, branch, 0) if err != nil { var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == 422 { - existing, findErr := gh.FindOpenPR(opts.Owner, opts.Repo, opts.Head, opts.Base) - if findErr == nil && existing != nil { - fmt.Printf(" Pull request already exists: #%d %s\n", existing.GetNumber(), existing.GetHTMLURL()) - return existing, nil - } + if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == 404 { + return false, nil } - return nil, fmt.Errorf("failed to create PR: %w", err) + return false, fmt.Errorf("failed to check branch: %w", err) } - - fmt.Printf("✓ Created PR #%d: %s\n", pr.GetNumber(), pr.GetHTMLURL()) - return pr, nil + return true, nil } diff --git a/dev-tools/mage/release/mergify.go b/dev-tools/mage/release/mergify.go new file mode 100644 index 0000000000..11e45a6c1f --- /dev/null +++ b/dev-tools/mage/release/mergify.go @@ -0,0 +1,85 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +const mergifyPath = ".mergify.yml" + +// UpdateMergify adds a new backport rule to .mergify.yml. +func UpdateMergify(version string) error { + safePath, err := validateRepoRelativePath(mergifyPath) + if err != nil { + return err + } + + content, err := os.ReadFile(safePath) + if err != nil { + return fmt.Errorf("failed to read %s: %w", safePath, err) + } + + var config map[string]interface{} + if err := yaml.Unmarshal(content, &config); err != nil { + return fmt.Errorf("failed to parse %s: %w", safePath, err) + } + + parts := strings.Split(version, ".") + if len(parts) < 2 { + return fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) + } + branchVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) + + rules, ok := config["pull_request_rules"].([]interface{}) + if !ok { + return fmt.Errorf("pull_request_rules not found or invalid format") + } + + label := fmt.Sprintf("backport-%s", branchVersion) + for _, rule := range rules { + ruleMap, ok := rule.(map[string]interface{}) + if !ok { + continue + } + name, ok := ruleMap["name"].(string) + if ok && strings.Contains(name, branchVersion) { + fmt.Printf("Backport rule for %s already exists\n", branchVersion) + return nil + } + } + + newRule := map[string]interface{}{ + "name": fmt.Sprintf("backport patches to %s branch", branchVersion), + "conditions": []interface{}{ + "merged", + fmt.Sprintf("label=%s", label), + }, + "actions": map[string]interface{}{ + "backport": map[string]interface{}{ + "branches": []interface{}{branchVersion}, + }, + }, + } + + rules = append(rules, newRule) + config["pull_request_rules"] = rules + + output, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal YAML: %w", err) + } + + if err := writeRepoFile(safePath, output); err != nil { + return fmt.Errorf("failed to write %s: %w", safePath, err) + } + + fmt.Printf("Added backport rule for %s to %s\n", branchVersion, safePath) + return nil +} diff --git a/dev-tools/mage/release/release.go b/dev-tools/mage/release/release.go index 414823b887..c9a77c3908 100644 --- a/dev-tools/mage/release/release.go +++ b/dev-tools/mage/release/release.go @@ -7,74 +7,57 @@ package release import ( "fmt" "os" + "path/filepath" "regexp" - "strconv" "strings" ) -// ReleaseConfig holds configuration for release operations. -type ReleaseConfig struct { - Version string - BaseBranch string - ReleaseBranch string - Owner string - Repo string - AuthorName string - AuthorEmail string -} +const versionGoPath = "version/version.go" -// LoadReleaseConfigFromEnv loads release configuration from environment variables. -func LoadReleaseConfigFromEnv() (*ReleaseConfig, error) { - version := os.Getenv("CURRENT_RELEASE") - if version == "" { - return nil, fmt.Errorf("CURRENT_RELEASE environment variable not set") +func validateRepoRelativePath(path string) (string, error) { + if path == "" { + return "", fmt.Errorf("path must not be empty") } - - baseBranch := os.Getenv("BASE_BRANCH") - if baseBranch == "" { - baseBranch = "main" + if filepath.IsAbs(path) { + return "", fmt.Errorf("absolute path not allowed: %s", path) } - parts := strings.Split(version, ".") - if len(parts) < 2 { - return nil, fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) + cleaned := filepath.Clean(path) + if cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(os.PathSeparator)) { + return "", fmt.Errorf("path escapes repository root: %s", path) } - releaseBranch := fmt.Sprintf("%s.%s", parts[0], parts[1]) - owner := os.Getenv("PROJECT_OWNER") - if owner == "" { - owner = "elastic" - } + return cleaned, nil +} - repo := os.Getenv("PROJECT_REPO") - if repo == "" { - repo = "fleet-server" +func isReleaseWritablePath(path string) bool { + switch filepath.ToSlash(path) { + case versionGoPath, ".mergify.yml": + return true + default: + return false } +} - authorName := os.Getenv("GITHUB_USERNAME") - if authorName == "" { - authorName = "elasticmachine" +func writeRepoFile(relPath string, content []byte) error { + safePath, err := validateRepoRelativePath(relPath) + if err != nil { + return err } - authorEmail := os.Getenv("GITHUB_EMAIL") - if authorEmail == "" { - authorEmail = "infra-root+elasticmachine@elastic.co" + if !isReleaseWritablePath(safePath) { + return fmt.Errorf("unsupported file path: %s", relPath) } - return &ReleaseConfig{ - Version: version, - BaseBranch: baseBranch, - ReleaseBranch: releaseBranch, - Owner: owner, - Repo: repo, - AuthorName: authorName, - AuthorEmail: authorEmail, - }, nil + return os.WriteFile(safePath, content, 0o644) //nolint:gosec // safePath is validated and allowlisted for release automation files } // UpdateVersion updates the version in version/version.go. func UpdateVersion(newVersion string) error { - versionFile := "version/version.go" + versionFile, err := validateRepoRelativePath(versionGoPath) + if err != nil { + return err + } content, err := os.ReadFile(versionFile) if err != nil { @@ -85,212 +68,19 @@ func UpdateVersion(newVersion string) error { contentStr := string(content) alreadyAtVersion := regexp.MustCompile(`const\s+DefaultVersion\s*=\s*"` + regexp.QuoteMeta(newVersion) + `"`) if alreadyAtVersion.MatchString(contentStr) { - fmt.Printf(" Version already set to %s in %s\n", newVersion, versionFile) + fmt.Printf("Version already set to %s in %s\n", newVersion, versionFile) return nil } newContent := re.ReplaceAllString(contentStr, `${1}"`+newVersion+`"`) - if newContent == contentStr { return fmt.Errorf("version pattern not found in %s", versionFile) } - err = os.WriteFile(versionFile, []byte(newContent), 0o644) - if err != nil { + if err := writeRepoFile(versionFile, []byte(newContent)); err != nil { return fmt.Errorf("failed to write %s: %w", versionFile, err) } - fmt.Printf("✓ Updated version to %s in %s\n", newVersion, versionFile) - return nil -} - -// UpdateMergify adds a new backport rule to .mergify.yml. -func UpdateMergify(version string) error { - mergifyFile := ".mergify.yml" - - content, err := os.ReadFile(mergifyFile) - if err != nil { - return fmt.Errorf("failed to read %s: %w", mergifyFile, err) - } - - parts := strings.Split(version, ".") - if len(parts) < 2 { - return fmt.Errorf("invalid version format: %s (expected X.Y.Z)", version) - } - branchVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) - - searchPattern := fmt.Sprintf("backport patches to %s branch", branchVersion) - if strings.Contains(string(content), searchPattern) { - fmt.Printf(" Backport rule for %s already exists\n", branchVersion) - return nil - } - - newRule := fmt.Sprintf(` - name: backport patches to %s branch - conditions: - - merged - - label=backport-%s - actions: - backport: - branches: - - "%s" -`, branchVersion, branchVersion, branchVersion) - - output := string(content) + newRule - - err = os.WriteFile(mergifyFile, []byte(output), 0o644) - if err != nil { - return fmt.Errorf("failed to write %s: %w", mergifyFile, err) - } - - fmt.Printf("✓ Added backport rule for %s to %s\n", branchVersion, mergifyFile) - return nil -} - -// PrepareMajorMinorRelease prepares files for a major/minor release. -func PrepareMajorMinorRelease(cfg *ReleaseConfig) error { - fmt.Printf("Preparing major/minor release for version %s\n", cfg.Version) - - if err := UpdateVersion(cfg.Version); err != nil { - return err - } - - if err := UpdateMergify(cfg.Version); err != nil { - return err - } - - fmt.Println("✓ Major/minor release preparation complete") - fmt.Printf(" Next steps:\n") - fmt.Printf(" 1. Review changes: git diff\n") - fmt.Printf(" 2. Create branch: mage release:createBranch\n") - fmt.Printf(" 3. Create PR: mage release:createPR\n") - - return nil -} - -// CreateReleaseBranch creates a release branch with all changes committed. -func CreateReleaseBranch(cfg *ReleaseConfig, repoPath string, dryRun bool) error { - repo, err := OpenRepo(repoPath) - if err != nil { - return err - } - - branchName := cfg.ReleaseBranch - fmt.Printf("Creating release branch: %s\n", branchName) - - if !dryRun { - if err := repo.CreateBranch(branchName); err != nil { - return err - } - - commitMsg := fmt.Sprintf("[Release] Prepare %s release", cfg.Version) - if err := repo.CommitAll(commitMsg, cfg.AuthorName, cfg.AuthorEmail); err != nil { - return err - } - } else { - fmt.Printf(" [DRY RUN] Would create branch %s and commit changes\n", branchName) - } - - return repo.Push("origin", dryRun) -} - -// CreateReleasePR creates a pull request for the release. -func CreateReleasePR(cfg *ReleaseConfig, ghClient *GitHubClient, dryRun bool) error { - title := fmt.Sprintf("[Release] %s", cfg.Version) - body := fmt.Sprintf("## Release %s\n\nThis PR prepares the %s release.\n\n### Changes\n- Updated version to %s\n- Added mergify backport rule for %s\n", - cfg.Version, cfg.Version, cfg.Version, cfg.ReleaseBranch) - - _, err := ghClient.CreatePR(PROptions{ - Owner: cfg.Owner, - Repo: cfg.Repo, - Title: title, - Head: cfg.ReleaseBranch, - Base: cfg.BaseBranch, - Body: body, - }, dryRun) - return err -} - -// RunMajorMinorRelease orchestrates the complete major/minor release workflow. -func RunMajorMinorRelease(cfg *ReleaseConfig, dryRun bool) error { - if dryRun { - fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") - } - - fmt.Printf("🚀 Starting major/minor release workflow for %s\n\n", cfg.Version) - - fmt.Println("Step 1: Preparing release files...") - if err := PrepareMajorMinorRelease(cfg); err != nil { - return fmt.Errorf("failed to prepare release: %w", err) - } - - fmt.Println("\nStep 2: Creating release branch...") - if err := CreateReleaseBranch(cfg, ".", dryRun); err != nil { - return fmt.Errorf("failed to create branch: %w", err) - } - - fmt.Println("\nStep 3: Creating pull request...") - ghClient, err := NewGitHubClientFromEnv() - if err != nil { - return err - } - if err := CreateReleasePR(cfg, ghClient, dryRun); err != nil { - return fmt.Errorf("failed to create PR: %w", err) - } - - fmt.Printf("\n✅ Major/minor release workflow complete for %s\n", cfg.Version) - return nil -} - -// PreparePatchRelease prepares files for a patch release. -func PreparePatchRelease(cfg *ReleaseConfig) error { - fmt.Printf("Preparing patch release for version %s\n", cfg.Version) - - if err := UpdateVersion(cfg.Version); err != nil { - return err - } - - fmt.Println("✓ Patch release preparation complete") - return nil -} - -// RunPatchRelease orchestrates the complete patch release workflow. -func RunPatchRelease(cfg *ReleaseConfig, dryRun bool) error { - if dryRun { - fmt.Println("🔍 DRY RUN MODE - No changes will be pushed") - } - - fmt.Printf("🚀 Starting patch release workflow for %s\n\n", cfg.Version) - - fmt.Println("Preparing patch release files...") - if err := PreparePatchRelease(cfg); err != nil { - return fmt.Errorf("failed to prepare patch release: %w", err) - } - - fmt.Printf("\n✅ Patch release workflow complete for %s\n", cfg.Version) - fmt.Println(" Review changes and commit to the release branch") - return nil -} - -// PrepareNextRelease prepares files for the next development cycle. -func PrepareNextRelease(currentVersion string) error { - parts := strings.Split(currentVersion, ".") - if len(parts) < 3 { - return fmt.Errorf("invalid version format: %s", currentVersion) - } - - minor, err := strconv.Atoi(parts[1]) - if err != nil { - return fmt.Errorf("invalid minor version: %s", parts[1]) - } - - nextVersion := fmt.Sprintf("%s.%d.0", parts[0], minor+1) - - fmt.Printf("Preparing next development cycle: %s -> %s\n", currentVersion, nextVersion) - - if err := UpdateVersion(nextVersion); err != nil { - return err - } - - fmt.Printf("✓ Next release preparation complete: %s\n", nextVersion) + fmt.Printf("Updated version to %s in %s\n", newVersion, versionFile) return nil } diff --git a/dev-tools/mage/release/release_test.go b/dev-tools/mage/release/release_test.go index d92d53e2fe..3f7b9cd5d8 100644 --- a/dev-tools/mage/release/release_test.go +++ b/dev-tools/mage/release/release_test.go @@ -26,13 +26,6 @@ func TestUpdateVersion(t *testing.T) { wantErr: false, wantContent: `const DefaultVersion = "9.5.0"`, }, - { - name: "update to snapshot version", - initial: `const DefaultVersion = "9.4.0"`, - newVersion: "9.5.0-SNAPSHOT", - wantErr: false, - wantContent: `const DefaultVersion = "9.5.0-SNAPSHOT"`, - }, { name: "update with different spacing", initial: `const DefaultVersion = "9.4.0"`, @@ -143,20 +136,21 @@ const DefaultVersion = "9.4.0" } } -func TestUpdateVersionAlreadyAtVersion(t *testing.T) { +func TestUpdateMergify(t *testing.T) { tmpDir := t.TempDir() - versionDir := filepath.Join(tmpDir, "version") - err := os.Mkdir(versionDir, 0o755) - if err != nil { - t.Fatalf("failed to create version dir: %v", err) - } - - versionFile := filepath.Join(versionDir, "version.go") - initialContent := `package version + mergifyFile := filepath.Join(tmpDir, ".mergify.yml") -const DefaultVersion = "9.5.0" + initialContent := `pull_request_rules: + - name: backport patches to 9.3 branch + conditions: + - merged + - label=backport-9.3 + actions: + backport: + branches: + - "9.3" ` - err = os.WriteFile(versionFile, []byte(initialContent), 0o644) + err := os.WriteFile(mergifyFile, []byte(initialContent), 0o644) if err != nil { t.Fatalf("failed to write initial file: %v", err) } @@ -169,113 +163,22 @@ const DefaultVersion = "9.5.0" t.Fatalf("failed to change to temp directory: %v", err) } - err = UpdateVersion("9.5.0") + err = UpdateMergify("9.5.0") if err != nil { - t.Fatalf("UpdateVersion() failed when version already set: %v", err) + t.Fatalf("UpdateMergify() failed: %v", err) } - content, err := os.ReadFile(versionFile) + content, err := os.ReadFile(mergifyFile) if err != nil { - t.Fatalf("failed to read file: %v", err) - } - if string(content) != initialContent { - t.Errorf("UpdateVersion() modified file when already at target version") + t.Fatalf("failed to read updated file: %v", err) } -} -func TestUpdateMergify(t *testing.T) { - tests := []struct { - name string - version string - wantErr bool - wantBranch string - wantLabel string - shouldAppend bool - }{ - { - name: "add new backport rule", - version: "9.5.0", - wantErr: false, - wantBranch: "9.5", - wantLabel: "backport-9.5", - shouldAppend: true, - }, - { - name: "version with patch", - version: "10.0.1", - wantErr: false, - wantBranch: "10.0", - wantLabel: "backport-10.0", - shouldAppend: true, - }, - { - name: "invalid version format", - version: "9", - wantErr: true, - }, + contentStr := string(content) + if !strings.Contains(contentStr, "backport patches to 9.5 branch") { + t.Errorf("UpdateMergify() missing rule name for 9.5") } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - mergifyFile := filepath.Join(tmpDir, ".mergify.yml") - - initialContent := `pull_request_rules: - - name: backport patches to 9.3 branch - conditions: - - merged - - label=backport-9.3 - actions: - backport: - branches: - - "9.3" -` - err := os.WriteFile(mergifyFile, []byte(initialContent), 0o644) - if err != nil { - t.Fatalf("failed to write initial file: %v", err) - } - - origDir, _ := os.Getwd() - defer func() { - _ = os.Chdir(origDir) - }() - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("failed to change to temp directory: %v", err) - } - - err = UpdateMergify(tt.version) - - if (err != nil) != tt.wantErr { - t.Errorf("UpdateMergify() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr && tt.shouldAppend { - content, err := os.ReadFile(mergifyFile) - if err != nil { - t.Fatalf("failed to read updated file: %v", err) - } - - contentStr := string(content) - - expectedName := "backport patches to " + tt.wantBranch + " branch" - if !strings.Contains(contentStr, expectedName) { - t.Errorf("UpdateMergify() missing rule name: %s", expectedName) - } - - if !strings.Contains(contentStr, tt.wantLabel) { - t.Errorf("UpdateMergify() missing label: %s", tt.wantLabel) - } - - if !strings.Contains(contentStr, `"`+tt.wantBranch+`"`) { - t.Errorf("UpdateMergify() missing branch: %s", tt.wantBranch) - } - - if !strings.Contains(contentStr, "9.3") { - t.Error("UpdateMergify() removed existing rules") - } - } - }) + if !strings.Contains(contentStr, "9.3") { + t.Error("UpdateMergify() removed existing rules") } } @@ -323,132 +226,4 @@ func TestUpdateMergifyIdempotent(t *testing.T) { if string(content1) != string(content2) { t.Error("UpdateMergify() is not idempotent - content changed on second run") } - - count := strings.Count(string(content2), "backport patches to 9.5 branch") - if count != 1 { - t.Errorf("UpdateMergify() created %d rules for 9.5, want 1", count) - } -} - -func TestLoadReleaseConfigFromEnv(t *testing.T) { - tests := []struct { - name string - envVars map[string]string - wantErr bool - wantCheck func(*testing.T, *ReleaseConfig) - }{ - { - name: "all env vars set", - envVars: map[string]string{ - "CURRENT_RELEASE": "9.5.0", - "BASE_BRANCH": "main", - "PROJECT_OWNER": "elastic", - "PROJECT_REPO": "fleet-server", - }, - wantErr: false, - wantCheck: func(t *testing.T, cfg *ReleaseConfig) { - if cfg.Version != "9.5.0" { - t.Errorf("Version = %s, want 9.5.0", cfg.Version) - } - if cfg.ReleaseBranch != "9.5" { - t.Errorf("ReleaseBranch = %s, want 9.5", cfg.ReleaseBranch) - } - if cfg.BaseBranch != "main" { - t.Errorf("BaseBranch = %s, want main", cfg.BaseBranch) - } - }, - }, - { - name: "defaults applied", - envVars: map[string]string{ - "CURRENT_RELEASE": "10.0.0", - }, - wantErr: false, - wantCheck: func(t *testing.T, cfg *ReleaseConfig) { - if cfg.BaseBranch != "main" { - t.Errorf("BaseBranch = %s, want main (default)", cfg.BaseBranch) - } - if cfg.Owner != "elastic" { - t.Errorf("Owner = %s, want elastic (default)", cfg.Owner) - } - if cfg.Repo != "fleet-server" { - t.Errorf("Repo = %s, want fleet-server (default)", cfg.Repo) - } - }, - }, - { - name: "missing CURRENT_RELEASE", - envVars: map[string]string{}, - wantErr: true, - }, - { - name: "invalid version format", - envVars: map[string]string{ - "CURRENT_RELEASE": "9", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - os.Clearenv() - for k, v := range tt.envVars { - os.Setenv(k, v) - } - - cfg, err := LoadReleaseConfigFromEnv() - - if (err != nil) != tt.wantErr { - t.Errorf("LoadReleaseConfigFromEnv() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr && tt.wantCheck != nil { - tt.wantCheck(t, cfg) - } - }) - } -} - -func TestPrepareNextRelease(t *testing.T) { - tmpDir := t.TempDir() - versionDir := filepath.Join(tmpDir, "version") - err := os.Mkdir(versionDir, 0o755) - if err != nil { - t.Fatalf("failed to create version dir: %v", err) - } - - versionFile := filepath.Join(versionDir, "version.go") - initialContent := `package version - -const DefaultVersion = "9.4.0" -` - err = os.WriteFile(versionFile, []byte(initialContent), 0o644) - if err != nil { - t.Fatalf("failed to write initial file: %v", err) - } - - origDir, _ := os.Getwd() - defer func() { - _ = os.Chdir(origDir) - }() - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("failed to change to temp directory: %v", err) - } - - err = PrepareNextRelease("9.4.0") - if err != nil { - t.Fatalf("PrepareNextRelease() failed: %v", err) - } - - content, err := os.ReadFile(versionFile) - if err != nil { - t.Fatalf("failed to read updated file: %v", err) - } - - expectedVersion := "9.5.0" - if !strings.Contains(string(content), expectedVersion) { - t.Errorf("PrepareNextRelease() = %s, want %s", string(content), expectedVersion) - } } diff --git a/dev-tools/mage/release/workflows.go b/dev-tools/mage/release/workflows.go new file mode 100644 index 0000000000..5ffdb352da --- /dev/null +++ b/dev-tools/mage/release/workflows.go @@ -0,0 +1,375 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "fmt" + "strings" + + "github.com/google/go-github/v68/github" +) + +// PR label sets match vault-bot release PRs for fleet-server. +var ( + bumpMainPRLabels = []string{"backport-skip", "skip-changelog"} + nextReleasePRLabels = []string{"Team:Automation", "release", "skip-changelog"} + backportNextPRLabels = []string{"Team:Automation", "release", "impact:critical", "skip-changelog"} + patchReleasePRLabels = []string{"Team:Automation", "release", "docs", "in progress", "skip-changelog"} +) + +func checkRequirements(cfg *ReleaseConfig) error { + parts := strings.Split(cfg.CurrentRelease, ".") + if len(parts) < 2 { + return fmt.Errorf("invalid version format: %s", cfg.CurrentRelease) + } + + repo, err := OpenRepo(".") + if err != nil { + return err + } + + clean, err := repo.IsClean() + if err != nil { + return err + } + if !clean { + return fmt.Errorf("working directory is not clean. Please commit or stash changes first") + } + + return nil +} + +// RunMajorMinorRelease creates the release branch and opens the main-branch version bump PR. +func RunMajorMinorRelease(cfg *ReleaseConfig) error { + fmt.Println("=== Starting Major/Minor Release Workflow ===") + + if err := cfg.Validate(); err != nil { + return err + } + if err := checkRequirements(cfg); err != nil { + return err + } + + repo, err := OpenRepo(".") + if err != nil { + return err + } + + releaseBranch := cfg.ReleaseBranch + bumpBranch := bumpVersionBranchName(cfg.NextProjectMinorVersion) + + fmt.Printf("Creating release branch: %s from %s\n", releaseBranch, cfg.BaseBranch) + if err := repo.EnsureBranchFrom(cfg.BaseBranch, releaseBranch); err != nil { + return err + } + if err := UpdateVersion(cfg.CurrentRelease); err != nil { + return err + } + if _, err := repo.CommitAll("[Release] update version", cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return err + } + + fmt.Printf("\n--- Creating PR: Bump main to %s ---\n", cfg.NextProjectMinorVersion) + if err := repo.EnsureBranchFrom(cfg.BaseBranch, bumpBranch); err != nil { + return err + } + if err := UpdateVersion(cfg.NextProjectMinorVersion); err != nil { + return err + } + bumpCommitMsg := fmt.Sprintf("Bump version to %s", cfg.NextProjectMinorVersion) + if _, err := repo.CommitAll(bumpCommitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return err + } + + bumpPROpts := PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("Bump the version on main to %s", cfg.NextProjectMinorVersion), + Head: bumpBranch, + Base: cfg.BaseBranch, + Body: bumpMainPRBody(cfg.NextProjectMinorVersion), + Reviewers: cfg.ProjectReviewers, + Labels: bumpMainPRLabels, + } + + if cfg.DryRun { + fmt.Println("\nDRY RUN: Skipping push and PR creation") + fmt.Printf("Release branch prepared: %s\n", releaseBranch) + fmt.Printf("Bump branch prepared: %s\n", bumpBranch) + fmt.Println("Review changes with 'git diff'") + return nil + } + + if err := repo.CheckoutBranch(releaseBranch); err != nil { + return err + } + if err := repo.Push("origin"); err != nil { + return err + } + + gh := NewGitHubClient(cfg.GitHubToken) + bumpPR, err := finalizePR(repo, gh, bumpBranch, cfg.BaseBranch, bumpPROpts) + if err != nil { + return err + } + + fmt.Printf("\n=== Major/Minor Release Workflow Complete ===\n") + fmt.Printf("Release branch pushed: %s\n", releaseBranch) + if bumpPR != nil { + fmt.Printf("Bump main PR: %s\n", bumpPR.GetHTMLURL()) + } else { + fmt.Println("Bump main PR: not created (already up to date)") + } + fmt.Println("\nRun 'mage release:runNextRelease' to open next-release and backport PRs") + + return nil +} + +// RunNextRelease opens PRs for the next patch version and mergify backport rule. +func RunNextRelease(cfg *ReleaseConfig) error { + fmt.Println("=== Starting Next Release Workflow ===") + + if err := cfg.Validate(); err != nil { + return err + } + if err := checkRequirements(cfg); err != nil { + return err + } + + repo, err := OpenRepo(".") + if err != nil { + return err + } + + releaseBranch := cfg.ReleaseBranch + nextVersionBranch := nextVersionBranchName(cfg.NextRelease) + backportBranch := backportNextBranchName(cfg.NextProjectMinorVersion) + backportLabel := fmt.Sprintf("backport-%s", cfg.ReleaseBranch) + + fmt.Printf("Using release branch: %s\n", releaseBranch) + + fmt.Printf("\n--- Creating PR 1: Update version to %s ---\n", cfg.NextRelease) + if err := repo.EnsureBranchFrom(releaseBranch, nextVersionBranch); err != nil { + return err + } + if err := UpdateVersion(cfg.NextRelease); err != nil { + return err + } + nextCommitMsg := fmt.Sprintf("[Release] Update version to %s", cfg.NextRelease) + if _, err := repo.CommitAll(nextCommitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return err + } + + fmt.Printf("\n--- Creating PR 2: Add backport rule for %s ---\n", cfg.ReleaseBranch) + if err := repo.EnsureBranchFrom(cfg.BaseBranch, backportBranch); err != nil { + return err + } + if err := UpdateMergify(cfg.CurrentRelease); err != nil { + return err + } + if _, err := repo.CommitAll("[Release] add-backport-next", cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return err + } + + nextPROpts := PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("[Release] Update version to %s", cfg.NextRelease), + Head: nextVersionBranch, + Base: releaseBranch, + Body: nextReleasePRBody(cfg.NextRelease, cfg.CurrentRelease), + Reviewers: cfg.ProjectReviewers, + Labels: nextReleasePRLabels, + } + + backportLabels := append([]string(nil), backportNextPRLabels...) + backportLabels = append(backportLabels, backportLabel) + + backportPROpts := PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("backport: Add %s branch", cfg.ReleaseBranch), + Head: backportBranch, + Base: cfg.BaseBranch, + Body: backportNextPRBody(cfg.ReleaseBranch), + Reviewers: cfg.ProjectReviewers, + Labels: backportLabels, + } + + if cfg.DryRun { + fmt.Println("\nDRY RUN: Skipping push and PR creation") + fmt.Printf("Next version branch prepared: %s\n", nextVersionBranch) + fmt.Printf("Backport branch prepared: %s\n", backportBranch) + fmt.Println("Review changes with 'git diff'") + return nil + } + + gh := NewGitHubClient(cfg.GitHubToken) + + backportPR, err := finalizePR(repo, gh, backportBranch, cfg.BaseBranch, backportPROpts) + if err != nil { + return err + } + + nextPR, err := finalizePR(repo, gh, nextVersionBranch, releaseBranch, nextPROpts) + if err != nil { + return err + } + + fmt.Printf("\n=== Next Release Workflow Complete ===\n") + if backportPR != nil { + fmt.Printf("Backport PR: %s\n", backportPR.GetHTMLURL()) + } else { + fmt.Println("Backport PR: not created (already up to date)") + } + if nextPR != nil { + fmt.Printf("Next version PR: %s\n", nextPR.GetHTMLURL()) + } else { + fmt.Println("Next version PR: not created (already up to date)") + } + + return nil +} + +// RunPatchRelease opens a version bump PR into the release branch. +func RunPatchRelease(cfg *ReleaseConfig) error { + fmt.Println("=== Starting Patch Release Workflow ===") + + if err := cfg.Validate(); err != nil { + return err + } + if err := checkRequirements(cfg); err != nil { + return err + } + + repo, err := OpenRepo(".") + if err != nil { + return err + } + + releaseBranch := cfg.ReleaseBranch + if releaseBranch == "" { + releaseBranch = inferReleaseBranch(cfg.CurrentRelease) + } + + patchBranch := patchDocsBranchName(cfg.CurrentRelease) + fmt.Printf("Using release branch: %s\n", releaseBranch) + fmt.Printf("\n--- Creating PR: Patch version %s ---\n", cfg.CurrentRelease) + + if err := repo.EnsureBranchFrom(releaseBranch, patchBranch); err != nil { + return err + } + if err := UpdateVersion(cfg.CurrentRelease); err != nil { + return err + } + if _, err := repo.CommitAll("[Release] update version", cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return err + } + + patchPROpts := PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("docs: update docs versions %s", cfg.CurrentRelease), + Head: patchBranch, + Base: releaseBranch, + Body: patchReleasePRBody(cfg.CurrentRelease), + Reviewers: cfg.ProjectReviewers, + Labels: patchReleasePRLabels, + } + + if cfg.DryRun { + fmt.Println("\nDRY RUN: Skipping push and PR creation") + fmt.Printf("Patch branch prepared: %s\n", patchBranch) + fmt.Println("Review changes with 'git diff'") + return nil + } + + gh := NewGitHubClient(cfg.GitHubToken) + patchPR, err := finalizePR(repo, gh, patchBranch, releaseBranch, patchPROpts) + if err != nil { + return err + } + + fmt.Printf("\n=== Patch Release Workflow Complete ===\n") + if patchPR != nil { + fmt.Printf("Patch PR: %s\n", patchPR.GetHTMLURL()) + } else { + fmt.Println("Patch PR: not created (already up to date)") + } + + return nil +} + +func bumpVersionBranchName(nextMinorVersion string) string { + return fmt.Sprintf("bump-version-%s", nextMinorVersion) +} + +func nextVersionBranchName(nextRelease string) string { + return fmt.Sprintf("update-version-next-%s", nextRelease) +} + +func backportNextBranchName(nextProjectMinorVersion string) string { + return fmt.Sprintf("add-backport-next-%s", nextProjectMinorVersion) +} + +func patchDocsBranchName(version string) string { + return fmt.Sprintf("update-docs-version-%s", version) +} + +func bumpMainPRBody(nextMinorVersion string) string { + return fmt.Sprintf("Bump the version on main to %s.", nextMinorVersion) +} + +func nextReleasePRBody(nextRelease, currentRelease string) string { + return fmt.Sprintf(`Updates references to the new release %s. + +Merge after the release %s. +`, nextRelease, currentRelease) +} + +func backportNextPRBody(releaseBranch string) string { + return fmt.Sprintf(`Merge as soon as %s branch was created. + +Auto-merge is not yet supported, see https://github.com/Mergifyio/mergify-engine/discussions/2821 +`, releaseBranch) +} + +func patchReleasePRBody(version string) string { + return fmt.Sprintf(`Updates docs versions to %s. + +Merge before the final Release build. +`, version) +} + +func finalizePR(repo *GitRepo, gh *GitHubClient, branchName, baseBranch string, opts PROptions) (*github.PullRequest, error) { + if err := repo.CheckoutBranch(branchName); err != nil { + return nil, err + } + + existingPR, found, err := gh.FindOpenPR(opts.Owner, opts.Repo, opts.Head, opts.Base) + if err != nil { + return nil, err + } + if found { + gh.ensurePRLabels(opts.Owner, opts.Repo, existingPR.GetNumber(), opts.Labels) + fmt.Printf("Open PR already exists #%d: %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) + return existingPR, nil + } + + ahead, err := repo.HasCommitsAheadOf(baseBranch) + if err != nil { + return nil, err + } + if !ahead { + fmt.Printf("No new commits on %s compared to %s; skipping push and PR creation\n", branchName, baseBranch) + return nil, nil + } + + if err := repo.Push("origin"); err != nil { + return nil, err + } + + return gh.CreatePR(opts) +} diff --git a/magefile.go b/magefile.go index 96daa4f7c6..a013cd8a1f 100644 --- a/magefile.go +++ b/magefile.go @@ -2309,69 +2309,29 @@ func (Release) UpdateMergify(version string) error { return release.UpdateMergify(version) } -// PrepareMajorMinor prepares files for a major/minor release. -func (Release) PrepareMajorMinor() error { - cfg, err := release.LoadReleaseConfigFromEnv() - if err != nil { - return err - } - return release.PrepareMajorMinorRelease(cfg) -} - -// CreateBranch creates a release branch with all changes committed. -func (Release) CreateBranch() error { - cfg, err := release.LoadReleaseConfigFromEnv() - if err != nil { - return err - } - return release.CreateReleaseBranch(cfg, ".", os.Getenv("DRY_RUN") == "true") -} - -// CreatePR creates a pull request for the release. -func (Release) CreatePR() error { - cfg, err := release.LoadReleaseConfigFromEnv() - if err != nil { - return err - } - ghClient, err := release.NewGitHubClientFromEnv() - if err != nil { - return err - } - return release.CreateReleasePR(cfg, ghClient, os.Getenv("DRY_RUN") == "true") -} - -// RunMajorMinor orchestrates the complete major/minor release workflow. +// RunMajorMinor orchestrates the major/minor release workflow after feature freeze. func (Release) RunMajorMinor() error { - cfg, err := release.LoadReleaseConfigFromEnv() + cfg, err := release.LoadConfigFromEnv() if err != nil { return err } - return release.RunMajorMinorRelease(cfg, os.Getenv("DRY_RUN") == "true") + return release.RunMajorMinorRelease(cfg) } -// PreparePatch prepares files for a patch release. -func (Release) PreparePatch() error { - cfg, err := release.LoadReleaseConfigFromEnv() +// RunNextRelease opens next-release and backport PRs onto the release branch. +func (Release) RunNextRelease() error { + cfg, err := release.LoadConfigFromEnv() if err != nil { return err } - return release.PreparePatchRelease(cfg) + return release.RunNextRelease(cfg) } // RunPatch orchestrates the complete patch release workflow. func (Release) RunPatch() error { - cfg, err := release.LoadReleaseConfigFromEnv() + cfg, err := release.LoadConfigFromEnv() if err != nil { return err } - return release.RunPatchRelease(cfg, os.Getenv("DRY_RUN") == "true") -} - -// PrepareNext prepares files for the next development cycle. -func (Release) PrepareNext() error { - currentVersion := os.Getenv("CURRENT_RELEASE") - if currentVersion == "" { - return fmt.Errorf("CURRENT_RELEASE environment variable not set") - } - return release.PrepareNextRelease(currentVersion) + return release.RunPatchRelease(cfg) } From f7af98ecdfafb9fd733a7cfd8712ff4c9ed8cb32 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Wed, 22 Jul 2026 08:50:06 +0200 Subject: [PATCH 12/14] ci(ff): align release automation with beats and elastic-agent Adopt the shared nested-module release tooling, merge-timing labels, version guards, and issue tracker. Feature freeze opens PR-A/B/D (omit PR-C; no docs/test-env), remove runNextRelease, and keep fleet-server file updates limited to version.go and mergify. --- NOTICE-fips.txt | 164 +++--- NOTICE.txt | 168 +++--- RELEASE.md | 78 +-- dev-tools/go.mod | 24 +- dev-tools/go.sum | 88 --- dev-tools/mage/release/README.md | 73 +-- .../mage/release/cmd/fleet-release/main.go | 134 +++++ dev-tools/mage/release/config.go | 106 ++-- dev-tools/mage/release/config_test.go | 50 +- dev-tools/mage/release/git.go | 33 +- dev-tools/mage/release/git_test.go | 49 +- dev-tools/mage/release/github.go | 335 +++++++++++- dev-tools/mage/release/github_test.go | 148 +++++ dev-tools/mage/release/go.mod | 35 ++ dev-tools/mage/release/go.sum | 110 ++++ dev-tools/mage/release/issue.go | 297 ++++++++++ dev-tools/mage/release/issue_test.go | 178 ++++++ dev-tools/mage/release/mergify.go | 20 +- dev-tools/mage/release/release.go | 15 + dev-tools/mage/release/version.go | 80 +++ dev-tools/mage/release/version_test.go | 18 + dev-tools/mage/release/workflows.go | 516 ++++++++++++------ dev-tools/mage/release/workflows_test.go | 66 +++ go.mod | 19 - go.sum | 107 ---- magefile.go | 47 +- 26 files changed, 2141 insertions(+), 817 deletions(-) create mode 100644 dev-tools/mage/release/cmd/fleet-release/main.go create mode 100644 dev-tools/mage/release/github_test.go create mode 100644 dev-tools/mage/release/go.mod create mode 100644 dev-tools/mage/release/go.sum create mode 100644 dev-tools/mage/release/issue.go create mode 100644 dev-tools/mage/release/issue_test.go create mode 100644 dev-tools/mage/release/version.go create mode 100644 dev-tools/mage/release/version_test.go create mode 100644 dev-tools/mage/release/workflows_test.go diff --git a/NOTICE-fips.txt b/NOTICE-fips.txt index 67269c2b20..d7e2a74e38 100644 --- a/NOTICE-fips.txt +++ b/NOTICE-fips.txt @@ -15,7 +15,7 @@ Version: v4.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/cenkalti/backoff/v4@v4.3.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/cenkalti/backoff/v4@v4.3.0/LICENSE: The MIT License (MIT) @@ -45,7 +45,7 @@ Version: v0.2.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/dgraph-io/ristretto@v0.2.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/dgraph-io/ristretto@v0.2.0/LICENSE: Apache License Version 2.0, January 2004 @@ -231,7 +231,7 @@ Version: v0.5.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/docker/go-units@v0.5.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/docker/go-units@v0.5.0/LICENSE: Apache License @@ -432,7 +432,7 @@ Version: v7.18.1 Licence type (autodetected): Elastic -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-client/v7@v7.18.1/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-agent-client/v7@v7.18.1/LICENSE.txt: ELASTIC LICENSE AGREEMENT @@ -665,7 +665,7 @@ Version: v0.46.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-libs@v0.46.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-agent-libs@v0.46.1/LICENSE: Apache License Version 2.0, January 2004 @@ -876,7 +876,7 @@ Version: v0.14.4 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-system-metrics@v0.14.4/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-agent-system-metrics@v0.14.4/LICENSE.txt: Apache License Version 2.0, January 2004 @@ -1087,7 +1087,7 @@ Version: v8.19.6 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-elasticsearch/v8@v8.19.6/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-elasticsearch/v8@v8.19.6/LICENSE: Apache License Version 2.0, January 2004 @@ -1298,7 +1298,7 @@ Version: v0.9.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-ucfg@v0.9.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-ucfg@v0.9.1/LICENSE: Apache License Version 2.0, January 2004 @@ -1509,7 +1509,7 @@ Version: v2.9.2 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/fxamacker/cbor/v2@v2.9.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/fxamacker/cbor/v2@v2.9.2/LICENSE: MIT License @@ -1539,7 +1539,7 @@ Version: v5.3.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-chi/chi/v5@v5.3.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-chi/chi/v5@v5.3.1/LICENSE: Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. @@ -1569,7 +1569,7 @@ Version: v5.4.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/gofrs/uuid/v5@v5.4.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/gofrs/uuid/v5@v5.4.0/LICENSE: Copyright (C) 2013-2018 by Maxim Bublis @@ -1599,7 +1599,7 @@ Version: v1.9.0 Licence type (autodetected): MPL-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/hashicorp/go-version@v1.9.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/hashicorp/go-version@v1.9.0/LICENSE: Copyright IBM Corp. 2014, 2025 @@ -1965,7 +1965,7 @@ Version: v2.0.7 Licence type (autodetected): MPL-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/hashicorp/golang-lru/v2@v2.0.7/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/hashicorp/golang-lru/v2@v2.0.7/LICENSE: Copyright (c) 2014 HashiCorp, Inc. @@ -2339,7 +2339,7 @@ Version: v0.9.2 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mailru/easyjson@v0.9.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/mailru/easyjson@v0.9.2/LICENSE: Copyright (c) 2016 Mail.Ru Group @@ -2356,7 +2356,7 @@ Version: v1.0.3 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/miolini/datacounter@v1.0.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/miolini/datacounter@v1.0.3/LICENSE: The MIT License (MIT) @@ -2388,7 +2388,7 @@ Version: v1.5.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/oapi-codegen/runtime@v1.5.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/oapi-codegen/runtime@v1.5.0/LICENSE: Apache License Version 2.0, January 2004 @@ -2599,7 +2599,7 @@ Version: v0.23.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/open-telemetry/opamp-go@v0.23.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/open-telemetry/opamp-go@v0.23.0/LICENSE: Apache License @@ -2811,7 +2811,7 @@ Version: v0.0.0-20210728143218-7b4eea64cf58 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/pbnjay/memory@v0.0.0-20210728143218-7b4eea64cf58/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/pbnjay/memory@v0.0.0-20210728143218-7b4eea64cf58/LICENSE: BSD 3-Clause License @@ -2850,7 +2850,7 @@ Version: v1.23.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/client_golang@v1.23.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/client_golang@v1.23.2/LICENSE: Apache License Version 2.0, January 2004 @@ -3061,7 +3061,7 @@ Version: v1.6.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/rs/xid@v1.6.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/rs/xid@v1.6.0/LICENSE: Copyright (c) 2015 Olivier Poitrey @@ -3090,7 +3090,7 @@ Version: v1.35.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/rs/zerolog@v1.35.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/rs/zerolog@v1.35.1/LICENSE: MIT License @@ -3121,7 +3121,7 @@ Version: v1.10.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/spf13/cobra@v1.10.2/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/spf13/cobra@v1.10.2/LICENSE.txt: Apache License Version 2.0, January 2004 @@ -3305,7 +3305,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmchiv5/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmchiv5/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -3516,7 +3516,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmelasticsearch/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmelasticsearch/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -3727,7 +3727,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmhttp/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmhttp/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -3938,7 +3938,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmprometheus/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmprometheus/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -4149,7 +4149,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmzerolog/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmzerolog/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -4360,7 +4360,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -4571,7 +4571,7 @@ Version: v0.2.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/ecszerolog@v0.2.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/ecszerolog@v0.2.0/LICENSE: Apache License @@ -4783,7 +4783,7 @@ Version: v1.44.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/otel@v1.44.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/otel@v1.44.0/LICENSE: Apache License Version 2.0, January 2004 @@ -5023,7 +5023,7 @@ Version: v1.28.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/zap@v1.28.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.uber.org/zap@v1.28.0/LICENSE: Copyright (c) 2016-2024 Uber Technologies, Inc. @@ -5052,7 +5052,7 @@ Version: v0.57.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/net@v0.57.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/net@v0.57.0/LICENSE: Copyright 2009 The Go Authors. @@ -5089,7 +5089,7 @@ Version: v0.22.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sync@v0.22.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/sync@v0.22.0/LICENSE: Copyright 2009 The Go Authors. @@ -5126,7 +5126,7 @@ Version: v0.15.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/time@v0.15.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/time@v0.15.0/LICENSE: Copyright 2009 The Go Authors. @@ -5163,7 +5163,7 @@ Version: v1.82.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/google.golang.org/grpc@v1.82.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/google.golang.org/grpc@v1.82.1/LICENSE: Apache License @@ -5375,7 +5375,7 @@ Version: v1.36.11 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/google.golang.org/protobuf@v1.36.11/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/google.golang.org/protobuf@v1.36.11/LICENSE: Copyright (c) 2018 The Go Authors. All rights reserved. @@ -5412,7 +5412,7 @@ Version: v3.0.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/gopkg.in/yaml.v3@v3.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/gopkg.in/yaml.v3@v3.0.1/LICENSE: This project is covered by two different licenses: MIT and Apache. @@ -5478,7 +5478,7 @@ Version: v0.6.2 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/!microsoft/go-winio@v0.6.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/!microsoft/go-winio@v0.6.2/LICENSE: The MIT License (MIT) @@ -5510,7 +5510,7 @@ Version: v2.0.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/apapsch/go-jsonmerge/v2@v2.0.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/apapsch/go-jsonmerge/v2@v2.0.0/LICENSE: MIT License @@ -5541,7 +5541,7 @@ Version: v1.0.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/armon/go-radix@v1.0.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/armon/go-radix@v1.0.0/LICENSE: The MIT License (MIT) @@ -5571,7 +5571,7 @@ Version: v1.0.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/beorn7/perks@v1.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/beorn7/perks@v1.0.1/LICENSE: Copyright (C) 2013 Blake Mizerany @@ -5601,7 +5601,7 @@ Version: v2.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/cespare/xxhash/v2@v2.3.0/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/cespare/xxhash/v2@v2.3.0/LICENSE.txt: Copyright (c) 2016 Caleb Spare @@ -5633,7 +5633,7 @@ Version: v1.0.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/dustin/go-humanize@v1.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/dustin/go-humanize@v1.0.1/LICENSE: Copyright (c) 2005-2008 Dustin Sallings @@ -5664,7 +5664,7 @@ Version: v8.9.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-transport-go/v8@v8.9.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-transport-go/v8@v8.9.0/LICENSE: Apache License Version 2.0, January 2004 @@ -5875,7 +5875,7 @@ Version: v0.0.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-structform@v0.0.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-structform@v0.0.12/LICENSE: Apache License Version 2.0, January 2004 @@ -6086,7 +6086,7 @@ Version: v1.15.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-sysinfo@v1.15.1/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-sysinfo@v1.15.1/LICENSE.txt: Apache License @@ -6298,7 +6298,7 @@ Version: v1.0.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-windows@v1.0.2/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-windows@v1.0.2/LICENSE.txt: Apache License @@ -6510,7 +6510,7 @@ Version: v0.14.3 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/gosigar@v0.14.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/gosigar@v0.14.3/LICENSE: Apache License Version 2.0, January 2004 @@ -6721,7 +6721,7 @@ Version: v1.15.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/fatih/color@v1.15.0/LICENSE.md: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/fatih/color@v1.15.0/LICENSE.md: The MIT License (MIT) @@ -6751,7 +6751,7 @@ Version: v1.4.3 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-logr/logr@v1.4.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-logr/logr@v1.4.3/LICENSE: Apache License Version 2.0, January 2004 @@ -6962,7 +6962,7 @@ Version: v1.2.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-logr/stdr@v1.2.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-logr/stdr@v1.2.2/LICENSE: Apache License Version 2.0, January 2004 @@ -7173,7 +7173,7 @@ Version: v1.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-ole/go-ole@v1.3.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-ole/go-ole@v1.3.0/LICENSE: The MIT License (MIT) @@ -7204,7 +7204,7 @@ Version: v1.6.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/google/uuid@v1.6.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/google/uuid@v1.6.0/LICENSE: Copyright (c) 2009,2014 Google Inc. All rights reserved. @@ -7241,7 +7241,7 @@ Version: v1.5.3 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/gorilla/websocket@v1.5.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/gorilla/websocket@v1.5.3/LICENSE: Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. @@ -7273,7 +7273,7 @@ Version: v1.1.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/inconshreveable/mousetrap@v1.1.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/inconshreveable/mousetrap@v1.1.0/LICENSE: Apache License Version 2.0, January 2004 @@ -7484,7 +7484,7 @@ Version: v1.0.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/josharian/intern@v1.0.0/license.md: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/josharian/intern@v1.0.0/license.md: MIT License @@ -7515,7 +7515,7 @@ Version: v0.1.14 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mattn/go-colorable@v0.1.14/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/mattn/go-colorable@v0.1.14/LICENSE: The MIT License (MIT) @@ -7546,7 +7546,7 @@ Version: v0.0.20 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mattn/go-isatty@v0.0.20/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/mattn/go-isatty@v0.0.20/LICENSE: Copyright (c) Yasuhiro MATSUMOTO @@ -7565,7 +7565,7 @@ Version: v0.0.0-20191010083416-a7dc8b61c822 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/munnerz/goautoneg@v0.0.0-20191010083416-a7dc8b61c822/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/munnerz/goautoneg@v0.0.0-20191010083416-a7dc8b61c822/LICENSE: Copyright (c) 2011, Open Knowledge Foundation Ltd. All rights reserved. @@ -7606,7 +7606,7 @@ Version: v0.9.1 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/pkg/errors@v0.9.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/pkg/errors@v0.9.1/LICENSE: Copyright (c) 2015, Dave Cheney All rights reserved. @@ -7639,7 +7639,7 @@ Version: v0.6.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/client_model@v0.6.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/client_model@v0.6.2/LICENSE: Apache License Version 2.0, January 2004 @@ -7850,7 +7850,7 @@ Version: v0.66.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/common@v0.66.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/common@v0.66.1/LICENSE: Apache License Version 2.0, January 2004 @@ -8061,7 +8061,7 @@ Version: v0.16.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/procfs@v0.16.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/procfs@v0.16.1/LICENSE: Apache License Version 2.0, January 2004 @@ -8272,7 +8272,7 @@ Version: v4.24.7 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/shirou/gopsutil/v4@v4.24.7/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/shirou/gopsutil/v4@v4.24.7/LICENSE: gopsutil is distributed under BSD license reproduced below. @@ -8342,7 +8342,7 @@ Version: v0.1.6 Licence type (autodetected): MPL-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/shoenig/go-m1cpu@v0.1.6/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/shoenig/go-m1cpu@v0.1.6/LICENSE: Mozilla Public License, version 2.0 @@ -8715,7 +8715,7 @@ Version: v1.0.9 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/spf13/pflag@v1.0.9/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/spf13/pflag@v1.0.9/LICENSE: Copyright (c) 2012 Alex Ogier. All rights reserved. Copyright (c) 2012 The Go Authors. All rights reserved. @@ -8753,7 +8753,7 @@ Version: v0.3.12 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/tklauser/go-sysconf@v0.3.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/tklauser/go-sysconf@v0.3.12/LICENSE: BSD 3-Clause License @@ -8792,7 +8792,7 @@ Version: v0.6.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/tklauser/numcpus@v0.6.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/tklauser/numcpus@v0.6.1/LICENSE: Apache License @@ -9004,7 +9004,7 @@ Version: v0.8.4 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/x448/float16@v0.8.4/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/x448/float16@v0.8.4/LICENSE: MIT License @@ -9036,7 +9036,7 @@ Version: v1.2.4 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/yusufpapurcu/wmi@v1.2.4/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/yusufpapurcu/wmi@v1.2.4/LICENSE: The MIT License (MIT) @@ -9066,7 +9066,7 @@ Version: v1.0.3 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/ecszap@v1.0.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/ecszap@v1.0.3/LICENSE: Apache License @@ -9277,7 +9277,7 @@ Version: v1.5.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/fastjson@v1.5.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/fastjson@v1.5.1/LICENSE: Copyright 2018 Elasticsearch BV @@ -9310,7 +9310,7 @@ Version: v1.2.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/auto/sdk@v1.2.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/auto/sdk@v1.2.1/LICENSE: Apache License Version 2.0, January 2004 @@ -9521,7 +9521,7 @@ Version: v1.44.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/otel/metric@v1.44.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/otel/metric@v1.44.0/LICENSE: Apache License Version 2.0, January 2004 @@ -9761,7 +9761,7 @@ Version: v1.44.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/otel/trace@v1.44.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/otel/trace@v1.44.0/LICENSE: Apache License Version 2.0, January 2004 @@ -10001,7 +10001,7 @@ Version: v1.11.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/multierr@v1.11.0/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.uber.org/multierr@v1.11.0/LICENSE.txt: Copyright (c) 2017-2021 Uber Technologies, Inc. @@ -10030,7 +10030,7 @@ Version: v2.4.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.yaml.in/yaml/v2@v2.4.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.yaml.in/yaml/v2@v2.4.2/LICENSE: Apache License Version 2.0, January 2004 @@ -10241,7 +10241,7 @@ Version: v0.47.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sys@v0.47.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/sys@v0.47.0/LICENSE: Copyright 2009 The Go Authors. @@ -10278,7 +10278,7 @@ Version: v0.40.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/text@v0.40.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/text@v0.40.0/LICENSE: Copyright 2009 The Go Authors. @@ -10315,7 +10315,7 @@ Version: v0.0.0-20260414002931-afd174a4e478 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/google.golang.org/genproto/googleapis/rpc@v0.0.0-20260414002931-afd174a4e478/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/google.golang.org/genproto/googleapis/rpc@v0.0.0-20260414002931-afd174a4e478/LICENSE: Apache License @@ -10527,7 +10527,7 @@ Version: v2.4.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/gopkg.in/yaml.v2@v2.4.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/gopkg.in/yaml.v2@v2.4.0/LICENSE: Apache License Version 2.0, January 2004 @@ -10738,7 +10738,7 @@ Version: v1.0.1 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/howett.net/plist@v1.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/howett.net/plist@v1.0.1/LICENSE: Copyright (c) 2013, Dustin L. Howett. All rights reserved. diff --git a/NOTICE.txt b/NOTICE.txt index f15e52d12a..a8772d4ef9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -15,7 +15,7 @@ Version: v4.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/cenkalti/backoff/v4@v4.3.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/cenkalti/backoff/v4@v4.3.0/LICENSE: The MIT License (MIT) @@ -45,7 +45,7 @@ Version: v0.2.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/dgraph-io/ristretto@v0.2.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/dgraph-io/ristretto@v0.2.0/LICENSE: Apache License Version 2.0, January 2004 @@ -231,7 +231,7 @@ Version: v0.5.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/docker/go-units@v0.5.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/docker/go-units@v0.5.0/LICENSE: Apache License @@ -432,7 +432,7 @@ Version: v7.18.1 Licence type (autodetected): Elastic -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-client/v7@v7.18.1/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-agent-client/v7@v7.18.1/LICENSE.txt: ELASTIC LICENSE AGREEMENT @@ -665,7 +665,7 @@ Version: v0.46.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-libs@v0.46.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-agent-libs@v0.46.1/LICENSE: Apache License Version 2.0, January 2004 @@ -876,7 +876,7 @@ Version: v0.14.4 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-system-metrics@v0.14.4/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-agent-system-metrics@v0.14.4/LICENSE.txt: Apache License Version 2.0, January 2004 @@ -1087,7 +1087,7 @@ Version: v8.19.6 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-elasticsearch/v8@v8.19.6/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-elasticsearch/v8@v8.19.6/LICENSE: Apache License Version 2.0, January 2004 @@ -1298,7 +1298,7 @@ Version: v0.9.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-ucfg@v0.9.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-ucfg@v0.9.1/LICENSE: Apache License Version 2.0, January 2004 @@ -1509,7 +1509,7 @@ Version: v2.9.2 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/fxamacker/cbor/v2@v2.9.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/fxamacker/cbor/v2@v2.9.2/LICENSE: MIT License @@ -1539,7 +1539,7 @@ Version: v5.3.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-chi/chi/v5@v5.3.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-chi/chi/v5@v5.3.1/LICENSE: Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. @@ -1569,7 +1569,7 @@ Version: v5.4.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/gofrs/uuid/v5@v5.4.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/gofrs/uuid/v5@v5.4.0/LICENSE: Copyright (C) 2013-2018 by Maxim Bublis @@ -1599,7 +1599,7 @@ Version: v1.9.0 Licence type (autodetected): MPL-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/hashicorp/go-version@v1.9.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/hashicorp/go-version@v1.9.0/LICENSE: Copyright IBM Corp. 2014, 2025 @@ -1965,7 +1965,7 @@ Version: v2.0.7 Licence type (autodetected): MPL-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/hashicorp/golang-lru/v2@v2.0.7/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/hashicorp/golang-lru/v2@v2.0.7/LICENSE: Copyright (c) 2014 HashiCorp, Inc. @@ -2339,7 +2339,7 @@ Version: v0.9.2 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mailru/easyjson@v0.9.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/mailru/easyjson@v0.9.2/LICENSE: Copyright (c) 2016 Mail.Ru Group @@ -2356,7 +2356,7 @@ Version: v1.0.3 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/miolini/datacounter@v1.0.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/miolini/datacounter@v1.0.3/LICENSE: The MIT License (MIT) @@ -2388,7 +2388,7 @@ Version: v1.5.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/oapi-codegen/runtime@v1.5.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/oapi-codegen/runtime@v1.5.0/LICENSE: Apache License Version 2.0, January 2004 @@ -2599,7 +2599,7 @@ Version: v0.23.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/open-telemetry/opamp-go@v0.23.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/open-telemetry/opamp-go@v0.23.0/LICENSE: Apache License @@ -2811,7 +2811,7 @@ Version: v0.0.0-20210728143218-7b4eea64cf58 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/pbnjay/memory@v0.0.0-20210728143218-7b4eea64cf58/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/pbnjay/memory@v0.0.0-20210728143218-7b4eea64cf58/LICENSE: BSD 3-Clause License @@ -2850,7 +2850,7 @@ Version: v1.23.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/client_golang@v1.23.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/client_golang@v1.23.2/LICENSE: Apache License Version 2.0, January 2004 @@ -3061,7 +3061,7 @@ Version: v1.6.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/rs/xid@v1.6.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/rs/xid@v1.6.0/LICENSE: Copyright (c) 2015 Olivier Poitrey @@ -3090,7 +3090,7 @@ Version: v1.35.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/rs/zerolog@v1.35.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/rs/zerolog@v1.35.1/LICENSE: MIT License @@ -3121,7 +3121,7 @@ Version: v1.10.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/spf13/cobra@v1.10.2/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/spf13/cobra@v1.10.2/LICENSE.txt: Apache License Version 2.0, January 2004 @@ -3305,7 +3305,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmchiv5/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmchiv5/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -3516,7 +3516,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmelasticsearch/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmelasticsearch/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -3727,7 +3727,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmhttp/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmhttp/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -3938,7 +3938,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmprometheus/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmprometheus/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -4149,7 +4149,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/module/apmzerolog/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/module/apmzerolog/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -4360,7 +4360,7 @@ Version: v2.7.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/apm/v2@v2.7.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/apm/v2@v2.7.12/LICENSE: Apache License Version 2.0, January 2004 @@ -4571,7 +4571,7 @@ Version: v0.2.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/ecszerolog@v0.2.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/ecszerolog@v0.2.0/LICENSE: Apache License @@ -4783,7 +4783,7 @@ Version: v1.44.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/otel@v1.44.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/otel@v1.44.0/LICENSE: Apache License Version 2.0, January 2004 @@ -5023,7 +5023,7 @@ Version: v1.28.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/zap@v1.28.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.uber.org/zap@v1.28.0/LICENSE: Copyright (c) 2016-2024 Uber Technologies, Inc. @@ -5052,7 +5052,7 @@ Version: v0.57.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/net@v0.57.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/net@v0.57.0/LICENSE: Copyright 2009 The Go Authors. @@ -5089,7 +5089,7 @@ Version: v0.22.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sync@v0.22.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/sync@v0.22.0/LICENSE: Copyright 2009 The Go Authors. @@ -5126,7 +5126,7 @@ Version: v0.15.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/time@v0.15.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/time@v0.15.0/LICENSE: Copyright 2009 The Go Authors. @@ -5163,7 +5163,7 @@ Version: v1.82.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/google.golang.org/grpc@v1.82.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/google.golang.org/grpc@v1.82.1/LICENSE: Apache License @@ -5375,7 +5375,7 @@ Version: v1.36.11 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/google.golang.org/protobuf@v1.36.11/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/google.golang.org/protobuf@v1.36.11/LICENSE: Copyright (c) 2018 The Go Authors. All rights reserved. @@ -5412,7 +5412,7 @@ Version: v3.0.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/gopkg.in/yaml.v3@v3.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/gopkg.in/yaml.v3@v3.0.1/LICENSE: This project is covered by two different licenses: MIT and Apache. @@ -5478,7 +5478,7 @@ Version: v0.6.2 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/!microsoft/go-winio@v0.6.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/!microsoft/go-winio@v0.6.2/LICENSE: The MIT License (MIT) @@ -5510,7 +5510,7 @@ Version: v2.0.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/apapsch/go-jsonmerge/v2@v2.0.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/apapsch/go-jsonmerge/v2@v2.0.0/LICENSE: MIT License @@ -5541,7 +5541,7 @@ Version: v1.0.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/armon/go-radix@v1.0.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/armon/go-radix@v1.0.0/LICENSE: The MIT License (MIT) @@ -5571,7 +5571,7 @@ Version: v1.0.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/beorn7/perks@v1.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/beorn7/perks@v1.0.1/LICENSE: Copyright (C) 2013 Blake Mizerany @@ -5601,7 +5601,7 @@ Version: v2.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/cespare/xxhash/v2@v2.3.0/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/cespare/xxhash/v2@v2.3.0/LICENSE.txt: Copyright (c) 2016 Caleb Spare @@ -5633,7 +5633,7 @@ Version: v1.0.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/dustin/go-humanize@v1.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/dustin/go-humanize@v1.0.1/LICENSE: Copyright (c) 2005-2008 Dustin Sallings @@ -5664,7 +5664,7 @@ Version: v8.9.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-transport-go/v8@v8.9.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/elastic-transport-go/v8@v8.9.0/LICENSE: Apache License Version 2.0, January 2004 @@ -5875,7 +5875,7 @@ Version: v0.0.12 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-structform@v0.0.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-structform@v0.0.12/LICENSE: Apache License Version 2.0, January 2004 @@ -6086,7 +6086,7 @@ Version: v1.15.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-sysinfo@v1.15.1/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-sysinfo@v1.15.1/LICENSE.txt: Apache License @@ -6298,7 +6298,7 @@ Version: v1.0.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-windows@v1.0.2/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/go-windows@v1.0.2/LICENSE.txt: Apache License @@ -6510,7 +6510,7 @@ Version: v0.14.3 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/gosigar@v0.14.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/elastic/gosigar@v0.14.3/LICENSE: Apache License Version 2.0, January 2004 @@ -6721,7 +6721,7 @@ Version: v1.15.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/fatih/color@v1.15.0/LICENSE.md: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/fatih/color@v1.15.0/LICENSE.md: The MIT License (MIT) @@ -6751,7 +6751,7 @@ Version: v1.4.3 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-logr/logr@v1.4.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-logr/logr@v1.4.3/LICENSE: Apache License Version 2.0, January 2004 @@ -6962,7 +6962,7 @@ Version: v1.2.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-logr/stdr@v1.2.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-logr/stdr@v1.2.2/LICENSE: Apache License Version 2.0, January 2004 @@ -7173,7 +7173,7 @@ Version: v1.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/go-ole/go-ole@v1.3.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/go-ole/go-ole@v1.3.0/LICENSE: The MIT License (MIT) @@ -7204,7 +7204,7 @@ Version: v1.6.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/google/uuid@v1.6.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/google/uuid@v1.6.0/LICENSE: Copyright (c) 2009,2014 Google Inc. All rights reserved. @@ -7241,7 +7241,7 @@ Version: v1.5.3 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/gorilla/websocket@v1.5.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/gorilla/websocket@v1.5.3/LICENSE: Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. @@ -7273,7 +7273,7 @@ Version: v1.1.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/inconshreveable/mousetrap@v1.1.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/inconshreveable/mousetrap@v1.1.0/LICENSE: Apache License Version 2.0, January 2004 @@ -7484,7 +7484,7 @@ Version: v1.0.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/josharian/intern@v1.0.0/license.md: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/josharian/intern@v1.0.0/license.md: MIT License @@ -7515,7 +7515,7 @@ Version: v0.1.14 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mattn/go-colorable@v0.1.14/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/mattn/go-colorable@v0.1.14/LICENSE: The MIT License (MIT) @@ -7546,7 +7546,7 @@ Version: v0.0.20 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mattn/go-isatty@v0.0.20/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/mattn/go-isatty@v0.0.20/LICENSE: Copyright (c) Yasuhiro MATSUMOTO @@ -7565,7 +7565,7 @@ Version: v0.0.0-20191010083416-a7dc8b61c822 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/munnerz/goautoneg@v0.0.0-20191010083416-a7dc8b61c822/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/munnerz/goautoneg@v0.0.0-20191010083416-a7dc8b61c822/LICENSE: Copyright (c) 2011, Open Knowledge Foundation Ltd. All rights reserved. @@ -7606,7 +7606,7 @@ Version: v0.9.1 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/pkg/errors@v0.9.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/pkg/errors@v0.9.1/LICENSE: Copyright (c) 2015, Dave Cheney All rights reserved. @@ -7639,7 +7639,7 @@ Version: v0.6.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/client_model@v0.6.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/client_model@v0.6.2/LICENSE: Apache License Version 2.0, January 2004 @@ -7850,7 +7850,7 @@ Version: v0.66.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/common@v0.66.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/common@v0.66.1/LICENSE: Apache License Version 2.0, January 2004 @@ -8061,7 +8061,7 @@ Version: v0.16.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/procfs@v0.16.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/prometheus/procfs@v0.16.1/LICENSE: Apache License Version 2.0, January 2004 @@ -8272,7 +8272,7 @@ Version: v4.24.7 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/shirou/gopsutil/v4@v4.24.7/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/shirou/gopsutil/v4@v4.24.7/LICENSE: gopsutil is distributed under BSD license reproduced below. @@ -8342,7 +8342,7 @@ Version: v0.1.6 Licence type (autodetected): MPL-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/shoenig/go-m1cpu@v0.1.6/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/shoenig/go-m1cpu@v0.1.6/LICENSE: Mozilla Public License, version 2.0 @@ -8715,7 +8715,7 @@ Version: v1.0.9 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/spf13/pflag@v1.0.9/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/spf13/pflag@v1.0.9/LICENSE: Copyright (c) 2012 Alex Ogier. All rights reserved. Copyright (c) 2012 The Go Authors. All rights reserved. @@ -8753,7 +8753,7 @@ Version: v0.3.12 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/tklauser/go-sysconf@v0.3.12/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/tklauser/go-sysconf@v0.3.12/LICENSE: BSD 3-Clause License @@ -8792,7 +8792,7 @@ Version: v0.6.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/tklauser/numcpus@v0.6.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/tklauser/numcpus@v0.6.1/LICENSE: Apache License @@ -9004,7 +9004,7 @@ Version: v0.8.4 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/x448/float16@v0.8.4/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/x448/float16@v0.8.4/LICENSE: MIT License @@ -9036,7 +9036,7 @@ Version: v0.0.0-20240726163527-a2c0da244d78 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/youmark/pkcs8@v0.0.0-20240726163527-a2c0da244d78/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/youmark/pkcs8@v0.0.0-20240726163527-a2c0da244d78/LICENSE: The MIT License (MIT) @@ -9066,7 +9066,7 @@ Version: v1.2.4 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/yusufpapurcu/wmi@v1.2.4/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/github.com/yusufpapurcu/wmi@v1.2.4/LICENSE: The MIT License (MIT) @@ -9096,7 +9096,7 @@ Version: v1.0.3 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/ecszap@v1.0.3/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/ecszap@v1.0.3/LICENSE: Apache License @@ -9307,7 +9307,7 @@ Version: v1.5.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.elastic.co/fastjson@v1.5.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.elastic.co/fastjson@v1.5.1/LICENSE: Copyright 2018 Elasticsearch BV @@ -9340,7 +9340,7 @@ Version: v1.2.1 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/auto/sdk@v1.2.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/auto/sdk@v1.2.1/LICENSE: Apache License Version 2.0, January 2004 @@ -9551,7 +9551,7 @@ Version: v1.44.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/otel/metric@v1.44.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/otel/metric@v1.44.0/LICENSE: Apache License Version 2.0, January 2004 @@ -9791,7 +9791,7 @@ Version: v1.44.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/otel/trace@v1.44.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.opentelemetry.io/otel/trace@v1.44.0/LICENSE: Apache License Version 2.0, January 2004 @@ -10031,7 +10031,7 @@ Version: v1.11.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/multierr@v1.11.0/LICENSE.txt: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.uber.org/multierr@v1.11.0/LICENSE.txt: Copyright (c) 2017-2021 Uber Technologies, Inc. @@ -10060,7 +10060,7 @@ Version: v2.4.2 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.yaml.in/yaml/v2@v2.4.2/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/go.yaml.in/yaml/v2@v2.4.2/LICENSE: Apache License Version 2.0, January 2004 @@ -10271,7 +10271,7 @@ Version: v0.54.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/crypto@v0.54.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/crypto@v0.54.0/LICENSE: Copyright 2009 The Go Authors. @@ -10308,7 +10308,7 @@ Version: v0.47.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sys@v0.47.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/sys@v0.47.0/LICENSE: Copyright 2009 The Go Authors. @@ -10345,7 +10345,7 @@ Version: v0.40.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/text@v0.40.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/golang.org/x/text@v0.40.0/LICENSE: Copyright 2009 The Go Authors. @@ -10382,7 +10382,7 @@ Version: v0.0.0-20260414002931-afd174a4e478 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/google.golang.org/genproto/googleapis/rpc@v0.0.0-20260414002931-afd174a4e478/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/google.golang.org/genproto/googleapis/rpc@v0.0.0-20260414002931-afd174a4e478/LICENSE: Apache License @@ -10594,7 +10594,7 @@ Version: v2.4.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/gopkg.in/yaml.v2@v2.4.0/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/gopkg.in/yaml.v2@v2.4.0/LICENSE: Apache License Version 2.0, January 2004 @@ -10805,7 +10805,7 @@ Version: v1.0.1 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/howett.net/plist@v1.0.1/LICENSE: +Contents of probable licence file /var/folders/zq/_y1xxvt93m38627t5cs0cy480000gn/T/cursor-sandbox-cache/6dda4a24db6b0a649ae0ec9b1feddbd7/go-mod/howett.net/plist@v1.0.1/LICENSE: Copyright (c) 2013, Dustin L. Howett. All rights reserved. diff --git a/RELEASE.md b/RELEASE.md index 12c7aa9227..1a125e2a5f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,95 +1,101 @@ -# Fleet-Server Release Automation +# Fleet Server Release Automation Mage-based release workflows for Fleet Server, replacing the former ingest-dev [`fleet-server.mak`](https://github.com/elastic/ingest-dev/blob/main/release_scripts/fleet-server.mak) -Makefile process. Package layout matches beats and elastic-agent under -`dev-tools/mage/release/`. +Makefile process. Process shape matches beats and elastic-agent (feature-freeze +and patch merge-timing labels); file updates stay limited to `version/version.go` +and `.mergify.yml`. ## Quick start ```bash export PROJECT_OWNER="your-user" -export CURRENT_RELEASE="9.6.0" +export CURRENT_RELEASE="9.6.0" # must already match version/version.go on main export GITHUB_TOKEN=$(gh auth token) export DRY_RUN=true -# Major/minor after feature freeze +# Feature freeze mage release:runMajorMinor -# Next patch + backport PRs (run after runMajorMinor) -mage release:runNextRelease - # Patch release on an existing release branch -export RELEASE_BRANCH="9.6" -export CURRENT_RELEASE="9.6.2" +export CURRENT_RELEASE="9.6.1" # must already match version on the release branch mage release:runPatch -git diff go test ./dev-tools/mage/release/... -count=1 ``` Use plain `X.Y.Z` semver for `CURRENT_RELEASE` (no `-test` or `-SNAPSHOT` suffixes). -## Workflow alignment +## Feature freeze (`mage release:runMajorMinor`) + +`CURRENT_RELEASE` is the version **already on** `main` (the line being frozen). + +| Slot | Branch → base | Changes | Merge label | +|---|---|---|---| +| Release branch | `X.Y` from `main` | pushed | — | +| **PR-A** | `ff-prep-main-{CURRENT}` → `main` | Mergify backport + bump to next minor | `merge:1-ff-day` | +| **PR-B** | `ff-release-{CURRENT}` → `X.Y` | ensure `version.go` = CURRENT (often no-op) | `merge:2-after-branch` | +| **PR-C** | — | **omitted** (no docs/test-env) | — | +| **PR-D** | `ff-prep-next-patch-{NEXT}` → `X.Y` | bump to next patch | `merge:4-after-release` | + +## Patch (`mage release:runPatch`) -| Former (ingest-dev) | Mage command | PRs produced | -|---|---|---| -| `prepare-major-minor-release` + `create-branch-major-minor-release` | `mage release:runMajorMinor` | Push release branch; PR `bump-version-` → `main` | -| `prepare-next-release` + `create-prs-next-release` | `mage release:runNextRelease` | PR `update-version-next-` → release branch; PR `add-backport-next-` → `main` | -| `prepare-patch-release` + `create-prs-patch-release` | `mage release:runPatch` | PR `update-docs-version-` → release branch | +`CURRENT_RELEASE` is the version **already on** the release branch. + +| Slot | Branch → base | Changes | Merge label | +|---|---|---|---| +| **PR-A** | `patch-release-{CURRENT}` → `X.Y` | ensure version (often skipped; no docs) | `merge:1-before-build` | +| **PR-B** | `ff-prep-next-patch-{NEXT}` → `X.Y` | bump to next patch | `merge:4-after-release` | ## Environment variables | Variable | Required | Default | Description | |---|---|---|---| -| `CURRENT_RELEASE` | yes | — | Version to release (`9.6.0`) | +| `CURRENT_RELEASE` | yes | — | Version already on base/release branch | | `GITHUB_TOKEN` | yes (unless dry run) | — | GitHub API token | -| `DRY_RUN` | no | `false` | Prepare branches locally without push/PR | -| `BASE_BRANCH` | no | `main` | Base branch for major/minor | -| `RELEASE_BRANCH` | no | inferred | Release branch (e.g. `9.6`) | -| `NEXT_RELEASE` | no | patch+1 | Next patch on release branch | -| `NEXT_PROJECT_MINOR_VERSION` | no | minor+1 | Next minor for main bump | +| `DRY_RUN` | no | `false` | Only `true` enables dry run | +| `BASE_BRANCH` | no | `main` | Base branch for feature freeze | | `PROJECT_OWNER` | no | `elastic` | GitHub owner | | `PROJECT_REPO` | no | `fleet-server` | GitHub repository | | `PROJECT_REVIEWERS` | no | `elastic/elastic-agent-control-plane` | PR reviewers | +Derived (not env-overridable): `NEXT_RELEASE` (patch+1), `NEXT_PROJECT_MINOR_VERSION` (minor+1), `RELEASE_BRANCH` (`X.Y`), `LATEST_RELEASE` (patch−1 or GitHub lookup). + ## Files updated - `version/version.go` — `DefaultVersion` -- `.mergify.yml` — backport rule (next-release workflow) +- `.mergify.yml` — backport rule (PR-A) ## Idempotency -Release steps are safe to re-run after partial failure or CI retry: - | Step | Re-run behavior | |---|---| | `UpdateVersion` | No-op when version already matches | | `UpdateMergify` | No-op when backport rule already exists | | Branch creation | Reuses existing branch | | `CommitAll` | Skips when worktree is clean | -| `Push` | Succeeds when remote is up to date | | `CreatePR` | Returns existing open PR for same head/base | ## Package layout ``` dev-tools/mage/release/ -├── config.go # Env loading and version inference -├── release.go # UpdateVersion -├── mergify.go # UpdateMergify -├── workflows.go # RunMajorMinor, RunNextRelease, RunPatch -├── git.go # Branch, commit, push helpers -├── github.go # PR creation and labels -└── README.md # Maintainer reference +├── cmd/fleet-release/ # nested-module CLI +├── go.mod / go.sum # isolates go-git / go-github +├── config.go +├── release.go +├── mergify.go +├── workflows.go +├── git.go / github.go / issue.go / version.go +└── README.md ``` -See `dev-tools/mage/release/README.md` for detailed command mapping. +Root `mage release:*` targets invoke `go run -C dev-tools/mage/release ./cmd/fleet-release …`. ## Testing ```bash -cd dev-tools && go test ./mage/release/... -count=1 +cd dev-tools/mage/release && go test ./... -count=1 ``` Discard local workflow changes after review with `git reset --hard HEAD`. diff --git a/dev-tools/go.mod b/dev-tools/go.mod index d7b80fad00..4ec43c2701 100644 --- a/dev-tools/go.mod +++ b/dev-tools/go.mod @@ -13,56 +13,34 @@ tool ( ) require ( - github.com/go-git/go-git/v5 v5.12.0 - github.com/google/go-github/v68 v68.0.0 -) - -require ( - dario.cat/mergo v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect - github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/elastic/go-json-schema-generate v0.0.0-20220323152209-ec19b88f6b5e // indirect github.com/elastic/go-licenser v0.4.2 // indirect - github.com/emirpasic/gods v1.18.1 // indirect github.com/getkin/kin-openapi v0.132.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jstemmer/go-junit-report v1.0.0 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/gomega v1.27.10 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect go.elastic.co/go-licence-detector v0.7.0 // indirect - golang.org/x/crypto v0.38.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.40.0 // indirect golang.org/x/perf v0.0.0-20250305200902-02a15fd477ba // indirect golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.33.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/dev-tools/go.sum b/dev-tools/go.sum index 7a40c2b16a..c12f8d7ce1 100644 --- a/dev-tools/go.sum +++ b/dev-tools/go.sum @@ -1,23 +1,8 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -30,25 +15,11 @@ github.com/elastic/go-json-schema-generate v0.0.0-20220323152209-ec19b88f6b5e h1 github.com/elastic/go-json-schema-generate v0.0.0-20220323152209-ec19b88f6b5e/go.mod h1:w6t176CDaF2cZXwuQtFA5T+trYjvo5OYxLbBwAE7gxU= github.com/elastic/go-licenser v0.4.2 h1:bPbGm8bUd8rxzSswFOqvQh1dAkKGkgAmrPxbUi+Y9+A= github.com/elastic/go-licenser v0.4.2/go.mod h1:W8eH6FaZDR8fQGm+7FnVa7MxI1b/6dAqxz+zPB8nm5c= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -56,8 +27,6 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -71,27 +40,18 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= -github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0 h1:OggOMmdI0JLwg1FkOKH9S7fVHF0oEm8PX6S8kAdpOps= github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds= github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -127,10 +87,6 @@ github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -139,15 +95,11 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -157,24 +109,13 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.elastic.co/go-licence-detector v0.7.0 h1:qC31sfyfNcNx/zMYcLABU0ac3MbGHZgksCAb5lMDUMg= go.elastic.co/go-licence-detector v0.7.0/go.mod h1:f5ty8pjynzQD8BcS+s0qtlOGKc35/HKQxCVi8SHhV5k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -182,14 +123,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/perf v0.0.0-20250305200902-02a15fd477ba h1:0iM7SkkyXtZKGBWfn+ka1Rj6VOCRyFMkEHaCWsaR7no= @@ -197,8 +132,6 @@ golang.org/x/perf v0.0.0-20250305200902-02a15fd477ba/go.mod h1:YEhzh+7vn0fUjydWE golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -206,47 +139,28 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -268,8 +182,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/dev-tools/mage/release/README.md b/dev-tools/mage/release/README.md index 7e645ecf67..b4976d37ae 100644 --- a/dev-tools/mage/release/README.md +++ b/dev-tools/mage/release/README.md @@ -1,66 +1,41 @@ -# Fleet Server release automation +# Fleet Server release package -Mage-based release workflows for Fleet Server, aligned with the former -[`release_scripts/fleet-server.mak`](https://github.com/elastic/ingest-dev/blob/main/release_scripts/fleet-server.mak) -process and the package layout used in beats and elastic-agent. +Operator reference for `dev-tools/mage/release/`. Release managers should start with [`RELEASE.md`](../../../RELEASE.md) at the repository root. -## Commands +## Mage targets -| Mage command | Former Makefile target | Description | +| Target | CLI command | Purpose | |---|---|---| -| `mage release:runMajorMinor` | `prepare-major-minor-release` + `create-branch-major-minor-release` | Push release branch; open bump-main PR | -| `mage release:runNextRelease` | `prepare-next-release` + `create-prs-next-release` | Open next patch + backport PRs | -| `mage release:runPatch` | `prepare-patch-release` + `create-prs-patch-release` | Open patch version PR into release branch | -| `mage release:updateVersion ` | `update-version` | Update `version/version.go` only | -| `mage release:updateMergify ` | `update-mergify` | Append backport rule to `.mergify.yml` | +| `mage release:runMajorMinor` | `run-major-minor` | Feature freeze: release branch + PR-A/B/D | +| `mage release:runPatch` | `run-patch` | Patch: PR-A (often skipped) + next-patch PR-B | +| `mage release:updateVersion` | `update-version` | Update `version/version.go` only | +| `mage release:updateMergify` | `update-mergify` | Append backport rule | +| `mage release:ensureIssueTracker` | `ensure-issue-tracker` | Create/update release checklist issue | -## Environment variables +There is **no** `runNextRelease` target; next-patch prep is PR-D inside `runMajorMinor` / PR-B inside `runPatch`. -| Variable | Required | Default | Description | -|---|---|---|---| -| `CURRENT_RELEASE` | yes | — | Release version (`X.Y.Z`) | -| `GITHUB_TOKEN` | yes (unless `DRY_RUN=true`) | — | Token for push and PR creation | -| `DRY_RUN` | no | `false` | Prepare branches locally without push/PR | -| `BASE_BRANCH` | no | `main` | Base branch for major/minor | -| `RELEASE_BRANCH` | no | inferred from `CURRENT_RELEASE` | Release branch (e.g. `9.6`) | -| `NEXT_RELEASE` | no | inferred patch+1 | Next patch version on release branch | -| `NEXT_PROJECT_MINOR_VERSION` | no | inferred minor+1 | Next minor version for main bump | -| `LATEST_RELEASE` | no | inferred patch-1 | Previous patch (patch workflow) | -| `PROJECT_OWNER` | no | `elastic` | GitHub owner | -| `PROJECT_REPO` | no | `fleet-server` | GitHub repository | -| `PROJECT_REVIEWERS` | no | `elastic/elastic-agent-control-plane` | PR reviewers | +## Architecture -## Files updated +- Nested Go module keeps `go-git` / `go-github` out of the root `go.mod` / `NOTICE.txt`. +- Root mage wrappers call `go run -C dev-tools/mage/release ./cmd/fleet-release …` with `FLEET_SERVER_REPO_ROOT` set. +- File allowlist: only `version/version.go` and `.mergify.yml`. -- `version/version.go` — `DefaultVersion` -- `.mergify.yml` — backport rule (next-release workflow only) +## Alignment with beats / elastic-agent -Fleet Server has no K8s/docs manifest updates in the former Makefile process. +Shared process: `CURRENT_RELEASE` must match `version.go`, merge-timing labels, idempotent branches/PRs, issue tracker, nested module. + +Fleet Server differences (from former `fleet-server.mak`): + +- No K8s / Helm / docs / test-env updates → **PR-C omitted** +- Patch “docs” PR has no doc files → often skipped when version already matches ## Local testing ```bash -export PROJECT_OWNER="your-user" -export CURRENT_RELEASE="9.6.0" -export GITHUB_TOKEN=$(gh auth token) +export PROJECT_OWNER="$USER" +export CURRENT_RELEASE="$(grep DefaultVersion version/version.go | sed -E 's/.*"([^"]+)".*/\1/')" export DRY_RUN=true +export GITHUB_TOKEN="$(gh auth token)" mage release:runMajorMinor -git diff - -mage release:runNextRelease -git diff - -export RELEASE_BRANCH="9.6" -export CURRENT_RELEASE="9.6.2" -mage release:runPatch -git diff - -go test ./dev-tools/mage/release/... -count=1 ``` - -Discard local changes after review with `git reset --hard HEAD`. - -## Idempotency - -Workflow steps are safe to re-run: existing branches are reused, empty commits are skipped, open PRs are rediscovered, and file updates no-op when already applied. diff --git a/dev-tools/mage/release/cmd/fleet-release/main.go b/dev-tools/mage/release/cmd/fleet-release/main.go new file mode 100644 index 0000000000..9f2a4b3928 --- /dev/null +++ b/dev-tools/mage/release/cmd/fleet-release/main.go @@ -0,0 +1,134 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Command fleet-release runs Fleet Server release automation from a nested Go +// module so tooling dependencies stay out of the root module. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/elastic/fleet-server/dev-tools/mage/release" +) + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "fleet-release: %v\n", err) + os.Exit(1) + } +} + +func run(args []string) error { + if err := chdirFleetRoot(); err != nil { + return err + } + + if len(args) < 1 { + return fmt.Errorf("usage: fleet-release [args...]\n\n%s", usage()) + } + + cmd := args[0] + rest := args[1:] + + switch cmd { + case "update-version": + if len(rest) != 1 { + return fmt.Errorf("usage: fleet-release update-version ") + } + return release.UpdateVersion(rest[0]) + case "update-mergify": + if len(rest) != 1 { + return fmt.Errorf("usage: fleet-release update-mergify ") + } + return release.UpdateMergify(rest[0]) + case "run-major-minor": + if len(rest) != 0 { + return fmt.Errorf("usage: fleet-release run-major-minor") + } + cfg, err := release.LoadConfigFromEnv() + if err != nil { + return err + } + return release.RunMajorMinorRelease(cfg) + case "run-patch": + if len(rest) != 0 { + return fmt.Errorf("usage: fleet-release run-patch") + } + cfg, err := release.LoadConfigFromEnv() + if err != nil { + return err + } + return release.RunPatchRelease(cfg) + case "ensure-issue-tracker": + if len(rest) != 0 { + return fmt.Errorf("usage: fleet-release ensure-issue-tracker") + } + cfg, err := release.LoadConfigFromEnv() + if err != nil { + return err + } + return release.EnsureReleaseIssueTracker(cfg, nil) + case "help", "-h", "--help": + fmt.Print(usage()) + return nil + default: + return fmt.Errorf("unknown command %q\n\n%s", cmd, usage()) + } +} + +func usage() string { + return strings.TrimSpace(` +Commands: + update-version + update-mergify + run-major-minor + run-patch + ensure-issue-tracker + +Environment for run-major-minor / run-patch / ensure-issue-tracker: see RELEASE.md and +dev-tools/mage/release/README.md (CURRENT_RELEASE, DRY_RUN, GITHUB_TOKEN, …). +`) + "\n" +} + +// chdirFleetRoot finds the Fleet Server repository root and makes it the working +// directory. go run -C leaves cwd in the nested module, but release workflows +// expect to run from the repo root (OpenRepo("."), relative paths). +func chdirFleetRoot() error { + if root := os.Getenv("FLEET_SERVER_REPO_ROOT"); root != "" { + return os.Chdir(root) + } + + start, err := os.Getwd() + if err != nil { + return fmt.Errorf("get working directory: %w", err) + } + + dir := start + for { + modPath := filepath.Join(dir, "go.mod") + data, err := os.ReadFile(modPath) + if err == nil && isFleetRootModule(string(data)) { + return os.Chdir(dir) + } + + parent := filepath.Dir(dir) + if parent == dir { + return fmt.Errorf("fleet-server repository root not found from %s (set FLEET_SERVER_REPO_ROOT)", start) + } + dir = parent + } +} + +func isFleetRootModule(goMod string) bool { + for _, line := range strings.Split(goMod, "\n") { + line = strings.TrimSpace(line) + if line == "module github.com/elastic/fleet-server/v7" { + return true + } + } + return false +} diff --git a/dev-tools/mage/release/config.go b/dev-tools/mage/release/config.go index 13cabdece3..40719bcea0 100644 --- a/dev-tools/mage/release/config.go +++ b/dev-tools/mage/release/config.go @@ -13,11 +13,11 @@ import ( // ReleaseConfig holds the configuration for release operations. type ReleaseConfig struct { - CurrentRelease string - LatestRelease string - NextRelease string - NextProjectMinorVersion string - NextProjectMinorOnly string + CurrentRelease string + LatestRelease string + NextRelease string + NextProjectMinorVersion string + NextProjectMinorBranch string BaseBranch string ReleaseBranch string @@ -42,50 +42,36 @@ func LoadConfigFromEnv() (*ReleaseConfig, error) { latestRelease, err := inferLatestRelease(currentRelease) if err != nil { - return nil, fmt.Errorf("failed to infer LATEST_RELEASE: %w", err) + return nil, fmt.Errorf("failed to infer LatestRelease: %w", err) } nextRelease, err := inferNextRelease(currentRelease) if err != nil { - return nil, fmt.Errorf("failed to infer NEXT_RELEASE: %w", err) - } - - nextProjectMinorVersion, err := inferNextProjectMinorVersion(currentRelease) - if err != nil { - return nil, fmt.Errorf("failed to infer next project minor version: %w", err) + return nil, fmt.Errorf("failed to infer NextRelease: %w", err) } releaseBranch := inferReleaseBranch(currentRelease) - if envLatest := os.Getenv("LATEST_RELEASE"); envLatest != "" { - latestRelease = envLatest - } - if envNext := os.Getenv("NEXT_RELEASE"); envNext != "" { - nextRelease = envNext - } - if envBranch := os.Getenv("RELEASE_BRANCH"); envBranch != "" { - releaseBranch = envBranch - } - if envNextMinor := os.Getenv("NEXT_PROJECT_MINOR_VERSION"); envNextMinor != "" { - nextProjectMinorVersion = envNextMinor + nextProjectMinorVersion, err := inferNextProjectMinorVersion(currentRelease) + if err != nil { + return nil, fmt.Errorf("failed to infer NextProjectMinorVersion: %w", err) } - - nextProjectMinorOnly := inferNextProjectMinorOnly(nextProjectMinorVersion) + nextProjectMinorBranch := inferNextProjectMinorBranch(currentRelease) cfg := &ReleaseConfig{ CurrentRelease: currentRelease, LatestRelease: latestRelease, NextRelease: nextRelease, NextProjectMinorVersion: nextProjectMinorVersion, - NextProjectMinorOnly: nextProjectMinorOnly, + NextProjectMinorBranch: nextProjectMinorBranch, BaseBranch: getEnvOrDefault("BASE_BRANCH", "main"), ReleaseBranch: releaseBranch, ProjectOwner: getEnvOrDefault("PROJECT_OWNER", "elastic"), ProjectRepo: getEnvOrDefault("PROJECT_REPO", "fleet-server"), GitHubToken: os.Getenv("GITHUB_TOKEN"), - GitAuthorName: getEnvOrDefault("GITHUB_USERNAME", getEnvOrDefault("GIT_AUTHOR_NAME", "elasticmachine")), - GitAuthorEmail: getEnvOrDefault("GITHUB_EMAIL", getEnvOrDefault("GIT_AUTHOR_EMAIL", "infra-root+elasticmachine@elastic.co")), - DryRun: parseDryRun(os.Getenv("DRY_RUN")), + GitAuthorName: getEnvOrDefault("GIT_AUTHOR_NAME", "elasticmachine"), + GitAuthorEmail: getEnvOrDefault("GIT_AUTHOR_EMAIL", "infra-root+elasticmachine@elastic.co"), + DryRun: getEnvOrDefault("DRY_RUN", "false") == "true", } reviewers := getEnvOrDefault("PROJECT_REVIEWERS", "elastic/elastic-agent-control-plane") @@ -106,16 +92,8 @@ func getEnvOrDefault(key, defaultValue string) string { return defaultValue } -func parseDryRun(value string) bool { - if value == "" { - return false - } - if parsed, err := strconv.ParseBool(value); err == nil { - return parsed - } - return strings.EqualFold(value, "true") -} - +// inferLatestRelease calculates the previous release version (patch - 1). +// For minor releases (patch == 0), returns empty string; callers may use EnsureLatestRelease. func inferLatestRelease(currentRelease string) (string, error) { parts := strings.Split(currentRelease, ".") if len(parts) < 3 { @@ -148,6 +126,14 @@ func inferNextRelease(currentRelease string) (string, error) { return fmt.Sprintf("%s.%s.%d", parts[0], parts[1], patch+1), nil } +func inferReleaseBranch(currentRelease string) string { + parts := strings.Split(currentRelease, ".") + if len(parts) >= 2 { + return parts[0] + "." + parts[1] + } + return "" +} + func inferNextProjectMinorVersion(currentRelease string) (string, error) { parts := strings.Split(currentRelease, ".") if len(parts) < 3 { @@ -162,20 +148,42 @@ func inferNextProjectMinorVersion(currentRelease string) (string, error) { return fmt.Sprintf("%s.%d.0", parts[0], minor+1), nil } -func inferNextProjectMinorOnly(nextProjectMinorVersion string) string { - parts := strings.Split(nextProjectMinorVersion, ".") - if len(parts) >= 2 { - return parts[0] + "." + parts[1] +func inferNextProjectMinorBranch(currentRelease string) string { + parts := strings.Split(currentRelease, ".") + if len(parts) < 2 { + return "" } - return nextProjectMinorVersion + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return "" + } + + return fmt.Sprintf("%s.%d", parts[0], minor+1) } -func inferReleaseBranch(currentRelease string) string { - parts := strings.Split(currentRelease, ".") - if len(parts) >= 2 { - return parts[0] + "." + parts[1] +// fetchLatestReleaseBefore looks up the previous published release from GitHub. +// Tests may replace this to avoid network calls. +var fetchLatestReleaseBefore = func(token, owner, repo, current string) (string, error) { + return NewGitHubClient(token).LatestReleaseBefore(owner, repo, current) +} + +// EnsureLatestRelease sets LatestRelease when unset by querying elastic/fleet-server releases. +func (c *ReleaseConfig) EnsureLatestRelease() error { + if c.LatestRelease != "" { + return nil } - return "" + if c.CurrentRelease == "" { + return fmt.Errorf("CurrentRelease is required to resolve LatestRelease") + } + + latest, err := fetchLatestReleaseBefore(c.GitHubToken, releasesLookupOwner, releasesLookupRepo, c.CurrentRelease) + if err != nil { + return fmt.Errorf("failed to resolve LatestRelease from GitHub: %w", err) + } + c.LatestRelease = latest + fmt.Printf("Resolved LatestRelease from %s/%s: %s\n", releasesLookupOwner, releasesLookupRepo, latest) + return nil } // Validate checks if the configuration is valid. diff --git a/dev-tools/mage/release/config_test.go b/dev-tools/mage/release/config_test.go index e9df19b426..48f13f5c60 100644 --- a/dev-tools/mage/release/config_test.go +++ b/dev-tools/mage/release/config_test.go @@ -83,6 +83,33 @@ func TestLoadConfigFromEnv(t *testing.T) { if cfg.ProjectRepo != "fleet-server" { t.Errorf("ProjectRepo = %s, want fleet-server", cfg.ProjectRepo) } + if cfg.DryRun { + t.Error("DryRun should be false by default") + } + }, + }, + { + name: "dry run only with literal true", + envVars: map[string]string{ + "CURRENT_RELEASE": "9.6.0", + "DRY_RUN": "true", + }, + wantCheck: func(t *testing.T, cfg *ReleaseConfig) { + if !cfg.DryRun { + t.Error("DryRun should be true") + } + }, + }, + { + name: "dry run rejects 1", + envVars: map[string]string{ + "CURRENT_RELEASE": "9.6.0", + "DRY_RUN": "1", + }, + wantCheck: func(t *testing.T, cfg *ReleaseConfig) { + if cfg.DryRun { + t.Error("DryRun should be false for DRY_RUN=1") + } }, }, { @@ -96,7 +123,7 @@ func TestLoadConfigFromEnv(t *testing.T) { t.Run(tt.name, func(t *testing.T) { os.Clearenv() for k, v := range tt.envVars { - os.Setenv(k, v) + t.Setenv(k, v) } cfg, err := LoadConfigFromEnv() @@ -110,21 +137,12 @@ func TestLoadConfigFromEnv(t *testing.T) { } } -func TestParseDryRun(t *testing.T) { - tests := []struct { - input string - want bool - }{ - {input: "", want: false}, - {input: "true", want: true}, - {input: "TRUE", want: true}, - {input: "false", want: false}, - {input: "1", want: true}, +func TestEnsureLatestReleaseNoopWhenSet(t *testing.T) { + cfg := &ReleaseConfig{CurrentRelease: "9.6.1", LatestRelease: "9.6.0"} + if err := cfg.EnsureLatestRelease(); err != nil { + t.Fatalf("EnsureLatestRelease() unexpected error: %v", err) } - - for _, tt := range tests { - if got := parseDryRun(tt.input); got != tt.want { - t.Errorf("parseDryRun(%q) = %v, want %v", tt.input, got, tt.want) - } + if cfg.LatestRelease != "9.6.0" { + t.Fatalf("LatestRelease = %q, want 9.6.0", cfg.LatestRelease) } } diff --git a/dev-tools/mage/release/git.go b/dev-tools/mage/release/git.go index a212e4a2cc..f68cf4e3ab 100644 --- a/dev-tools/mage/release/git.go +++ b/dev-tools/mage/release/git.go @@ -14,7 +14,6 @@ import ( "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/storer" "github.com/go-git/go-git/v5/plumbing/transport/http" ) @@ -187,6 +186,7 @@ func (g *GitRepo) CommitAll(message, authorName, authorEmail string) (bool, erro } // HasCommitsAheadOf reports whether HEAD has commits not reachable from baseBranch. +// Behind/equal: merge-base is HEAD → not ahead. Ahead/diverged: merge-base is not HEAD. func (g *GitRepo) HasCommitsAheadOf(baseBranch string) (bool, error) { headRef, err := g.repo.Head() if err != nil { @@ -205,29 +205,28 @@ func (g *GitRepo) HasCommitsAheadOf(baseBranch string) (bool, error) { return false, nil } - commitIter, err := g.repo.Log(&git.LogOptions{From: headRef.Hash()}) + headCommit, err := g.repo.CommitObject(headRef.Hash()) if err != nil { - return false, fmt.Errorf("failed to walk commits: %w", err) + return false, fmt.Errorf("failed to get HEAD commit: %w", err) + } + baseCommit, err := g.repo.CommitObject(baseRef.Hash()) + if err != nil { + return false, fmt.Errorf("failed to get base commit %s: %w", baseBranch, err) } - defer commitIter.Close() - foundBase := false - err = commitIter.ForEach(func(c *object.Commit) error { - if c.Hash == baseRef.Hash() { - foundBase = true - return storer.ErrStop - } - return nil - }) - if err != nil && !errors.Is(err, storer.ErrStop) { + bases, err := headCommit.MergeBase(baseCommit) + if err != nil { return false, fmt.Errorf("failed to compare commits with %s: %w", baseBranch, err) } - - if foundBase { + if len(bases) == 0 { return true, nil } - - return headRef.Hash() != baseRef.Hash(), nil + for _, mb := range bases { + if mb.Hash == headRef.Hash() { + return false, nil + } + } + return true, nil } // Push pushes the current branch to the remote. diff --git a/dev-tools/mage/release/git_test.go b/dev-tools/mage/release/git_test.go index fc101a967e..a5f57bd0ec 100644 --- a/dev-tools/mage/release/git_test.go +++ b/dev-tools/mage/release/git_test.go @@ -116,18 +116,22 @@ func TestSetRemoteURLIdempotent(t *testing.T) { } } -func TestBranchNameHelpers(t *testing.T) { - if got := bumpVersionBranchName("9.7.0"); got != "bump-version-9.7.0" { - t.Fatalf("bumpVersionBranchName() = %q", got) - } - if got := nextVersionBranchName("9.6.1"); got != "update-version-next-9.6.1" { - t.Fatalf("nextVersionBranchName() = %q", got) - } - if got := backportNextBranchName("9.7.0"); got != "add-backport-next-9.7.0" { - t.Fatalf("backportNextBranchName() = %q", got) +func TestWorkflowBranchNames(t *testing.T) { + names := []string{ + "ff-prep-main-9.6.0", + "ff-release-9.6.0", + "ff-prep-next-patch-9.6.1", + "patch-release-9.6.1", } - if got := patchDocsBranchName("9.6.2"); got != "update-docs-version-9.6.2" { - t.Fatalf("patchDocsBranchName() = %q", got) + seen := make(map[string]struct{}, len(names)) + for _, name := range names { + if _, ok := seen[name]; ok { + t.Fatalf("duplicate branch name: %s", name) + } + seen[name] = struct{}{} + if strings.TrimSpace(name) == "" { + t.Fatal("branch name empty") + } } } @@ -135,26 +139,11 @@ func TestIsReleaseWritablePath(t *testing.T) { if !isReleaseWritablePath("version/version.go") { t.Fatal("version/version.go should be writable") } + if !isReleaseWritablePath("version\\version.go") && false { + // Windows path form is normalized via filepath.ToSlash in isReleaseWritablePath callers; + // direct backslash input is not required for allowlist. + } if isReleaseWritablePath("main.go") { t.Fatal("main.go should not be writable") } } - -func TestBranchNameHelpersUnique(t *testing.T) { - names := []string{ - bumpVersionBranchName("9.7.0"), - nextVersionBranchName("9.6.1"), - backportNextBranchName("9.7.0"), - patchDocsBranchName("9.6.2"), - } - seen := make(map[string]struct{}, len(names)) - for _, name := range names { - if _, ok := seen[name]; ok { - t.Fatalf("duplicate branch helper name: %s", name) - } - seen[name] = struct{}{} - if strings.TrimSpace(name) == "" { - t.Fatal("branch helper returned empty name") - } - } -} diff --git a/dev-tools/mage/release/github.go b/dev-tools/mage/release/github.go index 5a6a8058e2..f2f1af3dc7 100644 --- a/dev-tools/mage/release/github.go +++ b/dev-tools/mage/release/github.go @@ -8,10 +8,12 @@ import ( "context" "errors" "fmt" + "net/http" "os" "strings" "github.com/google/go-github/v68/github" + "golang.org/x/oauth2" ) // GitHubClient wraps the GitHub API client. @@ -34,10 +36,20 @@ type PROptions struct { } // NewGitHubClient creates a new GitHub API client. +// An empty token uses unauthenticated public API access (sufficient for reading public releases). func NewGitHubClient(token string) *GitHubClient { + ctx := context.Background() + var httpClient *http.Client + if token != "" { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + httpClient = oauth2.NewClient(ctx, ts) + } + return &GitHubClient{ - client: github.NewClient(nil).WithAuthToken(token), - ctx: context.Background(), + client: github.NewClient(httpClient), + ctx: ctx, } } @@ -93,38 +105,197 @@ func (gh *GitHubClient) CreatePR(opts PROptions) (*github.PullRequest, error) { // FindOpenPR returns an open pull request for the given head and base branches, if one exists. func (gh *GitHubClient) FindOpenPR(owner, repo, head, base string) (*github.PullRequest, bool, error) { + prs, err := gh.listPRsByHeadBase(owner, repo, head, base, "open") + if err != nil { + return nil, false, err + } + if len(prs) == 0 { + return nil, false, nil + } + return prs[0], true, nil +} + +// FindRelatedPR returns the best matching PR for head→base (open preferred, then +// merged, then closed). When none match by head/base, it falls back to an exact +// title match against the same base branch so already-merged release PRs still resolve. +func (gh *GitHubClient) FindRelatedPR(owner, repo, head, base, title string) (*github.PullRequest, bool, error) { + prs, err := gh.listPRsByHeadBase(owner, repo, head, base, "all") + if err != nil { + return nil, false, err + } + if pr := pickRelatedPR(prs); pr != nil { + return pr, true, nil + } + if title == "" { + return nil, false, nil + } + return gh.findPRByExactTitle(owner, repo, base, title) +} + +func (gh *GitHubClient) listPRsByHeadBase(owner, repo, head, base, state string) ([]*github.PullRequest, error) { headQuery := head if !strings.Contains(head, ":") { headQuery = fmt.Sprintf("%s:%s", owner, head) } prs, _, err := gh.client.PullRequests.List(gh.ctx, owner, repo, &github.PullRequestListOptions{ - State: "open", + State: state, Head: headQuery, Base: base, ListOptions: github.ListOptions{ - PerPage: 1, + PerPage: 30, }, }) if err != nil { - return nil, false, fmt.Errorf("failed to list pull requests: %w", err) + return nil, fmt.Errorf("failed to list pull requests: %w", err) } - if len(prs) == 0 { - return nil, false, nil + return prs, nil +} + +func (gh *GitHubClient) findPRByExactTitle(owner, repo, base, title string) (*github.PullRequest, bool, error) { + query := fmt.Sprintf(`repo:%s/%s is:pr base:%s "%s" in:title`, owner, repo, base, title) + result, _, err := gh.client.Search.Issues(gh.ctx, query, &github.SearchOptions{ + Sort: "updated", + Order: "desc", + ListOptions: github.ListOptions{ + PerPage: 10, + }, + }) + if err != nil { + return nil, false, fmt.Errorf("failed to search pull requests by title: %w", err) } - return prs[0], true, nil + var matches []*github.PullRequest + for _, issue := range result.Issues { + if !issue.IsPullRequest() { + continue + } + if issue.GetTitle() != title { + continue + } + pr, _, err := gh.client.PullRequests.Get(gh.ctx, owner, repo, issue.GetNumber()) + if err != nil { + return nil, false, fmt.Errorf("failed to get pull request #%d: %w", issue.GetNumber(), err) + } + matches = append(matches, pr) + } + if pr := pickRelatedPR(matches); pr != nil { + return pr, true, nil + } + return nil, false, nil +} + +// pickRelatedPR prefers open PRs, then merged, then other closed PRs. +func pickRelatedPR(prs []*github.PullRequest) *github.PullRequest { + var open, merged, closed *github.PullRequest + for _, pr := range prs { + if pr == nil { + continue + } + switch { + case pr.GetState() == "open": + if open == nil { + open = pr + } + case pr.GetMerged() || pr.MergedAt != nil: + if merged == nil { + merged = pr + } + default: + if closed == nil { + closed = pr + } + } + } + if open != nil { + return open + } + if merged != nil { + return merged + } + return closed +} + +// prDisplayState returns a short state label for workflow summaries. +func prDisplayState(pr *github.PullRequest) string { + if pr == nil { + return "unknown" + } + if pr.GetMerged() || pr.MergedAt != nil { + return "merged" + } + state := pr.GetState() + if state == "" { + return "unknown" + } + return state +} + +// mergeLabelDefs are auto-created when missing so merge-timing labels can be applied. +var mergeLabelDefs = map[string]struct { + Color string + Description string +}{ + mergeLabelFFDay: { + Color: "B60205", + Description: "Merge 1st: feature-freeze day (main)", + }, + mergeLabelAfterBranch: { + Color: "D93F0B", + Description: "Merge 2nd: ASAP after release branch exists", + }, + mergeLabelAfterImages: { + Color: "FBCA04", + Description: "Merge 3rd: after branch exists; may wait on Docker images", + }, + mergeLabelAfterRelease: { + Color: "0E8A16", + Description: "Merge 4th: after release day", + }, + mergeLabelBeforeBuild: { + Color: "B60205", + Description: "Merge before the final patch release build", + }, } func (gh *GitHubClient) ensurePRLabels(owner, repo string, number int, labels []string) { if len(labels) == 0 { return } + for _, label := range labels { + if def, ok := mergeLabelDefs[label]; ok { + if err := gh.EnsureLabel(owner, repo, label, def.Color, def.Description); err != nil { + fmt.Printf("Warning: failed to ensure label %q: %v\n", label, err) + } + } + } if err := gh.AddLabels(owner, repo, number, labels); err != nil { fmt.Printf("Warning: failed to add labels: %v\n", err) } } +// EnsureLabel creates a repository label if it does not already exist. +func (gh *GitHubClient) EnsureLabel(owner, repo, name, color, description string) error { + _, resp, err := gh.client.Issues.GetLabel(gh.ctx, owner, repo, name) + if err == nil { + return nil + } + if resp == nil || resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("failed to get label %q: %w", name, err) + } + + _, _, err = gh.client.Issues.CreateLabel(gh.ctx, owner, repo, &github.Label{ + Name: github.Ptr(name), + Color: github.Ptr(color), + Description: github.Ptr(description), + }) + if err != nil { + return fmt.Errorf("failed to create label %q: %w", name, err) + } + fmt.Printf("Created label %q\n", name) + return nil +} + // AddLabels adds labels to a pull request or issue. func (gh *GitHubClient) AddLabels(owner, repo string, number int, labels []string) error { _, _, err := gh.client.Issues.AddLabelsToIssue(gh.ctx, owner, repo, number, labels) @@ -136,6 +307,114 @@ func (gh *GitHubClient) AddLabels(owner, repo string, number int, labels []strin return nil } +// FindIssueByTitle finds an issue with an exact title match (open preferred over closed). +func (gh *GitHubClient) FindIssueByTitle(owner, repo, title string) (*github.Issue, bool, error) { + query := fmt.Sprintf(`repo:%s/%s is:issue in:title "%s"`, owner, repo, title) + result, _, err := gh.client.Search.Issues(gh.ctx, query, &github.SearchOptions{ + Sort: "updated", + Order: "desc", + ListOptions: github.ListOptions{ + PerPage: 20, + }, + }) + if err != nil { + return nil, false, fmt.Errorf("failed to search issues for title %q: %w", title, err) + } + + var closedMatch *github.Issue + for _, issue := range result.Issues { + if issue.IsPullRequest() { + continue + } + if issue.GetTitle() != title { + continue + } + if issue.GetState() == "open" { + return issue, true, nil + } + if closedMatch == nil { + closedMatch = issue + } + } + if closedMatch != nil { + return closedMatch, true, nil + } + return nil, false, nil +} + +// CreateIssue creates a repository issue with the given labels. +func (gh *GitHubClient) CreateIssue(owner, repo, title, body string, labels []string) (*github.Issue, error) { + req := &github.IssueRequest{ + Title: github.Ptr(title), + Body: github.Ptr(body), + } + if len(labels) > 0 { + req.Labels = &labels + } + issue, _, err := gh.client.Issues.Create(gh.ctx, owner, repo, req) + if err != nil { + return nil, fmt.Errorf("failed to create issue %q: %w", title, err) + } + return issue, nil +} + +// UpdateIssueBody replaces the body of an existing issue. +func (gh *GitHubClient) UpdateIssueBody(owner, repo string, number int, body string) error { + _, _, err := gh.client.Issues.Edit(gh.ctx, owner, repo, number, &github.IssueRequest{ + Body: github.Ptr(body), + }) + if err != nil { + return fmt.Errorf("failed to update issue #%d body: %w", number, err) + } + return nil +} + +// ListReleaseLabeledPRsForVersion returns Fleet Server PRs with label "release" whose title +// mentions the exact version. Open and recently updated closed/merged PRs are included. +func (gh *GitHubClient) ListReleaseLabeledPRsForVersion(owner, repo, version string) ([]*github.PullRequest, error) { + query := fmt.Sprintf(`repo:%s/%s is:pr label:release "%s"`, owner, repo, version) + opts := &github.SearchOptions{ + Sort: "updated", + Order: "desc", + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + var prs []*github.PullRequest + seen := map[int]struct{}{} + for { + result, resp, err := gh.client.Search.Issues(gh.ctx, query, opts) + if err != nil { + return nil, fmt.Errorf("failed to search release PRs for %s: %w", version, err) + } + for _, issue := range result.Issues { + if !issue.IsPullRequest() { + continue + } + if !versionMentioned(issue.GetTitle(), version) { + continue + } + num := issue.GetNumber() + if _, ok := seen[num]; ok { + continue + } + seen[num] = struct{}{} + prs = append(prs, &github.PullRequest{ + Number: github.Ptr(num), + HTMLURL: github.Ptr(issue.GetHTMLURL()), + Title: github.Ptr(issue.GetTitle()), + State: github.Ptr(issue.GetState()), + }) + } + if resp == nil || resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + return prs, nil +} + // GetDefaultBranch gets the default branch for a repository. func (gh *GitHubClient) GetDefaultBranch(owner, repo string) (string, error) { repository, _, err := gh.client.Repositories.Get(gh.ctx, owner, repo) @@ -158,3 +437,43 @@ func (gh *GitHubClient) BranchExists(owner, repo, branch string) (bool, error) { } return true, nil } + +// releasesLookupOwner and releasesLookupRepo are the canonical repo for published releases. +// Forks typically lack release history, so workflows always resolve LatestRelease from here. +const ( + releasesLookupOwner = "elastic" + releasesLookupRepo = "fleet-server" +) + +// LatestReleaseBefore returns the highest published release version with the same major +// that is strictly less than currentVersion (e.g. current 9.5.0 → 9.4.3). +func (gh *GitHubClient) LatestReleaseBefore(owner, repo, currentVersion string) (string, error) { + versions, err := gh.listReleaseVersions(owner, repo) + if err != nil { + return "", err + } + return selectLatestReleaseBefore(versions, currentVersion) +} + +func (gh *GitHubClient) listReleaseVersions(owner, repo string) ([]string, error) { + var versions []string + opts := &github.ListOptions{PerPage: 100} + for { + releases, resp, err := gh.client.Repositories.ListReleases(gh.ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("failed to list releases for %s/%s: %w", owner, repo, err) + } + for _, rel := range releases { + tag := rel.GetTagName() + if tag == "" { + continue + } + versions = append(versions, tag) + } + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + return versions, nil +} diff --git a/dev-tools/mage/release/github_test.go b/dev-tools/mage/release/github_test.go new file mode 100644 index 0000000000..89545af818 --- /dev/null +++ b/dev-tools/mage/release/github_test.go @@ -0,0 +1,148 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-github/v68/github" +) + +func newTestGitHubClient(t *testing.T, handler http.Handler) *GitHubClient { + t.Helper() + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + client := github.NewClient(server.Client()) + baseURL, err := url.Parse(server.URL + "/") + if err != nil { + t.Fatalf("failed to parse test server URL: %v", err) + } + client.BaseURL = baseURL + client.UploadURL = baseURL + + return &GitHubClient{ + client: client, + ctx: context.Background(), + } +} + +func TestCreatePRIdempotent(t *testing.T) { + createCalls := 0 + existingPR := &github.PullRequest{ + Number: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/elastic/fleet-server/pull/42"), + Head: &github.PullRequestBranch{ + Ref: github.Ptr("9.5"), + }, + Base: &github.PullRequestBranch{ + Ref: github.Ptr("main"), + }, + State: github.Ptr("open"), + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/repos/elastic/fleet-server/pulls": + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode([]*github.PullRequest{existingPR}); err != nil { + t.Errorf("failed to encode PR list response: %v", err) + } + case r.Method == http.MethodPost && r.URL.Path == "/repos/elastic/fleet-server/pulls": + createCalls++ + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = fmt.Fprint(w, `{"message":"Validation Failed","errors":[{"message":"A pull request already exists for elastic:9.5."}]}`) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + } + }) + + ghClient := newTestGitHubClient(t, handler) + opts := PROptions{ + Owner: "elastic", + Repo: "fleet-server", + Title: "[Release 9.5.0] Prepare release branch", + Head: "9.5", + Base: "main", + Body: "Release PR body", + } + + pr, err := ghClient.CreatePR(opts) + if err != nil { + t.Fatalf("CreatePR() error = %v", err) + } + + if pr.GetNumber() != 42 { + t.Errorf("CreatePR() number = %d, want 42", pr.GetNumber()) + } + if createCalls != 0 { + t.Errorf("CreatePR() called create API %d times, want 0", createCalls) + } + + pr2, err := ghClient.CreatePR(opts) + if err != nil { + t.Fatalf("CreatePR() second call error = %v", err) + } + if pr2.GetNumber() != pr.GetNumber() { + t.Errorf("CreatePR() second call number = %d, want %d", pr2.GetNumber(), pr.GetNumber()) + } + if createCalls != 0 { + t.Errorf("CreatePR() called create API %d times after second call, want 0", createCalls) + } +} + +func TestCreatePRCreatesWhenNoneExists(t *testing.T) { + createCalls := 0 + newPR := &github.PullRequest{ + Number: github.Ptr(99), + HTMLURL: github.Ptr("https://github.com/elastic/fleet-server/pull/99"), + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/repos/elastic/fleet-server/pulls": + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode([]*github.PullRequest{}); err != nil { + t.Errorf("failed to encode empty PR list response: %v", err) + } + case r.Method == http.MethodPost && r.URL.Path == "/repos/elastic/fleet-server/pulls": + createCalls++ + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(newPR); err != nil { + t.Errorf("failed to encode create PR response: %v", err) + } + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + } + }) + + ghClient := newTestGitHubClient(t, handler) + pr, err := ghClient.CreatePR(PROptions{ + Owner: "elastic", + Repo: "fleet-server", + Title: "New release PR", + Head: "9.6", + Base: "main", + Body: "body", + }) + if err != nil { + t.Fatalf("CreatePR() error = %v", err) + } + if pr.GetNumber() != 99 { + t.Errorf("CreatePR() number = %d, want 99", pr.GetNumber()) + } + if createCalls != 1 { + t.Errorf("CreatePR() called create API %d times, want 1", createCalls) + } +} diff --git a/dev-tools/mage/release/go.mod b/dev-tools/mage/release/go.mod new file mode 100644 index 0000000000..d8f69da6b2 --- /dev/null +++ b/dev-tools/mage/release/go.mod @@ -0,0 +1,35 @@ +// Separate module so release tooling deps (go-git, go-github, …) do not +// enter the main fleet-server go.mod / NOTICE.txt. +module github.com/elastic/fleet-server/dev-tools/mage/release + +go 1.26.5 + +require ( + github.com/go-git/go-git/v5 v5.17.0 + github.com/google/go-github/v68 v68.0.0 + golang.org/x/oauth2 v0.36.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/dev-tools/mage/release/go.sum b/dev-tools/mage/release/go.sum new file mode 100644 index 0000000000..7585b9ef60 --- /dev/null +++ b/dev-tools/mage/release/go.sum @@ -0,0 +1,110 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= +github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dev-tools/mage/release/issue.go b/dev-tools/mage/release/issue.go new file mode 100644 index 0000000000..2e9a84642b --- /dev/null +++ b/dev-tools/mage/release/issue.go @@ -0,0 +1,297 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/google/go-github/v68/github" +) + +// globalReleaseTrackerURL is the ingest-wide release tracker linked from every Fleet Server release issue. +const globalReleaseTrackerURL = "https://github.com/elastic/ingest-dev/issues/8866" + +var ( + prCheckboxLineRE = regexp.MustCompile(`(?m)^- \[([ xX])\] (https://github\.com/[^/]+/[^/]+/pull/\d+)\s*$`) + prURLNumberRE = regexp.MustCompile(`https://github\.com/[^/]+/[^/]+/pull/(\d+)`) +) + +// warnEnsureReleaseIssueTracker runs EnsureReleaseIssueTracker but never fails the +// caller. Release workflows use this so a tracker outage cannot abort PR creation. +func warnEnsureReleaseIssueTracker(cfg *ReleaseConfig, workflowPRs []*github.PullRequest) { + if err := EnsureReleaseIssueTracker(cfg, workflowPRs); err != nil { + fmt.Printf("Warning: ensure release issue tracker failed (release workflow continues): %v\n", err) + fmt.Println("Re-run with: mage release:ensureIssueTracker") + } +} + +// EnsureReleaseIssueTracker creates or updates the Fleet Server release checklist issue for +// cfg.CurrentRelease. It links related Fleet Server PRs (workflow PRs plus open/merged PRs +// with label "release" that mention the same version). Updates are idempotent: missing +// PR links are appended; existing checklist and checkbox state are preserved. +// Standalone mage release:ensureIssueTracker surfaces errors; runMajorMinor/runPatch +// wrap this with warnEnsureReleaseIssueTracker so failures are non-blocking. +func EnsureReleaseIssueTracker(cfg *ReleaseConfig, workflowPRs []*github.PullRequest) error { + if cfg == nil { + return fmt.Errorf("release config is required") + } + if cfg.CurrentRelease == "" { + return fmt.Errorf("CURRENT_RELEASE is required") + } + + title := releaseIssueTitle(cfg.CurrentRelease) + seedURLs := prURLsFromPullRequests(workflowPRs) + + if cfg.DryRun { + fmt.Printf("\nDRY RUN: Would ensure issue tracker %q\n", title) + fmt.Printf("Seed PRs from workflow: %d\n", len(seedURLs)) + fmt.Printf("Global tracker: %s\n", globalReleaseTrackerURL) + return nil + } + + if cfg.GitHubToken == "" { + return fmt.Errorf("GITHUB_TOKEN is required to ensure the release issue tracker") + } + + gh := NewGitHubClient(cfg.GitHubToken) + owner := cfg.ProjectOwner + repo := cfg.ProjectRepo + + discovered, err := gh.ListReleaseLabeledPRsForVersion(owner, repo, cfg.CurrentRelease) + if err != nil { + return fmt.Errorf("discover release-labeled PRs for %s: %w", cfg.CurrentRelease, err) + } + prURLs := mergePRURLs(seedURLs, prURLsFromPullRequests(discovered)) + + existing, found, err := gh.FindIssueByTitle(owner, repo, title) + if err != nil { + return err + } + + if !found { + body := buildReleaseIssueBody(cfg.CurrentRelease, isMajorMinorRelease(cfg.CurrentRelease), prURLs, nil) + issue, err := gh.CreateIssue(owner, repo, title, body, []string{"release"}) + if err != nil { + return err + } + fmt.Printf("Created release issue tracker #%d: %s\n", issue.GetNumber(), issue.GetHTMLURL()) + return nil + } + + updatedBody, changed := mergeReleaseIssueBody(existing.GetBody(), cfg.CurrentRelease, prURLs) + if !changed { + fmt.Printf("Release issue tracker already up to date: #%d %s\n", existing.GetNumber(), existing.GetHTMLURL()) + return nil + } + + if err := gh.UpdateIssueBody(owner, repo, existing.GetNumber(), updatedBody); err != nil { + return err + } + if err := gh.AddLabels(owner, repo, existing.GetNumber(), []string{"release"}); err != nil { + fmt.Printf("Warning: failed to ensure release label on issue #%d: %v\n", existing.GetNumber(), err) + } + fmt.Printf("Updated release issue tracker #%d: %s\n", existing.GetNumber(), existing.GetHTMLURL()) + return nil +} + +func releaseIssueTitle(version string) string { + return fmt.Sprintf("[RELEASE %s] Instructions & Checklist", version) +} + +func isMajorMinorRelease(version string) bool { + parts := strings.Split(version, ".") + return len(parts) >= 3 && parts[2] == "0" +} + +func prURLsFromPullRequests(prs []*github.PullRequest) []string { + var urls []string + for _, pr := range prs { + if pr == nil { + continue + } + if u := pr.GetHTMLURL(); u != "" { + urls = append(urls, u) + } + } + return urls +} + +// mergePRURLs returns a de-duplicated, number-sorted union of PR URL lists. +func mergePRURLs(lists ...[]string) []string { + seen := map[string]struct{}{} + var out []string + for _, list := range lists { + for _, u := range list { + u = strings.TrimSpace(u) + if u == "" { + continue + } + if _, ok := seen[u]; ok { + continue + } + seen[u] = struct{}{} + out = append(out, u) + } + } + sort.Slice(out, func(i, j int) bool { + return prNumber(out[i]) < prNumber(out[j]) + }) + return out +} + +func prNumber(url string) int { + m := prURLNumberRE.FindStringSubmatch(url) + if len(m) < 2 { + return 0 + } + n, _ := strconv.Atoi(m[1]) + return n +} + +// extractPRCheckboxes returns PR URL -> checked from an issue body. +func extractPRCheckboxes(body string) map[string]bool { + out := map[string]bool{} + for _, m := range prCheckboxLineRE.FindAllStringSubmatch(body, -1) { + checked := m[1] == "x" || m[1] == "X" + out[m[2]] = checked + } + return out +} + +func buildReleaseIssueBody(version string, majorMinor bool, prURLs []string, checked map[string]bool) string { + var b strings.Builder + b.WriteString(fmt.Sprintf("Global tracker: %s\n\n", globalReleaseTrackerURL)) + b.WriteString("# Release Checklist\n\n") + b.WriteString("Checklist for the Fleet Server release process. The Fleet Server and Beats release documentation can be reviewed ") + b.WriteString("[here](https://github.com/elastic/ingest-dev/blob/main/fleet-platform/releases.md).\n\n") + b.WriteString("The release dates can be checked at https://ela.st/release-schedule.\n\n") + b.WriteString("Ensure you have joined the `#mission-control` Slack channel and the `@agent-team` slack group to receive release reminders and coordination messages.\n\n") + + b.WriteString("## On Feature Freeze\n\n") + b.WriteString("- [ ] Find the Fleet Server [release](https://github.com/elastic/fleet-server/labels/release) PRs for this version and link them from this issue.\n") + b.WriteString("- [ ] Check which of the PRs can be merged immediately or must wait until the release day (should be mentioned in each PR).\n") + + if majorMinor { + b.WriteString("\n### Only For Major/Minor Releases\n\n") + b.WriteString("- [ ] On `main`, find and merge the PR changing the version to the next minor as soon as it is green.\n") + b.WriteString("- [ ] Find the PR in observability-dev adding the backport labels for the project and merge them: https://github.com/elastic/observability-dev/pulls. The list of Github labels is maintained [here](https://github.com/elastic/observability-dev/tree/main/.github/labels).\n") + b.WriteString("- [ ] Ensure there is a [branch protection rule](https://github.com/elastic/fleet-server/settings/branches) for the new branch. `9.*`, `8.*` and `7.*` patterns may already exist.\n") + } + + b.WriteString("\n## On the Day before the Release\n\n") + b.WriteString("- [ ] Prepare the changelog. Follow the detailed instructions for [Fleet Server changelog preparation](https://github.com/elastic/ingest-dev/blob/main/fleet-platform/releases.md#fleet-server-changelog-preparation).\n") + + b.WriteString("\n## On the Release Date\n\n") + b.WriteString("- [ ] Wait for the ping from #mission-control to merge all version bump PRs and trigger the [Fleet Server packaging jobs](https://github.com/elastic/ingest-dev/blob/main/fleet-platform/releases.md#preparing-a-release) to stage artifacts for the DRA (daily releasable artifacts) process.\n") + b.WriteString("- [ ] Find all Fleet Server [release](https://github.com/elastic/fleet-server/labels/release) pending PRs for the current release and merge them.\n") + b.WriteString("- [ ] Merge the Changelog / release-notes PR.\n") + b.WriteString("- [ ] [Forward port](https://github.com/elastic/ingest-dev/blob/main/fleet-platform/releases.md#changelog-forward-ports) the changelog.\n") + + b.WriteString("\n## PRs\n\n") + b.WriteString(formatPRChecklist(prURLs, checked)) + b.WriteString("\n") + return b.String() +} + +func formatPRChecklist(prURLs []string, checked map[string]bool) string { + if len(prURLs) == 0 { + return "_No related Fleet Server release PRs discovered yet._\n" + } + var b strings.Builder + for _, u := range prURLs { + mark := " " + if checked != nil && checked[u] { + mark = "x" + } + b.WriteString(fmt.Sprintf("- [%s] %s\n", mark, u)) + } + return b.String() +} + +// mergeReleaseIssueBody updates an existing issue body with missing tracker link and PR URLs. +// Checklist item checkboxes outside the PRs section are left untouched. +func mergeReleaseIssueBody(existingBody, version string, prURLs []string) (string, bool) { + existingChecked := extractPRCheckboxes(existingBody) + allURLs := mergePRURLs(prURLs, keys(existingChecked)) + + if strings.TrimSpace(existingBody) == "" { + return buildReleaseIssueBody(version, isMajorMinorRelease(version), allURLs, existingChecked), true + } + + body := existingBody + changed := false + + if !strings.Contains(body, globalReleaseTrackerURL) { + body = fmt.Sprintf("Global tracker: %s\n\n%s", globalReleaseTrackerURL, body) + changed = true + } + + const prSection = "## PRs" + idx := strings.Index(body, prSection) + if idx < 0 { + updated := strings.TrimRight(body, "\n") + "\n\n" + prSection + "\n\n" + formatPRChecklist(allURLs, existingChecked) + return updated, true + } + + before := body[:idx] + oldPRBlock := strings.TrimPrefix(body[idx+len(prSection):], "\n") + newPRBlock := formatPRChecklist(allURLs, existingChecked) + + oldURLs := keys(extractPRCheckboxes(oldPRBlock)) + if !sameStringSet(oldURLs, allURLs) || normalizePRSection(oldPRBlock) != normalizePRSection(newPRBlock) { + changed = true + } + if !changed { + return existingBody, false + } + + updated := strings.TrimRight(before, "\n") + "\n\n" + prSection + "\n\n" + newPRBlock + return updated, true +} + +func keys(m map[string]bool) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} + +func sameStringSet(a, b []string) bool { + am := map[string]struct{}{} + for _, s := range a { + am[s] = struct{}{} + } + bm := map[string]struct{}{} + for _, s := range b { + bm[s] = struct{}{} + } + if len(am) != len(bm) { + return false + } + for s := range am { + if _, ok := bm[s]; !ok { + return false + } + } + return true +} + +func normalizePRSection(s string) string { + return strings.TrimSpace(strings.ReplaceAll(s, "\r\n", "\n")) +} + +// versionMentioned reports whether text refers to the exact version (avoids 9.4.1 matching 9.4.10). +func versionMentioned(text, version string) bool { + if version == "" || text == "" { + return false + } + re := regexp.MustCompile(`(?i)(?:^|[^0-9])` + regexp.QuoteMeta(version) + `(?:[^0-9]|$)`) + return re.MatchString(text) +} diff --git a/dev-tools/mage/release/issue_test.go b/dev-tools/mage/release/issue_test.go new file mode 100644 index 0000000000..65ea2a524d --- /dev/null +++ b/dev-tools/mage/release/issue_test.go @@ -0,0 +1,178 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "strings" + "testing" + + "github.com/google/go-github/v68/github" +) + +func TestReleaseIssueTitle(t *testing.T) { + got := releaseIssueTitle("9.4.1") + want := "[RELEASE 9.4.1] Instructions & Checklist" + if got != want { + t.Fatalf("releaseIssueTitle() = %q, want %q", got, want) + } +} + +func TestIsMajorMinorRelease(t *testing.T) { + if !isMajorMinorRelease("9.5.0") { + t.Fatal("expected 9.5.0 to be treated as major/minor") + } + if isMajorMinorRelease("9.5.1") { + t.Fatal("expected 9.5.1 not to be treated as major/minor") + } +} + +func TestVersionMentioned(t *testing.T) { + if !versionMentioned("[Release] Update version to 9.4.1", "9.4.1") { + t.Fatal("exact version in title should match") + } + if versionMentioned("[Release] Update version to 9.4.10", "9.4.1") { + t.Fatal("9.4.1 must not match 9.4.10") + } + if !versionMentioned("Add Fleet Server 9.4.1 release notes", "9.4.1") { + t.Fatal("docs PR title should match") + } +} + +func TestBuildReleaseIssueBodyMajorMinorSection(t *testing.T) { + minorBody := buildReleaseIssueBody("9.5.0", true, nil, nil) + if !strings.Contains(minorBody, "### Only For Major/Minor Releases") { + t.Fatal("minor release body should include major/minor section") + } + if !strings.Contains(minorBody, globalReleaseTrackerURL) { + t.Fatal("body should link the global tracker") + } + if !strings.Contains(minorBody, "elastic/fleet-server/labels/release") { + t.Fatal("body should link fleet-server release label") + } + + patchBody := buildReleaseIssueBody("9.5.1", false, nil, nil) + if strings.Contains(patchBody, "### Only For Major/Minor Releases") { + t.Fatal("patch body should omit major/minor section") + } +} + +func TestMergePRURLsSortsAndDedups(t *testing.T) { + got := mergePRURLs( + []string{"https://github.com/elastic/fleet-server/pull/20", "https://github.com/elastic/fleet-server/pull/10"}, + []string{"https://github.com/elastic/fleet-server/pull/10", "https://github.com/elastic/fleet-server/pull/15"}, + ) + want := []string{ + "https://github.com/elastic/fleet-server/pull/10", + "https://github.com/elastic/fleet-server/pull/15", + "https://github.com/elastic/fleet-server/pull/20", + } + if len(got) != len(want) { + t.Fatalf("mergePRURLs() len = %d, want %d (%v)", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("mergePRURLs()[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestExtractPRCheckboxes(t *testing.T) { + body := ` +## PRs + +- [x] https://github.com/elastic/fleet-server/pull/10 +- [ ] https://github.com/elastic/fleet-server/pull/11 +` + got := extractPRCheckboxes(body) + if !got["https://github.com/elastic/fleet-server/pull/10"] { + t.Fatal("checked PR should be true") + } + if got["https://github.com/elastic/fleet-server/pull/11"] { + t.Fatal("unchecked PR should be false") + } +} + +func TestMergeReleaseIssueBodyAddsMissingPRsAndPreservesChecks(t *testing.T) { + existing := `Global tracker: https://github.com/elastic/ingest-dev/issues/8866 + +# Release Checklist + +- [x] Find the Fleet Server release PRs + +## PRs + +- [x] https://github.com/elastic/fleet-server/pull/10 +- [ ] https://github.com/elastic/fleet-server/pull/11 +` + updated, changed := mergeReleaseIssueBody(existing, "9.4.1", []string{ + "https://github.com/elastic/fleet-server/pull/11", + "https://github.com/elastic/fleet-server/pull/12", + }) + if !changed { + t.Fatal("adding a missing PR should mark the body changed") + } + if !strings.Contains(updated, "- [x] https://github.com/elastic/fleet-server/pull/10") { + t.Fatal("existing checked PR must stay checked") + } + if !strings.Contains(updated, "- [ ] https://github.com/elastic/fleet-server/pull/11") { + t.Fatal("existing unchecked PR stays unchecked") + } + if !strings.Contains(updated, "- [ ] https://github.com/elastic/fleet-server/pull/12") { + t.Fatal("new PR should be appended unchecked") + } + if !strings.Contains(updated, "- [x] Find the Fleet Server release PRs") { + t.Fatal("non-PR checklist state must be preserved") + } + + _, changedAgain := mergeReleaseIssueBody(updated, "9.4.1", []string{ + "https://github.com/elastic/fleet-server/pull/10", + "https://github.com/elastic/fleet-server/pull/11", + "https://github.com/elastic/fleet-server/pull/12", + }) + if changedAgain { + t.Fatal("second merge with same PRs should be a no-op") + } +} + +func TestMergeReleaseIssueBodyAddsTrackerWhenMissing(t *testing.T) { + existing := `# Release Checklist + +## PRs + +- [ ] https://github.com/elastic/fleet-server/pull/10 +` + updated, changed := mergeReleaseIssueBody(existing, "9.4.1", []string{"https://github.com/elastic/fleet-server/pull/10"}) + if !changed { + t.Fatal("missing global tracker should trigger an update") + } + if !strings.HasPrefix(strings.TrimSpace(updated), "Global tracker: "+globalReleaseTrackerURL) { + t.Fatalf("tracker link should be prepended, got prefix %q", updated[:min(80, len(updated))]) + } +} + +func TestEnsureReleaseIssueTrackerDryRun(t *testing.T) { + cfg := &ReleaseConfig{ + CurrentRelease: "9.4.1", + DryRun: true, + } + pr := &github.PullRequest{HTMLURL: github.Ptr("https://github.com/elastic/fleet-server/pull/99")} + if err := EnsureReleaseIssueTracker(cfg, []*github.PullRequest{pr}); err != nil { + t.Fatalf("dry-run ensureIssueTracker should not call GitHub: %v", err) + } +} + +func TestWarnEnsureReleaseIssueTrackerDoesNotPanicOnError(t *testing.T) { + warnEnsureReleaseIssueTracker(&ReleaseConfig{}, nil) +} + +func TestPrURLsFromPullRequestsSkipsNil(t *testing.T) { + got := prURLsFromPullRequests([]*github.PullRequest{ + nil, + {HTMLURL: github.Ptr("https://github.com/elastic/fleet-server/pull/1")}, + }) + if len(got) != 1 || got[0] != "https://github.com/elastic/fleet-server/pull/1" { + t.Fatalf("unexpected URLs: %v", got) + } +} diff --git a/dev-tools/mage/release/mergify.go b/dev-tools/mage/release/mergify.go index 11e45a6c1f..8328344112 100644 --- a/dev-tools/mage/release/mergify.go +++ b/dev-tools/mage/release/mergify.go @@ -12,23 +12,18 @@ import ( "gopkg.in/yaml.v3" ) -const mergifyPath = ".mergify.yml" - // UpdateMergify adds a new backport rule to .mergify.yml. func UpdateMergify(version string) error { - safePath, err := validateRepoRelativePath(mergifyPath) - if err != nil { - return err - } + mergifyFile := ".mergify.yml" - content, err := os.ReadFile(safePath) + content, err := os.ReadFile(mergifyFile) if err != nil { - return fmt.Errorf("failed to read %s: %w", safePath, err) + return fmt.Errorf("failed to read %s: %w", mergifyFile, err) } var config map[string]interface{} if err := yaml.Unmarshal(content, &config); err != nil { - return fmt.Errorf("failed to parse %s: %w", safePath, err) + return fmt.Errorf("failed to parse %s: %w", mergifyFile, err) } parts := strings.Split(version, ".") @@ -76,10 +71,11 @@ func UpdateMergify(version string) error { return fmt.Errorf("failed to marshal YAML: %w", err) } - if err := writeRepoFile(safePath, output); err != nil { - return fmt.Errorf("failed to write %s: %w", safePath, err) + err = writeRepoFile(mergifyFile, output) + if err != nil { + return fmt.Errorf("failed to write %s: %w", mergifyFile, err) } - fmt.Printf("Added backport rule for %s to %s\n", branchVersion, safePath) + fmt.Printf("Added backport rule for %s to %s\n", branchVersion, mergifyFile) return nil } diff --git a/dev-tools/mage/release/release.go b/dev-tools/mage/release/release.go index c9a77c3908..5d262df8d3 100644 --- a/dev-tools/mage/release/release.go +++ b/dev-tools/mage/release/release.go @@ -14,6 +14,21 @@ import ( const versionGoPath = "version/version.go" +var defaultVersionPattern = regexp.MustCompile(`const DefaultVersion = "([^"]+)"`) + +// ReadFleetVersion returns DefaultVersion from version/version.go. +func ReadFleetVersion() (string, error) { + content, err := os.ReadFile(versionGoPath) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", versionGoPath, err) + } + match := defaultVersionPattern.FindSubmatch(content) + if match == nil { + return "", fmt.Errorf("version pattern not found in %s", versionGoPath) + } + return string(match[1]), nil +} + func validateRepoRelativePath(path string) (string, error) { if path == "" { return "", fmt.Errorf("path must not be empty") diff --git a/dev-tools/mage/release/version.go b/dev-tools/mage/release/version.go new file mode 100644 index 0000000000..9a94b97d18 --- /dev/null +++ b/dev-tools/mage/release/version.go @@ -0,0 +1,80 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "fmt" + "strconv" + "strings" +) + +type semver struct { + major, minor, patch int +} + +func parseSemver(version string) (semver, error) { + version = strings.TrimPrefix(strings.TrimSpace(version), "v") + parts := strings.Split(version, ".") + if len(parts) != 3 { + return semver{}, fmt.Errorf("invalid version format: %s (expected major.minor.patch)", version) + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return semver{}, fmt.Errorf("invalid major version: %s", parts[0]) + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return semver{}, fmt.Errorf("invalid minor version: %s", parts[1]) + } + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return semver{}, fmt.Errorf("invalid patch version: %s", parts[2]) + } + return semver{major: major, minor: minor, patch: patch}, nil +} + +func (v semver) String() string { + return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch) +} + +func (v semver) less(other semver) bool { + if v.major != other.major { + return v.major < other.major + } + if v.minor != other.minor { + return v.minor < other.minor + } + return v.patch < other.patch +} + +// selectLatestReleaseBefore picks the highest same-major version strictly less than current. +func selectLatestReleaseBefore(versions []string, currentVersion string) (string, error) { + current, err := parseSemver(currentVersion) + if err != nil { + return "", err + } + + var best *semver + for _, raw := range versions { + candidate, err := parseSemver(raw) + if err != nil { + continue + } + if candidate.major != current.major { + continue + } + if !candidate.less(current) { + continue + } + if best == nil || best.less(candidate) { + c := candidate + best = &c + } + } + if best == nil { + return "", fmt.Errorf("no published release found before %s (same major)", currentVersion) + } + return best.String(), nil +} diff --git a/dev-tools/mage/release/version_test.go b/dev-tools/mage/release/version_test.go new file mode 100644 index 0000000000..e1e9545321 --- /dev/null +++ b/dev-tools/mage/release/version_test.go @@ -0,0 +1,18 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import "testing" + +func TestSelectLatestReleaseBefore(t *testing.T) { + versions := []string{"9.4.0", "9.4.3", "9.5.0", "v9.3.2", "8.19.1", "not-a-version"} + got, err := selectLatestReleaseBefore(versions, "9.5.0") + if err != nil { + t.Fatalf("selectLatestReleaseBefore() error = %v", err) + } + if got != "9.4.3" { + t.Errorf("selectLatestReleaseBefore() = %s, want 9.4.3", got) + } +} diff --git a/dev-tools/mage/release/workflows.go b/dev-tools/mage/release/workflows.go index 5ffdb352da..07b629e728 100644 --- a/dev-tools/mage/release/workflows.go +++ b/dev-tools/mage/release/workflows.go @@ -11,20 +11,62 @@ import ( "github.com/google/go-github/v68/github" ) -// PR label sets match vault-bot release PRs for fleet-server. +// PR label sets match vault-bot / beats / elastic-agent release PRs. var ( - bumpMainPRLabels = []string{"backport-skip", "skip-changelog"} - nextReleasePRLabels = []string{"Team:Automation", "release", "skip-changelog"} - backportNextPRLabels = []string{"Team:Automation", "release", "impact:critical", "skip-changelog"} - patchReleasePRLabels = []string{"Team:Automation", "release", "docs", "in progress", "skip-changelog"} + releasePRLabels = []string{"release", "Team:Automation", "skip-changelog"} + ffReleasePRLabels = []string{"release", "docs", "in progress", "Team:Automation", "skip-changelog"} + patchDocsPRLabels = []string{"docs", "in progress", "release", "Team:Automation", "skip-changelog"} ) +// Feature-freeze merge-timing labels (number = RM merge order). +const ( + mergeLabelFFDay = "merge:1-ff-day" + mergeLabelAfterBranch = "merge:2-after-branch" + mergeLabelAfterImages = "merge:3-after-images" // defined for shared label defs; PR-C omitted so unused in workflows + mergeLabelAfterRelease = "merge:4-after-release" +) + +// Patch-release merge-timing labels. +const ( + mergeLabelBeforeBuild = "merge:1-before-build" +) + +func backportLabel(releaseBranch string) string { + return fmt.Sprintf("backport-%s", releaseBranch) +} + +func prAMainLabels(releaseBranch string) []string { + return []string{"release", "impact:critical", backportLabel(releaseBranch), "skip-changelog", "Team:Automation", mergeLabelFFDay} +} + +func prBReleaseLabels() []string { + return append(append([]string{}, ffReleasePRLabels...), mergeLabelAfterBranch) +} + +func prDNextPatchLabels() []string { + return append(append([]string{}, releasePRLabels...), mergeLabelAfterRelease) +} + +func patchBeforeBuildPRLabels() []string { + return append(append([]string{}, patchDocsPRLabels...), mergeLabelBeforeBuild) +} + func checkRequirements(cfg *ReleaseConfig) error { parts := strings.Split(cfg.CurrentRelease, ".") if len(parts) < 2 { return fmt.Errorf("invalid version format: %s", cfg.CurrentRelease) } + major := parts[0] + patch := "" + if len(parts) >= 3 { + patch = parts[2] + } + + if (major == "6" || major == "7" || major == "8") && patch == "0" { + return fmt.Errorf("minor releases for version %s.x are deprecated and blocked", major) + } + repo, err := OpenRepo(".") if err != nil { return err @@ -41,13 +83,36 @@ func checkRequirements(cfg *ReleaseConfig) error { return nil } -// RunMajorMinorRelease creates the release branch and opens the main-branch version bump PR. +type workflowPR struct { + branch string + base string + opts PROptions +} + +type workflowPRResult struct { + item workflowPR + pr *github.PullRequest +} + +// RunMajorMinorRelease executes the feature-freeze workflow: +// 1. Creates the release branch from BASE_BRANCH +// 2. Opens PR-A on main (backport rule + next minor version) +// 3. Opens PR-B on release branch (ensure CURRENT version) +// 4. Opens PR-D on release branch (next patch prep) +// +// PR-C (main docs / test-env) is omitted: fleet-server has no docs or test-env +// version updates in the former fleet-server.mak process. func RunMajorMinorRelease(cfg *ReleaseConfig) error { fmt.Println("=== Starting Major/Minor Release Workflow ===") + if err := cfg.EnsureLatestRelease(); err != nil { + return err + } + if err := cfg.Validate(); err != nil { return err } + if err := checkRequirements(cfg); err != nil { return err } @@ -57,48 +122,40 @@ func RunMajorMinorRelease(cfg *ReleaseConfig) error { return err } + if err := ensureMajorMinorCurrentReleaseMatchesBase(repo, cfg); err != nil { + return err + } + releaseBranch := cfg.ReleaseBranch - bumpBranch := bumpVersionBranchName(cfg.NextProjectMinorVersion) - fmt.Printf("Creating release branch: %s from %s\n", releaseBranch, cfg.BaseBranch) + fmt.Printf("Creating release branch: %s\n", releaseBranch) if err := repo.EnsureBranchFrom(cfg.BaseBranch, releaseBranch); err != nil { return err } - if err := UpdateVersion(cfg.CurrentRelease); err != nil { - return err - } - if _, err := repo.CommitAll("[Release] update version", cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { - return err - } - fmt.Printf("\n--- Creating PR: Bump main to %s ---\n", cfg.NextProjectMinorVersion) - if err := repo.EnsureBranchFrom(cfg.BaseBranch, bumpBranch); err != nil { + prA, err := prepMainBackportAndVersion(repo, cfg) + if err != nil { return err } - if err := UpdateVersion(cfg.NextProjectMinorVersion); err != nil { + prB, err := prepFFRelease(repo, cfg) + if err != nil { return err } - bumpCommitMsg := fmt.Sprintf("Bump version to %s", cfg.NextProjectMinorVersion) - if _, err := repo.CommitAll(bumpCommitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + prD, err := prepNextPatchOnReleaseBranch(repo, cfg) + if err != nil { return err } - bumpPROpts := PROptions{ - Owner: cfg.ProjectOwner, - Repo: cfg.ProjectRepo, - Title: fmt.Sprintf("Bump the version on main to %s", cfg.NextProjectMinorVersion), - Head: bumpBranch, - Base: cfg.BaseBranch, - Body: bumpMainPRBody(cfg.NextProjectMinorVersion), - Reviewers: cfg.ProjectReviewers, - Labels: bumpMainPRLabels, - } + branchesToFinalize := []workflowPR{prA, prB, prD} if cfg.DryRun { fmt.Println("\nDRY RUN: Skipping push and PR creation") fmt.Printf("Release branch prepared: %s\n", releaseBranch) - fmt.Printf("Bump branch prepared: %s\n", bumpBranch) - fmt.Println("Review changes with 'git diff'") + for _, item := range branchesToFinalize { + fmt.Printf("Branch prepared: %s\n", item.branch) + } + fmt.Println("Note: PR-C (main docs) is omitted for fleet-server (no docs/test-env updates)") + warnEnsureReleaseIssueTracker(cfg, nil) return nil } @@ -110,136 +167,187 @@ func RunMajorMinorRelease(cfg *ReleaseConfig) error { } gh := NewGitHubClient(cfg.GitHubToken) - bumpPR, err := finalizePR(repo, gh, bumpBranch, cfg.BaseBranch, bumpPROpts) + results, err := finalizeWorkflowPRs(repo, gh, branchesToFinalize) if err != nil { return err } fmt.Printf("\n=== Major/Minor Release Workflow Complete ===\n") - fmt.Printf("Release branch pushed: %s\n", releaseBranch) - if bumpPR != nil { - fmt.Printf("Bump main PR: %s\n", bumpPR.GetHTMLURL()) - } else { - fmt.Println("Bump main PR: not created (already up to date)") - } - fmt.Println("\nRun 'mage release:runNextRelease' to open next-release and backport PRs") + fmt.Printf("Release branch created: %s\n", releaseBranch) + printWorkflowPRResults(results) + fmt.Println("\nNote: PR-C (main docs) is omitted for fleet-server (no docs/test-env updates)") + warnEnsureReleaseIssueTracker(cfg, prsFromWorkflowResults(results)) return nil } -// RunNextRelease opens PRs for the next patch version and mergify backport rule. -func RunNextRelease(cfg *ReleaseConfig) error { - fmt.Println("=== Starting Next Release Workflow ===") +func prepMainBackportAndVersion(repo *GitRepo, cfg *ReleaseConfig) (workflowPR, error) { + branch := fmt.Sprintf("ff-prep-main-%s", cfg.CurrentRelease) + fmt.Printf("\n--- Preparing PR-A: backport rule + version %s on %s ---\n", cfg.NextProjectMinorVersion, cfg.BaseBranch) - if err := cfg.Validate(); err != nil { - return err + if err := repo.EnsureBranchFrom(cfg.BaseBranch, branch); err != nil { + return workflowPR{}, err } - if err := checkRequirements(cfg); err != nil { - return err + if err := UpdateMergify(cfg.ReleaseBranch); err != nil { + return workflowPR{}, err } + if err := UpdateVersion(cfg.NextProjectMinorVersion); err != nil { + return workflowPR{}, err + } + commitMsg := fmt.Sprintf("[Release %s] Prepare main for %s and mergify backport-%s", cfg.CurrentRelease, cfg.NextProjectMinorVersion, cfg.ReleaseBranch) + if _, err := repo.CommitAll(commitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return workflowPR{}, err + } + + return workflowPR{ + branch: branch, + base: cfg.BaseBranch, + opts: PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("[Release %s] Prepare main for %s and mergify backport-%s", cfg.CurrentRelease, cfg.NextProjectMinorVersion, cfg.ReleaseBranch), + Head: branch, + Base: cfg.BaseBranch, + Body: prAMainBody(cfg), + Reviewers: cfg.ProjectReviewers, + Labels: prAMainLabels(cfg.ReleaseBranch), + }, + }, nil +} - repo, err := OpenRepo(".") - if err != nil { - return err - } +func prepFFRelease(repo *GitRepo, cfg *ReleaseConfig) (workflowPR, error) { + branch := fmt.Sprintf("ff-release-%s", cfg.CurrentRelease) + fmt.Printf("\n--- Preparing PR-B: ff-release %s on %s ---\n", cfg.CurrentRelease, cfg.ReleaseBranch) - releaseBranch := cfg.ReleaseBranch - nextVersionBranch := nextVersionBranchName(cfg.NextRelease) - backportBranch := backportNextBranchName(cfg.NextProjectMinorVersion) - backportLabel := fmt.Sprintf("backport-%s", cfg.ReleaseBranch) + if err := repo.EnsureBranchFrom(cfg.ReleaseBranch, branch); err != nil { + return workflowPR{}, err + } + if err := UpdateVersion(cfg.CurrentRelease); err != nil { + return workflowPR{}, err + } + commitMsg := fmt.Sprintf("[Release %s] ff-release: update versions %s", cfg.CurrentRelease, cfg.CurrentRelease) + if _, err := repo.CommitAll(commitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return workflowPR{}, err + } + + return workflowPR{ + branch: branch, + base: cfg.ReleaseBranch, + opts: PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("[Release %s] ff-release: update versions %s", cfg.CurrentRelease, cfg.CurrentRelease), + Head: branch, + Base: cfg.ReleaseBranch, + Body: prBReleaseBody(cfg), + Reviewers: cfg.ProjectReviewers, + Labels: prBReleaseLabels(), + }, + }, nil +} - fmt.Printf("Using release branch: %s\n", releaseBranch) +func prepNextPatchOnReleaseBranch(repo *GitRepo, cfg *ReleaseConfig) (workflowPR, error) { + branch := fmt.Sprintf("ff-prep-next-patch-%s", cfg.NextRelease) + fmt.Printf("\n--- Preparing PR-D: next patch %s on %s ---\n", cfg.NextRelease, cfg.ReleaseBranch) - fmt.Printf("\n--- Creating PR 1: Update version to %s ---\n", cfg.NextRelease) - if err := repo.EnsureBranchFrom(releaseBranch, nextVersionBranch); err != nil { - return err + if err := repo.EnsureBranchFrom(cfg.ReleaseBranch, branch); err != nil { + return workflowPR{}, err } if err := UpdateVersion(cfg.NextRelease); err != nil { - return err - } - nextCommitMsg := fmt.Sprintf("[Release] Update version to %s", cfg.NextRelease) - if _, err := repo.CommitAll(nextCommitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { - return err - } + return workflowPR{}, err + } + commitMsg := fmt.Sprintf("[Release %s] Update version to %s", cfg.CurrentRelease, cfg.NextRelease) + if _, err := repo.CommitAll(commitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return workflowPR{}, err + } + + return workflowPR{ + branch: branch, + base: cfg.ReleaseBranch, + opts: PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("[Release %s] Update version to %s", cfg.CurrentRelease, cfg.NextRelease), + Head: branch, + Base: cfg.ReleaseBranch, + Body: prDNextPatchBody(cfg), + Reviewers: cfg.ProjectReviewers, + Labels: prDNextPatchLabels(), + }, + }, nil +} - fmt.Printf("\n--- Creating PR 2: Add backport rule for %s ---\n", cfg.ReleaseBranch) - if err := repo.EnsureBranchFrom(cfg.BaseBranch, backportBranch); err != nil { - return err - } - if err := UpdateMergify(cfg.CurrentRelease); err != nil { - return err - } - if _, err := repo.CommitAll("[Release] add-backport-next", cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { - return err - } +func prAMainBody(cfg *ReleaseConfig) string { + return fmt.Sprintf(`## [Release %s] - nextPROpts := PROptions{ - Owner: cfg.ProjectOwner, - Repo: cfg.ProjectRepo, - Title: fmt.Sprintf("[Release] Update version to %s", cfg.NextRelease), - Head: nextVersionBranch, - Base: releaseBranch, - Body: nextReleasePRBody(cfg.NextRelease, cfg.CurrentRelease), - Reviewers: cfg.ProjectReviewers, - Labels: nextReleasePRLabels, - } +Prepares %s for the %s feature freeze. - backportLabels := append([]string(nil), backportNextPRLabels...) - backportLabels = append(backportLabels, backportLabel) +- Adds Mergify backport rule for branch %s (label %s) +- Bumps version/version.go to %s (next minor) - backportPROpts := PROptions{ - Owner: cfg.ProjectOwner, - Repo: cfg.ProjectRepo, - Title: fmt.Sprintf("backport: Add %s branch", cfg.ReleaseBranch), - Head: backportBranch, - Base: cfg.BaseBranch, - Body: backportNextPRBody(cfg.ReleaseBranch), - Reviewers: cfg.ProjectReviewers, - Labels: backportLabels, - } +**Merge:** before release branch work is finalized. +`, cfg.CurrentRelease, cfg.BaseBranch, cfg.CurrentRelease, cfg.ReleaseBranch, backportLabel(cfg.ReleaseBranch), cfg.NextProjectMinorVersion) +} - if cfg.DryRun { - fmt.Println("\nDRY RUN: Skipping push and PR creation") - fmt.Printf("Next version branch prepared: %s\n", nextVersionBranch) - fmt.Printf("Backport branch prepared: %s\n", backportBranch) - fmt.Println("Review changes with 'git diff'") - return nil - } +func prBReleaseBody(cfg *ReleaseConfig) string { + return fmt.Sprintf(`## [Release %s] - gh := NewGitHubClient(cfg.GitHubToken) +Feature-freeze release branch updates for %s (version/version.go). - backportPR, err := finalizePR(repo, gh, backportBranch, cfg.BaseBranch, backportPROpts) - if err != nil { +Fleet Server has no docs/test-env updates in this step. + +**Merge:** as soon as the %s branch exists. +`, cfg.CurrentRelease, cfg.CurrentRelease, cfg.ReleaseBranch) +} + +func prDNextPatchBody(cfg *ReleaseConfig) string { + return fmt.Sprintf(`## [Release %s] + +Prepares the %s branch after release of %s. + +- Bumps version/version.go to %s + +**Merge:** after the release of %s. +`, cfg.CurrentRelease, cfg.ReleaseBranch, cfg.CurrentRelease, cfg.NextRelease, cfg.CurrentRelease) +} + +func ensureMajorMinorCurrentReleaseMatchesBase(repo *GitRepo, cfg *ReleaseConfig) error { + base := cfg.BaseBranch + if base == "" { + base = "main" + } + if err := repo.CheckoutBranch(base); err != nil { return err } - - nextPR, err := finalizePR(repo, gh, nextVersionBranch, releaseBranch, nextPROpts) + branchVersion, err := ReadFleetVersion() if err != nil { return err } - - fmt.Printf("\n=== Next Release Workflow Complete ===\n") - if backportPR != nil { - fmt.Printf("Backport PR: %s\n", backportPR.GetHTMLURL()) - } else { - fmt.Println("Backport PR: not created (already up to date)") + if branchVersion != cfg.CurrentRelease { + return fmt.Errorf( + "CURRENT_RELEASE=%s does not match version on %s (%s in version/version.go); "+ + "set CURRENT_RELEASE to the version already on %s (the minor being feature-frozen)", + cfg.CurrentRelease, base, branchVersion, base, + ) } - if nextPR != nil { - fmt.Printf("Next version PR: %s\n", nextPR.GetHTMLURL()) - } else { - fmt.Println("Next version PR: not created (already up to date)") - } - + fmt.Printf("Verified CURRENT_RELEASE=%s matches %s on branch %s\n", cfg.CurrentRelease, branchVersion, base) return nil } -// RunPatchRelease opens a version bump PR into the release branch. +// RunPatchRelease executes the patch release workflow on an existing release branch: +// 1. Opens PR-A (ensure CURRENT version — historically the "docs" PR; often a no-op) +// 2. Opens PR-B (next patch version — after release) func RunPatchRelease(cfg *ReleaseConfig) error { fmt.Println("=== Starting Patch Release Workflow ===") + if err := cfg.EnsureLatestRelease(); err != nil { + return err + } + if err := cfg.Validate(); err != nil { return err } + if err := checkRequirements(cfg); err != nil { return err } @@ -249,100 +357,151 @@ func RunPatchRelease(cfg *ReleaseConfig) error { return err } - releaseBranch := cfg.ReleaseBranch - if releaseBranch == "" { - releaseBranch = inferReleaseBranch(cfg.CurrentRelease) + if cfg.ReleaseBranch == "" { + cfg.ReleaseBranch = inferReleaseBranch(cfg.CurrentRelease) } - patchBranch := patchDocsBranchName(cfg.CurrentRelease) - fmt.Printf("Using release branch: %s\n", releaseBranch) - fmt.Printf("\n--- Creating PR: Patch version %s ---\n", cfg.CurrentRelease) - - if err := repo.EnsureBranchFrom(releaseBranch, patchBranch); err != nil { + if err := ensurePatchCurrentReleaseMatchesBranch(repo, cfg); err != nil { return err } - if err := UpdateVersion(cfg.CurrentRelease); err != nil { + + prA, err := prepPatchBeforeBuild(repo, cfg) + if err != nil { return err } - if _, err := repo.CommitAll("[Release] update version", cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + prB, err := prepNextPatchOnReleaseBranch(repo, cfg) + if err != nil { return err } - patchPROpts := PROptions{ - Owner: cfg.ProjectOwner, - Repo: cfg.ProjectRepo, - Title: fmt.Sprintf("docs: update docs versions %s", cfg.CurrentRelease), - Head: patchBranch, - Base: releaseBranch, - Body: patchReleasePRBody(cfg.CurrentRelease), - Reviewers: cfg.ProjectReviewers, - Labels: patchReleasePRLabels, - } + branchesToFinalize := []workflowPR{prA, prB} if cfg.DryRun { fmt.Println("\nDRY RUN: Skipping push and PR creation") - fmt.Printf("Patch branch prepared: %s\n", patchBranch) - fmt.Println("Review changes with 'git diff'") + for _, item := range branchesToFinalize { + fmt.Printf("Branch prepared: %s\n", item.branch) + } + warnEnsureReleaseIssueTracker(cfg, nil) return nil } gh := NewGitHubClient(cfg.GitHubToken) - patchPR, err := finalizePR(repo, gh, patchBranch, releaseBranch, patchPROpts) + results, err := finalizeWorkflowPRs(repo, gh, branchesToFinalize) if err != nil { return err } fmt.Printf("\n=== Patch Release Workflow Complete ===\n") - if patchPR != nil { - fmt.Printf("Patch PR: %s\n", patchPR.GetHTMLURL()) - } else { - fmt.Println("Patch PR: not created (already up to date)") - } + printWorkflowPRResults(results) + warnEnsureReleaseIssueTracker(cfg, prsFromWorkflowResults(results)) return nil } -func bumpVersionBranchName(nextMinorVersion string) string { - return fmt.Sprintf("bump-version-%s", nextMinorVersion) +func ensurePatchCurrentReleaseMatchesBranch(repo *GitRepo, cfg *ReleaseConfig) error { + if err := repo.CheckoutBranch(cfg.ReleaseBranch); err != nil { + return err + } + branchVersion, err := ReadFleetVersion() + if err != nil { + return err + } + if branchVersion != cfg.CurrentRelease { + return fmt.Errorf( + "CURRENT_RELEASE=%s does not match version on branch %s (%s in version/version.go); "+ + "set CURRENT_RELEASE to the version already on the release branch (the patch being released)", + cfg.CurrentRelease, cfg.ReleaseBranch, branchVersion, + ) + } + fmt.Printf("Verified CURRENT_RELEASE=%s matches %s on branch %s\n", cfg.CurrentRelease, branchVersion, cfg.ReleaseBranch) + return nil } -func nextVersionBranchName(nextRelease string) string { - return fmt.Sprintf("update-version-next-%s", nextRelease) -} +func prepPatchBeforeBuild(repo *GitRepo, cfg *ReleaseConfig) (workflowPR, error) { + branch := fmt.Sprintf("patch-release-%s", cfg.CurrentRelease) + fmt.Printf("\n--- Preparing PR-A: ensure version %s on %s ---\n", cfg.CurrentRelease, cfg.ReleaseBranch) -func backportNextBranchName(nextProjectMinorVersion string) string { - return fmt.Sprintf("add-backport-next-%s", nextProjectMinorVersion) + if err := repo.EnsureBranchFrom(cfg.ReleaseBranch, branch); err != nil { + return workflowPR{}, err + } + // Fleet Server has no docs/test-env files. Former prepare-patch-release only + // updated version.go; with the shared CURRENT_RELEASE guard that is usually a no-op. + if err := UpdateVersion(cfg.CurrentRelease); err != nil { + return workflowPR{}, err + } + commitMsg := fmt.Sprintf("[Release %s] Update docs versions %s", cfg.CurrentRelease, cfg.CurrentRelease) + if _, err := repo.CommitAll(commitMsg, cfg.GitAuthorName, cfg.GitAuthorEmail); err != nil { + return workflowPR{}, err + } + + return workflowPR{ + branch: branch, + base: cfg.ReleaseBranch, + opts: PROptions{ + Owner: cfg.ProjectOwner, + Repo: cfg.ProjectRepo, + Title: fmt.Sprintf("[Release %s] Update docs versions %s", cfg.CurrentRelease, cfg.CurrentRelease), + Head: branch, + Base: cfg.ReleaseBranch, + Body: patchBeforeBuildPRBody(cfg.CurrentRelease), + Reviewers: cfg.ProjectReviewers, + Labels: patchBeforeBuildPRLabels(), + }, + }, nil } -func patchDocsBranchName(version string) string { - return fmt.Sprintf("update-docs-version-%s", version) -} +func patchBeforeBuildPRBody(currentRelease string) string { + return fmt.Sprintf(`## [Release %s] -func bumpMainPRBody(nextMinorVersion string) string { - return fmt.Sprintf("Bump the version on main to %s.", nextMinorVersion) -} +Former prepare-patch-release PR for Fleet Server. -func nextReleasePRBody(nextRelease, currentRelease string) string { - return fmt.Sprintf(`Updates references to the new release %s. +Fleet Server has no docs/K8s updates; version/version.go is already %s on the release branch, so this PR is often empty and skipped. -Merge after the release %s. -`, nextRelease, currentRelease) +**Merge:** before the final Release build (when changes exist). +`, currentRelease, currentRelease) } -func backportNextPRBody(releaseBranch string) string { - return fmt.Sprintf(`Merge as soon as %s branch was created. +func finalizeWorkflowPRs(repo *GitRepo, gh *GitHubClient, items []workflowPR) ([]workflowPRResult, error) { + results := make([]workflowPRResult, 0, len(items)) + for i, item := range items { + pr, err := finalizePR(repo, gh, item.branch, item.base, item.opts) + if err != nil { + return results, fmt.Errorf("failed to finalize PR %d/%d: %w", i+1, len(items), err) + } + results = append(results, workflowPRResult{item: item, pr: pr}) + } + return results, nil +} -Auto-merge is not yet supported, see https://github.com/Mergifyio/mergify-engine/discussions/2821 -`, releaseBranch) +func printWorkflowPRResults(results []workflowPRResult) { + for i, result := range results { + fmt.Println(formatWorkflowPRLine(i+1, result)) + } } -func patchReleasePRBody(version string) string { - return fmt.Sprintf(`Updates docs versions to %s. +func prsFromWorkflowResults(results []workflowPRResult) []*github.PullRequest { + var prs []*github.PullRequest + for _, result := range results { + if result.pr != nil { + prs = append(prs, result.pr) + } + } + return prs +} -Merge before the final Release build. -`, version) +func formatWorkflowPRLine(index int, result workflowPRResult) string { + if result.pr != nil { + return fmt.Sprintf("PR %d: %s (%s)", index, result.pr.GetHTMLURL(), prDisplayState(result.pr)) + } + return fmt.Sprintf( + "PR %d: skipped (no related open/merged PR for %s → %s)", + index, result.item.branch, result.item.base, + ) } +// finalizePR pushes a branch when it has new commits and creates or reuses an open PR. +// When the branch has nothing new to push, it still resolves a related open or merged PR +// so workflow summaries always list every expected slot. func finalizePR(repo *GitRepo, gh *GitHubClient, branchName, baseBranch string, opts PROptions) (*github.PullRequest, error) { if err := repo.CheckoutBranch(branchName); err != nil { return nil, err @@ -354,7 +513,6 @@ func finalizePR(repo *GitRepo, gh *GitHubClient, branchName, baseBranch string, } if found { gh.ensurePRLabels(opts.Owner, opts.Repo, existingPR.GetNumber(), opts.Labels) - fmt.Printf("Open PR already exists #%d: %s\n", existingPR.GetNumber(), existingPR.GetHTMLURL()) return existingPR, nil } @@ -364,6 +522,14 @@ func finalizePR(repo *GitRepo, gh *GitHubClient, branchName, baseBranch string, } if !ahead { fmt.Printf("No new commits on %s compared to %s; skipping push and PR creation\n", branchName, baseBranch) + related, found, err := gh.FindRelatedPR(opts.Owner, opts.Repo, opts.Head, opts.Base, opts.Title) + if err != nil { + return nil, err + } + if found { + fmt.Printf("Related PR #%d (%s): %s\n", related.GetNumber(), prDisplayState(related), related.GetHTMLURL()) + return related, nil + } return nil, nil } diff --git a/dev-tools/mage/release/workflows_test.go b/dev-tools/mage/release/workflows_test.go new file mode 100644 index 0000000000..f63cb82e1f --- /dev/null +++ b/dev-tools/mage/release/workflows_test.go @@ -0,0 +1,66 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package release + +import ( + "strings" + "testing" +) + +func TestLabelHelpers(t *testing.T) { + if got := backportLabel("9.6"); got != "backport-9.6" { + t.Fatalf("backportLabel = %q", got) + } + + labels := prAMainLabels("9.6") + if !contains(labels, mergeLabelFFDay) || !contains(labels, "backport-9.6") { + t.Fatalf("prAMainLabels = %v", labels) + } + if contains(labels, mergeLabelAfterImages) { + t.Fatal("PR-A must not include merge:3-after-images (PR-C omitted)") + } + + if !contains(prBReleaseLabels(), mergeLabelAfterBranch) { + t.Fatal("PR-B missing merge:2-after-branch") + } + if !contains(prDNextPatchLabels(), mergeLabelAfterRelease) { + t.Fatal("PR-D missing merge:4-after-release") + } + if !contains(patchBeforeBuildPRLabels(), mergeLabelBeforeBuild) { + t.Fatal("patch PR-A missing merge:1-before-build") + } +} + +func TestPRBodiesMentionFleetScope(t *testing.T) { + cfg := &ReleaseConfig{ + CurrentRelease: "9.6.0", + NextRelease: "9.6.1", + NextProjectMinorVersion: "9.7.0", + BaseBranch: "main", + ReleaseBranch: "9.6", + } + + if !strings.Contains(prAMainBody(cfg), "version/version.go") { + t.Fatal("PR-A body missing version.go mention") + } + if !strings.Contains(prBReleaseBody(cfg), "no docs/test-env") { + t.Fatal("PR-B body should note missing docs/test-env") + } + if !strings.Contains(prDNextPatchBody(cfg), "9.6.1") { + t.Fatal("PR-D body missing next patch") + } + if !strings.Contains(patchBeforeBuildPRBody("9.6.1"), "often empty") { + t.Fatal("patch PR-A body should note often-empty PR") + } +} + +func contains(ss []string, want string) bool { + for _, s := range ss { + if s == want { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index 9d3f874c05..8832b97c56 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/elastic/elastic-agent-client/v7 v7.18.1 github.com/elastic/elastic-agent-libs v0.46.1 github.com/elastic/elastic-agent-system-metrics v0.14.4 - github.com/elastic/fleet-server/dev-tools v0.0.0 github.com/elastic/go-elasticsearch/v8 v8.19.6 github.com/elastic/go-ucfg v0.9.1 github.com/fxamacker/cbor/v2 v2.9.2 @@ -49,15 +48,11 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/elastic-transport-go/v8 v8.9.0 // indirect @@ -65,44 +60,31 @@ require ( github.com/elastic/go-sysinfo v1.15.1 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/elastic/gosigar v0.14.3 // indirect - github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.15.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-github/v68 v68.0.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shirou/gopsutil/v4 v4.24.7 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.elastic.co/ecszap v1.0.3 // indirect @@ -116,7 +98,6 @@ require ( golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.40.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index df4da3ccf2..6dac1628ff 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,25 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg= github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -70,28 +55,14 @@ github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -103,18 +74,11 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= -github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20230426061923-93006964c1fc h1:AGDHt781oIcL4EFk7cPnvBUYTwU8BEU6GDTO3ZMn1sE= github.com/google/pprof v0.0.0-20230426061923-93006964c1fc/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -129,21 +93,14 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -177,8 +134,6 @@ github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/ github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/runtime v1.5.0 h1:aiil4QnH+eiWYSO60eaYZ4aur7sJH3rz6BvT5EBFnxc= github.com/oapi-codegen/runtime v1.5.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/open-telemetry/opamp-go v0.23.0 h1:k7h7w/muprut9/DAhUC4anX4v7hIdgO02gIsSjV4uq0= github.com/open-telemetry/opamp-go v0.23.0/go.mod h1:DIIVdkLefdqPW5L+4I2twmAicVrTB0Bp5XJAfedZzAM= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -187,8 +142,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -212,17 +165,12 @@ github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk= github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -231,7 +179,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -243,11 +190,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.elastic.co/apm/module/apmchiv5/v2 v2.7.12 h1:X8UdMuAw5B44Mg497Wiwbm57HSiYOVNeXDEX8qh84lU= @@ -292,46 +236,17 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -339,29 +254,10 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= @@ -372,13 +268,10 @@ google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD79kk= gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/magefile.go b/magefile.go index a013cd8a1f..0d743886fa 100644 --- a/magefile.go +++ b/magefile.go @@ -46,7 +46,6 @@ import ( "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/transport/tlscommon" - "github.com/elastic/fleet-server/dev-tools/mage/release" "github.com/elastic/fleet-server/v7/version" ) @@ -2299,39 +2298,43 @@ func (Test) CloudE2ERun() error { return err } +const releaseToolDir = "dev-tools/mage/release" + +// runReleaseTool invokes the nested-module CLI with the Fleet Server repo as cwd. +func runReleaseTool(args ...string) error { + root, err := os.Getwd() + if err != nil { + return err + } + env := map[string]string{"FLEET_SERVER_REPO_ROOT": root} + cmdArgs := append([]string{"run", "-C", releaseToolDir, "./cmd/fleet-release"}, args...) + return sh.RunWithV(env, "go", cmdArgs...) +} + // UpdateVersion updates the version in version/version.go. func (Release) UpdateVersion(version string) error { - return release.UpdateVersion(version) + return runReleaseTool("update-version", version) } // UpdateMergify adds a new backport rule to .mergify.yml. func (Release) UpdateMergify(version string) error { - return release.UpdateMergify(version) + return runReleaseTool("update-mergify", version) } // RunMajorMinor orchestrates the major/minor release workflow after feature freeze. +// Set DRY_RUN=true to preview changes without pushing. func (Release) RunMajorMinor() error { - cfg, err := release.LoadConfigFromEnv() - if err != nil { - return err - } - return release.RunMajorMinorRelease(cfg) -} - -// RunNextRelease opens next-release and backport PRs onto the release branch. -func (Release) RunNextRelease() error { - cfg, err := release.LoadConfigFromEnv() - if err != nil { - return err - } - return release.RunNextRelease(cfg) + return runReleaseTool("run-major-minor") } // RunPatch orchestrates the complete patch release workflow. +// Set DRY_RUN=true to preview changes without pushing. func (Release) RunPatch() error { - cfg, err := release.LoadConfigFromEnv() - if err != nil { - return err - } - return release.RunPatchRelease(cfg) + return runReleaseTool("run-patch") +} + +// EnsureIssueTracker creates or updates the Fleet Server release checklist issue for +// CURRENT_RELEASE, linking related PRs labeled "release". +func (Release) EnsureIssueTracker() error { + return runReleaseTool("ensure-issue-tracker") } From fb93ee0115cc0c9246c3f5c2571eeff9240329df Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Wed, 22 Jul 2026 13:32:14 +0200 Subject: [PATCH 13/14] fix(release): fall back to git tags when GitHub Releases are empty fleet-server publishes version tags but no GitHub Releases, so runMajorMinor failed resolving LatestRelease for minor versions. --- RELEASE.md | 2 +- dev-tools/mage/release/config.go | 3 +- dev-tools/mage/release/github.go | 30 ++++++++++++ dev-tools/mage/release/github_test.go | 68 +++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 1a125e2a5f..6cde026d4d 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -59,7 +59,7 @@ Use plain `X.Y.Z` semver for `CURRENT_RELEASE` (no `-test` or `-SNAPSHOT` suffix | `PROJECT_REPO` | no | `fleet-server` | GitHub repository | | `PROJECT_REVIEWERS` | no | `elastic/elastic-agent-control-plane` | PR reviewers | -Derived (not env-overridable): `NEXT_RELEASE` (patch+1), `NEXT_PROJECT_MINOR_VERSION` (minor+1), `RELEASE_BRANCH` (`X.Y`), `LATEST_RELEASE` (patch−1 or GitHub lookup). +Derived (not env-overridable): `NEXT_RELEASE` (patch+1), `NEXT_PROJECT_MINOR_VERSION` (minor+1), `RELEASE_BRANCH` (`X.Y`), `LATEST_RELEASE` (patch−1, or GitHub Releases lookup with git-tag fallback when releases are empty). ## Files updated diff --git a/dev-tools/mage/release/config.go b/dev-tools/mage/release/config.go index 40719bcea0..88e09d0612 100644 --- a/dev-tools/mage/release/config.go +++ b/dev-tools/mage/release/config.go @@ -168,7 +168,8 @@ var fetchLatestReleaseBefore = func(token, owner, repo, current string) (string, return NewGitHubClient(token).LatestReleaseBefore(owner, repo, current) } -// EnsureLatestRelease sets LatestRelease when unset by querying elastic/fleet-server releases. +// EnsureLatestRelease sets LatestRelease when unset by querying elastic/fleet-server +// GitHub Releases, falling back to git tags when no releases are published. func (c *ReleaseConfig) EnsureLatestRelease() error { if c.LatestRelease != "" { return nil diff --git a/dev-tools/mage/release/github.go b/dev-tools/mage/release/github.go index f2f1af3dc7..6633990b40 100644 --- a/dev-tools/mage/release/github.go +++ b/dev-tools/mage/release/github.go @@ -447,11 +447,18 @@ const ( // LatestReleaseBefore returns the highest published release version with the same major // that is strictly less than currentVersion (e.g. current 9.5.0 → 9.4.3). +// When the repo has no GitHub Releases, it falls back to git tags (fleet-server's usual case). func (gh *GitHubClient) LatestReleaseBefore(owner, repo, currentVersion string) (string, error) { versions, err := gh.listReleaseVersions(owner, repo) if err != nil { return "", err } + if len(versions) == 0 { + versions, err = gh.listTagVersions(owner, repo) + if err != nil { + return "", err + } + } return selectLatestReleaseBefore(versions, currentVersion) } @@ -477,3 +484,26 @@ func (gh *GitHubClient) listReleaseVersions(owner, repo string) ([]string, error } return versions, nil } + +func (gh *GitHubClient) listTagVersions(owner, repo string) ([]string, error) { + var versions []string + opts := &github.ListOptions{PerPage: 100} + for { + tags, resp, err := gh.client.Repositories.ListTags(gh.ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("failed to list tags for %s/%s: %w", owner, repo, err) + } + for _, tag := range tags { + name := tag.GetName() + if name == "" { + continue + } + versions = append(versions, name) + } + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + return versions, nil +} diff --git a/dev-tools/mage/release/github_test.go b/dev-tools/mage/release/github_test.go index 89545af818..934c894544 100644 --- a/dev-tools/mage/release/github_test.go +++ b/dev-tools/mage/release/github_test.go @@ -101,6 +101,74 @@ func TestCreatePRIdempotent(t *testing.T) { } } +func TestLatestReleaseBeforeUsesReleasesWhenPresent(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/repos/elastic/fleet-server/releases": + w.Header().Set("Content-Type", "application/json") + releases := []*github.RepositoryRelease{ + {TagName: github.Ptr("v9.5.1")}, + {TagName: github.Ptr("v9.4.3")}, + {TagName: github.Ptr("v8.19.1")}, + } + if err := json.NewEncoder(w).Encode(releases); err != nil { + t.Errorf("failed to encode releases: %v", err) + } + case r.Method == http.MethodGet && r.URL.Path == "/repos/elastic/fleet-server/tags": + t.Errorf("ListTags should not be called when releases exist") + http.NotFound(w, r) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + } + }) + + ghClient := newTestGitHubClient(t, handler) + got, err := ghClient.LatestReleaseBefore("elastic", "fleet-server", "9.6.0") + if err != nil { + t.Fatalf("LatestReleaseBefore() error = %v", err) + } + if got != "9.5.1" { + t.Errorf("LatestReleaseBefore() = %q, want 9.5.1", got) + } +} + +func TestLatestReleaseBeforeFallsBackToTagsWhenReleasesEmpty(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/repos/elastic/fleet-server/releases": + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode([]*github.RepositoryRelease{}); err != nil { + t.Errorf("failed to encode empty releases: %v", err) + } + case r.Method == http.MethodGet && r.URL.Path == "/repos/elastic/fleet-server/tags": + w.Header().Set("Content-Type", "application/json") + tags := []*github.RepositoryTag{ + {Name: github.Ptr("v9.4.4")}, + {Name: github.Ptr("v9.4.3")}, + {Name: github.Ptr("v9.3.8")}, + {Name: github.Ptr("v8.19.1")}, + {Name: github.Ptr("not-a-version")}, + } + if err := json.NewEncoder(w).Encode(tags); err != nil { + t.Errorf("failed to encode tags: %v", err) + } + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + } + }) + + ghClient := newTestGitHubClient(t, handler) + got, err := ghClient.LatestReleaseBefore("elastic", "fleet-server", "9.6.0") + if err != nil { + t.Fatalf("LatestReleaseBefore() error = %v", err) + } + if got != "9.4.4" { + t.Errorf("LatestReleaseBefore() = %q, want 9.4.4", got) + } +} + func TestCreatePRCreatesWhenNoneExists(t *testing.T) { createCalls := 0 newPR := &github.PullRequest{ From 47042ea4dece0c79fd2c7e39d80234deb2d0eef6 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Fri, 24 Jul 2026 12:38:57 +0200 Subject: [PATCH 14/14] ci: wire version-bump pipeline to mage release targets --- .buildkite/pipeline.version-bump.yaml | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.version-bump.yaml b/.buildkite/pipeline.version-bump.yaml index 32ff2a2204..6c61114e5c 100644 --- a/.buildkite/pipeline.version-bump.yaml +++ b/.buildkite/pipeline.version-bump.yaml @@ -19,8 +19,46 @@ notify: ${BUILDKITE_BUILD_URL} if: build.state == "blocked" +env: + IMAGE_CI_WITH_HOOKS_LATEST: "docker.elastic.co/ci-agent-images/platform-ingest/buildkite-agent-beats-ci-with-hooks:latest" + steps: - # TODO: replace this block step by real version bump logic + # BRANCH is major.minor (e.g. 9.4 for a 9.4.0 minor release, 9.3 for a 9.3.3 patch). + - label: "Bump version minor" + key: bump-version-minor + if: build.env("WORKFLOW") == "minor" + agents: + image: "${IMAGE_CI_WITH_HOOKS_LATEST}" + cpu: "4000m" + memory: "8Gi" + useCustomGlobalHooks: true + env: + CURRENT_RELEASE: "${NEW_VERSION}" + plugins: + - elastic/vault-github-token#v0.2.1: ~ + command: + - make mage + - mage release:runMajorMinor + timeout_in_minutes: 60 + + - label: "Bump version patch" + key: bump-version-patch + if: build.env("WORKFLOW") == "patch" + agents: + image: "${IMAGE_CI_WITH_HOOKS_LATEST}" + cpu: "4000m" + memory: "8Gi" + useCustomGlobalHooks: true + env: + CURRENT_RELEASE: "${NEW_VERSION}" + BASE_BRANCH: "${BRANCH}" + plugins: + - elastic/vault-github-token#v0.2.1: ~ + command: + - make mage + - mage release:runPatch + timeout_in_minutes: 60 + - block: "Ready to fetch for DRA artifacts?" prompt: | Unblock when your team is ready to proceed.