Skip to content

Commit d264d63

Browse files
committed
feat: rushjs support & example
1 parent df4a080 commit d264d63

26 files changed

Lines changed: 1619 additions & 18 deletions

.github/workflows/node.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Node
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
build:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: "lts/*"
24+
# TODO: cache doesn't support rush
25+
cache-dependency-path: examples/stack/svc2/common/config/rush/pnpm-lock.yaml
26+
- name: "svc2: Rush Install"
27+
run: node common/scripts/install-run-rush.js install
28+
- name: "svc2: Rush Build"
29+
run: node common/scripts/install-run-rush.js build --verbose
30+
- name: "svc2: Rush Deploy"
31+
run: node common/scripts/install-run-rush.js deploy
32+
- name: "svc2: Build Docker Image"
33+
uses: docker/build-push-action@v6
34+
with:
35+
context: examples/stack/svc2/common/deploy/
36+
file: examples/stack/svc2/app/Dockerfile
37+
load: true
38+
push: false

.github/workflows/release.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
go-version: '~1.25.0-rc.2'
2828

2929
- name: Tag Release
30+
id: semrel
3031
uses: go-semantic-release/action@v1
3132
with:
3233
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -41,7 +42,29 @@ jobs:
4142
ko build \
4243
--base-import-paths \
4344
--image-label org.opencontainers.image.authors="fastcat" \
44-
--image-label org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }}/examples/stack/svc1 \
45+
--image-label org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}/tree/${{ github.sha }}/examples/stack/svc1 \
4546
--image-label org.opencontainers.image.url=${{ github.repositoryUrl }} \
4647
--image-label org.opencontainers.image.vendor="fastcat" \
48+
--tags "latest,${{ steps.semrel.outputs.version }}" \
4749
./examples/stack/svc1
50+
51+
- uses: actions/setup-node@v4
52+
with:
53+
node-version: "lts/*"
54+
# TODO: cache doesn't support rush
55+
cache-dependency-path: examples/stack/svc2/common/config/rush/pnpm-lock.yaml
56+
- name: "svc2: Rush Install"
57+
run: node common/scripts/install-run-rush.js install
58+
- name: "svc2: Rush Build"
59+
run: node common/scripts/install-run-rush.js build --verbose
60+
- name: "svc2: Rush Deploy"
61+
run: node common/scripts/install-run-rush.js deploy
62+
- name: "svc2: Build & Publish Docker Image"
63+
uses: docker/build-push-action@v6
64+
with:
65+
context: examples/stack/svc2
66+
file: examples/stack/svc2/app/Dockerfile
67+
push: true
68+
tags: |
69+
fastcat/gdev-example-svc2:${{ steps.semrel.outputs.version }}
70+
fastcat/gdev-example-svc2:latest

addons/build/services.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func Services(
2929
if err != nil {
3030
return fmt.Errorf("error getting local source for service %s: %w", svc.Name(), err)
3131
}
32-
root = filepath.Clean(root)
32+
root, subdir = filepath.Clean(root), filepath.Clean(subdir)
3333
if idx, ok := root2idx[root]; ok {
3434
// append the subdir
35-
subdirs[idx] = append(subdirs[idx], filepath.Clean(subdir))
35+
subdirs[idx] = append(subdirs[idx], subdir)
3636
continue
3737
}
3838

@@ -44,7 +44,7 @@ func Services(
4444
}
4545
builders = append(builders, b)
4646
strategies = append(strategies, sn)
47-
subdirs = append(subdirs, nil)
47+
subdirs = append(subdirs, []string{subdir})
4848
}
4949

5050
// run the builders

addons/nodejs/addon.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ func Configure(opts ...option) {
3939
var configureBuild = sync.OnceFunc(func() {
4040
build.Configure(
4141
build.WithStrategy("npm", detectNPM, nil),
42+
build.WithStrategy("rush", detectRush, nil),
4243
)
4344
})

addons/nodejs/file-formats.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ type PackageJSON struct {
77
Workspaces []string `json:"workspaces,omitempty"`
88
}
99

10+
// PNPMWorkspaceYAML is a partial representation of a pnpm-workspace.yaml file.
1011
type PNPMWorkspaceYAML struct {
1112
Packages []string `json:"packages,omitempty"`
1213
}
14+
15+
// RushJSON is a partial representation of a rush.json file.
16+
type RushJSON struct {
17+
Projects []RushProject `json:"projects,omitempty,omitzero"`
18+
}
19+
20+
type RushProject struct {
21+
PackageName string `json:"packageName,omitempty"`
22+
ProjectFolder string `json:"projectFolder,omitempty"`
23+
// other fields omitted as not needed
24+
}

addons/nodejs/npm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (b *npmBuilder) BuildDirs(ctx context.Context, dirs []string, opts build.Op
119119

120120
for _, dir := range dirs {
121121
// `./` prefix is required for the filter to be understood as a path in
122-
// pnpm, is OK for npmq
122+
// pnpm, is OK for npm
123123
if !strings.HasPrefix(dir, "./") {
124124
dir = "./" + dir
125125
}

addons/nodejs/rush.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package nodejs
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
10+
"fastcat.org/go/gdev/addons/build"
11+
"fastcat.org/go/gdev/internal"
12+
"fastcat.org/go/gdev/shx"
13+
)
14+
15+
func detectRush(root string) (build.Builder, error) {
16+
rjPath := filepath.Join(root, "rush.json")
17+
if _, err := os.Stat(rjPath); err != nil {
18+
return nil, nil // no rush.json, not a Rush project
19+
}
20+
rj, err := internal.ReadJSONFile[RushJSON](rjPath)
21+
if err != nil {
22+
return nil, fmt.Errorf("failed to read %s: %w", rjPath, err)
23+
}
24+
return &rushBuilder{
25+
root: root,
26+
rj: rj,
27+
}, nil
28+
}
29+
30+
// Root implements build.Builder.
31+
func (b *rushBuilder) Root() string {
32+
return b.root
33+
}
34+
35+
type rushBuilder struct {
36+
root string
37+
rj RushJSON
38+
39+
// populated on demand
40+
dirToPkg map[string]string
41+
}
42+
43+
func (b *rushBuilder) withExtra() {
44+
if b.dirToPkg == nil {
45+
b.dirToPkg = make(map[string]string, len(b.rj.Projects))
46+
for _, p := range b.rj.Projects {
47+
b.dirToPkg[p.ProjectFolder] = p.PackageName
48+
}
49+
}
50+
}
51+
52+
func (b *rushBuilder) build(
53+
ctx context.Context,
54+
args []string,
55+
opts build.Options,
56+
) error {
57+
shOpts := []shx.Option{shx.WithCwd(b.root)}
58+
shOpts = append(shOpts, opts.ShellOpts()...)
59+
// always tell rush to emit verbose output so we can emit it on errors, rush's
60+
// tendency to only emit the tail of a build error often doesn't include
61+
// enough context
62+
cna := []string{"rush", "build", "--verbose"}
63+
cna = append(cna, args...)
64+
res, err := shx.Run(ctx, cna, shOpts...)
65+
if err != nil {
66+
return fmt.Errorf("failed to start rush build: %w", err)
67+
}
68+
defer res.Close() //nolint:errcheck
69+
if err = res.Err(); err != nil {
70+
if !opts.Verbose {
71+
_, _ = io.Copy(os.Stderr, res.Stdout())
72+
}
73+
return fmt.Errorf("rush build failed: %w", err)
74+
}
75+
if err := res.Close(); err != nil {
76+
return fmt.Errorf("error cleaning up after rush build: %w", err)
77+
}
78+
return nil
79+
}
80+
81+
// BuildAll implements build.Builder.
82+
func (b *rushBuilder) BuildAll(ctx context.Context, opts build.Options) error {
83+
return b.build(ctx, nil, opts)
84+
}
85+
86+
// BuildDirs implements build.Builder.
87+
func (b *rushBuilder) BuildDirs(ctx context.Context, dirs []string, opts build.Options) error {
88+
args := make([]string, 0, len(dirs)*2)
89+
b.withExtra()
90+
for _, dir := range dirs {
91+
if pkg, ok := b.dirToPkg[dir]; ok {
92+
args = append(args, "--to", pkg)
93+
} else {
94+
return fmt.Errorf("unknown directory %s to build for %s", dir, b.root)
95+
}
96+
}
97+
return b.build(ctx, args, opts)
98+
}
99+
100+
// ValidSubdirs implements build.Builder.
101+
func (b *rushBuilder) ValidSubdirs(context.Context) ([]string, error) {
102+
ret := make([]string, 0, len(b.rj.Projects))
103+
for _, p := range b.rj.Projects {
104+
ret = append(ret, p.ProjectFolder)
105+
}
106+
return ret, nil
107+
}

examples/stack/main.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fastcat.org/go/gdev/addons/build"
77
"fastcat.org/go/gdev/addons/docker"
88
"fastcat.org/go/gdev/addons/golang"
9+
"fastcat.org/go/gdev/addons/nodejs"
910
"fastcat.org/go/gdev/addons/pm"
1011
"fastcat.org/go/gdev/addons/pm/api"
1112
"fastcat.org/go/gdev/addons/pm/resource"
@@ -19,7 +20,7 @@ import (
1920
// When you have many services using a common pattern, defining a recipe
2021
// function like this is helpful to avoid stuttering of the service name and
2122
// repetition of the resource pattern.
22-
func myStandardService(
23+
func myGoService(
2324
name string,
2425
repo, subDir string,
2526
imageName string,
@@ -45,35 +46,62 @@ func myStandardService(
4546
return service.New(name, allOpts...)
4647
}
4748

49+
func myNodeService(
50+
name string,
51+
repo, subDir string,
52+
imageName string,
53+
opts ...service.BasicOpt,
54+
) service.Service {
55+
allOpts := []service.BasicOpt{
56+
service.WithModalResources(
57+
service.ModeDefault,
58+
docker.Container(name, imageName).WithPorts("8081"),
59+
),
60+
service.WithModalResources(
61+
service.ModeLocal,
62+
resource.PMStatic(api.Child{
63+
Name: name + "-local",
64+
Main: api.Exec{
65+
Cmd: "node",
66+
Args: []string{"."},
67+
},
68+
}),
69+
),
70+
}
71+
allOpts = append(allOpts, opts...)
72+
return service.New(name, allOpts...)
73+
}
74+
4875
func main() {
4976
// cspell:ignore sdev
5077
instance.SetAppName("sdev")
5178
pm.Configure()
5279
docker.Configure()
5380
build.Configure()
5481
golang.Configure()
82+
nodejs.Configure()
5583

5684
svc1Repo := filepath.Join(shx.HomeDir(), "src", "gdev")
5785
svc1Subdir := "examples/stack/svc1"
5886
stack.AddService(
59-
myStandardService(
87+
myGoService(
6088
"svc1",
6189
svc1Repo, svc1Subdir,
6290
"ghcr.io/fastcat/gdev/svc1",
6391
service.WithSource(svc1Repo, svc1Subdir, "", ""),
6492
),
6593
)
66-
stack.AddService(service.New("svc2",
67-
service.WithResources(
68-
resource.PMStatic(api.Child{
69-
Name: "svc2",
70-
Main: api.Exec{
71-
Cmd: "sleep",
72-
Args: []string{"1h"},
73-
},
74-
}),
94+
// use this so build detection sees the rush project
95+
svc2Repo := filepath.Join(shx.HomeDir(), "src", "gdev", "examples", "stack", "svc2")
96+
svc2Subdir := "app"
97+
stack.AddService(
98+
myNodeService(
99+
"svc2",
100+
svc2Repo, svc2Subdir,
101+
"ghcr.io/fastcat/gdev/svc2",
102+
service.WithSource(svc2Repo, svc2Subdir, "", ""),
75103
),
76-
))
104+
)
77105

78106
cmd.Main()
79107
}

examples/stack/svc1/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ func main() {
2424
func getRoot(w http.ResponseWriter, r *http.Request) {
2525
w.Header().Set("Content-Type", "text/plain")
2626
w.WriteHeader(http.StatusOK)
27-
_, _ = w.Write([]byte("Hello, World!"))
27+
_, _ = w.Write([]byte("Hello, World!\nThis is svc1!\n"))
2828
}

examples/stack/svc2/.gitattributes

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Don't allow people to merge changes to these generated files, because the result
2+
# may be invalid. You need to run "rush update" again.
3+
pnpm-lock.yaml merge=text
4+
shrinkwrap.yaml merge=binary
5+
npm-shrinkwrap.json merge=binary
6+
yarn.lock merge=binary
7+
8+
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
9+
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
10+
# may also require a special configuration to allow comments in JSON.
11+
#
12+
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
13+
#
14+
*.json linguist-language=JSON-with-Comments

0 commit comments

Comments
 (0)