+28
.woodpecker/build_steam.yml
+28
.woodpecker/build_steam.yml
···
1
+
when:
2
+
- event: push
3
+
branch: main
4
+
5
+
steps:
6
+
- name: Build plugin
7
+
image: docker
8
+
commands:
9
+
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD ghcr.io
10
+
- docker build -t ghcr.io/bigaston/steamci:latest steamci
11
+
- docker push ghcr.io/bigaston/steamci:latest
12
+
13
+
# Codeberg
14
+
- docker login -u $DOCKER_CODEBERG_USERNAME -p $DOCKER_CODEBERG_PASSWORD codeberg.org
15
+
- docker image tag ghcr.io/bigaston/steamci:latest codeberg.org/bigaston/steamci:latest
16
+
- docker push codeberg.org/bigaston/steamci:latest
17
+
18
+
volumes:
19
+
- /var/run/docker.sock:/var/run/docker.sock
20
+
environment:
21
+
DOCKER_USERNAME:
22
+
from_secret: DOCKER_USERNAME
23
+
DOCKER_PASSWORD:
24
+
from_secret: DOCKER_PASSWORD
25
+
DOCKER_CODEBERG_USERNAME:
26
+
from_secret: DOCKER_CODEBERG_USERNAME
27
+
DOCKER_CODEBERG_PASSWORD:
28
+
from_secret: DOCKER_CODEBERG_PASSWORD
+14
-16
examples/build_steam.yml
+14
-16
examples/build_steam.yml
···
24
24
- godot --headless --export-release ${TARGET}
25
25
26
26
- name: Upload Steam
27
-
image: debian
28
-
commands:
29
-
- apt update
30
-
- apt install -y lib32gcc-s1 curl tar
31
-
32
-
- curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar zxvf -
33
-
- chmod +x steamcmd.sh
34
-
35
-
- mkdir -p /root/Steam/config
36
-
- echo "$STEAM_AUTH_VDF" | base64 -d > /root/Steam/config/config.vdf
37
-
- chmod 777 /root/Steam/config/config.vdf
38
-
39
-
- ./steamcmd.sh +login username +quit
40
-
- ./steamcmd.sh +login username +run_app_build support/steam/upload_config/upload_${TARGET}.vdf +quit
41
-
environment:
42
-
STEAM_AUTH_VDF:
27
+
image: ghcr.io/bigaston/steamci
28
+
settings:
29
+
steam_username: something_deploy
30
+
steam_auth_vdf:
43
31
from_secret: STEAM_AUTH_VDF
32
+
app_id: 000
33
+
content_root: ".build"
34
+
depot_id:
35
+
Windows: 000
36
+
Linux: 001
37
+
local_path:
38
+
Windows: "Windows/*"
39
+
Linux: "Linux/*"
40
+
matrix: ${TARGET}
41
+
set_live: "beta"
44
42
45
43
- name: Discord Notification Success
46
44
image: ghcr.io/bigaston/discordnotification
+62
examples/build_steam_old.yml
+62
examples/build_steam_old.yml
···
1
+
when:
2
+
- event: push
3
+
branch: main
4
+
5
+
matrix:
6
+
TARGET:
7
+
- Linux
8
+
- Windows
9
+
10
+
steps:
11
+
- name: Build the game for ${TARGET}
12
+
image: ghcr.io/bigaston/godotci:4.5-beta3
13
+
commands:
14
+
- godot --version
15
+
16
+
- "> commit.txt"
17
+
- short_commit=$(git rev-parse --short HEAD)
18
+
- echo "$short_commit" > commit.txt
19
+
20
+
- godot --headless --editor --import
21
+
22
+
- mkdir -p .build/${TARGET}
23
+
24
+
- godot --headless --export-release ${TARGET}
25
+
26
+
- name: Upload Steam
27
+
image: debian
28
+
commands:
29
+
- apt update
30
+
- apt install -y lib32gcc-s1 curl tar
31
+
32
+
- curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar zxvf -
33
+
- chmod +x steamcmd.sh
34
+
35
+
- mkdir -p /root/Steam/config
36
+
- echo "$STEAM_AUTH_VDF" | base64 -d > /root/Steam/config/config.vdf
37
+
- chmod 777 /root/Steam/config/config.vdf
38
+
39
+
- ./steamcmd.sh +login username +quit
40
+
- ./steamcmd.sh +login username +run_app_build support/steam/upload_config/upload_${TARGET}.vdf +quit
41
+
environment:
42
+
STEAM_AUTH_VDF:
43
+
from_secret: STEAM_AUTH_VDF
44
+
45
+
- name: Discord Notification Success
46
+
image: ghcr.io/bigaston/discordnotification
47
+
settings:
48
+
result: success
49
+
matrix: ${TARGET}
50
+
webhook:
51
+
from_secret: WEBHOOK
52
+
when:
53
+
- status: ["success"]
54
+
- name: Discord Notification Failure
55
+
image: ghcr.io/bigaston/discordnotification
56
+
settings:
57
+
result: failure
58
+
matrix: ${TARGET}
59
+
webhook:
60
+
from_secret: WEBHOOK
61
+
when:
62
+
- status: ["failure"]
+28
steamci/.gitignore
+28
steamci/.gitignore
···
1
+
# ---> Go
2
+
# If you prefer the allow list template instead of the deny list, see community template:
3
+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
4
+
#
5
+
# Binaries for programs and plugins
6
+
*.exe
7
+
*.exe~
8
+
*.dll
9
+
*.so
10
+
*.dylib
11
+
12
+
# Test binary, built with `go test -c`
13
+
*.test
14
+
15
+
# Output of the go coverage tool, specifically when used with LiteIDE
16
+
*.out
17
+
18
+
# Dependency directories (remove the comment below to include it)
19
+
# vendor/
20
+
21
+
# Go workspace file
22
+
go.work
23
+
go.work.sum
24
+
25
+
# env file
26
+
.env
27
+
28
+
steamci
+21
steamci/.woodpecker/build.yml
+21
steamci/.woodpecker/build.yml
···
1
+
when:
2
+
- event: push
3
+
branch: main
4
+
5
+
steps:
6
+
- name: Build SteamCI
7
+
image: docker
8
+
commands:
9
+
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY
10
+
- docker build -t $DOCKER_REGISTRY/${CI_REPO_OWNER,,}/${CI_REPO_NAME,,}:latest .
11
+
- docker push $DOCKER_REGISTRY/${CI_REPO_OWNER,,}/${CI_REPO_NAME,,}:latest
12
+
13
+
volumes:
14
+
- /var/run/docker.sock:/var/run/docker.sock
15
+
environment:
16
+
DOCKER_USERNAME:
17
+
from_secret: DOCKER_USERNAME
18
+
DOCKER_PASSWORD:
19
+
from_secret: DOCKER_PASSWORD
20
+
DOCKER_REGISTRY:
21
+
from_secret: DOCKER_REGISTRY
+23
steamci/Dockerfile
+23
steamci/Dockerfile
···
1
+
FROM golang:bookworm
2
+
3
+
COPY * .
4
+
ADD templates templates
5
+
6
+
RUN go build -o /bin/steamci .
7
+
RUN ls
8
+
9
+
FROM debian
10
+
11
+
COPY --from=0 /bin/steamci /bin/steamci
12
+
13
+
WORKDIR /home/steam
14
+
RUN apt update
15
+
RUN apt install -y lib32gcc-s1 curl tar
16
+
17
+
RUN curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar zxvf -
18
+
RUN chmod +x steamcmd.sh
19
+
RUN /home/steam/steamcmd.sh +quit
20
+
21
+
RUN rm -rf /var/lib/apt/lists/*
22
+
23
+
CMD ["/bin/steamci"]
+52
steamci/README.md
+52
steamci/README.md
···
1
+
# SteamCI
2
+
3
+
This is a all contained Woodpecker CI plugins that will generate the .vdf of Steam for you, and then upload your game on the specified depot.
4
+
5
+
## Settings
6
+
- steam_username: The username linked with the auth vdf
7
+
- steam_auth_vdf: (More doc coming) A base64 encoded string that contains all the content of config.vdf that you will get when you execute the local +login command. A bit clanky because Steam don't provide easy access token, so you have to login on local to pass SteamGuard, and then copy the content of the file
8
+
- app_id: Your Steam App Id
9
+
- content_root: The root of the published folder
10
+
- depot_id: Can be a single int or a mapping from matrix:depot_id
11
+
- local_path: Where your builded game finish. Can be a single string or a mapping from matrix:local_path
12
+
- matrix: A parameter to map your depot_id and local_path (Like Windows, Linux...)
13
+
- set_live: Optionnal, deploy the build on a Steam branch. Can't be *default* due to security reason
14
+
- description: Optionnal, a description that you may find on your Steamworks Dashboard (Default to the commit SHA)
15
+
16
+
## Example
17
+
```yml
18
+
when:
19
+
- event: push
20
+
branch: main
21
+
22
+
matrix:
23
+
TARGET:
24
+
- Windows
25
+
- Linux
26
+
27
+
steps:
28
+
- name: Build the game for ${TARGET}
29
+
image: ghcr.io/bigaston/godotci:4.5-beta3
30
+
commands:
31
+
- godot --version
32
+
- godot --headless --editor --import
33
+
- mkdir -p .build/${TARGET}
34
+
- godot --headless --export-release ${TARGET}
35
+
36
+
- name: Upload Steam
37
+
image: ghcr.io/bigaston/steamci
38
+
settings:
39
+
steam_username: something_deploy
40
+
steam_auth_vdf:
41
+
from_secret: STEAM_AUTH_VDF
42
+
app_id: 000
43
+
content_root: ".build"
44
+
depot_id:
45
+
Windows: 000
46
+
Linux: 001
47
+
local_path:
48
+
Windows: "Windows/*"
49
+
Linux: "Linux/*"
50
+
matrix: ${TARGET}
51
+
set_live: "beta"
52
+
```
+153
steamci/main.go
+153
steamci/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"bytes"
5
+
"encoding/base64"
6
+
"encoding/json"
7
+
"fmt"
8
+
"html/template"
9
+
"os"
10
+
"os/exec"
11
+
"path"
12
+
"strconv"
13
+
)
14
+
15
+
func main() {
16
+
vdfData := VdfTemplate{
17
+
AppId: getRequiredEnv("PLUGIN_APP_ID"),
18
+
ContentRoot: getRequiredEnv("PLUGIN_CONTENT_ROOT"),
19
+
}
20
+
21
+
description, hasDescription := os.LookupEnv("PLUGIN_DESCRIPTION")
22
+
23
+
if hasDescription {
24
+
vdfData.Description = description
25
+
} else {
26
+
vdfData.Description = fmt.Sprintf("Pushed from CI at commit %s", os.Getenv("CI_COMMIT_SHA"))
27
+
}
28
+
29
+
setLive, hasSetLive := os.LookupEnv("PLUGIN_SET_LIVE")
30
+
31
+
if hasSetLive {
32
+
vdfData.HasSetLive = true
33
+
vdfData.SetLive = setLive
34
+
}
35
+
36
+
// Get Depot Id
37
+
depotId := getRequiredEnv("PLUGIN_DEPOT_ID")
38
+
parsed := make(map[string]json.RawMessage)
39
+
err := json.Unmarshal([]byte(depotId), &parsed)
40
+
41
+
if err == nil {
42
+
matrix := getRequiredEnv("PLUGIN_MATRIX")
43
+
44
+
var depot_id int
45
+
parsedValue, ok := parsed[matrix]
46
+
47
+
if !ok {
48
+
panic(fmt.Sprintf("You have specified %s in your matrix, but field doesn't exist in depot_id", matrix))
49
+
}
50
+
51
+
err := json.Unmarshal(parsedValue, &depot_id)
52
+
check(err)
53
+
54
+
fmt.Printf("Found DepotID %d for matrix %s\n", depot_id, matrix)
55
+
56
+
vdfData.DepotId = strconv.Itoa(depot_id)
57
+
} else {
58
+
vdfData.DepotId = depotId
59
+
}
60
+
61
+
// Get Local Path
62
+
localPath := getRequiredEnv("PLUGIN_LOCAL_PATH")
63
+
parsed = make(map[string]json.RawMessage)
64
+
err = json.Unmarshal([]byte(localPath), &parsed)
65
+
66
+
if err == nil {
67
+
matrix := getRequiredEnv("PLUGIN_MATRIX")
68
+
69
+
var local_path string
70
+
parsedValue, ok := parsed[matrix]
71
+
72
+
if !ok {
73
+
panic(fmt.Sprintf("You have specified %s in your matrix, but field doesn't exist in local_path", matrix))
74
+
}
75
+
76
+
err := json.Unmarshal(parsedValue, &local_path)
77
+
check(err)
78
+
79
+
fmt.Printf("Found LocalPath %s for matrix %s\n", local_path, matrix)
80
+
81
+
vdfData.LocalPath = local_path
82
+
} else {
83
+
vdfData.LocalPath = localPath
84
+
}
85
+
86
+
tmpl, err := template.New("vdf.tmpl").ParseFS(templates, "templates/vdf.tmpl")
87
+
check(err)
88
+
89
+
buf := &bytes.Buffer{}
90
+
err = tmpl.Execute(buf, vdfData)
91
+
check(err)
92
+
93
+
err = os.WriteFile(path.Join(os.Getenv("CI_WORKSPACE"), "__upload__.vdf"), buf.Bytes(), os.ModeAppend)
94
+
check(err)
95
+
96
+
fmt.Println("Uploading on Steam using VDF:")
97
+
fmt.Println(buf.String())
98
+
99
+
// Start the Steam Command
100
+
steamAuthVdf := getRequiredEnv("PLUGIN_STEAM_AUTH_VDF")
101
+
base64Decoded, hasError := base64Decode(steamAuthVdf)
102
+
103
+
if hasError {
104
+
panic("Error while decoding STEAM_AUTH_VDF base64")
105
+
}
106
+
107
+
err = os.WriteFile("/root/Steam/config/config.vdf", base64Decoded, 0777)
108
+
check(err)
109
+
110
+
steamUsername := getRequiredEnv("PLUGIN_STEAM_USERNAME")
111
+
112
+
err = runSteamCommand([]string{"+login", steamUsername, "+quit"})
113
+
check(err)
114
+
115
+
err = runSteamCommand([]string{"+login", steamUsername, "+run_app_build", path.Join(os.Getenv("CI_WORKSPACE"), "__upload__.vdf"), "+quit"})
116
+
check(err)
117
+
118
+
fmt.Println("Everything went fine!")
119
+
}
120
+
121
+
func getRequiredEnv(envName string) string {
122
+
result, isPresent := os.LookupEnv(envName)
123
+
124
+
if !isPresent {
125
+
panic(fmt.Sprintf("Variable %s is not present but required", envName))
126
+
}
127
+
128
+
return result
129
+
}
130
+
131
+
func base64Decode(str string) ([]byte, bool) {
132
+
data, err := base64.StdEncoding.DecodeString(str)
133
+
if err != nil {
134
+
return []byte{}, true
135
+
}
136
+
return data, false
137
+
}
138
+
139
+
func runSteamCommand(params []string) error {
140
+
cmd := exec.Command("/home/steam/steamcmd.sh", params...)
141
+
142
+
cmd.Dir = "/woodpecker"
143
+
cmd.Stderr = os.Stderr
144
+
cmd.Stdout = os.Stdout
145
+
146
+
return cmd.Run()
147
+
}
148
+
149
+
func check(err error) {
150
+
if err != nil {
151
+
panic(err)
152
+
}
153
+
}
+16
steamci/templates.go
+16
steamci/templates.go
+24
steamci/templates/vdf.tmpl
+24
steamci/templates/vdf.tmpl
···
1
+
"AppBuild"
2
+
{
3
+
"AppID" "{{ .AppId }}"
4
+
"Desc" "{{ .Description }}"
5
+
6
+
"ContentRoot" "{{ .ContentRoot }}"
7
+
"BuildOutput" ".\output\"
8
+
{{ if .HasSetLive}}
9
+
"SetLive" "{{ .SetLive }}"
10
+
{{ end }}
11
+
12
+
"Depots"
13
+
{
14
+
"{{ .DepotId }}"
15
+
{
16
+
"FileMapping"
17
+
{
18
+
"LocalPath" "{{ .LocalPath }}"
19
+
"DepotPath" "."
20
+
"recursive" "1"
21
+
}
22
+
}
23
+
}
24
+
}