Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use precompiled binaries (Go) #3

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tools/build-tool.exe
tools/build-tool
42 changes: 21 additions & 21 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,19 @@ runs:
FILE_FORMAT: "${{ inputs.file_format }}"
run: |
set -e
chmod +x "${{ github.action_path }}/src/scripts/translation_paths.sh"

. "${{ github.action_path }}/src/scripts/translation_paths.sh"

store_translation_paths
CMD_PATH="${{ github.action_path }}/bin/store_translation_paths"
chmod +x "$CMD_PATH"
$("$CMD_PATH") || {
echo "Error: store_translation_paths script failed with exit code $?"
exit 1
}

- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files_from_source_file: paths.txt
files_from_source_file: lok_action_paths_temp.txt
separator: ','

- name: Check if this is the first run on the branch
Expand Down Expand Up @@ -96,15 +98,12 @@ runs:
set -e
chmod +x "${{ github.action_path }}/src/scripts/translation_paths.sh"

. "${{ github.action_path }}/src/scripts/translation_paths.sh"

if find_all_translation_files; then
echo "Translation files found and set."
echo "has_files=true" >> $GITHUB_OUTPUT
else
echo "No translation files found."
echo "has_files=false" >> $GITHUB_OUTPUT
fi
CMD_PATH="${{ github.action_path }}/bin/find_all_files"
chmod +x "$CMD_PATH"
$("$CMD_PATH") || {
echo "Error: find_all_files script failed with exit code $?"
exit 1
}

- name: Install Lokalise CLI
if: steps.find-files.outputs.has_files == 'true' || steps.changed-files.outputs.any_changed == 'true'
Expand All @@ -126,20 +125,21 @@ runs:
set -e

if [ "${{ steps.check-first-run.outputs.first_run }}" == "true" ]; then
ALL_FILES="${{ steps.find-files.outputs.ALL_FILES }}"
FILES="${{ steps.find-files.outputs.ALL_FILES }}"
else
ALL_CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"
FILES="${{ steps.changed-files.outputs.all_changed_files }}"
fi

if [ -z "${ALL_FILES}" ] && [ -z "${ALL_CHANGED_FILES}" ]; then
if [ -z "$FILES" ]; then
echo "No files to upload."
exit 0
exit 1
fi

chmod +x "${{ github.action_path }}/src/scripts/lokalise_upload.sh"

CMD_PATH="${{ github.action_path }}/bin/lokalise_upload"
chmod +x "$CMD_PATH"

set +e
echo "${ALL_FILES:-$ALL_CHANGED_FILES}" | tr ',' '\n' | xargs -P 6 -I {} bash "${{ github.action_path }}/src/scripts/lokalise_upload.sh" "{}" "${{ inputs.project_id }}" "${{ inputs.api_token }}"
echo "$FILES" | tr ',' '\n' | xargs -P 6 -I {} "$CMD_PATH" "{}" "${{ inputs.project_id }}" "${{ inputs.api_token }}"
xargs_exit_code=$?
set -e

Expand Down
Binary file added bin/find_all_files
Binary file not shown.
Binary file added bin/lokalise_upload
Binary file not shown.
Binary file added bin/store_translation_paths
Binary file not shown.
5 changes: 5 additions & 0 deletions src/find_all_files/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module find_all_files

go 1.23.2

require github.com/bodrovis/lokalise-actions-common v1.0.0
4 changes: 4 additions & 0 deletions src/find_all_files/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/bodrovis/lokalise-actions-common v0.0.0-20241108132327-9fd58feb3433 h1:mZto7xhDD+BP0b/8YOlWKHvk6u4AORynNqGZWRcKWgs=
github.com/bodrovis/lokalise-actions-common v0.0.0-20241108132327-9fd58feb3433/go.mod h1:yVhX1+ARn5SupEKVqxLq37bkhRDzD8vQG4jPrNGnxq8=
github.com/bodrovis/lokalise-actions-common v1.0.0 h1:tqkwMMmrL+zL2gpRn4I/70Ow7fGaqgxfRQu8evNpjzM=
github.com/bodrovis/lokalise-actions-common v1.0.0/go.mod h1:yVhX1+ARn5SupEKVqxLq37bkhRDzD8vQG4jPrNGnxq8=
116 changes: 116 additions & 0 deletions src/find_all_files/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/bodrovis/lokalise-actions-common/githuboutput"

"github.com/bodrovis/lokalise-actions-common/parsepaths"
)

// This program finds all translation files based on environment configurations.
// It supports both flat and nested directory naming conventions and outputs the list
// of translation files found to GitHub Actions output.

// findAllTranslationFiles searches for translation files based on the given paths and naming conventions.
// It supports both flat naming (all translations in one file) and nested directories per language.
func findAllTranslationFiles(paths []string, flatNaming bool, baseLang, fileFormat string) ([]string, error) {
var allFiles []string

for _, path := range paths {
if path == "" {
continue // Skip empty paths
}

if flatNaming {
// For flat naming, look for a single translation file named as baseLang.fileFormat in the path
targetFile := filepath.Join(path, fmt.Sprintf("%s.%s", baseLang, fileFormat))
if info, err := os.Stat(targetFile); err == nil && !info.IsDir() {
allFiles = append(allFiles, targetFile)
} else if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("error accessing file %s: %v", targetFile, err)
}
// File does not exist, continue to next path
}
} else {
// For nested directories, look for a directory named baseLang and search for translation files within it
targetDir := filepath.Join(path, baseLang)
if info, err := os.Stat(targetDir); err == nil && info.IsDir() {
// Walk through the directory recursively to find all translation files
err := filepath.Walk(targetDir, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("error walking through directory %s: %v", targetDir, err)
}
if !info.IsDir() && strings.HasSuffix(info.Name(), fmt.Sprintf(".%s", fileFormat)) {
allFiles = append(allFiles, filePath)
}
return nil
})
if err != nil {
return nil, err // Return error encountered during file walk
}
} else if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("error accessing directory %s: %v", targetDir, err)
}
// Directory does not exist, continue to next path
}
}
}

return allFiles, nil
}

func main() {
// Read and validate environment variables
translationsPaths := parsepaths.ParsePaths(os.Getenv("TRANSLATIONS_PATH"))
flatNamingEnv := os.Getenv("FLAT_NAMING")
baseLang := os.Getenv("BASE_LANG")
fileFormat := os.Getenv("FILE_FORMAT")

// Ensure that required environment variables are set
if len(translationsPaths) == 0 || baseLang == "" || fileFormat == "" {
returnWithError("missing required environment variables")
}

// Parse flatNaming as boolean
flatNaming := false
if flatNamingEnv != "" {
var err error
flatNaming, err = strconv.ParseBool(flatNamingEnv)
if err != nil {
returnWithError("invalid value for FLAT_NAMING environment variable; expected true or false")
}
}

// Find all translation files based on the provided configurations
allFiles, err := findAllTranslationFiles(translationsPaths, flatNaming, baseLang, fileFormat)
if err != nil {
returnWithError(fmt.Sprintf("unable to find translation files: %v", err))
}

// Write whether files were found to GitHub Actions output
if len(allFiles) > 0 {
// Join all file paths into a comma-separated string
allFilesStr := strings.Join(allFiles, ",")
// Write the list of files and set has_files to true
if !githuboutput.WriteToGitHubOutput("ALL_FILES", allFilesStr) || !githuboutput.WriteToGitHubOutput("has_files", "true") {
returnWithError("cannot write to GITHUB_OUTPUT")
}
} else {
// No files found, set has_files to false
if !githuboutput.WriteToGitHubOutput("has_files", "false") {
returnWithError("cannot write to GITHUB_OUTPUT")
}
}
}

func returnWithError(message string) {
fmt.Fprintf(os.Stderr, "Error: %s\n", message)
os.Exit(1)
}
3 changes: 3 additions & 0 deletions src/lokalise_upload/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module lokalise_upload

go 1.23.2
163 changes: 163 additions & 0 deletions src/lokalise_upload/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package main

import (
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"time"
)

const (
defaultMaxRetries = 3 // Default number of retries if the upload is rate-limited
defaultSleepTime = 1 // Default initial sleep time in seconds between retries
maxSleepTime = 60 // Maximum sleep time in seconds between retries
maxTotalTime = 300 // Maximum total retry time in seconds
)

func main() {
// Ensure the required command-line arguments are provided
if len(os.Args) < 4 {
returnWithError("Usage: lokalise_upload <file> <project_id> <token>")
}

filePath := os.Args[1]
projectID := os.Args[2]
token := os.Args[3]

// Start the file upload process
uploadFile(filePath, projectID, token)
}

// uploadFile uploads a file to Lokalise using the lokalise2 CLI tool.
// It handles rate limiting by retrying the upload with exponential backoff.
func uploadFile(filePath, projectID, token string) {
// Retrieve necessary environment variables
langISO := os.Getenv("BASE_LANG")
additionalParams := os.Getenv("CLI_ADD_PARAMS")
githubRefName := os.Getenv("GITHUB_REF_NAME")
maxRetries := getEnvAsInt("MAX_RETRIES", defaultMaxRetries)
sleepTime := getEnvAsInt("SLEEP_TIME", defaultSleepTime)

// Validate required inputs
validateFile(filePath)
if projectID == "" || token == "" || langISO == "" {
returnWithError("Project ID, API token, and base language are required and cannot be empty.")
}
if githubRefName == "" {
returnWithError("GITHUB_REF_NAME is required and cannot be empty.")
}

fmt.Printf("Starting to upload file %s\n", filePath)

startTime := time.Now()

// Attempt to upload the file, retrying if rate-limited
for attempt := 1; attempt <= maxRetries; attempt++ {
fmt.Printf("Attempt %d of %d\n", attempt, maxRetries)

// Construct command arguments for the lokalise2 CLI tool
args := []string{
fmt.Sprintf("--token=%s", token),
fmt.Sprintf("--project-id=%s", projectID),
"file", "upload",
fmt.Sprintf("--file=%s", filePath),
fmt.Sprintf("--lang-iso=%s", langISO),
"--replace-modified",
"--include-path",
"--distinguish-by-file",
"--poll",
"--poll-timeout=120s",
"--tag-inserted-keys",
"--tag-skipped-keys=true",
"--tag-updated-keys",
"--tags", githubRefName,
}

// Append any additional parameters specified in the environment variable
if additionalParams != "" {
args = append(args, strings.Fields(additionalParams)...)
}

// Execute the command to upload the file
cmd := exec.Command("./bin/lokalise2", args...)
cmd.Stdout = io.Discard // Discard standard output
cmd.Stderr = os.Stderr // Redirect standard error to stderr

err := cmd.Run()
if err == nil {
// Upload succeeded
fmt.Printf("Successfully uploaded file %s\n", filePath)
return
}

// Check if the error is due to rate limiting (HTTP status code 429)
if isRateLimitError(err) {
// Sleep for the current sleep time before retrying
time.Sleep(time.Duration(sleepTime) * time.Second)

// Check if the total retry time has exceeded the maximum allowed time
if time.Since(startTime).Seconds() > maxTotalTime {
returnWithError(fmt.Sprintf("Max retry time exceeded (%d seconds) for %s. Exiting.", maxTotalTime, filePath))
}

// Exponentially increase the sleep time for the next retry, capped at maxSleepTime
sleepTime = min(sleepTime*2, maxSleepTime)
continue // Retry the upload
}

// If the error is not due to rate limiting, exit with an error message
returnWithError(fmt.Sprintf("Permanent error during upload for %s: %v", filePath, err))
}

// If all retries have been exhausted, exit with an error message
returnWithError(fmt.Sprintf("Failed to upload file %s after %d attempts.", filePath, maxRetries))
}

// Helper functions

// getEnvAsInt retrieves an environment variable as an integer.
// Returns the default value if the variable is not set.
// Exits with an error if the value is not a positive integer.
func getEnvAsInt(key string, defaultVal int) int {
valStr := os.Getenv(key)
if valStr == "" {
return defaultVal
}
val, err := strconv.Atoi(valStr)
if err != nil || val < 1 {
returnWithError(fmt.Sprintf("Environment variable %s must be a positive integer.", key))
}
return val
}

// validateFile checks if the file exists and is not empty.
func validateFile(filePath string) {
if filePath == "" {
returnWithError("File path is required and cannot be empty.")
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
returnWithError(fmt.Sprintf("File %s does not exist.", filePath))
}
}

// isRateLimitError checks if the error is due to rate limiting (HTTP status code 429).
func isRateLimitError(err error) bool {
return strings.Contains(err.Error(), "API request error 429")
}

// min returns the smaller of two integers.
func min(a, b int) int {
if a < b {
return a
}
return b
}

// returnWithError prints an error message to stderr and exits the program with a non-zero status code.
func returnWithError(message string) {
fmt.Fprintf(os.Stderr, "Error: %s\n", message)
os.Exit(1)
}
Loading