Skip to content

Commit bb4eb23

Browse files
authored
Refactor: streamline workflow sorting and enhance workflow management (#187)
* Refactor: streamline workflow sorting and enhance workflow management - Updated workflow sorting to be alphabetical for improved organization. - Refactored workflow_page_controller.dart to ensure workflows are ordered by name. - Added deletion alert feature for workflows to enhance user experience. * Refactor: enhance Firestore transaction handling in VM Runner - Introduced a new function `runFirestoreTransaction` to encapsulate Firestore transaction logic. - Updated Firestore client variable naming for clarity. - Improved error handling and logging for fetching build jobs and updating document statuses. - Streamlined the process of updating build job statuses from "queued" to "inProgress". * Refactor: improve Firestore transaction handling and remove redundant build status update - Removed unnecessary build status update call in RunnerCommand. - Enhanced runFirestoreTransaction to return the fetched build job document snapshot. - Improved error handling and logging for Firestore transactions in VM Runner. - Streamlined the process of handling build job documents. * Enhance workflow fetching in VM Runner - Added functionality to fetch all workflow documents from Firestore. - Improved error handling and logging for workflow retrieval. - Updated logging to include details of found workflow documents. * feat: add GitHub installation token retrieval and update dependencies - Introduced functionality to retrieve GitHub installation tokens using JWT authentication. - Added a new command-line flag for specifying the GitHub App's PEM file. - Updated dependencies in go.mod to include `cloud.google.com/go/firestore` and `github.com/dgrijalva/jwt-go`. - Improved logging for workflow and build job processing in the VM Runner. * add: execute ssh command * refactor * Refactor: streamline logging in VM Runner - Removed logger parameters from RunApp function and initialized loggers within the function. - Deleted the unused binary file 'vm-runner' to clean up the project structure. * Refactor: enhance VM process handling and SSH command execution - Improved error handling in RunApp to log VM process failures without stopping execution. - Added cleanup logic for stopping and deleting VMs after execution. - Refactored ExecuteSSHCommand to return structured results, including stdout, stderr, and exit code. - Updated logging for SSH command outputs for better clarity. * Refactor: simplify VM cleanup logic in handleVMProcess - Moved VM cleanup logic into a separate function `cleanupVM` for better readability and maintainability. - Updated the deferred function to call `cleanupVM`, ensuring VMs are stopped and deleted after execution. - Improved logging for VM stop and delete actions. * add: firebase storage for storing log * feat: get installation token * refactor * fix: correct SSH command in handleVMProcess - Updated the SSH command from 'lsa' to 'ls' in the handleVMProcess function to ensure proper execution and output retrieval. - This change improves the accuracy of the command being executed during VM process handling. * refactor: update BuildJob structure and enhance VM command execution - Renamed fields in BuildJob from `repoUrl` to `repositoryUrl` and `branch` to `buildBranch` for clarity. - Introduced a new `cloneCommand` function to streamline the repository cloning process using the specified build branch and GitHub token. - Enhanced command execution in `handleVMProcess` to iterate over workflow steps, improving flexibility in command handling. - Added a new `Step` struct to represent individual commands in the workflow, allowing for better organization and management of execution steps. * feat: secrets * feat: cli with golang
1 parent ad0dba5 commit bb4eb23

File tree

18 files changed

+1428
-4
lines changed

18 files changed

+1428
-4
lines changed

apps/firebase_functions/functions/src/index.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,83 @@ import { getFirestore } from "firebase-admin/firestore";
44
import { githubApp } from "./github/github_app.js";
55
import { updateGitHubCheckStatus } from "./github/update_github_checks_status.js";
66
import { updateGitHubChecksLog } from "./github/update_github_checks_log.js";
7+
import { onObjectFinalized } from "firebase-functions/v2/storage";
8+
import type { OpenCIGithub } from "./models/BuildJob.js";
9+
import { getGitHubInstallationToken } from "./github/get_github_installation_token.js";
10+
import { Octokit } from "@octokit/rest";
11+
import { getStorage } from "firebase-admin/storage";
712

813
const firebaseApp = initializeApp();
914
export const firestore = getFirestore(firebaseApp);
15+
const storage = getStorage(firebaseApp);
1016

1117
export const githubAppFunction = githubApp;
1218
export const updateGitHubCheckStatusFunction = updateGitHubCheckStatus;
1319
export const updateGitHubChecksLogFunction = updateGitHubChecksLog;
20+
21+
export const generateThumbnail = onObjectFinalized(
22+
{
23+
region: "asia-northeast1",
24+
secrets: ["APP_ID", "PRIVATE_KEY", "GITHUB_WEBHOOK_SECRET"],
25+
},
26+
async (event) => {
27+
console.log(`event.source: ${event.source}`);
28+
console.log(`event.data: ${event.data}`);
29+
console.log(`event.data.bucket: ${event.data.bucket}`);
30+
console.log(`event.data.name: ${event.data.name}`);
31+
console.log(`event.data.contentType: ${event.data.contentType}`);
32+
33+
const bucket = storage.bucket(event.data.bucket);
34+
const file = bucket.file(event.data.name);
35+
36+
const [content] = await file.download();
37+
const logs = content.toString("utf-8");
38+
39+
const jobId = event.data.name.split("/")[1];
40+
const docs = await firestore.collection("build_jobs_v3").doc(jobId).get();
41+
const data = docs.data();
42+
const openciGitHub = data?.github as OpenCIGithub | null;
43+
console.log(`openciGitHub: ${openciGitHub}`);
44+
45+
if (openciGitHub == null) {
46+
console.log("No openciGitHub found");
47+
return;
48+
}
49+
50+
const installationId = openciGitHub.installationId;
51+
52+
const appId = process.env.APP_ID;
53+
const privateKey = process.env.PRIVATE_KEY;
54+
55+
if (!appId || !privateKey) {
56+
console.error("Missing appId or privateKey");
57+
return;
58+
}
59+
60+
const token = await getGitHubInstallationToken(
61+
installationId,
62+
appId,
63+
privateKey,
64+
);
65+
const octokit = new Octokit({ auth: token });
66+
67+
await setGitHubCheckStatusToInProgress(octokit, openciGitHub, logs);
68+
},
69+
);
70+
71+
async function setGitHubCheckStatusToInProgress(
72+
octokit: Octokit,
73+
github: OpenCIGithub,
74+
logs: string,
75+
) {
76+
await octokit.checks.update({
77+
owner: github.owner,
78+
repo: github.repositoryName,
79+
check_run_id: github.checkRunId,
80+
output: {
81+
title: "Updated Build Results",
82+
summary: "Build progress update",
83+
text: logs,
84+
},
85+
});
86+
}

apps/openci_runner/lib/src/commands/runner_command.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,6 @@ class RunnerCommand extends Command<int> {
131131
final vmName = generateUUID;
132132
final logId = generateUUID;
133133
try {
134-
await updateBuildStatus(
135-
jobId: buildJob.id,
136-
);
137-
138134
final workflow =
139135
await getWorkflowModel(firestore, buildJob.workflowId);
140136
if (workflow == null) {

apps/vm-runner/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
run.sh

apps/vm-runner/build_job.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"time"
9+
10+
"cloud.google.com/go/firestore"
11+
"github.com/getsentry/sentry-go"
12+
)
13+
14+
type BuildJob struct {
15+
ID string `json:"id"`
16+
GitHub struct {
17+
RepoURL string `json:"repositoryUrl"`
18+
InstallationId int `json:"installationId"`
19+
AppId int `json:"appId"`
20+
BuildBranch string `json:"buildBranch"`
21+
} `json:"github"`
22+
WorkflowId string `json:"workflowId"`
23+
Status string `json:"status"`
24+
}
25+
26+
func ParseBuildJob(jsonData []byte) (*BuildJob, error) {
27+
var job BuildJob
28+
29+
err := json.Unmarshal(jsonData, &job)
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to unmarshal build job: %w", err)
32+
}
33+
34+
fmt.Printf("Successfully parsed BuildJob: %v\n", job)
35+
36+
return &job, nil
37+
}
38+
39+
func GetBuildJob(ctx context.Context, firestoreClient *firestore.Client, infoLogger *log.Logger) (*BuildJob, error) {
40+
var snap *firestore.DocumentSnapshot
41+
42+
err := firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
43+
ref := firestoreClient.Collection("build_jobs_v3").
44+
Where("buildStatus", "==", "queued").
45+
OrderBy("createdAt", firestore.Asc).
46+
Limit(1)
47+
48+
docs, err := ref.Documents(ctx).GetAll()
49+
if err != nil {
50+
return fmt.Errorf("failed to fetch build jobs: %v", err)
51+
}
52+
53+
if len(docs) == 0 {
54+
return fmt.Errorf("no queued build jobs found")
55+
}
56+
57+
docRef := docs[0].Ref
58+
freshDocSnap, err := tx.Get(docRef)
59+
if err != nil {
60+
return fmt.Errorf("failed to get document snapshot: %v", err)
61+
}
62+
63+
if !freshDocSnap.Exists() {
64+
return fmt.Errorf("document does not exist")
65+
}
66+
67+
data := freshDocSnap.Data()
68+
freshStatus, ok := data["buildStatus"].(string)
69+
if !ok {
70+
return fmt.Errorf("invalid buildStatus field")
71+
}
72+
73+
if freshStatus == "queued" {
74+
err = tx.Update(docRef, []firestore.Update{
75+
{
76+
Path: "buildStatus",
77+
Value: "inProgress",
78+
},
79+
{
80+
Path: "updatedAt",
81+
Value: time.Now(),
82+
},
83+
})
84+
if err != nil {
85+
return fmt.Errorf("failed to update document: %v", err)
86+
}
87+
infoLogger.Printf("Document %s updated to inProgress.", docRef.ID)
88+
} else {
89+
return fmt.Errorf("build job status is not queued: %s", freshStatus)
90+
}
91+
92+
snap = freshDocSnap
93+
return nil
94+
})
95+
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
if snap == nil {
101+
return nil, fmt.Errorf("no valid build job found")
102+
}
103+
104+
data := snap.Data()
105+
106+
jsonData, err := json.Marshal(data)
107+
if err != nil {
108+
return nil, fmt.Errorf("failed to marshal workflow data: %v", err)
109+
}
110+
111+
job, err := ParseBuildJob(jsonData)
112+
if err == nil {
113+
infoLogger.Printf("Successfully parsed BuildJob: %v\n", job)
114+
} else {
115+
sentry.CaptureMessage(fmt.Sprintf("Failed to parse BuildJob: %v", err))
116+
return nil, fmt.Errorf("failed to parse BuildJob: %v", err)
117+
}
118+
119+
return job, nil
120+
}

apps/vm-runner/firebase.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"regexp"
8+
9+
firebase "firebase.google.com/go"
10+
)
11+
12+
func UploadLogToFirebaseStorage(
13+
ctx context.Context,
14+
app *firebase.App,
15+
jobId string,
16+
cmdRes SSHCommandResult,
17+
) error {
18+
fmt.Printf("UploadLogToFirebaseStorage: %+v", cmdRes)
19+
fileName := logPath(jobId)
20+
fmt.Printf("fileName: %s", fileName)
21+
newContent := cmdLog(cmdRes)
22+
23+
storageClient, err := app.Storage(ctx)
24+
if err != nil {
25+
return fmt.Errorf("failed to create storage client: %v", err)
26+
}
27+
28+
bucket, err := storageClient.DefaultBucket()
29+
if err != nil {
30+
return fmt.Errorf("failed to get default storage bucket: %v", err)
31+
}
32+
33+
obj := bucket.Object(fileName)
34+
reader, err := obj.NewReader(ctx)
35+
36+
var content string
37+
if err == nil {
38+
existingContent, err := io.ReadAll(reader)
39+
reader.Close()
40+
if err != nil {
41+
return fmt.Errorf("failed to read existing log: %v", err)
42+
}
43+
content = string(existingContent) + "\n" + newContent
44+
} else {
45+
content = newContent
46+
}
47+
48+
writer := obj.NewWriter(ctx)
49+
_, err = writer.Write([]byte(content))
50+
if err != nil {
51+
writer.Close()
52+
return fmt.Errorf("failed to write log to storage: %v", err)
53+
}
54+
55+
if err = writer.Close(); err != nil {
56+
return fmt.Errorf("failed to close writer: %v", err)
57+
}
58+
59+
return nil
60+
}
61+
62+
func logPath(buildId string) string {
63+
return fmt.Sprintf("logs_v1/%s/openci_log.log", buildId)
64+
}
65+
66+
func cmdLog(output SSHCommandResult) string {
67+
logContent := fmt.Sprintf(
68+
"=== Command Execution Result ===\n"+
69+
"Command: %s\n"+
70+
"Exit Code: %d\n"+
71+
"=== Stdout ===\n%s\n"+
72+
"=== Stderr ===\n%s\n"+
73+
"=========================",
74+
output.Command,
75+
output.ExitCode,
76+
output.Stdout,
77+
output.Stderr,
78+
)
79+
80+
return redactSensitiveInfo(logContent)
81+
}
82+
83+
func redactSensitiveInfo(input string) string {
84+
patterns := []struct {
85+
regex string
86+
replacement string
87+
}{
88+
{`[A-Za-z0-9+/]{24,}={0,3}`, "REDACTED_BASE64"},
89+
{`(ghs_|github_)[0-9a-f]{40}`, "REDACTED_GITHUB_TOKEN"},
90+
{`[A-Za-z0-9_\-]{40,}`, "REDACTED_ACCESS_TOKEN"},
91+
{`(?i)password=([^\s&]+)`, "password=REDACTED_PASSWORD"},
92+
}
93+
94+
result := input
95+
for _, pattern := range patterns {
96+
re := regexp.MustCompile(pattern.regex)
97+
result = re.ReplaceAllString(result, pattern.replacement)
98+
}
99+
return result
100+
}

apps/vm-runner/github.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"os"
10+
"time"
11+
12+
"github.com/golang-jwt/jwt"
13+
)
14+
15+
func GetGitHubInstallationToken(pemPath string, installationID int64, appID int64, infoLogger *log.Logger) (string, error) {
16+
infoLogger.Printf("Getting GitHub installation token for installation ID: %d, app ID: %d, pemPath: %v\n", installationID, appID, pemPath)
17+
pemBytes, err := os.ReadFile(pemPath)
18+
if err != nil {
19+
return "", fmt.Errorf("failed to read pem file: %v", err)
20+
}
21+
22+
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(pemBytes)
23+
if err != nil {
24+
return "", fmt.Errorf("failed to parse private key: %v", err)
25+
}
26+
27+
now := time.Now()
28+
claims := jwt.StandardClaims{
29+
IssuedAt: now.Unix(),
30+
ExpiresAt: now.Add(9 * time.Minute).Unix(),
31+
Issuer: fmt.Sprintf("%d", appID),
32+
}
33+
34+
jwtToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(privateKey)
35+
if err != nil {
36+
return "", fmt.Errorf("failed to sign jwt: %v", err)
37+
}
38+
39+
url := fmt.Sprintf("https://api.github.com/app/installations/%d/access_tokens", installationID)
40+
req, err := http.NewRequest("POST", url, nil)
41+
if err != nil {
42+
return "", fmt.Errorf("failed to create request for installation token: %v", err)
43+
}
44+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwtToken))
45+
req.Header.Set("Accept", "application/vnd.github.v3+json")
46+
47+
client := &http.Client{Timeout: 10 * time.Second}
48+
resp, err := client.Do(req)
49+
if err != nil {
50+
return "", fmt.Errorf("failed to request GitHub installation token: %v", err)
51+
}
52+
defer resp.Body.Close()
53+
54+
if resp.StatusCode != http.StatusCreated {
55+
return "", fmt.Errorf("failed to get installation token, status code: %d", resp.StatusCode)
56+
}
57+
58+
body, err := io.ReadAll(resp.Body)
59+
if err != nil {
60+
return "", fmt.Errorf("failed to read response body: %v", err)
61+
}
62+
63+
var tokenResponse struct {
64+
Token string `json:"token"`
65+
}
66+
if err := json.Unmarshal(body, &tokenResponse); err != nil {
67+
return "", fmt.Errorf("failed to unmarshal token response: %v", err)
68+
}
69+
70+
if len(tokenResponse.Token) == 0 {
71+
return "", fmt.Errorf("installation token not found in response")
72+
}
73+
74+
return tokenResponse.Token, nil
75+
}

0 commit comments

Comments
 (0)