Add Gitea CI and release pipeline
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
- "release-*"
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]+*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: "1.26.0"
|
||||||
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||||
|
ANDROID_NDK_ROOT: /opt/android-ndk
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-25-openjdk
|
||||||
|
DIST_DIR: dist
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-test:
|
||||||
|
runs-on:
|
||||||
|
- self-hosted
|
||||||
|
- linux
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
state_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$state_dir"' EXIT
|
||||||
|
KEEPASSGO_STATE_DIR="$state_dir" go tool golangci-lint run
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
state_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$state_dir"' EXIT
|
||||||
|
KEEPASSGO_STATE_DIR="$state_dir" go test ./...
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: lint-test
|
||||||
|
runs-on:
|
||||||
|
- self-hosted
|
||||||
|
- linux
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Prepare dist directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "${DIST_DIR}"
|
||||||
|
|
||||||
|
- name: Build desktop binaries
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for target in \
|
||||||
|
"linux amd64" \
|
||||||
|
"linux arm64" \
|
||||||
|
"windows amd64" \
|
||||||
|
"windows arm64"
|
||||||
|
do
|
||||||
|
set -- ${target}
|
||||||
|
goos="$1"
|
||||||
|
goarch="$2"
|
||||||
|
ext=""
|
||||||
|
if [[ "${goos}" == "windows" ]]; then
|
||||||
|
ext=".exe"
|
||||||
|
fi
|
||||||
|
out="${DIST_DIR}/keepassgo-${goos}-${goarch}${ext}"
|
||||||
|
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -o "${out}" .
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build APK
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
make apk
|
||||||
|
cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
|
||||||
|
|
||||||
|
- name: Upload CI artifacts
|
||||||
|
uses: christopherhx/gitea-upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: keepassgo-${{ gitea.sha }}
|
||||||
|
path: |
|
||||||
|
dist/keepassgo-linux-amd64
|
||||||
|
dist/keepassgo-linux-arm64
|
||||||
|
dist/keepassgo-windows-amd64.exe
|
||||||
|
dist/keepassgo-windows-arm64.exe
|
||||||
|
dist/keepassgo.apk
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Publish release artifacts
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 scripts/gitea_release.py \
|
||||||
|
--server "${GITEA_SERVER_URL}" \
|
||||||
|
--repo "${GITEA_REPOSITORY}" \
|
||||||
|
--token "${GITEA_TOKEN}" \
|
||||||
|
--tag "${GITEA_REF_NAME}" \
|
||||||
|
"${DIST_DIR}/keepassgo-linux-amd64" \
|
||||||
|
"${DIST_DIR}/keepassgo-linux-arm64" \
|
||||||
|
"${DIST_DIR}/keepassgo-windows-amd64.exe" \
|
||||||
|
"${DIST_DIR}/keepassgo-windows-arm64.exe" \
|
||||||
|
"${DIST_DIR}/keepassgo.apk"
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
def request_json(method: str, url: str, token: str, payload: dict | None = None) -> dict:
|
||||||
|
data = None
|
||||||
|
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
|
||||||
|
if payload is not None:
|
||||||
|
data = json.dumps(payload).encode()
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return json.loads(resp.read().decode())
|
||||||
|
|
||||||
|
|
||||||
|
def request_no_content(method: str, url: str, token: str) -> None:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/json"},
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_release(server: str, repo: str, token: str, tag: str) -> dict:
|
||||||
|
base = f"{server.rstrip('/')}/api/v1/repos/{repo}"
|
||||||
|
tag_url = f"{base}/releases/tags/{urllib.parse.quote(tag, safe='')}"
|
||||||
|
try:
|
||||||
|
return request_json("GET", tag_url, token)
|
||||||
|
except urllib.error.HTTPError as err:
|
||||||
|
if err.code != 404:
|
||||||
|
raise
|
||||||
|
payload = {
|
||||||
|
"tag_name": tag,
|
||||||
|
"name": tag,
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": False,
|
||||||
|
}
|
||||||
|
return request_json("POST", f"{base}/releases", token, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_asset(server: str, repo: str, token: str, release: dict, path: pathlib.Path) -> None:
|
||||||
|
base = f"{server.rstrip('/')}/api/v1/repos/{repo}"
|
||||||
|
assets = release.get("assets", [])
|
||||||
|
for asset in assets:
|
||||||
|
if asset.get("name") == path.name:
|
||||||
|
request_no_content("DELETE", f"{base}/releases/{release['id']}/assets/{asset['id']}", token)
|
||||||
|
query = urllib.parse.urlencode({"name": path.name})
|
||||||
|
url = f"{base}/releases/{release['id']}/assets?{query}"
|
||||||
|
content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=path.read_bytes(),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": content_type,
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--server", required=True)
|
||||||
|
parser.add_argument("--repo", required=True)
|
||||||
|
parser.add_argument("--token", required=True)
|
||||||
|
parser.add_argument("--tag", required=True)
|
||||||
|
parser.add_argument("artifacts", nargs="+")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
paths = [pathlib.Path(p) for p in args.artifacts]
|
||||||
|
missing = [str(p) for p in paths if not p.is_file()]
|
||||||
|
if missing:
|
||||||
|
print(f"missing artifacts: {', '.join(missing)}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
release = get_or_create_release(args.server, args.repo, args.token, args.tag)
|
||||||
|
for path in paths:
|
||||||
|
upload_asset(args.server, args.repo, args.token, release, path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user