1

Initial commit

This commit is contained in:
2025-06-13 04:15:35 +04:00
commit 1c350b8dfb
14 changed files with 500 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dists
.env

35
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,35 @@
matrix:
platform:
- linux/amd64
- linux/arm64
labels:
platform: ${platform}
when:
branch: main
event: [push, pull_request, tag]
steps:
test-build:
image: docker:27-dind
commands:
- docker buildx build -t source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-${platform##linux/} .
volumes:
- /var/run/docker.sock:/var/run/docker.sock
when:
event: pull_request
build-to-registry:
image: docker:27-dind
environment:
CI_USER_PASSWORD:
from_secret: registry_token
commands:
- docker login -u ${CI_REPO_OWNER} -p $${CI_USER_PASSWORD} source.hodakov.me
- docker buildx build -t source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-${platform##linux/} .
- docker push source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-${platform##linux/}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
when:
event: [push, tag]

46
.woodpecker/tag.yaml Normal file
View File

@@ -0,0 +1,46 @@
when:
branch: main
event: [push, tag]
depends_on:
- build
steps:
tag latest:
image: docker:27-dind
environment:
CI_USER_PASSWORD:
from_secret: registry_token
commands:
- docker login -u ${CI_REPO_OWNER} -p $${CI_USER_PASSWORD} source.hodakov.me
- docker pull source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-amd64
- docker pull source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-arm64
- docker manifest create source.hodakov.me/hdkv/github-release:latest source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-amd64 source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-arm64
- docker manifest push source.hodakov.me/hdkv/github-release:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
when:
event: push
tag semver:
image: docker:27-dind
environment:
CI_USER_PASSWORD:
from_secret: registry_token
commands:
- set -eu
- docker login -u ${CI_REPO_OWNER} -p $${CI_USER_PASSWORD} source.hodakov.me
- source ./scripts/semver.sh
- echo "Creating manifest for $${DOCKER_EXACT_TAG}, $${DOCKER_MAJOR_TAG} and $${DOCKER_MINOR_TAG}"
- docker pull source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-amd64
- docker pull source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-arm64
- docker manifest create source.hodakov.me/hdkv/github-release:$${DOCKER_EXACT_TAG} source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-amd64 source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-arm64
- docker manifest create source.hodakov.me/hdkv/github-release:$${DOCKER_MINOR_TAG} source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-amd64 source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-arm64
- docker manifest create source.hodakov.me/hdkv/github-release:$${DOCKER_MAJOR_TAG} source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-amd64 source.hodakov.me/hdkv/github-release:${CI_COMMIT_SHA:0:10}-arm64
- docker manifest push source.hodakov.me/hdkv/github-release:$${DOCKER_EXACT_TAG}
- docker manifest push source.hodakov.me/hdkv/github-release:$${DOCKER_MINOR_TAG}
- docker manifest push source.hodakov.me/hdkv/github-release:$${DOCKER_MAJOR_TAG}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
when:
event: tag

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
ARG GOLANG_VERSION=1.24
FROM golang:${GOLANG_VERSION} as build
COPY . /src
WORKDIR /src
RUN go build ./cmd/github-release
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /src/github-release /bin/github-release
ENTRYPOINT ["/bin/github-release"]

View File

@@ -0,0 +1,43 @@
package main
import (
"context"
"os"
"source.hodakov.me/hdkv/github-release/lib/app"
"source.hodakov.me/hdkv/github-release/lib/github"
)
func main() {
ctx := context.Background()
app := app.Init(ctx)
app.Logger().Info("Started Github Releaser")
err := app.Settings().Populate()
if err != nil {
app.Logger().WithError(err).Fatal("Can't populate the settings")
}
gh := github.New(app)
if err = gh.Connect(); err != nil {
app.Logger().WithError(err).Fatal("Can't connect to Github")
}
app.Logger().Info("Connected to Github")
releaseID, err := gh.Release()
if err != nil {
app.Logger().WithError(err).Fatal("Can't create Github release")
}
err = gh.Upload(*releaseID)
if err != nil {
app.Logger().WithError(err).Fatal("Can't upload artifacts to Github release")
}
app.Logger().Info("Release created successfully!")
os.Exit(0)
}

16
go.mod Normal file
View File

@@ -0,0 +1,16 @@
module source.hodakov.me/hdkv/github-release
go 1.24.4
require (
github.com/caarlos0/env/v11 v11.3.1
github.com/google/go-github/v72 v72.0.0
github.com/sirupsen/logrus v1.9.3
golang.org/x/oauth2 v0.30.0
)
require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

29
go.sum Normal file
View File

@@ -0,0 +1,29 @@
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
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/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/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

42
lib/app/app.go Normal file
View File

@@ -0,0 +1,42 @@
package app
import (
"context"
"github.com/sirupsen/logrus"
"source.hodakov.me/hdkv/github-release/lib/settings"
)
type App struct {
ctx context.Context
logger *logrus.Logger
settings *settings.Settings
}
func (a *App) Context() context.Context {
return a.ctx
}
func (a *App) Logger() *logrus.Logger {
return a.logger
}
func (a *App) Settings() *settings.Settings {
return a.settings
}
func Init(ctx context.Context) *App {
app := new(App)
app.settings = new(settings.Settings)
app.logger = logrus.StandardLogger()
app.logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
app.ctx = ctx
return app
}

37
lib/github/connect.go Normal file
View File

@@ -0,0 +1,37 @@
package github
import (
"context"
"net/http"
"net/url"
"github.com/google/go-github/v72/github"
"golang.org/x/oauth2"
)
func (g *Github) Connect() error {
httpClient := new(http.Client)
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: g.app.Settings().APIKey})
tc := oauth2.NewClient(
context.WithValue(g.app.Context(), oauth2.HTTPClient, httpClient), ts,
)
client := github.NewClient(tc)
var err error
client.BaseURL, err = url.Parse("https://api.github.com/")
if err != nil {
return err
}
client.UploadURL, err = url.Parse("https://uploads.github.com/")
if err != nil {
return err
}
g.client = client
return nil
}

17
lib/github/github.go Normal file
View File

@@ -0,0 +1,17 @@
package github
import (
"github.com/google/go-github/v72/github"
"source.hodakov.me/hdkv/github-release/lib/app"
)
type Github struct {
app *app.App
client *github.Client
}
func New(app *app.App) *Github {
return &Github{
app: app,
}
}

32
lib/github/release.go Normal file
View File

@@ -0,0 +1,32 @@
package github
import (
"github.com/google/go-github/v72/github"
)
func (g *Github) Release() (*int64, error) {
relInfo := &github.RepositoryRelease{
Name: &g.app.Settings().Title,
Body: &g.app.Settings().Description,
Draft: &g.app.Settings().Draft,
TagName: &g.app.Settings().Tag,
}
if *relInfo.Draft {
g.app.Logger().Info("Release will be created as draft (unpublished) release")
} else {
g.app.Logger().Info("Release will be created and published")
}
release, _, err := g.client.Repositories.CreateRelease(
g.app.Context(), g.app.Settings().Owner, g.app.Settings().Repo, relInfo)
if err != nil {
g.app.Logger().WithError(err).Error("Failed to create release")
return nil, err
}
g.app.Logger().WithField("release-id", *release.ID).Info("Successfully created release")
return release.ID, nil
}

83
lib/github/upload.go Normal file
View File

@@ -0,0 +1,83 @@
package github
import (
"fmt"
"os"
"github.com/google/go-github/v72/github"
)
func (g *Github) Upload(releaseID int64) error {
var assets []*github.ReleaseAsset
listOpts := &github.ListOptions{}
for {
asset, resp, err := g.client.Repositories.ListReleaseAssets(
g.app.Context(), g.app.Settings().Owner, g.app.Settings().Repo,
releaseID, listOpts,
)
if err != nil {
g.app.Logger().WithError(err).Error("Failed to fetch existing assets")
return err
}
assets = append(assets, asset...)
if resp.NextPage == 0 {
break
}
listOpts.Page = resp.NextPage
}
uploadFiles, err := os.ReadDir(g.app.Settings().Storage)
if err != nil {
g.app.Logger().WithError(err).Error("Failed to read assets storage")
}
for _, file := range uploadFiles {
if !file.IsDir() {
for _, asset := range assets {
if file.Name() == *asset.Name {
g.app.Logger().WithField("name", file.Name()).Error("File exists")
return fmt.Errorf("file exists: %s", file.Name())
}
}
}
}
for _, file := range uploadFiles {
if file.IsDir() {
continue
}
fileReader, err := os.Open(
fmt.Sprintf("%s/%s", g.app.Settings().Storage, file.Name()),
)
if err != nil {
g.app.Logger().
WithError(err).WithField("name", file.Name()).
Error("Failed to read artifact")
return err
}
uploadOptions := &github.UploadOptions{Name: file.Name()}
if _, _, err = g.client.Repositories.UploadReleaseAsset(
g.app.Context(), g.app.Settings().Owner, g.app.Settings().Repo,
releaseID, uploadOptions, fileReader); err != nil {
g.app.Logger().
WithError(err).WithField("name", file.Name()).
Error("Failed to upload artifact")
return err
}
g.app.Logger().WithField("name", file.Name()).Info("Successfully uploaded artifact")
}
return nil
}

20
lib/settings/settings.go Normal file
View File

@@ -0,0 +1,20 @@
package settings
import (
"github.com/caarlos0/env/v11"
)
type Settings struct {
APIKey string `env:"RELEASER_API_KEY,required"`
Owner string `env:"RELEASER_OWNER,required"`
Repo string `env:"RELEASER_REPO,required"`
Tag string `env:"RELEASER_TAG,required"`
Draft bool `env:"RELEASER_DRAFT,required"`
Title string `env:"RELEASER_TITLE,required"`
Description string `env:"RELEASER_DESCRIPTION,required"`
Storage string `env:"RELEASER_STORAGE,required"`
}
func (s *Settings) Populate() error {
return env.Parse(s)
}

83
scripts/semver.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/bin/bash -eu
# This script prints out git metadata information for a given commit (or head by default)
# It assumes tags of the form `v1.2.3`, and will print prerelease versions when
# not on an exact tag.
#
# It is intended to be used like so:
#
# cd <some-git-repo>
# $(../semver-from-git.sh)
#
# or
#
# $(../semver-from-git.sh <COMMIT_HASH>)
#
# For prerelease, it prints:
#
# export GIT_BRANCH="master"
# export GIT_SEMVER_FROM_TAG="1.0.1-master+3.ge675710"
#
# where GIT_BRANCH is the first branch it finds with the given commit, and the version string is:
# <MAJOR>.<MINOR>.<PATCH>-<BRANCH>+<COMMITS-SINCE-TAG>.<COMMIT_HASH>
#
# When on an exact tag, it prints:
#
# export GIT_BRANCH="master"
# export GIT_EXACT_TAG=v1.0.2
# export GIT_SEMVER_FROM_TAG=1.0.2
#
# When the working tree is dirty, it will also put ".SNAPSHOT.<HOSTNAME>" on the end of the version
# This is useful if someone has deployed something from their local machine.
# (C) Timothy Jones, 2019, released under BSD License 2.0 (3-clause BSD license)
# https://github.com/TimothyJones
if [ -z "${1:-}" ]; then
COMMIT="HEAD"
# Test whether the working tree is dirty or not
if [ -z "$(git status -s)" ]; then
STATUS=""
else
STATUS=".SNAPSHOT.$(hostname -s)"
fi
else
COMMIT="$1"
STATUS="" # When looking at an exact commit, the working tree is irrelevant
fi
DESCRIBE=$(git describe --always --tags "$COMMIT")
VERSION=$(echo "$DESCRIBE" | sed 's/\(.*\)-\(.*\)-g\(.*\)/\1+\2.\3/' | sed 's/v\(.*\)/\1/')
BRANCH=$(git branch --contains "$COMMIT" | grep -e "^\*" | sed 's/^\* //')
echo "export GIT_BRANCH=$BRANCH"
export GIT_BRANCH=$BRANCH
EXACT_TAG=$(git describe --always --exact-match --tags "$COMMIT" 2> /dev/null || true)
if [ ! -z "$EXACT_TAG" ] ; then
echo "export GIT_EXACT_TAG=${EXACT_TAG}"
echo "export GIT_SEMVER_FROM_TAG=$VERSION$STATUS"
export GIT_EXACT_TAG=${EXACT_TAG}
export GIT_SEMVER_FROM_TAG=$VERSION$STATUS
else
# We split up the prefix and suffix so that we don't accidentally
# give sed commands via the branch name
PREFIX="$(echo "$VERSION" | sed 's/\(.*\)\(\+.*\)/\1/')"
SUFFIX="$(echo "$VERSION" | sed 's/\(.*\)\(\+.*\)/\2/')"
if [ -z $(echo "$PREFIX" | grep "-") ]; then
echo "export GIT_SEMVER_FROM_TAG=$PREFIX-$BRANCH""$SUFFIX$STATUS"
export GIT_SEMVER_FROM_TAG=$PREFIX-$BRANCH""$SUFFIX$STATUS
else
echo "export GIT_SEMVER_FROM_TAG=$PREFIX.$BRANCH""$SUFFIX$STATUS"
export GIT_SEMVER_FROM_TAG=$PREFIX.$BRANCH""$SUFFIX$STATUS
fi
fi
### Add docker tags
echo "export DOCKER_EXACT_TAG=${GIT_EXACT_TAG/v}"
export DOCKER_EXACT_TAG=${GIT_EXACT_TAG/v}
echo "export DOCKER_MAJOR_TAG=${DOCKER_EXACT_TAG%.*.*}"
export DOCKER_MAJOR_TAG=${DOCKER_EXACT_TAG%.*.*}
echo "export DOCKER_MINOR_TAG=${DOCKER_EXACT_TAG%.*}"
export DOCKER_MINOR_TAG=${DOCKER_EXACT_TAG%.*}