From 44c5fc171230c42af04eeff08c465b6c92c4e76a Mon Sep 17 00:00:00 2001 From: Sudaman Shrestha Date: Tue, 9 Sep 2025 09:06:59 +0200 Subject: [PATCH 1/5] first iteration: includes linting and publishing the package to the github packages second iteration: fixed linters, improved workflow and structure push nextjs build step --- .github/workflows/js-mage.yaml | 98 +++++++++++++++++++ internal/core/core.go | 44 +++++++++ internal/docker/docker.go | 42 ++++++++ internal/git/git.go | 18 ++++ internal/javascript/javascript.go | 154 ++++++++++++++++++++++++++++++ targets/jsapp/app.Dockerfile | 49 ++++++++++ targets/jsapp/main.go | 131 +++++++++++++++++++++++++ targets/jslib/main.go | 27 ++++++ 8 files changed, 563 insertions(+) create mode 100644 .github/workflows/js-mage.yaml create mode 100644 internal/javascript/javascript.go create mode 100644 targets/jsapp/app.Dockerfile create mode 100644 targets/jsapp/main.go create mode 100644 targets/jslib/main.go diff --git a/.github/workflows/js-mage.yaml b/.github/workflows/js-mage.yaml new file mode 100644 index 00000000..5971b249 --- /dev/null +++ b/.github/workflows/js-mage.yaml @@ -0,0 +1,98 @@ +name: mage + +on: + workflow_call: + inputs: + project-type: + description: 'Is it js library or application. Use "jslib" or "jsapp"' + required: true + type: string + skip-build: + description: 'Set to true if project do not need to be build' + type: boolean + default: false + required: false + build-command: + description: 'NPM project build command. Defaults to build' + type: string + default: 'build' + required: false + lint: + default: true + type: boolean + required: false + description: 'Set to true if you want to run' + test: + description: 'Set to true if project needs to run test' + type: boolean + default: false + required: false + private: + description: 'Set to true if npm package to be publish is a private' + type: boolean + required: false + default: false + app-deploy-env: + type: string + description: 'Use "production", "dev", "test", "staging", etc' + required: false + default: "production" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: read + +jobs: + mage: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '>=1.24.0' + cache-dependency-path: "**/go.sum" + + - name: Install go Tools + run: go install tool + + - name: Lint + if: ${{ inputs.lint }} + run: "go tool mage -v ${{ inputs.project-type}}:lint" + + - name: Publish + if: ${{ github.ref == 'refs/heads/main' && github.event.release.tag_name && inputs.project-type == 'jslib' }} + run: "go tool mage -v jslib:publish" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRIVATE: ${{ inputs.is-npm-package-private }} + DIST_DIR: ${{ inputs.build-output-dir }} + GITHUB_TAGNAME: ${{ github.event.release.tag_name }} + SKIP_BUILD: ${{ github.skip-build }} + + - name: Build app and Push to docker repository + if: ${{ github.ref == 'refs/heads/main' && inputs.project-type == 'jsapp' }} + run: "go tool mage -v jsapp:build" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_COMMAND: ${{ inputs.build-command }} + DEPLOY_ENV: ${{ inputs.app-deploy-env }} + + - id: oci-images + name: Output OCI images references + if: ${{ github.ref == 'refs/heads/main' && inputs.project-type == 'jsapp' }} + run: | + if [ -f ./var/oci-images.json ]; then + echo "images=$(cat ./var/oci-images.json)" >> $GITHUB_OUTPUT + else + echo "images={}" >> $GITHUB_OUTPUT + fi + - name: Show output + if: ${{ github.ref == 'refs/heads/main' && inputs.project-type == 'jsapp' }} + run: tree var/ diff --git a/internal/core/core.go b/internal/core/core.go index 03f7febf..784fdfd2 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -2,7 +2,9 @@ package core import ( "fmt" + "io" "io/fs" + "log" "os" "path" "path/filepath" @@ -117,3 +119,45 @@ func CompareChangesToPaths(changes []string, paths []string, additionalGlobs []s } return false, nil } + +// FileExists checks if the file in given path exists or not. +func FileExists(path string) bool { + _, err := os.Stat(path) + + return err == nil +} + +// WriteFile creates a file with a given content or appends content to existing file +// at the specified path +func WriteFile(path string, content string) error { + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + + defer func() { + if err := file.Close(); err != nil { + log.Printf("Failed to close file: %v", err) + } + }() + + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + + if _, err := io.WriteString(file, content); err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + + return nil +} + +// IsDirectoryEmpty checks if the specified directory is empty +// and returns true if it contains no files or subdirectories. +// Also return error if there is any while reading the directory +func IsDirectoryEmpty(dirPath string) (bool, error) { + entries, err := os.ReadDir(dirPath) + + if err != nil { + return true, err + } + + return len(entries) == 0, nil +} diff --git a/internal/docker/docker.go b/internal/docker/docker.go index a62966fb..ac87228a 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -7,6 +7,7 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" "time" @@ -21,6 +22,12 @@ const ( imageNameBaseFallback = "ocreg.invalid/coopnorge" ) +const ( + // PushEnv is the name of the environmental variable used to trigger + // pushing of OCI images. Set PUSH_IMAGE to true to push images. + PushEnv = "PUSH_IMAGE" +) + // Validate the content of a Dockerfile func Validate(dockerfileContent string) error { dockerfilePath, cleanup, err := core.WriteTempFile("./var", "Dockerfile", dockerfileContent) @@ -90,6 +97,24 @@ func BuildAndPush(dockerfileContent, platforms, image, dockerContext, imagePath, ) } + githubToken := os.Getenv("GITHUB_TOKEN") + + if githubToken != "" { + filename, cleanup, err := core.WriteTempFile(".", "github_token", githubToken) + + if err != nil { + return err + } + + args = append( + args, + "--secret", + fmt.Sprintf("id=github_token,src=%s", filename), + ) + + defer cleanup() + } + args = append(args, "-f", dockerfilePath, dockerContext, @@ -192,14 +217,31 @@ func Images(imageDir string) (AppImages, error) { result[metadata.App][metadata.Binary]["tag"] = metadata.Tag result[metadata.App][metadata.Binary]["image"] = metadata.ImageName } + return result, nil } // FullyQualifiedlImageName ... func FullyQualifiedlImageName(app, binary string) string { + if binary == "" { + return fmt.Sprintf("%s/%s", imageBase(), app) + } return fmt.Sprintf("%s/%s/%s", imageBase(), app, binary) } +// ShouldPush checks if docker image should be pushed to the repository or not +func ShouldPush() (bool, error) { + val, ok := os.LookupEnv(PushEnv) + if !ok || val == "" { + return false, nil + } + boolValue, err := strconv.ParseBool(val) + if err != nil { + return false, err + } + return boolValue, nil +} + func imageBase() string { imageBase, ok := os.LookupEnv(imageBaseEnv) if !ok || imageBase == "" { diff --git a/internal/git/git.go b/internal/git/git.go index 7cf74fd0..2265f378 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -2,11 +2,29 @@ package git import ( "fmt" + "path" "strings" "github.com/magefile/mage/sh" ) +// RepoNameFromURL returns the repository name +func RepoNameFromURL() (string, error) { + url, err := RepoURL() + + if err != nil { + return "", err + } + + // 1. Get the last element of the URL path + base := path.Base(url) + + // 2. Remove the ".git" suffix if it exists + projectName := strings.TrimSuffix(base, ".git") + + return projectName, nil +} + // RepoURL returns the remote URL to the git repository func RepoURL() (string, error) { remote, err := sh.Output("git", "remote", "get-url", "origin") diff --git a/internal/javascript/javascript.go b/internal/javascript/javascript.go new file mode 100644 index 00000000..ad5428b6 --- /dev/null +++ b/internal/javascript/javascript.go @@ -0,0 +1,154 @@ +package javascript + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/coopnorge/mage/internal/core" + "github.com/magefile/mage/sh" +) + +// Lint checks for the biome config file and runs the linting in a docker container +// Prints error and exits +func Lint() error { + if !core.FileExists("biome.json") { + return errors.New("biome not setup in your project. Install @coopnorge/web-devtools") + } + return devtoolBiomeLint() +} + +// PublishLib checks if package.json file exists or not, checks if distribution/build-output folder +// exists or not, checks if .npmrc file exits or not +func PublishLib() error { + githubToken := os.Getenv("GITHUB_TOKEN") + privateEnv := os.Getenv("PRIVATE") + isPrivate := strings.ToLower(privateEnv) == "true" || privateEnv == "1" + githubTagname := os.Getenv("GITHUB_TAGNAME") + skipBuild := os.Getenv("SKIP_BUILD") + buildCommand := os.Getenv("BUILD_COMMAND") + newVersion := strings.TrimPrefix(githubTagname, "v") + + distDir := "dist" + + access := "public" + + if isPrivate { + access = "restricted" + } + + if newVersion == "" { + return errors.New("no new package version set. Set GITHUB_TAGNAME env variable") + } + + if skipBuild == "" { + isDistDirEmpty, errOnCheckDistDir := core.IsDirectoryEmpty(distDir) + + if isDistDirEmpty { + return errors.New("no build files to publish") + } + + if errOnCheckDistDir != nil { + return errOnCheckDistDir + } + } + + if !core.FileExists(".npmrc") { + return errors.New(".npmrc file missing") + } + + if !IsNpmrcValidForPublish(".") { + return errors.New(".npmrc has no auth configuration") + } + + if !core.FileExists("package.json") { + return errors.New("not a js node project") + } + + // Run build if build command is set or skip build is not set. + // Some pakages won't need to be build + if buildCommand != "" || skipBuild == "" { + if buildCommand == "" { + buildCommand = "build" + } + buildCommand = fmt.Sprintf("npm install && npm run %s", buildCommand) + } + + commands := fmt.Sprintf("%s && npm version %s && npm publish --access %s", buildCommand, newVersion, access) + + return devtoolPublishNpmLib(commands, githubToken) +} + +// IsNpmrcValidForPublish checks if the .npmrc file is configured for GitHub +// Packages. +func IsNpmrcValidForPublish(directory string) bool { + if directory == "" { + directory = "." + } + + registryURL := "npm.pkg.github.com" + scope := "@coopnorge" + tokenIndicator := "_authToken=" + + npmrcContent, err := os.ReadFile(fmt.Sprintf("%s/.npmrc", directory)) + + if err != nil { + return false + } + + contentStr := string(npmrcContent) + + if !strings.Contains(contentStr, registryURL) && !strings.Contains(contentStr, scope) && !strings.Contains(contentStr, tokenIndicator) { + return false + } + + return true +} + +func devtoolBiomeLint() error { + // Get the current working directory to mount it. + cwd, err := os.Getwd() + + if err != nil { + return err + } + + dockerArgs := []string{ + "--volume", fmt.Sprintf("%s:/app", cwd), + "--workdir", "/app", + } + + return Run("ghcr.io/biomejs/biome:1.8.3", dockerArgs, "lint") +} + +func devtoolPublishNpmLib(commands string, githubToken string) error { + // Get the current working directory to mount it. + cwd, err := os.Getwd() + + if err != nil { + return err + } + + dockerArgs := []string{ + "-e", fmt.Sprintf("GITHUB_TOKEN=%s", githubToken), + "--volume", fmt.Sprintf("%s:/app", cwd), + "--workdir", "/app", + } + + return Run("node:slim", dockerArgs, "sh", "-c", commands) +} + +// Run will run the specified command with arguments in the +// specified Docker image +func Run(image string, dockerRunArgs []string, args ...string) error { + call := []string{ + "run", + "--rm", + } + + call = append(call, dockerRunArgs...) + call = append(call, image) + call = append(call, args...) + return sh.RunV("docker", call...) +} diff --git a/targets/jsapp/app.Dockerfile b/targets/jsapp/app.Dockerfile new file mode 100644 index 00000000..31bc167a --- /dev/null +++ b/targets/jsapp/app.Dockerfile @@ -0,0 +1,49 @@ +FROM node:lts-slim@sha256:fe64023c6490eb001c7a28e9f92ef8deb6e40e1b7fc5352d695dcaef59e1652d AS builder + +ARG BUILD_SCRIPT=build + +WORKDIR /app +COPY . . + +RUN --mount=type=secret,id=github_token \ + GITHUB_TOKEN=$(cat /run/secrets/github_token) npm install + +RUN npm run ${BUILD_SCRIPT} + +FROM builder AS runner +WORKDIR /app + +# Uncomment the following line in case you want to disable telemetry during runtime. +ENV NEXT_TELEMETRY_DISABLED=1 + +ARG GROUP=nodejs +ARG USER=nextjs +ARG DISTFOLDER=.next + +RUN addgroup --system --gid 1001 ${GROUP} +RUN adduser --system --uid 1001 ${USER} + +COPY --from=builder --chown=${USER}:${GROUP} /app/${DISTFOLDER}/standalone ./ +COPY --from=builder --chown=${USER}:${GROUP} /app/${DISTFOLDER}/static ./static +COPY --from=builder --chown=${USER}:${GROUP} /app/public ./public + + +ARG GIT_REPOSITORY_URL +ARG GIT_COMMIT_SHA + + +LABEL org.opencontainers.image.source=${GIT_REPOSITORY_URL} +LABEL org.opencontainers.image.revision=${GIT_COMMIT_SHA} + +USER ${USER}:${GROUP} + +ENV DD_GIT_REPOSITORY_URL=${GIT_REPOSITORY_URL} +ENV DD_GIT_COMMIT_SHA=${GIT_COMMIT_SHA} +EXPOSE 3000 +ENV PORT=3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/targets/jsapp/main.go b/targets/jsapp/main.go new file mode 100644 index 00000000..a843bcb3 --- /dev/null +++ b/targets/jsapp/main.go @@ -0,0 +1,131 @@ +package jsapp + +import ( + "context" + _ "embed" + "encoding/json" + "os" + "path" + + "github.com/coopnorge/mage/internal/core" + "github.com/coopnorge/mage/internal/docker" + "github.com/coopnorge/mage/internal/git" + "github.com/coopnorge/mage/internal/javascript" + "github.com/magefile/mage/mg" +) + +var ( + //go:embed app.Dockerfile + dockerfile string +) + +const ( + platforms = "linux/amd64,linux/arm64" +) + +// JSApp is the magefile namespace to group JSAPP commands +type JSApp mg.Namespace + +// BuildAndPush OCI image. Setting push to true will push the images to the +// registries. When push is true images are not tagged with latest. +// +// [BuildApp] will create: +// +// ./var +// ├── oci-images.json +// └── app +// └── oci +// ├── production +// │   ├── image.tar +// │   └── metadata.json +// └── testing +// ├── image.tar +// └── metadata.json +// +// oci-images.json will contain a map over the images and tags for app per +// environment. Use case: We add data-test-id for automating browser testing. +// These are quite a lot of ids and we remove them for production build/env. +// +// { +// "app": { +// "testing": { +// "image": "ocreg.invalid/coopnorge/app/testing:v2025.03.11135857", +// "tag": "v2025.03.11135857" +// }, +// "production": { +// "image": "ocreg.invalid/coopnorge/app1/production:v2025.03.11135857", +// "tag": "v2025.03.11135857" +// } +// } +// } + +// BuildApp creates deployable artifacts from the source code in the repository, +// to push the resulting images set the environmental variable PUSH_IMAGE to +// true. Setting PUSH_IMAGE to true will disable the latest image tag. +func (JSApp) BuildApp(ctx context.Context) error { + shouldPush, err := docker.ShouldPush() + if err != nil { + return err + } + + mg.SerialCtxDeps(ctx, JSApp.Validate, mg.F(buildAndPush, shouldPush)) + return writeImageMetadata() +} + +// Lint checks all javascript/typescript codd for code standards and formats +// +// See [javascript.Lint] for details. +func (JSApp) Lint(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Lint) + return nil +} + +// Validate Dockerfiles +func (JSApp) Validate(_ context.Context) error { + return docker.Validate(dockerfile) +} + +func buildAndPush(shouldPush bool) error { + env := os.Getenv("DEPLOY_ENV") + + if env == "" { + env = "production" + } + + app, err := git.RepoNameFromURL() + + if err != nil { + return err + } + + imageName := docker.FullyQualifiedlImageName(app, env) + imagePath := imagePath(app, env) + metadataPath := metadataPath(app, env) + + return docker.BuildAndPush(dockerfile, platforms, imageName, ".", imagePath, metadataPath, app, env, shouldPush) +} + +func imageDir(app string, env string) string { + return path.Join(core.OutputDir, app, "oci", env) +} + +func imagePath(app string, env string) string { + return path.Join(imageDir(app, env), "image.tar") +} + +func metadataPath(app string, env string) string { + return path.Join(imageDir(app, env), "metadata.json") +} + +func writeImageMetadata() error { + images, err := docker.Images(core.OutputDir) + if err != nil { + return err + } + + jsonString, err := json.Marshal(images) + if err != nil { + return err + } + return os.WriteFile(path.Join(core.OutputDir, "oci-images.json"), jsonString, 0644) +} diff --git a/targets/jslib/main.go b/targets/jslib/main.go new file mode 100644 index 00000000..3d8546dd --- /dev/null +++ b/targets/jslib/main.go @@ -0,0 +1,27 @@ +package jslib + +import ( + "context" + + "github.com/coopnorge/mage/internal/javascript" + "github.com/magefile/mage/mg" +) + +// JSLib is the magefile namespace to group Javascript language specific commands +type JSLib mg.Namespace + +// Lint checks all javascript/typescript codd for code standards and formats +// +// See [javascript.Lint] for details. +func (JSLib) Lint(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Lint) + return nil +} + +// Publish publish npm package to the github package +// +// See [javascript.Publish] for details. +func (JSLib) Publish(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.PublishLib) + return nil +} From 4fba6415f98e7494623450f5394ac259e84d5264 Mon Sep 17 00:00:00 2001 From: Sudaman Shrestha Date: Mon, 10 Nov 2025 16:14:00 +0100 Subject: [PATCH 2/5] changes in targets --- .../workflows/{js-mage.yaml => js-app.yaml} | 54 ++----- .github/workflows/js-lib.yaml | 67 ++++++++ internal/core/core.go | 4 +- internal/javascript/javascript.go | 121 +------------- internal/targets/javascript/javascript.go | 123 ++++++++++++++ targets/jsapp/javascript.go | 150 ++++++++++++++++++ targets/jsapp/main.go | 133 +++------------- targets/jslib/javascript.go | 85 ++++++++++ targets/jslib/main.go | 40 +++-- 9 files changed, 501 insertions(+), 276 deletions(-) rename .github/workflows/{js-mage.yaml => js-app.yaml} (50%) create mode 100644 .github/workflows/js-lib.yaml create mode 100644 internal/targets/javascript/javascript.go create mode 100644 targets/jsapp/javascript.go create mode 100644 targets/jslib/javascript.go diff --git a/.github/workflows/js-mage.yaml b/.github/workflows/js-app.yaml similarity index 50% rename from .github/workflows/js-mage.yaml rename to .github/workflows/js-app.yaml index 5971b249..265aa319 100644 --- a/.github/workflows/js-mage.yaml +++ b/.github/workflows/js-app.yaml @@ -1,37 +1,23 @@ -name: mage +name: js-app on: workflow_call: inputs: - project-type: - description: 'Is it js library or application. Use "jslib" or "jsapp"' - required: true - type: string - skip-build: - description: 'Set to true if project do not need to be build' - type: boolean - default: false - required: false build-command: description: 'NPM project build command. Defaults to build' type: string default: 'build' required: false - lint: - default: true - type: boolean - required: false - description: 'Set to true if you want to run' - test: - description: 'Set to true if project needs to run test' + skip-unit-tests: + description: 'Set to true if project needs to skip running unit tests' type: boolean default: false required: false - private: - description: 'Set to true if npm package to be publish is a private' + skip-e2e-tests: + description: 'Set to true if project needs to skip running browser tests' type: boolean - required: false default: false + required: false app-deploy-env: type: string description: 'Use "production", "dev", "test", "staging", etc' @@ -47,7 +33,8 @@ permissions: packages: read jobs: - mage: + js-app-build: + name: Build nextjs app runs-on: ubuntu-latest steps: - name: Checkout code @@ -62,31 +49,22 @@ jobs: - name: Install go Tools run: go install tool - - name: Lint - if: ${{ inputs.lint }} - run: "go tool mage -v ${{ inputs.project-type}}:lint" - - - name: Publish - if: ${{ github.ref == 'refs/heads/main' && github.event.release.tag_name && inputs.project-type == 'jslib' }} - run: "go tool mage -v jslib:publish" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PRIVATE: ${{ inputs.is-npm-package-private }} - DIST_DIR: ${{ inputs.build-output-dir }} - GITHUB_TAGNAME: ${{ github.event.release.tag_name }} - SKIP_BUILD: ${{ github.skip-build }} + - name: Setup Node.js (for the JS project) + uses: actions/setup-node@v4 + with: + node-version: '20' - name: Build app and Push to docker repository - if: ${{ github.ref == 'refs/heads/main' && inputs.project-type == 'jsapp' }} - run: "go tool mage -v jsapp:build" + run: "go tool mage -v buildandpublish" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_COMMAND: ${{ inputs.build-command }} DEPLOY_ENV: ${{ inputs.app-deploy-env }} + SKIP_UNIT_TEST: ${{ github.skip-unit-tests }} + SKIP_E2E_TEST: ${{ github.skip-e2e-tests }} - id: oci-images name: Output OCI images references - if: ${{ github.ref == 'refs/heads/main' && inputs.project-type == 'jsapp' }} run: | if [ -f ./var/oci-images.json ]; then echo "images=$(cat ./var/oci-images.json)" >> $GITHUB_OUTPUT @@ -94,5 +72,5 @@ jobs: echo "images={}" >> $GITHUB_OUTPUT fi - name: Show output - if: ${{ github.ref == 'refs/heads/main' && inputs.project-type == 'jsapp' }} run: tree var/ + diff --git a/.github/workflows/js-lib.yaml b/.github/workflows/js-lib.yaml new file mode 100644 index 00000000..6e58bdbe --- /dev/null +++ b/.github/workflows/js-lib.yaml @@ -0,0 +1,67 @@ +name: mage + +on: + workflow_call: + inputs: + skip-build: + description: 'Set to true if project do not need to be build' + type: boolean + default: false + required: false + build-command: + description: 'NPM project build command. Defaults to build:library' + type: string + default: 'build:library' + required: false + skip-unit-tests: + description: 'Set to true if want to skip unit tests' + type: boolean + default: false + required: false + private: + description: 'Set to true if npm package to be publish is a private' + type: boolean + required: false + default: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: read + +jobs: + publish-js-lib: + name: Publish to github npm repository + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '>=1.24.0' + cache-dependency-path: "**/go.sum" + + - name: Install go Tools + run: go install tool + + - name: Setup Node.js (for the JS project) + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build and publish + run: go tool mage -v buildandpublish + env: + BUILD_COMMAND: ${{ github.build-command }} + SKIP_BUILD: ${{ gibhub.skip-build }} + SKIP_UNIT_TEST: ${{ github.skip-unit-tests }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRIVATE: ${{ inputs.is-npm-package-private }} + GITHUB_TAGNAME: ${{ github.event.release.tag_name }} + + diff --git a/internal/core/core.go b/internal/core/core.go index 2532bf6c..e18515e9 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -130,9 +130,9 @@ func FileExists(path string) bool { return err == nil } -// WriteFile creates a file with a given content or appends content to existing file +// CreateOrAppendFile creates a file with a given content or appends content to existing file // at the specified path -func WriteFile(path string, content string) error { +func CreateOrAppendFile(path string, content string) error { file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) defer func() { diff --git a/internal/javascript/javascript.go b/internal/javascript/javascript.go index ad5428b6..065f8377 100644 --- a/internal/javascript/javascript.go +++ b/internal/javascript/javascript.go @@ -10,79 +10,9 @@ import ( "github.com/magefile/mage/sh" ) -// Lint checks for the biome config file and runs the linting in a docker container -// Prints error and exits -func Lint() error { - if !core.FileExists("biome.json") { - return errors.New("biome not setup in your project. Install @coopnorge/web-devtools") - } - return devtoolBiomeLint() -} - -// PublishLib checks if package.json file exists or not, checks if distribution/build-output folder -// exists or not, checks if .npmrc file exits or not -func PublishLib() error { - githubToken := os.Getenv("GITHUB_TOKEN") - privateEnv := os.Getenv("PRIVATE") - isPrivate := strings.ToLower(privateEnv) == "true" || privateEnv == "1" - githubTagname := os.Getenv("GITHUB_TAGNAME") - skipBuild := os.Getenv("SKIP_BUILD") - buildCommand := os.Getenv("BUILD_COMMAND") - newVersion := strings.TrimPrefix(githubTagname, "v") - - distDir := "dist" - - access := "public" - - if isPrivate { - access = "restricted" - } - - if newVersion == "" { - return errors.New("no new package version set. Set GITHUB_TAGNAME env variable") - } - - if skipBuild == "" { - isDistDirEmpty, errOnCheckDistDir := core.IsDirectoryEmpty(distDir) - - if isDistDirEmpty { - return errors.New("no build files to publish") - } - - if errOnCheckDistDir != nil { - return errOnCheckDistDir - } - } - - if !core.FileExists(".npmrc") { - return errors.New(".npmrc file missing") - } - - if !IsNpmrcValidForPublish(".") { - return errors.New(".npmrc has no auth configuration") - } - - if !core.FileExists("package.json") { - return errors.New("not a js node project") - } - - // Run build if build command is set or skip build is not set. - // Some pakages won't need to be build - if buildCommand != "" || skipBuild == "" { - if buildCommand == "" { - buildCommand = "build" - } - buildCommand = fmt.Sprintf("npm install && npm run %s", buildCommand) - } - - commands := fmt.Sprintf("%s && npm version %s && npm publish --access %s", buildCommand, newVersion, access) - - return devtoolPublishNpmLib(commands, githubToken) -} - -// IsNpmrcValidForPublish checks if the .npmrc file is configured for GitHub +// IsNpmrcConfiguredForPrivateRepo checks if the .npmrc file is configured for GitHub // Packages. -func IsNpmrcValidForPublish(directory string) bool { +func IsNpmrcConfiguredForPrivateRepo(directory string) bool { if directory == "" { directory = "." } @@ -106,49 +36,10 @@ func IsNpmrcValidForPublish(directory string) bool { return true } -func devtoolBiomeLint() error { - // Get the current working directory to mount it. - cwd, err := os.Getwd() - - if err != nil { - return err - } - - dockerArgs := []string{ - "--volume", fmt.Sprintf("%s:/app", cwd), - "--workdir", "/app", - } - - return Run("ghcr.io/biomejs/biome:1.8.3", dockerArgs, "lint") -} - -func devtoolPublishNpmLib(commands string, githubToken string) error { - // Get the current working directory to mount it. - cwd, err := os.Getwd() - - if err != nil { - return err - } - - dockerArgs := []string{ - "-e", fmt.Sprintf("GITHUB_TOKEN=%s", githubToken), - "--volume", fmt.Sprintf("%s:/app", cwd), - "--workdir", "/app", - } - - return Run("node:slim", dockerArgs, "sh", "-c", commands) +func HasBiomeConfig() bool { + return core.FileExists("biome.json") } -// Run will run the specified command with arguments in the -// specified Docker image -func Run(image string, dockerRunArgs []string, args ...string) error { - call := []string{ - "run", - "--rm", - } - - call = append(call, dockerRunArgs...) - call = append(call, image) - call = append(call, args...) - return sh.RunV("docker", call...) +func HasPackageConfig() bool { + return core.FileExists("package.json") } diff --git a/internal/targets/javascript/javascript.go b/internal/targets/javascript/javascript.go new file mode 100644 index 00000000..f8a91b84 --- /dev/null +++ b/internal/targets/javascript/javascript.go @@ -0,0 +1,123 @@ +package javascript + +import ( + "fmt" + "os" + "path/filepath" + "github.com/coopnorge/mage/internal/javascript" + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +// Install fetches all Node.js dependencies. +func Install() error { + if javascript.HasPackageConfig() { + tokenName := "GITHUB_TOKEN" + tokenValue := os.Getenv(tokenName) + + + env := map[string]string{} + + if githubToken != "" && IsNpmrcConfiguredForPrivateRepo() { + env[tokenName] = tokenValue + } + + if err := sh.RunWithV(env, "npm", "install"); err != nil { + return fmt.Errorf("dependency installation failed: %w", err) + } + } + + return nil +} + +// Lint runs the standard linting script defined in package.json. +func Lint() error { + envData := os.Getenv("SKIP_LINT") + skip := strings.ToLower(envData) == "true" || envData == "1" + + if (skip) { + return nil + } + + if javascript.HasBiomeConfig() { + return errors.New("biome not setup in your project. Install @coopnorge/web-devtools") + } + + if err := sh.RunV("npm", "run", "lint"); err != nil { + return fmt.Errorf("linting failed: %w", err) + } + + return nil +} + +// Format runs the standard formatting check script defined in package.json. +func Format() error { + envData := os.Getenv("SKIP_FORMAT") + skip := strings.ToLower(envData) == "true" || envData == "1" + + if (skip) { + return nil + } + + if javascript.HasBiomeConfig() { + return errors.New("biome not setup in your project. Install @coopnorge/web-devtools") + } + + if err := sh.RunV("npm", "run", "format:code"); err != nil { + return fmt.Errorf("linting failed: %w", err) + } + + return nil +} + +// UnitTest runs unit tests using the package.json script. +func UnitTest() error { + envData := os.Getenv("SKIP_UNIT_TEST") + skip := strings.ToLower(envData) == "true" || envData == "1" + + if (skip) { + return nil + } + + if err := sh.RunV("npm", "run", "test:unit"); err != nil { + return fmt.Errorf("unit tests failed: %w", err) + } + + return nil +} + +// E2ETest runs End-to-End tests (often separate and slower). +func E2ETest() error { + envData := os.Getenv("SKIP_E2E_TEST") + skip := strings.ToLower(envData) == "true" || envData == "1" + + if (skip) { + return nil + } + + if err := sh.RunV("npm", "run", "test:e2e"); err != nil { + return fmt.Errorf("E2E tests failed: %w", err) + } + + return nil +} + +// Build compiles the JavaScript/TypeScript into distribution files. +func Build(buildCommand string) error { + envData := os.Getenv("SKIP_BUILD") + skip := strings.ToLower(envData) == "true" || envData == "1" + + if (skip) { + return nil + } + + if buildCommand == "" { + buildCommand = "build" + } + + if err := sh.RunV("npm", "run", buildCommand); err != nil { + return fmt.Errorf("build failed: %w", err) + } + + return nil +} diff --git a/targets/jsapp/javascript.go b/targets/jsapp/javascript.go new file mode 100644 index 00000000..52ed3dec --- /dev/null +++ b/targets/jsapp/javascript.go @@ -0,0 +1,150 @@ +package jsapp + +import ( + "context" + _ "embed" + "encoding/json" + "os" + "path" + + "github.com/coopnorge/mage/internal/targets/javascript" + "github.com/coopnorge/mage/internal/core" + "github.com/coopnorge/mage/internal/docker" + "github.com/coopnorge/mage/internal/git" + "github.com/magefile/mage/mg" +) + +var ( + //go:embed app.Dockerfile + dockerfile string +) + +const ( + platforms = "linux/amd64,linux/arm64" +) + +// JavaScript is the magefile namespace to group javascript/typescript commands +type JavaScript mg.Namespace + +// BuildAndPush OCI image. Setting push to true will push the images to the +// registries. When push is true images are not tagged with latest. +// +// [BuildApp] will create: +// +// ./var +// ├── oci-images.json +// └── app +// └── oci +// ├── production +// │   ├── image.tar +// │   └── metadata.json +// └── testing +// ├── image.tar +// └── metadata.json +// +// oci-images.json will contain a map over the images and tags for app per +// environment. Use case: We add data-test-id for automating browser testing. +// These are quite a lot of ids and we remove them for production build/env. +// +// { +// "app": { +// "testing": { +// "image": "ocreg.invalid/coopnorge/app/testing:v2025.03.11135857", +// "tag": "v2025.03.11135857" +// }, +// "production": { +// "image": "ocreg.invalid/coopnorge/app1/production:v2025.03.11135857", +// "tag": "v2025.03.11135857" +// } +// } +// } + +// BuildAndPushDockerImage creates deployable artifacts from the source code in the repository, +// to push the resulting images set the environmental variable PUSH_IMAGE to +// true. Setting PUSH_IMAGE to true will disable the latest image tag. +func (JavaScript) BuildAndPushDockerImage(ctx context.Context) error { + shouldPush, err := docker.ShouldPush() + if err != nil { + return err + } + + mg.SerialCtxDeps(ctx, mg.F(buildAndPush, shouldPush)) + return writeImageMetadata() +} + + +// Install fetches all Node.js dependencies. +func (JavaScript) Install(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Install) + return nil +} + +// Lint runs the standard linting script defined in package.json. +func (JavaScript) Lint(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Lint) + return nil +} + +// Format runs the standard formatting check script defined in package.json. +func (JavaScript) Format(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Format) + return nil +} + +// UnitTest unit tests using the package.json script. +func (JavaScript) UnitTest(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.UnitTest) + return nil +} + +// E2ETest runs browser tests using the package.json script. +func (JavaScript) E2ETest(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.E2ETest) + return nil +} + + +func buildAndPush(shouldPush bool) error { + env := os.Getenv("DEPLOY_ENV") + + if env == "" { + env = "production" + } + + app, err := git.RepoNameFromURL() + + if err != nil { + return err + } + + imageName := docker.FullyQualifiedlImageName(app, env) + imagePath := imagePath(app, env) + metadataPath := metadataPath(app, env) + + return docker.BuildAndPush(dockerfile, platforms, imageName, ".", imagePath, metadataPath, app, env, shouldPush) +} + +func imageDir(app string, env string) string { + return path.Join(core.OutputDir, app, "oci", env) +} + +func imagePath(app string, env string) string { + return path.Join(imageDir(app, env), "image.tar") +} + +func metadataPath(app string, env string) string { + return path.Join(imageDir(app, env), "metadata.json") +} + +func writeImageMetadata() error { + images, err := docker.Images(core.OutputDir) + if err != nil { + return err + } + + jsonString, err := json.Marshal(images) + if err != nil { + return err + } + return os.WriteFile(path.Join(core.OutputDir, "oci-images.json"), jsonString, 0644) +} diff --git a/targets/jsapp/main.go b/targets/jsapp/main.go index a843bcb3..45d7a812 100644 --- a/targets/jsapp/main.go +++ b/targets/jsapp/main.go @@ -2,130 +2,47 @@ package jsapp import ( "context" - _ "embed" - "encoding/json" - "os" - "path" - "github.com/coopnorge/mage/internal/core" - "github.com/coopnorge/mage/internal/docker" - "github.com/coopnorge/mage/internal/git" - "github.com/coopnorge/mage/internal/javascript" "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" ) -var ( - //go:embed app.Dockerfile - dockerfile string -) - -const ( - platforms = "linux/amd64,linux/arm64" -) - -// JSApp is the magefile namespace to group JSAPP commands -type JSApp mg.Namespace - -// BuildAndPush OCI image. Setting push to true will push the images to the -// registries. When push is true images are not tagged with latest. -// -// [BuildApp] will create: -// -// ./var -// ├── oci-images.json -// └── app -// └── oci -// ├── production -// │   ├── image.tar -// │   └── metadata.json -// └── testing -// ├── image.tar -// └── metadata.json -// -// oci-images.json will contain a map over the images and tags for app per -// environment. Use case: We add data-test-id for automating browser testing. -// These are quite a lot of ids and we remove them for production build/env. -// -// { -// "app": { -// "testing": { -// "image": "ocreg.invalid/coopnorge/app/testing:v2025.03.11135857", -// "tag": "v2025.03.11135857" -// }, -// "production": { -// "image": "ocreg.invalid/coopnorge/app1/production:v2025.03.11135857", -// "tag": "v2025.03.11135857" -// } -// } -// } - -// BuildApp creates deployable artifacts from the source code in the repository, +// BuildAndPublish creates deployable artifacts from the source code in the repository, // to push the resulting images set the environmental variable PUSH_IMAGE to // true. Setting PUSH_IMAGE to true will disable the latest image tag. -func (JSApp) BuildApp(ctx context.Context) error { - shouldPush, err := docker.ShouldPush() - if err != nil { - return err - } - - mg.SerialCtxDeps(ctx, JSApp.Validate, mg.F(buildAndPush, shouldPush)) - return writeImageMetadata() -} - -// Lint checks all javascript/typescript codd for code standards and formats -// -// See [javascript.Lint] for details. -func (JSApp) Lint(ctx context.Context) error { - mg.CtxDeps(ctx, javascript.Lint) +func BuildAndPublish(ctx context.Context) error { + mg.SerialCtxDeps(ctx, Install, Lint, Format, UnitTest, E2ETest, JavaScript.BuildAndPushDockerImage) return nil } -// Validate Dockerfiles -func (JSApp) Validate(_ context.Context) error { - return docker.Validate(dockerfile) -} - -func buildAndPush(shouldPush bool) error { - env := os.Getenv("DEPLOY_ENV") - if env == "" { - env = "production" - } - - app, err := git.RepoNameFromURL() - - if err != nil { - return err - } - - imageName := docker.FullyQualifiedlImageName(app, env) - imagePath := imagePath(app, env) - metadataPath := metadataPath(app, env) - - return docker.BuildAndPush(dockerfile, platforms, imageName, ".", imagePath, metadataPath, app, env, shouldPush) +// Install fetches all Node.js dependencies. +func Install(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.Install) + return nil } -func imageDir(app string, env string) string { - return path.Join(core.OutputDir, app, "oci", env) +// Lint runs the standard linting script defined in package.json. +func Lint(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.Lint) + return nil } -func imagePath(app string, env string) string { - return path.Join(imageDir(app, env), "image.tar") +// Format runs the standard formatting check script defined in package.json. +func Format(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.Format) + return nil } -func metadataPath(app string, env string) string { - return path.Join(imageDir(app, env), "metadata.json") +// UnitTest unit tests using the package.json script. +func UnitTest(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.UnitTest) + return nil } -func writeImageMetadata() error { - images, err := docker.Images(core.OutputDir) - if err != nil { - return err - } - - jsonString, err := json.Marshal(images) - if err != nil { - return err - } - return os.WriteFile(path.Join(core.OutputDir, "oci-images.json"), jsonString, 0644) +// E2ETest runs browser tests using the package.json script. +func E2ETest(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.E2ETest) + return nil } + diff --git a/targets/jslib/javascript.go b/targets/jslib/javascript.go new file mode 100644 index 00000000..ade6b38d --- /dev/null +++ b/targets/jslib/javascript.go @@ -0,0 +1,85 @@ +package jslib + +import ( + "context" + + "github.com/coopnorge/mage/internal/targets/javascript" + + "github.com/magefile/mage/mg" +) + +// JavaScript is the magefile namespace to group javascript/typescript commands +type JavaScript mg.Namespace + + +// Install fetches all Node.js dependencies. +func (JavaScript) Install(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Install) + return nil +} + +// Lint runs the standard linting script defined in package.json. +func (JavaScript) Lint(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Lint) + return nil +} + +// Format runs the standard formatting check script defined in package.json. +func (JavaScript) Format(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.Format) + return nil +} + +// UnitTest runs unit tests using the package.json script. +func (JavaScript) UnitTest(ctx context.Context) error { + mg.CtxDeps(ctx, javascript.UnitTest) + return nil +} + +// Build compiles the JavaScript/TypeScript into distribution files. +func (JavaScript) Build(ctx context.Context) error { + mg.CtxDeps(ctx, mg.F(javascript.Build, "build:library")) + return nil +} + +// NpmPublish publishes npm repository into a github packages +func (JavaScript) NpmPublish() error { + tokenName := "GITHUB_TOKEN" + tokenValue := os.Getenv(tokenName) + privateEnv := os.Getenv("PRIVATE") + isPrivate := strings.ToLower(privateEnv) == "true" || privateEnv == "1" + githubTagname := os.Getenv("GITHUB_TAGNAME") + + env := map[string]string{} + + if githubToken != "" && IsNpmrcConfiguredForPrivateRepo() { + env[tokenName] = tokenValue + } + + access := "public" + + if isPrivate { + access = "restricted" + } + + if newVersion == "" { + return errors.New("no new package version set. Set GITHUB_TAGNAME env variable") + } + + if !core.FileExists(".npmrc") { + return errors.New(".npmrc file missing") + } + + if !IsNpmrcConfiguredForPrivateRepo(".") { + return errors.New(".npmrc has no auth configuration") + } + + if err := sh.RunV("npm", "run", "version", newVersion); err != nil { + return fmt.Errorf("bumping version failed: %w", err) + } + + if err := sh.RunWithV(env, "npm", "publish", "--access", access); err != nil { + return fmt.Errorf("bumping version failed: %w", err) + } +} + diff --git a/targets/jslib/main.go b/targets/jslib/main.go index 3d8546dd..81cc6bcb 100644 --- a/targets/jslib/main.go +++ b/targets/jslib/main.go @@ -3,25 +3,39 @@ package jslib import ( "context" - "github.com/coopnorge/mage/internal/javascript" "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" ) -// JSLib is the magefile namespace to group Javascript language specific commands -type JSLib mg.Namespace +// BuildAndPublish checks for linting and formatting issue runs for unit test +// if not skipped and builds the project and publish it to the npm repository +func BuildAndPublish(ctx context.Context) error { + mg.SerialCtxDeps(ctx, Lint, Format, UnitTest, JavaScript.Build, JavaScript.Publish) + return nil +} + + +// Install fetches all Node.js dependencies. +func Install(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.Install) + return nil +} -// Lint checks all javascript/typescript codd for code standards and formats -// -// See [javascript.Lint] for details. -func (JSLib) Lint(ctx context.Context) error { - mg.CtxDeps(ctx, javascript.Lint) +// Lint runs the standard linting script defined in package.json. +func Lint(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.Lint) return nil } -// Publish publish npm package to the github package -// -// See [javascript.Publish] for details. -func (JSLib) Publish(ctx context.Context) error { - mg.CtxDeps(ctx, javascript.PublishLib) +// Format runs the standard formatting check script defined in package.json. +func Format(ctx context.Context) error { + mg.CtxDeps(ctx, JavaScript.Format) + return nil +} + +// UnitTest unit tests using the package.json script. +func UnitTest(ctx context.Context) error { + + mg.CtxDeps(ctx, JavaScript.UnitTest) return nil } From d4282dfdc6ff43a2f0a792720d871ecb4da6df55 Mon Sep 17 00:00:00 2001 From: Sudaman Shrestha Date: Tue, 11 Nov 2025 09:42:16 +0100 Subject: [PATCH 3/5] fix typo --- .github/workflows/js-lib.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/js-lib.yaml b/.github/workflows/js-lib.yaml index 6e58bdbe..0f9ec84e 100644 --- a/.github/workflows/js-lib.yaml +++ b/.github/workflows/js-lib.yaml @@ -58,7 +58,7 @@ jobs: run: go tool mage -v buildandpublish env: BUILD_COMMAND: ${{ github.build-command }} - SKIP_BUILD: ${{ gibhub.skip-build }} + SKIP_BUILD: ${{ github.skip-build }} SKIP_UNIT_TEST: ${{ github.skip-unit-tests }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRIVATE: ${{ inputs.is-npm-package-private }} From 15ac75b47ef6f030941a4653490aadea540e3526 Mon Sep 17 00:00:00 2001 From: Sudaman Shrestha Date: Tue, 11 Nov 2025 12:05:20 +0100 Subject: [PATCH 4/5] fix format and linting --- internal/git/git.go | 2 +- internal/javascript/javascript.go | 45 ---------- internal/targets/javascript/javascript.go | 104 +++++++++++++++++++--- targets/jsapp/javascript.go | 4 +- targets/jsapp/main.go | 3 - targets/jslib/javascript.go | 44 +-------- targets/jslib/main.go | 5 +- 7 files changed, 97 insertions(+), 110 deletions(-) delete mode 100644 internal/javascript/javascript.go diff --git a/internal/git/git.go b/internal/git/git.go index 5d541e8a..ddad3430 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -2,8 +2,8 @@ package git import ( "fmt" - "path" "os" + "path" "strings" "github.com/magefile/mage/sh" diff --git a/internal/javascript/javascript.go b/internal/javascript/javascript.go deleted file mode 100644 index 065f8377..00000000 --- a/internal/javascript/javascript.go +++ /dev/null @@ -1,45 +0,0 @@ -package javascript - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/coopnorge/mage/internal/core" - "github.com/magefile/mage/sh" -) - -// IsNpmrcConfiguredForPrivateRepo checks if the .npmrc file is configured for GitHub -// Packages. -func IsNpmrcConfiguredForPrivateRepo(directory string) bool { - if directory == "" { - directory = "." - } - - registryURL := "npm.pkg.github.com" - scope := "@coopnorge" - tokenIndicator := "_authToken=" - - npmrcContent, err := os.ReadFile(fmt.Sprintf("%s/.npmrc", directory)) - - if err != nil { - return false - } - - contentStr := string(npmrcContent) - - if !strings.Contains(contentStr, registryURL) && !strings.Contains(contentStr, scope) && !strings.Contains(contentStr, tokenIndicator) { - return false - } - - return true -} - -func HasBiomeConfig() bool { - return core.FileExists("biome.json") -} - -func HasPackageConfig() bool { - return core.FileExists("package.json") -} diff --git a/internal/targets/javascript/javascript.go b/internal/targets/javascript/javascript.go index f8a91b84..66e0e7a7 100644 --- a/internal/targets/javascript/javascript.go +++ b/internal/targets/javascript/javascript.go @@ -1,24 +1,24 @@ package javascript import ( + "errors" "fmt" "os" - "path/filepath" - "github.com/coopnorge/mage/internal/javascript" - "github.com/magefile/mage/mg" + "strings" + + "github.com/coopnorge/mage/internal/core" "github.com/magefile/mage/sh" ) // Install fetches all Node.js dependencies. func Install() error { - if javascript.HasPackageConfig() { + if HasPackageConfig() { tokenName := "GITHUB_TOKEN" tokenValue := os.Getenv(tokenName) - env := map[string]string{} - if githubToken != "" && IsNpmrcConfiguredForPrivateRepo() { + if tokenValue != "" && IsNpmrcConfiguredForPrivateRepo() { env[tokenName] = tokenValue } @@ -35,11 +35,11 @@ func Lint() error { envData := os.Getenv("SKIP_LINT") skip := strings.ToLower(envData) == "true" || envData == "1" - if (skip) { + if skip { return nil } - if javascript.HasBiomeConfig() { + if HasBiomeConfig() { return errors.New("biome not setup in your project. Install @coopnorge/web-devtools") } @@ -55,11 +55,11 @@ func Format() error { envData := os.Getenv("SKIP_FORMAT") skip := strings.ToLower(envData) == "true" || envData == "1" - if (skip) { + if skip { return nil } - if javascript.HasBiomeConfig() { + if HasBiomeConfig() { return errors.New("biome not setup in your project. Install @coopnorge/web-devtools") } @@ -75,7 +75,7 @@ func UnitTest() error { envData := os.Getenv("SKIP_UNIT_TEST") skip := strings.ToLower(envData) == "true" || envData == "1" - if (skip) { + if skip { return nil } @@ -91,7 +91,7 @@ func E2ETest() error { envData := os.Getenv("SKIP_E2E_TEST") skip := strings.ToLower(envData) == "true" || envData == "1" - if (skip) { + if skip { return nil } @@ -107,7 +107,7 @@ func Build(buildCommand string) error { envData := os.Getenv("SKIP_BUILD") skip := strings.ToLower(envData) == "true" || envData == "1" - if (skip) { + if skip { return nil } @@ -121,3 +121,81 @@ func Build(buildCommand string) error { return nil } + +// NpmPublish publishes npm repository into a github packages +func NpmPublish() error { + tokenName := "GITHUB_TOKEN" + tokenValue := os.Getenv(tokenName) + privateEnv := os.Getenv("PRIVATE") + isPrivate := strings.ToLower(privateEnv) == "true" || privateEnv == "1" + githubTagname := os.Getenv("GITHUB_TAGNAME") + newVersion := strings.TrimPrefix(githubTagname, "v") + + env := map[string]string{} + + if tokenValue != "" && IsNpmrcConfiguredForPrivateRepo() { + env[tokenName] = tokenValue + } + + access := "public" + + if isPrivate { + access = "restricted" + } + + if newVersion == "" { + return errors.New("no new package version set. Set GITHUB_TAGNAME env variable") + } + + if !core.FileExists(".npmrc") { + return errors.New(".npmrc file missing") + } + + if !IsNpmrcConfiguredForPrivateRepo() { + return errors.New(".npmrc has no auth configuration") + } + + if err := sh.RunV("npm", "run", "version", newVersion); err != nil { + return fmt.Errorf("bumping version failed: %w", err) + } + + if err := sh.RunWithV(env, "npm", "publish", "--access", access); err != nil { + return fmt.Errorf("bumping version failed: %w", err) + } + + return nil +} + +// IsNpmrcConfiguredForPrivateRepo checks if the project is setup for private +// repositories +func IsNpmrcConfiguredForPrivateRepo() bool { + directory := "." + + registryURL := "npm.pkg.github.com" + scope := "@coopnorge" + tokenIndicator := "_authToken=" + + npmrcContent, err := os.ReadFile(fmt.Sprintf("%s/.npmrc", directory)) + + if err != nil { + return false + } + + contentStr := string(npmrcContent) + + if !strings.Contains(contentStr, registryURL) && !strings.Contains(contentStr, scope) && !strings.Contains(contentStr, tokenIndicator) { + return false + } + + return true +} + +// HasBiomeConfig checks if project has biome setup +func HasBiomeConfig() bool { + return core.FileExists("biome.json") +} + +// HasPackageConfig checks if project has package.json file +func HasPackageConfig() bool { + return core.FileExists("package.json") +} diff --git a/targets/jsapp/javascript.go b/targets/jsapp/javascript.go index 52ed3dec..3e913bd2 100644 --- a/targets/jsapp/javascript.go +++ b/targets/jsapp/javascript.go @@ -7,10 +7,10 @@ import ( "os" "path" - "github.com/coopnorge/mage/internal/targets/javascript" "github.com/coopnorge/mage/internal/core" "github.com/coopnorge/mage/internal/docker" "github.com/coopnorge/mage/internal/git" + "github.com/coopnorge/mage/internal/targets/javascript" "github.com/magefile/mage/mg" ) @@ -72,7 +72,6 @@ func (JavaScript) BuildAndPushDockerImage(ctx context.Context) error { return writeImageMetadata() } - // Install fetches all Node.js dependencies. func (JavaScript) Install(ctx context.Context) error { mg.CtxDeps(ctx, javascript.Install) @@ -103,7 +102,6 @@ func (JavaScript) E2ETest(ctx context.Context) error { return nil } - func buildAndPush(shouldPush bool) error { env := os.Getenv("DEPLOY_ENV") diff --git a/targets/jsapp/main.go b/targets/jsapp/main.go index 45d7a812..54e61118 100644 --- a/targets/jsapp/main.go +++ b/targets/jsapp/main.go @@ -4,7 +4,6 @@ import ( "context" "github.com/magefile/mage/mg" - "github.com/magefile/mage/sh" ) // BuildAndPublish creates deployable artifacts from the source code in the repository, @@ -15,7 +14,6 @@ func BuildAndPublish(ctx context.Context) error { return nil } - // Install fetches all Node.js dependencies. func Install(ctx context.Context) error { mg.CtxDeps(ctx, JavaScript.Install) @@ -45,4 +43,3 @@ func E2ETest(ctx context.Context) error { mg.CtxDeps(ctx, JavaScript.E2ETest) return nil } - diff --git a/targets/jslib/javascript.go b/targets/jslib/javascript.go index ade6b38d..8618ee78 100644 --- a/targets/jslib/javascript.go +++ b/targets/jslib/javascript.go @@ -4,14 +4,12 @@ import ( "context" "github.com/coopnorge/mage/internal/targets/javascript" - "github.com/magefile/mage/mg" ) // JavaScript is the magefile namespace to group javascript/typescript commands type JavaScript mg.Namespace - // Install fetches all Node.js dependencies. func (JavaScript) Install(ctx context.Context) error { mg.CtxDeps(ctx, javascript.Install) @@ -43,43 +41,7 @@ func (JavaScript) Build(ctx context.Context) error { } // NpmPublish publishes npm repository into a github packages -func (JavaScript) NpmPublish() error { - tokenName := "GITHUB_TOKEN" - tokenValue := os.Getenv(tokenName) - privateEnv := os.Getenv("PRIVATE") - isPrivate := strings.ToLower(privateEnv) == "true" || privateEnv == "1" - githubTagname := os.Getenv("GITHUB_TAGNAME") - - env := map[string]string{} - - if githubToken != "" && IsNpmrcConfiguredForPrivateRepo() { - env[tokenName] = tokenValue - } - - access := "public" - - if isPrivate { - access = "restricted" - } - - if newVersion == "" { - return errors.New("no new package version set. Set GITHUB_TAGNAME env variable") - } - - if !core.FileExists(".npmrc") { - return errors.New(".npmrc file missing") - } - - if !IsNpmrcConfiguredForPrivateRepo(".") { - return errors.New(".npmrc has no auth configuration") - } - - if err := sh.RunV("npm", "run", "version", newVersion); err != nil { - return fmt.Errorf("bumping version failed: %w", err) - } - - if err := sh.RunWithV(env, "npm", "publish", "--access", access); err != nil { - return fmt.Errorf("bumping version failed: %w", err) - } +func (JavaScript) NpmPublish(ctx context.Context) error { + mg.CtxDeps(ctx, mg.F(javascript.NpmPublish, "build:library")) + return nil } - diff --git a/targets/jslib/main.go b/targets/jslib/main.go index 81cc6bcb..29612f33 100644 --- a/targets/jslib/main.go +++ b/targets/jslib/main.go @@ -4,17 +4,15 @@ import ( "context" "github.com/magefile/mage/mg" - "github.com/magefile/mage/sh" ) // BuildAndPublish checks for linting and formatting issue runs for unit test // if not skipped and builds the project and publish it to the npm repository func BuildAndPublish(ctx context.Context) error { - mg.SerialCtxDeps(ctx, Lint, Format, UnitTest, JavaScript.Build, JavaScript.Publish) + mg.SerialCtxDeps(ctx, Lint, Format, UnitTest, JavaScript.Build, JavaScript.NpmPublish) return nil } - // Install fetches all Node.js dependencies. func Install(ctx context.Context) error { mg.CtxDeps(ctx, JavaScript.Install) @@ -35,7 +33,6 @@ func Format(ctx context.Context) error { // UnitTest unit tests using the package.json script. func UnitTest(ctx context.Context) error { - mg.CtxDeps(ctx, JavaScript.UnitTest) return nil } From cfa5aa828c91338dfbc55f6dcffa32bbf69eeec7 Mon Sep 17 00:00:00 2001 From: Sudaman Shrestha Date: Tue, 11 Nov 2025 12:12:24 +0100 Subject: [PATCH 5/5] fix target --- .github/workflows/js-app.yaml | 2 +- .github/workflows/js-lib.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/js-app.yaml b/.github/workflows/js-app.yaml index 265aa319..26e10a67 100644 --- a/.github/workflows/js-app.yaml +++ b/.github/workflows/js-app.yaml @@ -55,7 +55,7 @@ jobs: node-version: '20' - name: Build app and Push to docker repository - run: "go tool mage -v buildandpublish" + run: "go tool mage -v buildAndPublish" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_COMMAND: ${{ inputs.build-command }} diff --git a/.github/workflows/js-lib.yaml b/.github/workflows/js-lib.yaml index 0f9ec84e..c30e3fd1 100644 --- a/.github/workflows/js-lib.yaml +++ b/.github/workflows/js-lib.yaml @@ -55,7 +55,7 @@ jobs: node-version: '20' - name: Build and publish - run: go tool mage -v buildandpublish + run: go tool mage -v buildAndPublish env: BUILD_COMMAND: ${{ github.build-command }} SKIP_BUILD: ${{ github.skip-build }}