Skip to content

Commit ccd5aee

Browse files
authored
Merge pull request #257 from circleci/parallel-uploader
Replace slow Python+BASH tooling with Go
2 parents 3adf264 + 30d3c64 commit ccd5aee

File tree

10 files changed

+529
-330
lines changed

10 files changed

+529
-330
lines changed

.circleci/config.yml

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ parameters:
88
orbs:
99
aws-cli: circleci/[email protected]
1010
vale: circleci/[email protected]
11+
go: circleci/[email protected]
1112

1213
executors:
1314
node_executor:
1415
docker:
1516
- image: cimg/node:22.15.1
1617
working_directory: ~/project
1718

18-
python_executor:
19+
go_executor:
1920
docker:
20-
- image: cimg/python:3.12.11
21+
- image: cimg/go:1.24
2122
working_directory: ~/project
2223

2324
ruby_executor:
@@ -107,11 +108,6 @@ jobs:
107108
paths:
108109
- build
109110
- build.zip
110-
- scripts
111-
- persist_to_workspace:
112-
root: .
113-
paths:
114-
- build
115111
- notify_error:
116112
message: "Build job failed for branch ${CIRCLE_BRANCH}"
117113

@@ -190,7 +186,7 @@ jobs:
190186
message: "Deploy preview job failed for branch ${CIRCLE_BRANCH}"
191187

192188
deploy-production:
193-
executor: python_executor
189+
executor: go_executor
194190
parameters:
195191
bucket_name:
196192
description: The name of the s3 bucket where static assets are stored.
@@ -200,39 +196,27 @@ jobs:
200196
description: The path to the docs build directory
201197
type: string
202198
steps:
199+
- checkout
203200
- attach_workspace:
204201
at: .
205202
- aws-setup
206-
- run:
207-
name: Deploy Production Site to S3
208-
command: |
209-
AWS_S3_BUCKET=<< parameters.bucket_name >>
210-
BUILD_DIRECTORY=<< parameters.build_dir >>
211-
212-
set -e
213-
echo "[INFO] Deploying production site..."
214-
aws s3 sync "$BUILD_DIRECTORY" "s3://$AWS_S3_BUCKET/"
215-
- run:
216-
name: install pyyaml requests
217-
command: |
218-
set -e
219-
echo "[INFO] Installing pyyaml requests..."
220-
pip install pyyaml requests
221-
- run:
222-
name: Deploy Redirects to S3
223-
command: |
224-
AWS_S3_BUCKET=<< parameters.bucket_name >>
225-
226-
set -e
227-
echo "[INFO] Deploying redirects..."
228-
#REMEMBER TO UPDATE THE START/END PARAMETER IN THE VALIDATE JOB TO MATCH
229-
python scripts/create-redirects.py $AWS_S3_BUCKET
230-
- run:
231-
name: Validate Redirects
232-
command: |
233-
set -e
234-
echo "[INFO] Validating redirects..."
235-
python scripts/validate-redirects.py https://circleci.com/docs-preview
203+
- go/with-cache:
204+
steps:
205+
- go/mod-download
206+
- run:
207+
name: Deploy Production Site to S3
208+
command: |
209+
AWS_S3_BUCKET=<< parameters.bucket_name >>
210+
BUILD_DIRECTORY=<< parameters.build_dir >>
211+
212+
echo "[INFO] Deploying production site..."
213+
aws s3 sync "$BUILD_DIRECTORY" "s3://$AWS_S3_BUCKET/"
214+
- run:
215+
name: Deploy Redirects to S3
216+
command: go run ./cmd/create-redirects "<< parameters.bucket_name >>"
217+
- run:
218+
name: Validate Redirects
219+
command: go run ./cmd/validate-redirects https://circleci.com
236220
- notify_error:
237221
message: "Production deployment job failed for branch ${CIRCLE_BRANCH}"
238222

cmd/create-redirects/main.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/config"
12+
"github.com/aws/aws-sdk-go-v2/service/s3"
13+
"github.com/lmittmann/tint"
14+
"golang.org/x/sync/errgroup"
15+
"golang.org/x/sync/semaphore"
16+
17+
"github.com/circleci/circleci-docs-static/internal/redirects"
18+
)
19+
20+
type options struct {
21+
Bucket string
22+
Start int
23+
End int
24+
}
25+
26+
const (
27+
concurrency = 20
28+
docsPath = "docs-preview"
29+
)
30+
31+
func main() {
32+
ctx := context.Background()
33+
34+
slog.SetDefault(slog.New(
35+
tint.NewHandler(os.Stderr, &tint.Options{
36+
Level: slog.LevelDebug,
37+
}),
38+
))
39+
40+
opts := options{}
41+
flag.IntVar(&opts.Start, "start", 0, "Start index (inclusive)")
42+
flag.IntVar(&opts.End, "end", -1, "Start index (inclusive)")
43+
flag.Parse()
44+
opts.Bucket = flag.Arg(0)
45+
46+
if opts.Bucket == "" {
47+
slog.Error("Bucket is required")
48+
os.Exit(1)
49+
}
50+
51+
err := run(ctx, opts)
52+
if err != nil {
53+
slog.Error("Unexpected error", "error", err)
54+
os.Exit(1)
55+
}
56+
}
57+
58+
func run(ctx context.Context, opts options) error {
59+
awsConfig, err := config.LoadDefaultConfig(ctx)
60+
if err != nil {
61+
return err
62+
}
63+
64+
s3Client := s3.NewFromConfig(awsConfig)
65+
66+
redirs, err := redirects.Load(redirects.DefaultFile)
67+
if err != nil {
68+
return err
69+
}
70+
71+
if opts.End < 0 {
72+
opts.End = len(redirs)
73+
}
74+
75+
sliceRedirects := redirs[opts.Start:opts.End]
76+
slog.Info("Redirects",
77+
"start", opts.Start,
78+
"end", opts.End,
79+
"total", len(sliceRedirects),
80+
)
81+
82+
u := newUploader(opts.Bucket, s3Client, concurrency)
83+
g, ctx := errgroup.WithContext(ctx)
84+
for _, r := range sliceRedirects {
85+
g.Go(func() (err error) {
86+
return u.Upload(ctx, r)
87+
})
88+
}
89+
err = g.Wait()
90+
if err != nil {
91+
return err
92+
}
93+
94+
slog.Info("Successfully processed redirects")
95+
return nil
96+
}
97+
98+
type uploader struct {
99+
bucket string
100+
client *s3.Client
101+
sem *semaphore.Weighted
102+
}
103+
104+
func newUploader(bucket string, client *s3.Client, max int64) *uploader {
105+
return &uploader{
106+
bucket: bucket,
107+
client: client,
108+
sem: semaphore.NewWeighted(max),
109+
}
110+
}
111+
112+
func (u *uploader) Upload(ctx context.Context, r redirects.Redirect) error {
113+
err := u.sem.Acquire(ctx, 1)
114+
if err != nil {
115+
return err
116+
}
117+
118+
defer u.sem.Release(1)
119+
120+
key := fmt.Sprintf("%s%sindex.html", docsPath, r.Old)
121+
redir := fmt.Sprintf("/%s%s", docsPath, r.New)
122+
slog.Info("Creating redirect",
123+
"old", r.Old,
124+
"new", r.New,
125+
"key", key,
126+
"redirect", redir,
127+
)
128+
_, err = u.client.PutObject(ctx, &s3.PutObjectInput{
129+
Bucket: aws.String(u.bucket),
130+
Key: aws.String(key),
131+
WebsiteRedirectLocation: aws.String(redir),
132+
ContentType: aws.String("text/html"),
133+
ContentLength: aws.Int64(0),
134+
})
135+
return err
136+
}

0 commit comments

Comments
 (0)