mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
/tmp
|
||||
/.idea
|
||||
objdiff-*
|
||||
config.yml
|
||||
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/encounter/decompal/config"
|
||||
"github.com/encounter/decompal/handlers"
|
||||
"github.com/gregjones/httpcache"
|
||||
"github.com/palantir/go-baseapp/baseapp"
|
||||
"github.com/palantir/go-githubapp/githubapp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"goji.io/pat"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration from a file
|
||||
cfg, err := config.ReadConfig("config.yml")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "failed to read config"))
|
||||
}
|
||||
logger := baseapp.NewLogger(cfg.Logging)
|
||||
zerolog.DefaultContextLogger = &logger
|
||||
|
||||
// Configure a temporary directory
|
||||
//if cfg.App.TmpDir == "" {
|
||||
// cfg.App.TmpDir, err = os.MkdirTemp(os.TempDir(), "decompal")
|
||||
// if err != nil {
|
||||
// logger.Fatal().
|
||||
// Err(err).
|
||||
// Str("path", cfg.App.TmpDir).
|
||||
// Msg("failed to create temporary directory")
|
||||
// }
|
||||
//}
|
||||
//if _, err := os.Stat(cfg.App.TmpDir); err != nil {
|
||||
// if os.IsNotExist(err) {
|
||||
// err := os.MkdirAll(cfg.App.TmpDir, 0755)
|
||||
// if err != nil {
|
||||
// logger.Fatal().
|
||||
// Err(err).
|
||||
// Str("path", cfg.App.TmpDir).
|
||||
// Msg("failed to create temporary directory")
|
||||
// }
|
||||
// } else {
|
||||
// logger.Fatal().
|
||||
// Err(err).
|
||||
// Str("path", cfg.App.TmpDir).
|
||||
// Msg("failed to stat temporary directory")
|
||||
// }
|
||||
//}
|
||||
//logger.Debug().
|
||||
// Str("path", cfg.App.TmpDir).
|
||||
// Msg("Using temporary directory")
|
||||
//// Delete the temporary directory on exit
|
||||
//defer func() {
|
||||
// if err := os.RemoveAll(cfg.App.TmpDir); err != nil {
|
||||
// logger.Error().
|
||||
// Err(err).
|
||||
// Str("path", cfg.App.TmpDir).
|
||||
// Msg("failed to remove temporary directory")
|
||||
// }
|
||||
//}()
|
||||
|
||||
// Create a task queue
|
||||
//queueFactory := memqueue.NewFactory()
|
||||
//taskQueue := queueFactory.RegisterQueue(&taskq.QueueOptions{
|
||||
// Name: "background-tasks",
|
||||
//})
|
||||
|
||||
// Create the server
|
||||
serverParams := baseapp.DefaultParams(logger, "decompal.")
|
||||
server, err := baseapp.NewServer(cfg.Server, serverParams...)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to create server")
|
||||
}
|
||||
|
||||
// Create GitHub app client
|
||||
cc, err := githubapp.NewDefaultCachingClientCreator(
|
||||
cfg.GitHub,
|
||||
githubapp.WithClientUserAgent("decompal/1.0.0"),
|
||||
githubapp.WithClientTimeout(5*time.Second),
|
||||
githubapp.WithClientCaching(false, func() httpcache.Cache { return httpcache.NewMemoryCache() }),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to create GitHub app client")
|
||||
}
|
||||
|
||||
// Register GitHub webhook handlers
|
||||
taskCtx, taskCancel := context.WithCancel(context.Background())
|
||||
defer taskCancel()
|
||||
server.Mux().Handle(pat.Post(githubapp.DefaultWebhookRoute), githubapp.NewDefaultEventDispatcher(
|
||||
cfg.GitHub,
|
||||
handlers.NewPullRequestHandler(cc, &cfg.App, taskCtx),
|
||||
handlers.NewWorkflowRunHandler(cc, &cfg.App, taskCtx),
|
||||
))
|
||||
|
||||
// Start the server (blocking)
|
||||
if err = server.Start(); err != nil {
|
||||
logger.Fatal().Err(err).Msg("server failed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
server:
|
||||
address: 127.0.0.1
|
||||
port: 8080
|
||||
shutdown_wait_time: 15s
|
||||
|
||||
logging:
|
||||
pretty: true
|
||||
level: debug
|
||||
|
||||
app:
|
||||
objdiff_path: ./objdiff-cli-linux-x86_64
|
||||
|
||||
github:
|
||||
v3_api_url: https://api.github.com/
|
||||
v4_api_url: https://api.github.com/graphql
|
||||
app:
|
||||
integration_id: 1234
|
||||
webhook_secret: webhook_secret
|
||||
private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----
|
||||
oauth:
|
||||
client_id: client_id
|
||||
client_secret: client_secret
|
||||
@@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/palantir/go-baseapp/baseapp"
|
||||
"github.com/palantir/go-githubapp/githubapp"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server baseapp.HTTPConfig `yaml:"server"`
|
||||
Logging baseapp.LoggingConfig `yaml:"logging"`
|
||||
GitHub githubapp.Config `yaml:"github"`
|
||||
App AppConfig `yaml:"app"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
//TmpDir string `yaml:"tmp_dir"`
|
||||
ObjdiffPath string `yaml:"objdiff_path"`
|
||||
}
|
||||
|
||||
func ReadConfig(path string) (Config, error) {
|
||||
var c Config
|
||||
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return c, errors.Wrapf(err, "failed reading server config file: %s", path)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(bytes, &c); err != nil {
|
||||
return c, errors.Wrap(err, "failed parsing configuration file")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh -e
|
||||
SRC_DIR=../objdiff/objdiff-cli/protos
|
||||
protoc -I=$SRC_DIR \
|
||||
--go_out=paths=source_relative:objdiff \
|
||||
--go_opt=Mreport.proto=github.com/encounter/decompal/objdiff \
|
||||
$SRC_DIR/*.proto
|
||||
@@ -0,0 +1,34 @@
|
||||
module github.com/encounter/decompal
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/google/go-github/v63 v63.0.0
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||
github.com/palantir/go-baseapp v0.5.2
|
||||
github.com/palantir/go-githubapp v0.27.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
goji.io v2.0.2+incompatible
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bluekeyes/hatpear v0.1.2 // indirect
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/google/go-github/v62 v62.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
golang.org/x/oauth2 v0.22.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
github.com/bluekeyes/hatpear v0.1.2 h1:D5dz9W/W5YxKlmqn7hRJi218+yVkXylhs/R6Ns/jj4A=
|
||||
github.com/bluekeyes/hatpear v0.1.2/go.mod h1:2bh+rl4wLhqzzL0hT7Q4SVGXIivrE8oKgH2WYM3ubt0=
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag=
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.5.2/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/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
|
||||
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
|
||||
github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE=
|
||||
github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA=
|
||||
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/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/palantir/go-baseapp v0.5.2 h1:b1ukx7AXo2/E4NkUvTFlW+185uwCcifzd2XzLrG4oS8=
|
||||
github.com/palantir/go-baseapp v0.5.2/go.mod h1:uijQMPfmgV69oiMu2jkskum/4HiYuEP/gzrnphD+/Co=
|
||||
github.com/palantir/go-githubapp v0.27.0 h1:YTSnLElOVOSSFOqCiAyc1wpZMQjGmMvjas0W/F9W4Jk=
|
||||
github.com/palantir/go-githubapp v0.27.0/go.mod h1:yi9WLdjHkIfeZMetaZyCFJLcxocmrYdd+e5BJTfkimI=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
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/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
|
||||
goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,123 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/encounter/decompal/config"
|
||||
"github.com/encounter/decompal/objdiff"
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/palantir/go-githubapp/githubapp"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type pullRequestHandler struct {
|
||||
githubapp.ClientCreator
|
||||
config *config.AppConfig
|
||||
taskCtx context.Context
|
||||
}
|
||||
|
||||
func NewPullRequestHandler(
|
||||
cc githubapp.ClientCreator,
|
||||
config *config.AppConfig,
|
||||
taskCtx context.Context,
|
||||
) githubapp.EventHandler {
|
||||
return &pullRequestHandler{
|
||||
ClientCreator: cc,
|
||||
config: config,
|
||||
taskCtx: taskCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *pullRequestHandler) Handles() []string {
|
||||
return []string{"pull_request"}
|
||||
}
|
||||
|
||||
func (h *pullRequestHandler) Handle(_ context.Context, eventType, deliveryID string, payload []byte) error {
|
||||
event := &github.PullRequestEvent{}
|
||||
if err := json.Unmarshal(payload, event); err != nil {
|
||||
return errors.Wrap(err, "failed to parse pull request event payload")
|
||||
}
|
||||
if event.GetAction() != "opened" {
|
||||
return nil
|
||||
}
|
||||
|
||||
installationID := githubapp.GetInstallationIDFromEvent(event)
|
||||
client, err := h.NewInstallationClient(installationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go backgroundTask(h.taskCtx, eventType, deliveryID, func(ctx context.Context) error {
|
||||
repo := event.GetRepo()
|
||||
// Re-prepare logger instead of using the one from the request context
|
||||
ctx, logger := githubapp.PrepareRepoContext(ctx, installationID, repo)
|
||||
|
||||
// Find any completed workflow runs for the current PR
|
||||
repoOwner := repo.GetOwner().GetLogin()
|
||||
repoName := repo.GetName()
|
||||
pr := event.GetPullRequest()
|
||||
sha := pr.GetHead().GetSHA()
|
||||
runs, _, err := client.Actions.ListRepositoryWorkflowRuns(
|
||||
ctx,
|
||||
repoOwner,
|
||||
repoName,
|
||||
&github.ListWorkflowRunsOptions{
|
||||
Status: "completed",
|
||||
HeadSHA: sha,
|
||||
ExcludePullRequests: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list workflow runs")
|
||||
}
|
||||
if len(runs.WorkflowRuns) == 0 {
|
||||
logger.Debug().Msg("No workflow runs found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find report files in any completed workflow runs
|
||||
var files []objdiff.ReportFile
|
||||
var run *github.WorkflowRun
|
||||
for _, run = range runs.WorkflowRuns {
|
||||
files, err = objdiff.FetchReportFiles(
|
||||
ctx,
|
||||
logger,
|
||||
client,
|
||||
repoOwner,
|
||||
repoName,
|
||||
sha,
|
||||
run.GetID(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if run == nil || len(files) == 0 {
|
||||
logger.Info().Msg("No report files found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate changes and create a PR comment
|
||||
err = processPR(
|
||||
ctx,
|
||||
h.config,
|
||||
installationID,
|
||||
pr,
|
||||
sha,
|
||||
client,
|
||||
repo,
|
||||
run.GetWorkflowID(),
|
||||
files,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/encounter/decompal/config"
|
||||
"github.com/encounter/decompal/objdiff"
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/palantir/go-githubapp/githubapp"
|
||||
"github.com/pkg/errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func processPR(
|
||||
ctx context.Context,
|
||||
config *config.AppConfig,
|
||||
installationID int64,
|
||||
pr *github.PullRequest,
|
||||
sha string,
|
||||
client *github.Client,
|
||||
repo *github.Repository,
|
||||
workflowID int64,
|
||||
files []objdiff.ReportFile,
|
||||
) error {
|
||||
prNum := pr.GetNumber()
|
||||
ctx, logger := githubapp.PreparePRContext(ctx, installationID, repo, prNum)
|
||||
|
||||
// Sanity check
|
||||
head := pr.GetHead()
|
||||
if head.GetSHA() != sha {
|
||||
logger.Debug().
|
||||
Str("head_sha", head.GetSHA()).
|
||||
Str("commit_sha", sha).
|
||||
Msg("Head SHA does not match workflow run SHA")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find workflows runs for the PR base commit
|
||||
base := pr.GetBase()
|
||||
repoOwner := repo.GetOwner().GetLogin()
|
||||
repoName := repo.GetName()
|
||||
runs, _, err := client.Actions.ListWorkflowRunsByID(
|
||||
ctx,
|
||||
repoOwner,
|
||||
repoName,
|
||||
workflowID,
|
||||
&github.ListWorkflowRunsOptions{
|
||||
Status: "completed",
|
||||
HeadSHA: base.GetSHA(),
|
||||
ExcludePullRequests: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list workflow runs by file name")
|
||||
}
|
||||
if len(runs.WorkflowRuns) == 0 {
|
||||
logger.Debug().
|
||||
Str("commit_sha", base.GetSHA()).
|
||||
Msg("No base workflow runs found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch report files for the PR base commit
|
||||
baseRun := runs.WorkflowRuns[0]
|
||||
baseFiles, err := objdiff.FetchReportFiles(
|
||||
ctx,
|
||||
logger,
|
||||
client,
|
||||
repoOwner,
|
||||
repoName,
|
||||
base.GetSHA(),
|
||||
baseRun.GetID(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(baseFiles) == 0 {
|
||||
logger.Info().
|
||||
Str("commit_sha", base.GetSHA()).
|
||||
Int64("workflow_run_id", baseRun.GetID()).
|
||||
Msg("No base report files found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate changes for each report file
|
||||
type versionChange struct {
|
||||
Version string
|
||||
Body string
|
||||
}
|
||||
versionChanges := make([]versionChange, 0)
|
||||
for _, baseFile := range baseFiles {
|
||||
for _, file := range files {
|
||||
if baseFile.Version == file.Version {
|
||||
logger := logger.With().
|
||||
Str("version", file.Version).
|
||||
Str("from_sha", baseFile.Sha).
|
||||
Str("to_sha", file.Sha).
|
||||
Logger()
|
||||
logger.Info().Msg("Generating changes")
|
||||
changes, err := objdiff.GenerateChanges(config, logger, &baseFile, &file)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to generate changes")
|
||||
return err
|
||||
}
|
||||
body := createChanges(changes)
|
||||
if body != "" {
|
||||
versionChanges = append(versionChanges, versionChange{
|
||||
Version: file.Version,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(versionChanges) == 0 {
|
||||
logger.Info().Msg("No changes found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate PR comment body
|
||||
body := "## Changes\n\n"
|
||||
for _, vc := range versionChanges {
|
||||
body += fmt.Sprintf(
|
||||
"<details><summary>Version %s</summary>\n\n%s\n\n</details>\n\n",
|
||||
vc.Version,
|
||||
vc.Body,
|
||||
)
|
||||
}
|
||||
|
||||
// Update or create PR comment
|
||||
err = upsertComment(ctx, client, repoOwner, repoName, prNum, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertComment(
|
||||
ctx context.Context,
|
||||
client *github.Client,
|
||||
repoOwner, repoName string,
|
||||
prNum int,
|
||||
body string,
|
||||
) error {
|
||||
sort := "created"
|
||||
direction := "asc"
|
||||
existing, _, err := client.Issues.ListComments(ctx, repoOwner, repoName, prNum, &github.IssueListCommentsOptions{
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list existing comments")
|
||||
}
|
||||
commentID := int64(0)
|
||||
for _, comment := range existing {
|
||||
// TODO: update go-github to expose performed_via_github_app
|
||||
if comment.GetUser().GetLogin() == "decompal[bot]" {
|
||||
commentID = comment.GetID()
|
||||
if comment.GetBody() == body {
|
||||
// No changes
|
||||
return nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if commentID != 0 {
|
||||
_, _, err = client.Issues.EditComment(ctx, repoOwner, repoName, commentID, &github.IssueComment{
|
||||
Body: github.String(body),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to edit comment")
|
||||
}
|
||||
} else {
|
||||
_, _, err = client.Issues.CreateComment(ctx, repoOwner, repoName, prNum, &github.IssueComment{
|
||||
Body: github.String(body),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create comment")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createChanges(changes *objdiff.Changes) string {
|
||||
out := "### Overall\n\n"
|
||||
overallTable := changeInfoTable(changes.From, changes.To)
|
||||
if overallTable == "" {
|
||||
if len(changes.Units) == 0 {
|
||||
return ""
|
||||
}
|
||||
out += "No changes\n\n"
|
||||
} else {
|
||||
out += overallTable + "\n\n"
|
||||
}
|
||||
for _, unit := range changes.Units {
|
||||
out += fmt.Sprintf("---\n### `%s`\n\n", unit.Name)
|
||||
unitTable := changeInfoTable(unit.From, unit.To)
|
||||
if unitTable != "" {
|
||||
out += unitTable + "\n\n"
|
||||
}
|
||||
functionsTable := changeItemTable("Functions", unit.Functions)
|
||||
if functionsTable != "" {
|
||||
out += functionsTable + "\n\n"
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func changeItemTable(name string, items []*objdiff.ChangeItem) string {
|
||||
header := fmt.Sprintf("|%s|Previous|Current|Change|\n|-|-|-|-|", name)
|
||||
rows := make([]string, 0)
|
||||
for _, item := range items {
|
||||
row := changeItemInfoRow(item)
|
||||
if row != "" {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
return header + "\n" + strings.Join(rows, "\n")
|
||||
}
|
||||
|
||||
const (
|
||||
incArrow = "${\\color{green}▲}$"
|
||||
decArrow = "${\\color{red}▼}$"
|
||||
)
|
||||
|
||||
func floatArrow(diff float32) string {
|
||||
if diff > 0 {
|
||||
return " " + incArrow
|
||||
}
|
||||
if diff < 0 {
|
||||
return " " + decArrow
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func intArrow(diff int64) string {
|
||||
if diff > 0 {
|
||||
return " " + incArrow
|
||||
}
|
||||
if diff < 0 {
|
||||
return " " + decArrow
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func changeItemInfoRow(item *objdiff.ChangeItem) string {
|
||||
var fromPercent, toPercent float32
|
||||
if item.From != nil {
|
||||
fromPercent = item.From.FuzzyMatchPercent
|
||||
}
|
||||
if item.To != nil {
|
||||
toPercent = item.To.FuzzyMatchPercent
|
||||
}
|
||||
if fromPercent == toPercent {
|
||||
return ""
|
||||
}
|
||||
diff := toPercent - fromPercent
|
||||
return fmt.Sprintf(
|
||||
"|`%s`|%.2f%%|%.2f%%|%.2f%%%s|",
|
||||
item.Name,
|
||||
fromPercent,
|
||||
toPercent,
|
||||
diff,
|
||||
floatArrow(diff),
|
||||
)
|
||||
}
|
||||
|
||||
func changeInfoTable(prev, curr *objdiff.ChangeInfo) string {
|
||||
if prev == nil && curr == nil {
|
||||
return ""
|
||||
} else if prev == nil {
|
||||
// TODO: added
|
||||
prev = &objdiff.ChangeInfo{}
|
||||
} else if curr == nil {
|
||||
// TODO: removed
|
||||
curr = &objdiff.ChangeInfo{}
|
||||
}
|
||||
header := "|Metric|Previous|Current|Change|\n|-|-|-|-|"
|
||||
rows := make([]string, 0)
|
||||
if prev.FuzzyMatchPercent != curr.FuzzyMatchPercent {
|
||||
rows = append(rows, floatRow("Fuzzy match", prev.FuzzyMatchPercent, curr.FuzzyMatchPercent))
|
||||
}
|
||||
if prev.TotalCode != curr.TotalCode {
|
||||
rows = append(rows, sizeRow("Total code", prev.TotalCode, curr.TotalCode))
|
||||
}
|
||||
if prev.MatchedCode != curr.MatchedCode ||
|
||||
prev.MatchedCodePercent != curr.MatchedCodePercent {
|
||||
rows = append(rows, intPercentRow(
|
||||
"Matched code",
|
||||
prev.MatchedCode,
|
||||
prev.MatchedCodePercent,
|
||||
curr.MatchedCode,
|
||||
curr.MatchedCodePercent,
|
||||
))
|
||||
}
|
||||
if prev.TotalData != curr.TotalData {
|
||||
rows = append(rows, sizeRow("Total data", prev.TotalData, curr.TotalData))
|
||||
}
|
||||
if prev.MatchedData != curr.MatchedData ||
|
||||
prev.MatchedDataPercent != curr.MatchedDataPercent {
|
||||
rows = append(rows, intPercentRow(
|
||||
"Matched data",
|
||||
prev.MatchedData,
|
||||
prev.MatchedDataPercent,
|
||||
curr.MatchedData,
|
||||
curr.MatchedDataPercent,
|
||||
))
|
||||
}
|
||||
if prev.TotalFunctions != curr.TotalFunctions {
|
||||
rows = append(rows, intRow("Total functions", prev.TotalFunctions, curr.TotalFunctions))
|
||||
}
|
||||
if prev.MatchedFunctions != curr.MatchedFunctions ||
|
||||
prev.MatchedFunctionsPercent != curr.MatchedFunctionsPercent {
|
||||
rows = append(rows, intPercentRow(
|
||||
"Matched functions",
|
||||
uint64(prev.MatchedFunctions),
|
||||
prev.MatchedFunctionsPercent,
|
||||
uint64(curr.MatchedFunctions),
|
||||
curr.MatchedFunctionsPercent,
|
||||
))
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
return header + "\n" + strings.Join(rows, "\n")
|
||||
}
|
||||
|
||||
func floatRow(name string, prev, curr float32) string {
|
||||
diff := curr - prev
|
||||
return fmt.Sprintf(
|
||||
"|%s|%.2f%%|%.2f%%|%.2f%%%s|",
|
||||
name,
|
||||
prev,
|
||||
curr,
|
||||
diff,
|
||||
floatArrow(diff),
|
||||
)
|
||||
}
|
||||
|
||||
func intRow(name string, prev, curr uint32) string {
|
||||
diff := int64(curr) - int64(prev)
|
||||
return fmt.Sprintf(
|
||||
"|%s|%d|%d|%d%s|",
|
||||
name,
|
||||
prev,
|
||||
curr,
|
||||
diff,
|
||||
intArrow(diff),
|
||||
)
|
||||
}
|
||||
|
||||
func sizeRow(name string, prev, curr uint64) string {
|
||||
// TODO: format size
|
||||
diff := int64(curr) - int64(prev)
|
||||
return fmt.Sprintf(
|
||||
"|%s|%d|%d|%d%s|",
|
||||
name,
|
||||
prev,
|
||||
curr,
|
||||
diff,
|
||||
intArrow(diff),
|
||||
)
|
||||
}
|
||||
|
||||
func intPercentRow(
|
||||
name string,
|
||||
prevInt uint64,
|
||||
prevPercent float32,
|
||||
currInt uint64,
|
||||
currPercent float32,
|
||||
) string {
|
||||
diff := int64(currInt) - int64(prevInt)
|
||||
return fmt.Sprintf(
|
||||
"|%s|%d (%.2f%%)|%d (%.2f%%)|%d (%.2f%%)%s|",
|
||||
name,
|
||||
prevInt,
|
||||
prevPercent,
|
||||
currInt,
|
||||
currPercent,
|
||||
diff,
|
||||
currPercent-prevPercent,
|
||||
intArrow(diff),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/palantir/go-githubapp/githubapp"
|
||||
"github.com/rs/zerolog"
|
||||
"time"
|
||||
)
|
||||
|
||||
func backgroundTask(taskCtx context.Context, eventType, deliveryID string, run func(context.Context) error) {
|
||||
logger := zerolog.Ctx(taskCtx).With().
|
||||
Str(githubapp.LogKeyDeliveryID, deliveryID).
|
||||
Str(githubapp.LogKeyEventType, eventType).
|
||||
Logger()
|
||||
ctx, cancel := context.WithDeadline(taskCtx, time.Now().Add(time.Minute))
|
||||
defer cancel()
|
||||
ctx = logger.WithContext(ctx)
|
||||
err := run(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Background task failed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/encounter/decompal/config"
|
||||
"github.com/encounter/decompal/objdiff"
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/palantir/go-githubapp/githubapp"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type workflowRunHandler struct {
|
||||
githubapp.ClientCreator
|
||||
config *config.AppConfig
|
||||
taskCtx context.Context
|
||||
}
|
||||
|
||||
func NewWorkflowRunHandler(
|
||||
cc githubapp.ClientCreator,
|
||||
config *config.AppConfig,
|
||||
taskCtx context.Context,
|
||||
) githubapp.EventHandler {
|
||||
return &workflowRunHandler{
|
||||
ClientCreator: cc,
|
||||
config: config,
|
||||
taskCtx: taskCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *workflowRunHandler) Handles() []string {
|
||||
return []string{"workflow_run"}
|
||||
}
|
||||
|
||||
func (h *workflowRunHandler) Handle(ctx context.Context, eventType, deliveryID string, payload []byte) error {
|
||||
event := &github.WorkflowRunEvent{}
|
||||
if err := json.Unmarshal(payload, event); err != nil {
|
||||
return errors.Wrap(err, "failed to parse workflow run event payload")
|
||||
}
|
||||
|
||||
installationID := githubapp.GetInstallationIDFromEvent(event)
|
||||
ctx, logger := githubapp.PrepareRepoContext(ctx, installationID, event.GetRepo())
|
||||
status := event.GetWorkflowRun().GetStatus()
|
||||
if status != "completed" {
|
||||
logger.Debug().
|
||||
Str("status", status).
|
||||
Msg("Workflow run event is not completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := h.NewInstallationClient(installationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go backgroundTask(h.taskCtx, eventType, deliveryID, func(ctx context.Context) error {
|
||||
repo := event.GetRepo()
|
||||
// Re-prepare logger instead of using the one from the request context
|
||||
ctx, logger := githubapp.PrepareRepoContext(ctx, installationID, repo)
|
||||
|
||||
// Fetch report files for the current workflow run
|
||||
repoOwner := repo.GetOwner().GetLogin()
|
||||
repoName := repo.GetName()
|
||||
run := event.GetWorkflowRun()
|
||||
runId := run.GetID()
|
||||
sha := run.GetHeadSHA()
|
||||
files, err := objdiff.FetchReportFiles(
|
||||
ctx,
|
||||
logger,
|
||||
client,
|
||||
repoOwner,
|
||||
repoName,
|
||||
sha,
|
||||
runId,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
logger.Info().Msg("No report files found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process all pull requests associated with the workflow run
|
||||
prs := event.GetWorkflowRun().PullRequests
|
||||
if prs != nil {
|
||||
for _, pr := range prs {
|
||||
err = processPR(
|
||||
ctx,
|
||||
h.config,
|
||||
installationID,
|
||||
pr,
|
||||
sha,
|
||||
client,
|
||||
repo,
|
||||
run.GetWorkflowID(),
|
||||
files,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package objdiff
|
||||
|
||||
import (
|
||||
"github.com/encounter/decompal/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func GenerateChanges(
|
||||
config *config.AppConfig,
|
||||
logger zerolog.Logger,
|
||||
prev *ReportFile,
|
||||
curr *ReportFile,
|
||||
) (*Changes, error) {
|
||||
if config.ObjdiffPath == "" {
|
||||
return nil, errors.New("objdiff_path not set")
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(&ChangesInput{
|
||||
From: prev.Report,
|
||||
To: curr.Report,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to encode changes input")
|
||||
}
|
||||
|
||||
// Run objdiff with proto input and output
|
||||
// The `--` is to delimit the end of flags and the start of positional arguments
|
||||
// Otherwise the argument parser gets confused
|
||||
cmd := exec.Command(
|
||||
config.ObjdiffPath,
|
||||
"report",
|
||||
"changes",
|
||||
"-f",
|
||||
"proto",
|
||||
"--",
|
||||
"-",
|
||||
"-",
|
||||
)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open stdin pipe")
|
||||
}
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
if _, err := stdin.Write(data); err != nil {
|
||||
logger.Err(err).Msg("failed to write data to objdiff")
|
||||
}
|
||||
}()
|
||||
// It shouldn't have any stderr output when successful, but we want to capture errors
|
||||
// May need to change this if objdiff changes
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to generate changes: %s", string(output))
|
||||
}
|
||||
|
||||
changes := &Changes{}
|
||||
err = proto.Unmarshal(output, changes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode changes file")
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package objdiff
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Older JSON report types
|
||||
type legacyReport struct {
|
||||
FuzzyMatchPercent float32 `json:"fuzzy_match_percent"`
|
||||
TotalCode uint64 `json:"total_code"`
|
||||
MatchedCode uint64 `json:"matched_code"`
|
||||
MatchedCodePercent float32 `json:"matched_code_percent"`
|
||||
TotalData uint64 `json:"total_data"`
|
||||
MatchedData uint64 `json:"matched_data"`
|
||||
MatchedDataPercent float32 `json:"matched_data_percent"`
|
||||
TotalFunctions uint32 `json:"total_functions"`
|
||||
MatchedFunctions uint32 `json:"matched_functions"`
|
||||
MatchedFunctionsPercent float32 `json:"matched_functions_percent"`
|
||||
Units []legacyReportUnit `json:"units"`
|
||||
}
|
||||
|
||||
type legacyReportUnit struct {
|
||||
Name string `json:"name"`
|
||||
FuzzyMatchPercent float32 `json:"fuzzy_match_percent"`
|
||||
TotalCode uint64 `json:"total_code"`
|
||||
MatchedCode uint64 `json:"matched_code"`
|
||||
TotalData uint64 `json:"total_data"`
|
||||
MatchedData uint64 `json:"matched_data"`
|
||||
TotalFunctions uint32 `json:"total_functions"`
|
||||
MatchedFunctions uint32 `json:"matched_functions"`
|
||||
Complete *bool `json:"complete,omitempty"`
|
||||
ModuleName *string `json:"module_name,omitempty"`
|
||||
ModuleID *uint32 `json:"module_id,omitempty"`
|
||||
Sections []legacyReportItem `json:"sections"`
|
||||
Functions []legacyReportItem `json:"functions"`
|
||||
}
|
||||
|
||||
type legacyReportItem struct {
|
||||
Name string `json:"name"`
|
||||
DemangledName *string `json:"demangled_name,omitempty"`
|
||||
Address *string `json:"address,omitempty"` // hex string
|
||||
Size uint64 `json:"size"`
|
||||
FuzzyMatchPercent float32 `json:"fuzzy_match_percent"`
|
||||
}
|
||||
|
||||
func (r *legacyReport) convert() *Report {
|
||||
report := &Report{
|
||||
FuzzyMatchPercent: r.FuzzyMatchPercent,
|
||||
TotalCode: r.TotalCode,
|
||||
MatchedCode: r.MatchedCode,
|
||||
MatchedCodePercent: r.MatchedCodePercent,
|
||||
TotalData: r.TotalData,
|
||||
MatchedData: r.MatchedData,
|
||||
MatchedDataPercent: r.MatchedDataPercent,
|
||||
TotalFunctions: r.TotalFunctions,
|
||||
MatchedFunctions: r.MatchedFunctions,
|
||||
MatchedFunctionsPercent: r.MatchedFunctionsPercent,
|
||||
Units: make([]*ReportUnit, 0, len(r.Units)),
|
||||
}
|
||||
for _, unit := range r.Units {
|
||||
report.Units = append(report.Units, unit.convert())
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func (u *legacyReportUnit) convert() *ReportUnit {
|
||||
unit := &ReportUnit{
|
||||
Name: u.Name,
|
||||
FuzzyMatchPercent: u.FuzzyMatchPercent,
|
||||
TotalCode: u.TotalCode,
|
||||
MatchedCode: u.MatchedCode,
|
||||
TotalData: u.TotalData,
|
||||
MatchedData: u.MatchedData,
|
||||
TotalFunctions: u.TotalFunctions,
|
||||
MatchedFunctions: u.MatchedFunctions,
|
||||
Complete: u.Complete,
|
||||
ModuleName: u.ModuleName,
|
||||
ModuleId: u.ModuleID,
|
||||
Sections: make([]*ReportItem, 0, len(u.Sections)),
|
||||
Functions: make([]*ReportItem, 0, len(u.Functions)),
|
||||
}
|
||||
for _, section := range u.Sections {
|
||||
unit.Sections = append(unit.Sections, section.convert())
|
||||
}
|
||||
for _, function := range u.Functions {
|
||||
unit.Functions = append(unit.Functions, function.convert())
|
||||
}
|
||||
return unit
|
||||
}
|
||||
|
||||
func (i *legacyReportItem) convert() *ReportItem {
|
||||
var address uint64
|
||||
if i.Address != nil {
|
||||
addressStr := *i.Address
|
||||
if strings.HasPrefix(addressStr, "0x") {
|
||||
address, _ = strconv.ParseUint(addressStr[2:], 16, 64)
|
||||
} else {
|
||||
address, _ = strconv.ParseUint(addressStr, 10, 64)
|
||||
}
|
||||
}
|
||||
return &ReportItem{
|
||||
Name: i.Name,
|
||||
DemangledName: i.DemangledName,
|
||||
Address: &address,
|
||||
Size: i.Size,
|
||||
FuzzyMatchPercent: i.FuzzyMatchPercent,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package objdiff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/encounter/decompal/zipstream"
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReportFile struct {
|
||||
Version string
|
||||
Sha string
|
||||
Report *Report
|
||||
}
|
||||
|
||||
var artifactNameRegex = regexp.MustCompile(`^(?P<version>[A-z0-9_\-]+)_report$`)
|
||||
|
||||
func FetchReportFiles(
|
||||
ctx context.Context,
|
||||
logger zerolog.Logger,
|
||||
client *github.Client,
|
||||
repoOwner, repoName, sha string,
|
||||
runId int64,
|
||||
) ([]ReportFile, error) {
|
||||
logger = logger.With().
|
||||
Str("commit_sha", sha).
|
||||
Int64("workflow_run_id", runId).
|
||||
Logger()
|
||||
|
||||
artifacts, _, err := client.Actions.ListWorkflowRunArtifacts(ctx, repoOwner, repoName, runId, nil)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("Failed to list workflow run artifacts")
|
||||
return nil, errors.Wrap(err, "failed to list workflow run artifacts")
|
||||
}
|
||||
|
||||
files := make([]ReportFile, 0)
|
||||
for _, artifact := range artifacts.Artifacts {
|
||||
logger := logger.With().
|
||||
Str("artifact_name", artifact.GetName()).
|
||||
Int64("artifact_id", artifact.GetID()).
|
||||
Logger()
|
||||
|
||||
matches := artifactNameRegex.FindStringSubmatch(artifact.GetName())
|
||||
if matches == nil {
|
||||
//logger.Debug().Msg("Skipping artifact")
|
||||
continue
|
||||
}
|
||||
version := matches[artifactNameRegex.SubexpIndex("version")]
|
||||
|
||||
artifactUrl, _, err := client.Actions.DownloadArtifact(ctx, repoOwner, repoName, artifact.GetID(), 3)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get artifact download url")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, artifactUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create download request")
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", client.UserAgent)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to download artifact")
|
||||
}
|
||||
|
||||
report, err := findReportFile(logger, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if report != nil {
|
||||
files = append(files, ReportFile{
|
||||
Version: version,
|
||||
Sha: sha,
|
||||
Report: report,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort files by version
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Version < files[j].Version
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// findReportFile reads the zip stream and writes the report file to the output path
|
||||
// Returns true if the report file was found and written
|
||||
func findReportFile(logger zerolog.Logger, r io.Reader) (*Report, error) {
|
||||
zr := zipstream.NewReader(r)
|
||||
for {
|
||||
entry, err := zr.Next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, errors.Wrap(err, "failed to get next entry")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(entry)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read report file")
|
||||
}
|
||||
if strings.HasSuffix(entry.Name, "report.json") {
|
||||
report := &Report{}
|
||||
err := parseJson(data, report)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read report file")
|
||||
}
|
||||
logger.Info().
|
||||
Str("filename", entry.Name).
|
||||
Msg("Read report file")
|
||||
return report, nil
|
||||
} else if strings.HasSuffix(entry.Name, "report.binpb") ||
|
||||
strings.HasSuffix(entry.Name, "report.pb") {
|
||||
report := &Report{}
|
||||
err = proto.Unmarshal(data, report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Info().
|
||||
Str("filename", entry.Name).
|
||||
Msg("Read report file")
|
||||
return report, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseJson(data []byte, v *Report) error {
|
||||
err := protojson.Unmarshal(data, v)
|
||||
if err != nil {
|
||||
// Try to parse as legacy report
|
||||
legacy := &legacyReport{}
|
||||
if other := json.Unmarshal(data, legacy); other != nil {
|
||||
// Return the original error
|
||||
return err
|
||||
}
|
||||
*v = *legacy.convert()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
// Package zipstream
|
||||
// A streaming zip reader that specifically supports GitHub Actions artifacts files.
|
||||
// The standard library's `archive/zip` uses the central directory at the end of the file,
|
||||
// so it would require downloading the entire file before reading it.
|
||||
// This package reads the local file headers and supports DEFLATE streams with an unknown size,
|
||||
// allowing us to read files as the response is being downloaded.
|
||||
package zipstream
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"compress/flate"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
headerIdentifierLen = 4
|
||||
fileHeaderLen = 26
|
||||
dataDescriptorLen = 16 // four uint32: descriptor signature, crc32, compressed size, size
|
||||
fileHeaderSignature = 0x04034b50
|
||||
directoryHeaderSignature = 0x02014b50
|
||||
directoryEndSignature = 0x06054b50
|
||||
dataDescriptorSignature = 0x08074b50
|
||||
zip64ExtraID = 0x0001 // Zip64 extended information
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
r io.Reader
|
||||
rBuf *bufio.Reader
|
||||
fr io.ReadCloser
|
||||
localFileEnd bool
|
||||
curEntry *Entry
|
||||
}
|
||||
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
return &Reader{
|
||||
r: r,
|
||||
fr: nil,
|
||||
}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
zip.FileHeader
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (e *Entry) hasDataDescriptor() bool {
|
||||
return e.Flags&8 != 0
|
||||
}
|
||||
|
||||
// IsDir just simply check whether the entry name ends with "/"
|
||||
func (e *Entry) IsDir() bool {
|
||||
return len(e.Name) > 0 && e.Name[len(e.Name)-1] == '/'
|
||||
}
|
||||
|
||||
func (e *Entry) Read(p []byte) (n int, err error) {
|
||||
return e.r.Read(p)
|
||||
}
|
||||
|
||||
//goland:noinspection GoDeprecation
|
||||
func (z *Reader) readEntry() (*Entry, error) {
|
||||
buf := make([]byte, fileHeaderLen)
|
||||
if _, err := io.ReadFull(z.r, buf); err != nil {
|
||||
return nil, fmt.Errorf("unable to read local file header: %w", err)
|
||||
}
|
||||
|
||||
lr := readBuf(buf)
|
||||
readerVersion := lr.uint16()
|
||||
flags := lr.uint16()
|
||||
method := lr.uint16()
|
||||
modifiedTime := lr.uint16()
|
||||
modifiedDate := lr.uint16()
|
||||
crc32Sum := lr.uint32()
|
||||
compressedSize := lr.uint32()
|
||||
uncompressedSize := lr.uint32()
|
||||
filenameLen := int(lr.uint16())
|
||||
extraAreaLen := int(lr.uint16())
|
||||
|
||||
entry := &Entry{
|
||||
FileHeader: zip.FileHeader{
|
||||
ReaderVersion: readerVersion,
|
||||
Flags: flags,
|
||||
Method: method,
|
||||
ModifiedTime: modifiedTime,
|
||||
ModifiedDate: modifiedDate,
|
||||
CRC32: crc32Sum,
|
||||
CompressedSize: compressedSize,
|
||||
UncompressedSize: uncompressedSize,
|
||||
CompressedSize64: uint64(compressedSize),
|
||||
UncompressedSize64: uint64(uncompressedSize),
|
||||
},
|
||||
r: nil,
|
||||
}
|
||||
|
||||
nameAndExtraBuf := make([]byte, filenameLen+extraAreaLen)
|
||||
if _, err := io.ReadFull(z.r, nameAndExtraBuf); err != nil {
|
||||
return nil, fmt.Errorf("unable to read entry name and extra area: %w", err)
|
||||
}
|
||||
|
||||
entry.Name = string(nameAndExtraBuf[:filenameLen])
|
||||
entry.Extra = nameAndExtraBuf[filenameLen:]
|
||||
|
||||
entry.NonUTF8 = flags&0x800 == 0
|
||||
if flags&1 == 1 {
|
||||
return nil, fmt.Errorf("encrypted ZIP entry not supported")
|
||||
}
|
||||
if flags&8 == 8 && method != zip.Deflate {
|
||||
return nil, fmt.Errorf("only DEFLATED entries can have data descriptor")
|
||||
}
|
||||
|
||||
needCSize := entry.CompressedSize == ^uint32(0)
|
||||
needUSize := entry.UncompressedSize == ^uint32(0)
|
||||
|
||||
ler := readBuf(entry.Extra)
|
||||
for len(ler) >= 4 { // need at least tag and size
|
||||
fieldTag := ler.uint16()
|
||||
fieldSize := int(ler.uint16())
|
||||
if len(ler) < fieldSize {
|
||||
break
|
||||
}
|
||||
fieldBuf := ler.sub(fieldSize)
|
||||
|
||||
switch fieldTag {
|
||||
case zip64ExtraID:
|
||||
// update directory values from the zip64 extra block.
|
||||
// They should only be consulted if the sizes read earlier
|
||||
// are maxed out.
|
||||
// See golang.org/issue/13367.
|
||||
if needUSize {
|
||||
needUSize = false
|
||||
if len(fieldBuf) < 8 {
|
||||
return nil, zip.ErrFormat
|
||||
}
|
||||
entry.UncompressedSize64 = fieldBuf.uint64()
|
||||
}
|
||||
if needCSize {
|
||||
needCSize = false
|
||||
if len(fieldBuf) < 8 {
|
||||
return nil, zip.ErrFormat
|
||||
}
|
||||
entry.CompressedSize64 = fieldBuf.uint64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needCSize {
|
||||
return nil, zip.ErrFormat
|
||||
}
|
||||
|
||||
if method == zip.Store {
|
||||
entry.r = io.LimitReader(z.r, int64(entry.UncompressedSize64))
|
||||
} else if method == zip.Deflate {
|
||||
var reader io.Reader
|
||||
if entry.CompressedSize64 > 0 {
|
||||
reader = io.LimitReader(z.r, int64(entry.CompressedSize64))
|
||||
} else {
|
||||
// unknown size; read until deflate EOF,
|
||||
// but we need z.r to be an io.ByteReader for flate to not overread
|
||||
if _, ok := z.r.(io.ByteReader); !ok {
|
||||
z.r = bufio.NewReader(z.r)
|
||||
}
|
||||
reader = z.r
|
||||
}
|
||||
if z.fr == nil {
|
||||
z.fr = flate.NewReader(reader)
|
||||
} else {
|
||||
z.fr.(flate.Resetter).Reset(reader, nil)
|
||||
}
|
||||
entry.r = z.fr
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown compression method %d", method)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (z *Reader) Next() (*Entry, error) {
|
||||
if z.localFileEnd {
|
||||
return nil, io.EOF
|
||||
}
|
||||
if z.curEntry != nil {
|
||||
// Read any remaining data for the current file, if necessary.
|
||||
if _, err := io.Copy(io.Discard, z.curEntry); err != nil {
|
||||
return nil, fmt.Errorf("read previous file data fail: %w", err)
|
||||
}
|
||||
// Read the data descriptor if present.
|
||||
if z.curEntry.hasDataDescriptor() {
|
||||
if err := readDataDescriptor(z.r); err != nil {
|
||||
return nil, fmt.Errorf("read previous entry's data descriptor fail: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
headerIDBuf := make([]byte, headerIdentifierLen)
|
||||
if _, err := io.ReadFull(z.r, headerIDBuf); err != nil {
|
||||
return nil, fmt.Errorf("unable to read header identifier: %w", err)
|
||||
}
|
||||
headerID := binary.LittleEndian.Uint32(headerIDBuf)
|
||||
if headerID != fileHeaderSignature {
|
||||
if headerID == directoryHeaderSignature || headerID == directoryEndSignature {
|
||||
z.localFileEnd = true
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, zip.ErrFormat
|
||||
}
|
||||
entry, err := z.readEntry()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read zip file header: %w", err)
|
||||
}
|
||||
z.curEntry = entry
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func readDataDescriptor(r io.Reader) error {
|
||||
var buf [dataDescriptorLen]byte
|
||||
// The spec says: "Although not originally assigned a
|
||||
// signature, the value 0x08074b50 has commonly been adopted
|
||||
// as a signature value for the data descriptor record.
|
||||
// Implementers should be aware that ZIP files may be
|
||||
// encountered with or without this signature marking data
|
||||
// descriptors and should account for either case when reading
|
||||
// ZIP files to ensure compatibility."
|
||||
//
|
||||
// dataDescriptorLen includes the size of the signature but
|
||||
// first read just those 4 bytes to see if it exists.
|
||||
_, err := io.ReadFull(r, buf[:4])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
off := 0
|
||||
maybeSig := readBuf(buf[:4])
|
||||
if maybeSig.uint32() != dataDescriptorSignature {
|
||||
// No data descriptor signature. Keep these four bytes.
|
||||
off += 4
|
||||
}
|
||||
_, err = io.ReadFull(r, buf[off:12])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
func (b *readBuf) uint8() uint8 {
|
||||
v := (*b)[0]
|
||||
*b = (*b)[1:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint16() uint16 {
|
||||
v := binary.LittleEndian.Uint16(*b)
|
||||
*b = (*b)[2:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint32() uint32 {
|
||||
v := binary.LittleEndian.Uint32(*b)
|
||||
*b = (*b)[4:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint64() uint64 {
|
||||
v := binary.LittleEndian.Uint64(*b)
|
||||
*b = (*b)[8:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) sub(n int) readBuf {
|
||||
b2 := (*b)[:n]
|
||||
*b = (*b)[n:]
|
||||
return b2
|
||||
}
|
||||
Reference in New Issue
Block a user