1 Commits
Author SHA1 Message Date
paul 5e0d7837f1 Nix CI tryouts
Build image / build-image (push) Failing after 16s
2025-10-06 01:43:05 +02:00
5 changed files with 162 additions and 288 deletions
+15 -14
View File
@@ -6,35 +6,36 @@ on:
jobs:
build-image:
container:
image: node:24-bookworm
image: catthehacker/ubuntu:act-latest
env:
USER: node
HOME: /home/node
NIX_CONFIG: experimental-features = nix-command flakes
volumes:
- /var/lib/pbri/docker/nix_runner_nix:/nix
- /var/lib/pbri/docker/nix_runner_etc:/etc/nix
- /var/lib/pbri/docker/nix_runner_home_node:/home/node
- /home/paul/nix-ci/nix-runner-nix:/nix
- /home/paul/nix-ci/nix-runner-etc-nix:/etc/nix
- /home/paul/nix-ci/nix-runner-home-node:/home/node
options: --user 1000:1000
steps:
- name: Prep nix env
run: |
- run: |
cat /var/run/act/workflow/0.sh
if [ ! -f ~/.nix-profile/etc/profile.d/nix.sh ]; then
curl -L https://nixos.org/nix/install | bash -s -- --no-daemon
fi
. ~/.nix-profile/etc/profile.d/nix.sh
env >> "$GITHUB_ENV"
- name: Checkout repo
uses: actions/checkout@v4
- name: Get image meta
run: nix build .#image-meta -o image-meta
run: |
. ~/.nix-profile/etc/profile.d/nix.sh
nix build .#image-meta -o image-meta
- name: Version check
run: |
VERSION=$(cat image-meta/version)
[ "$GITHUB_REF_NAME" = v"$VERSION" ]
- run: nix build --log-format raw --print-build-logs .#image -o image.tar.gz
- run: gunzip -c image.tar.gz > image.tar
- run: nix run .#crane -- auth login git.pbrinkmeier.de -u paul -p "$PASSWORD"
- run: |
. ~/.nix-profile/etc/profile.d/nix.sh
nix build --log-format raw --print-build-logs .#image -o image.tar.gz
- run: docker login -u paul -p "$PASSWORD" git.pbrinkmeier.de
env:
PASSWORD: "${{ secrets.REGISTRY_PASSWORD }}"
- run: nix run .#crane -- push image.tar $(cat image-meta/name)
- run: docker load < image.tar.gz
- run: docker image push $(cat image-meta/name)
-4
View File
@@ -1,4 +0,0 @@
.PHONY: format
format:
gofmt -w $$(git ls-files '*.go')
+1 -231
View File
@@ -1,233 +1,19 @@
package main
import "cmp"
import "encoding/json"
import "errors"
import "fmt"
import "net/http"
import "net/url"
import "slices"
import "strconv"
import "strings"
import "time"
type EFAClient interface {
GetName() string
BuildRequest(string) (*http.Request, error)
ParseResponse(*http.Response) (Departures, error)
}
func FetchDepartures(c EFAClient, stopId string) (Departures, error) {
req, err := c.BuildRequest(stopId)
if err != nil {
return Departures{}, err
}
// Send the request, wait max 10 seconds
client := http.Client{
Timeout: 10 * time.Second,
}
res, err := client.Do(req)
if err != nil {
return Departures{}, err
}
return c.ParseResponse(res)
}
// JSON unmarshaling types for departure monitor API
type DMResponse struct {
DateTime DMDateTime `json:"dateTime"`
DepartureList []DMDeparture `json:"departureList"`
}
type DMDeparture struct {
StopName string `json:"stopName"`
Platform string `json:"platform"`
Countdown string `json:"countdown"`
DateTime DMDateTime `json:"dateTime"`
RealDateTime *DMDateTime `json:"realDateTime"`
ServingLine DMServingLine `json:"servingLine"`
OnwardStopSeq []DMStop `json:"onwardStopSeq"`
}
type DMServingLine struct {
Symbol string `json:"symbol"`
Direction string `json:"direction"`
}
type DMStop struct {
PlaceID string `json:"placeID"`
Place string `json:"place"`
NameWO string `json:"nameWO"`
}
type DMDateTime struct {
Hour string `json:"hour"`
Minute string `json:"minute"`
}
// Types for JSON marshaling
type Departures struct {
Departures []Departure `json:"departures"`
ServerTime string `json:"serverTime"`
}
type Departure struct {
Platform string `json:"platform"`
Symbol string `json:"symbol"`
Direction string `json:"direction"`
Leaving string `json:"leaving"`
Countdown int `json:"-"`
}
func ParseDMResponse(res *http.Response) (Departures, error) {
defer res.Body.Close()
var dmResponse DMResponse
err := json.NewDecoder(res.Body).Decode(&dmResponse)
if err != nil {
return Departures{}, err
}
return parseDepartures(dmResponse)
}
func parseDepartures(response DMResponse) (Departures, error) {
var ds []Departure
for _, d := range response.DepartureList {
direction := d.ServingLine.Direction
// If available, use name of last stop as direction
// Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set.
if len(d.OnwardStopSeq) != 0 {
last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1]
if slices.Contains([]string{"5", "6"}, last.PlaceID) {
direction = last.NameWO
}
}
leaving := fmt.Sprintf("%s min", d.Countdown)
countdown, err := strconv.Atoi(d.Countdown)
if err != nil {
return Departures{}, errors.New("could not parse countdown")
}
isInThePast := countdown < 0
if isInThePast {
continue
}
if countdown == 0 {
leaving = "sofort"
}
if countdown > 20 {
dt := d.DateTime
if d.RealDateTime != nil {
dt = *d.RealDateTime
}
leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute)
}
ds = append(ds, Departure{
d.Platform,
d.ServingLine.Symbol,
direction,
leaving,
countdown,
})
}
slices.SortFunc(ds, func(a Departure, b Departure) int {
return cmp.Compare(a.Countdown, b.Countdown)
})
dt := response.DateTime
return Departures{
ds,
fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute),
}, nil
}
var allEfaClients []EFAClient = []EFAClient{
IVBEFAClient{},
BwegtEFAClient{},
VRNEFAClient{},
KVVEFAClient{},
VAGEFAClient{},
VRNEFAClient{},
}
type IVBEFAClient struct {
}
func (c IVBEFAClient) GetName() string {
return "IVB"
}
func (c IVBEFAClient) BuildRequest(stopId string) (*http.Request, error) {
// curl https://smartinfo.ivb.at/api/JSON/PASSAGE'?stopID=64003&count=3'
req, err := http.NewRequest("GET", "https://smartinfo.ivb.at/api/JSON/PASSAGE", nil)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("stopID", stopId)
req.URL.RawQuery = query.Encode()
return req, nil
}
func (c IVBEFAClient) ParseResponse(res *http.Response) (Departures, error) {
defer res.Body.Close()
var response []IVBResponseElem
err := json.NewDecoder(res.Body).Decode(&response)
if err != nil {
return Departures{}, err
}
var ds []Departure
for _, responseElem := range response {
if responseElem.Smartinfo == nil {
continue
}
si := responseElem.Smartinfo
leaving := si.Time
if leaving == "0 min" {
leaving = "sofort"
}
ds = append(ds, Departure{
si.Plabel,
si.Route,
si.Direction,
leaving,
// TODO
0,
})
}
return Departures{
ds,
fmt.Sprintf("%02d:%02d", 0, 0),
}, nil
}
// IVB smartinfo API unmarshaling types
type IVBResponseElem struct {
Smartinfo *IVBSmartInfo `json:"smartinfo"`
}
type IVBSmartInfo struct {
Uid string `json:"uid"`
Route string `json:"route"`
Direction string `json:"direction"`
Time string `json:"time"`
Plabel string `json:"plabel"`
}
type VRNEFAClient struct {
@@ -262,10 +48,6 @@ func (c VRNEFAClient) BuildRequest(stopId string) (*http.Request, error) {
return req, nil
}
func (c VRNEFAClient) ParseResponse(res *http.Response) (Departures, error) {
return ParseDMResponse(res)
}
type KVVEFAClient struct {
}
@@ -296,10 +78,6 @@ func (c KVVEFAClient) BuildRequest(stopId string) (*http.Request, error) {
return req, nil
}
func (c KVVEFAClient) ParseResponse(res *http.Response) (Departures, error) {
return ParseDMResponse(res)
}
type BwegtEFAClient struct {
}
@@ -332,10 +110,6 @@ func (c BwegtEFAClient) BuildRequest(stopId string) (*http.Request, error) {
return req, nil
}
func (c BwegtEFAClient) ParseResponse(res *http.Response) (Departures, error) {
return ParseDMResponse(res)
}
type VAGEFAClient struct {
}
@@ -367,7 +141,3 @@ func (c VAGEFAClient) BuildRequest(stopId string) (*http.Request, error) {
return req, nil
}
func (c VAGEFAClient) ParseResponse(res *http.Response) (Departures, error) {
return ParseDMResponse(res)
}
+3 -16
View File
@@ -16,22 +16,15 @@
vrnp-static = pkgs.buildGoModule {
pname = "vrnp";
version = "0.0.11";
version = "0.0.10";
vendorHash = null;
# For building the package, we use only the files not ignored by Git as inputs.
# Also, flake.nix, flake.lock and .gitea are not included to avoid annoying rebuilds when
# working on them.
src = pkgs.lib.cleanSourceWith {
src = ./.;
filter = gitignore.lib.gitignoreFilterWith {
basePath = ./.;
extraRules = ''
flake.nix
flake.lock
.gitea
'';
};
src = gitignore.lib.gitignoreSource ./.;
filter = path: type: builtins.baseNameOf path != "flake.nix" && builtins.baseNameOf path != "flake.lock" && builtins.match "^\\.gitea.*" path != null;
};
# Avoid linking against libc
@@ -62,12 +55,6 @@
config.Cmd = [ "${vrnp-static}/bin/vrnp" ];
};
};
apps = {
crane = {
type = "app";
program = "${pkgs.crane}/bin/crane";
};
};
}
);
}
+142 -22
View File
@@ -1,13 +1,148 @@
package main
import "cmp"
import "encoding/json"
import "errors"
import "fmt"
import "log"
import "net/http"
import "os"
import "slices"
import "strings"
import "strconv"
import "sync/atomic"
import "time"
// JSON unmarshaling types for departure monitor API
type DMResponse struct {
DateTime DMDateTime `json:"dateTime"`
DepartureList []DMDeparture `json:"departureList"`
}
type DMDeparture struct {
StopName string `json:"stopName"`
Platform string `json:"platform"`
Countdown string `json:"countdown"`
DateTime DMDateTime `json:"dateTime"`
RealDateTime *DMDateTime `json:"realDateTime"`
ServingLine DMServingLine `json:"servingLine"`
OnwardStopSeq []DMStop `json:"onwardStopSeq"`
}
type DMServingLine struct {
Symbol string `json:"symbol"`
Direction string `json:"direction"`
}
type DMStop struct {
PlaceID string `json:"placeID"`
Place string `json:"place"`
NameWO string `json:"nameWO"`
}
type DMDateTime struct {
Hour string `json:"hour"`
Minute string `json:"minute"`
}
func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) {
req, err := c.BuildRequest(stopId)
if err != nil {
return DMResponse{}, err
}
// Send the request, wait max 10 seconds
client := http.Client{
Timeout: 10 * time.Second,
}
res, err := client.Do(req)
if err != nil {
return DMResponse{}, err
}
defer res.Body.Close()
var dmResponse DMResponse
err = json.NewDecoder(res.Body).Decode(&dmResponse)
if err != nil {
return DMResponse{}, err
}
return dmResponse, nil
}
// Types for JSON marshaling
type Departures struct {
Departures []Departure `json:"departures"`
ServerTime string `json:"serverTime"`
}
type Departure struct {
Platform string `json:"platform"`
Symbol string `json:"symbol"`
Direction string `json:"direction"`
Leaving string `json:"leaving"`
Countdown int `json:"-"`
}
func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, error) {
var ds []Departure
for _, d := range response.DepartureList {
direction := d.ServingLine.Direction
// If available, use name of last stop as direction
// Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set.
if len(d.OnwardStopSeq) != 0 {
last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1]
if slices.Contains([]string{"5", "6"}, last.PlaceID) {
direction = last.NameWO
}
}
leaving := fmt.Sprintf("%s min", d.Countdown)
countdown, err := strconv.Atoi(d.Countdown)
if err != nil {
return Departures{}, errors.New("could not parse countdown")
}
matchesPlatform := allowedPlatform == nil || d.Platform == *allowedPlatform
isInThePast := countdown < 0
if !matchesPlatform || isInThePast {
continue
}
if countdown == 0 {
leaving = "sofort"
}
if countdown > 20 {
dt := d.DateTime
if d.RealDateTime != nil {
dt = *d.RealDateTime
}
leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute)
}
ds = append(ds, Departure{
d.Platform,
d.ServingLine.Symbol,
direction,
leaving,
countdown,
})
}
slices.SortFunc(ds, func(a Departure, b Departure) int {
return cmp.Compare(a.Countdown, b.Countdown)
})
dt := response.DateTime
return Departures{
ds,
fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute),
}, nil
}
func main() {
password := os.Getenv("VRNP_PASSWORD")
@@ -48,14 +183,17 @@ func main() {
}
c := efaClients[(efaClient.Add(1) - 1) % uint64(len(efaClients))]
departures, err := FetchDepartures(c, stopId)
ds, err := FetchDepartures(c, stopId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// TODO: Filter for platform here
filterAndLimitDepartures(&departures, platform, 8)
departures, err := ParseDepartures(ds, platform)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Does not handle multiple media types
if r.Header.Get("Accept") == "application/json" {
@@ -65,7 +203,7 @@ func main() {
// plain text
for _, d := range departures.Departures {
fmt.Fprintf(w, "%2s %-11s %6s\n",
d.Symbol[:min(2, len(d.Symbol))],
d.Symbol,
d.Direction[:min(11, len(d.Direction))],
d.Leaving,
)
@@ -75,21 +213,3 @@ func main() {
})
log.Fatal(http.ListenAndServe(":8000", nil))
}
func filterAndLimitDepartures(departures *Departures, platform *string, limit int) {
oldDepartures := departures.Departures
departures.Departures = []Departure{}
for _, departure := range oldDepartures {
if len(departures.Departures) >= limit {
break
}
isPlatformMatch := platform == nil || departure.Platform == *platform
if !isPlatformMatch {
continue
}
departures.Departures = append(departures.Departures, departure)
}
}