Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/normalize.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ jobs:
go build -o xsr ./cmd/xraysubrefiner

- name: Run
env:
XRAY_SUBREFINER_CONFIG: ${{ vars.XRAY_SUBREFINER_CONFIG }}
run: |
mkdir -p ./export
./xsr -config config.yaml -out export
./xsr -out export

- name: Commit & push changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
cd ./export
git init
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add origin https://github-action:$GITHUB_TOKEN@github.com/ircfspace/XrayRefiner.git
git remote add origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git branch -M export
git add .
git commit -m "Update export"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ A ready-to-use workflow is included at `.github/workflows/normalize.yml`:
- Triggers every hour (`cron: "0 * * * *"`).
- Can be run manually via `workflow_dispatch`.
- Builds the tool, runs it, and commits any changes to `export/` back to the repo.
- Reads the config from the GitHub Actions variable `XRAY_SUBREFINER_CONFIG`.

To use GitHub variables, add `XRAY_SUBREFINER_CONFIG` in the repo or environment settings. It can contain either the YAML content itself or a file path that exists on the runner.

The repository no longer ships a default `config.yaml`, so local runs must provide `-config` or set `XRAY_SUBREFINER_CONFIG`.

## Troubleshooting

Expand Down
63 changes: 58 additions & 5 deletions cmd/xraysubrefiner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"gopkg.in/yaml.v3"
)

const configEnvVar = "XRAY_SUBREFINER_CONFIG"

type Subscription struct {
Key string `yaml:"key"`
URL string `yaml:"url"`
Expand Down Expand Up @@ -51,12 +53,20 @@ func must(err error) {
}

func main() {
cfgPath := flag.String("config", "config.yaml", "path to config.yaml")
cfgPath := flag.String("config", "", "path to config.yaml")
outDir := flag.String("out", "export", "output directory")
timeout := flag.Duration("timeout", 20*time.Second, "HTTP client timeout")
flag.Parse()

cfg, err := loadConfig(*cfgPath)
var explicitConfig bool
flag.Visit(func(f *flag.Flag) {
if f.Name == "config" {
explicitConfig = true
}
})

configSource := resolveConfigSource(explicitConfig, *cfgPath, os.Getenv(configEnvVar))
cfg, err := loadConfig(configSource)
must(err)

client := &http.Client{Timeout: *timeout}
Comment on lines +68 to 72
Expand Down Expand Up @@ -130,11 +140,44 @@ func main() {
}
}

func loadConfig(path string) (*Config, error) {
b, err := os.ReadFile(path)
if err != nil {
func resolveConfigSource(explicitConfig bool, flagValue, envValue string) string {
flagValue = strings.TrimSpace(flagValue)
if explicitConfig && flagValue != "" {
return flagValue
}
if envValue != "" {
return envValue
}
return flagValue
}

func loadConfig(source string) (*Config, error) {
source = strings.TrimSpace(source)
if source == "" {
return nil, fmt.Errorf("config source is empty")
}

if looksLikeInlineYAML(source) {
return parseConfig([]byte(source))
}

if info, err := os.Stat(source); err == nil {
if info.IsDir() {
return nil, fmt.Errorf("config path %q is a directory", source)
}
b, err := os.ReadFile(source)
if err != nil {
return nil, err
}
return parseConfig(b)
} else if err != nil && !os.IsNotExist(err) {
return nil, err
}

return nil, fmt.Errorf("config source %q not found", source)
}
Comment on lines +154 to +178

func parseConfig(b []byte) (*Config, error) {
var cfg Config
if err := yaml.Unmarshal(b, &cfg); err != nil {
return nil, err
Expand All @@ -148,6 +191,16 @@ func loadConfig(path string) (*Config, error) {
return &cfg, nil
}

func looksLikeInlineYAML(source string) bool {
if strings.Contains(source, "\n") || strings.Contains(source, "\r") {
return true
}
if strings.Contains(source, ":") || strings.Contains(source, "{") || strings.Contains(source, "[") {
return true
}
return false
}

func fetch(client *http.Client, rawurl string) ([]byte, error) {
req, err := http.NewRequest("GET", rawurl, nil)
if err != nil {
Expand Down
64 changes: 64 additions & 0 deletions cmd/xraysubrefiner/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"os"
"path/filepath"
"testing"
)

func TestResolveConfigSourcePrefersEnvValueWhenFlagUnset(t *testing.T) {
t.Setenv(configEnvVar, "allowed_schemes:\n - vless\n")

got := resolveConfigSource(false, "", os.Getenv(configEnvVar))
if got != "allowed_schemes:\n - vless\n" {
t.Fatalf("resolveConfigSource() = %q, want env content", got)
}
}

func TestLoadConfigParsesRawYAMLContent(t *testing.T) {
cfg, err := loadConfig("allowed_schemes:\n - vless\nlite:\n n: 12\n")
if err != nil {
t.Fatalf("loadConfig(raw yaml) returned error: %v", err)
}
if len(cfg.AllowedSchemes) != 1 || cfg.AllowedSchemes[0] != "vless" {
t.Fatalf("unexpected allowed schemes: %#v", cfg.AllowedSchemes)
}
if cfg.Lite.N != 12 {
t.Fatalf("expected lite.n to be 12, got %d", cfg.Lite.N)
}
}

func TestLoadConfigParsesGitHubVariableStyleMultilineYAML(t *testing.T) {
raw := "allowed_schemes:\n - vless\nsubscriptions:\n - key: \"location/FR\"\n url: \"https://example.com/subscription\"\n"

cfg, err := loadConfig(raw)
if err != nil {
t.Fatalf("loadConfig(multiline yaml) returned error: %v", err)
}
if len(cfg.Subscriptions) != 1 || cfg.Subscriptions[0].Key != "location/FR" {
t.Fatalf("unexpected subscriptions: %#v", cfg.Subscriptions)
}
}

func TestLoadConfigReadsFilePath(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
content := []byte("allowed_schemes:\n - vmess\nsubscriptions: []\n")
if err := os.WriteFile(path, content, 0o644); err != nil {
t.Fatalf("write temp config: %v", err)
}

cfg, err := loadConfig(path)
if err != nil {
t.Fatalf("loadConfig(file path) returned error: %v", err)
}
if len(cfg.AllowedSchemes) != 1 || cfg.AllowedSchemes[0] != "vmess" {
t.Fatalf("unexpected allowed schemes: %#v", cfg.AllowedSchemes)
}
}

func TestLoadConfigRejectsEmptySource(t *testing.T) {
if _, err := loadConfig(""); err == nil {
t.Fatal("loadConfig(\"\") expected an error")
}
}
115 changes: 0 additions & 115 deletions config.yaml

This file was deleted.