diff --git a/.github/actions/prep/action.yml b/.github/actions/prep/action.yml deleted file mode 100644 index ca1a144..0000000 --- a/.github/actions/prep/action.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Prep -description: 'Reusable action to generate docker/helm names/tags as a single source of truth' - -outputs: - # Docker - docker_image_name: - description: "Only docker image name" - value: ${{ steps.prep.outputs.docker_image_name }} - docker_image_tag: - description: "Only docker image tag" - value: ${{ steps.prep.outputs.docker_image_tag }} - docker_image: - description: "docker image with tag" - value: ${{ steps.prep.outputs.docker_image }} - # Helm - helm_oci_repo: - description: "Helm OCI repo" - value: ${{ steps.prep.outputs.helm_oci_repo }} - helm_chart: - description: "Helm chart name" - value: ${{ steps.prep.outputs.helm_chart }} - helm_target_revision: - description: "Helm target revision" - value: ${{ steps.prep.outputs.helm_target_revision }} - -runs: - using: "composite" - steps: - - name: Prepare - id: prep - env: - REPO_NAME: ghcr.io/${{ github.repository }} - OCI_REPO: oci://ghcr.io/${{ github.repository_owner }} - shell: bash - run: | - # If it's a pull request ref, fallback to 'pr-' format - if [[ "$GITHUB_REF_NAME" == *"pull/"* ]]; then - PR_NUMBER=$(echo "$GITHUB_REF_NAME" | grep -oP 'pull/\K[0-9]+') - TAG="pr-${PR_NUMBER}" - else - TAG=$(echo "$GITHUB_REF_NAME" | \ - sed 's|:|-|g' | \ - tr '[:upper:]' '[:lower:]' | \ - sed 's/[^a-z0-9_.-]/-/g' | \ - cut -c1-100 | \ - sed 's/^[.-]*//' | \ - sed 's/[-.]*$//') - fi - - REPO_NAME=$(echo $REPO_NAME | tr '[:upper:]' '[:lower:]') - OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]') - - # Docker - echo "docker_image_name=${REPO_NAME}" >> $GITHUB_OUTPUT - echo "docker_image_tag=${TAG}" >> $GITHUB_OUTPUT - echo "docker_image=${REPO_NAME}:${TAG}" >> $GITHUB_OUTPUT - - # Helm - HELM_TARGET_REVISION=$(helm show chart ./helm/ | grep '^version:' | awk '{print $2}') - HELM_CHART=$(helm show chart ./helm/ | grep '^name:' | awk '{print $2}') - - echo "helm_oci_repo=$OCI_REPO" >> $GITHUB_OUTPUT - echo "helm_chart=$HELM_CHART" >> $GITHUB_OUTPUT - echo "helm_target_revision=$HELM_TARGET_REVISION" >> $GITHUB_OUTPUT diff --git a/.github/actions/publish-web-app-serve/action.yml b/.github/actions/publish-web-app-serve/action.yml deleted file mode 100644 index 6e13a8c..0000000 --- a/.github/actions/publish-web-app-serve/action.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Publish web-app-serve -description: 'Reusable action to publish web-app-serve (For external usages)' - -inputs: - # https://github.com/actions/runner/issues/1557 - github_token: - description: "github token for docker push to ghcr.io" - required: true - docker_target: - description: "docker target for web-app-serve" - default: web-app-serve - -outputs: - # Docker - docker_image_name: - description: "Only docker image name" - value: ${{ steps.prep.outputs.docker_image_name }} - docker_image_tag: - description: "Only docker image tag" - value: ${{ steps.prep.outputs.docker_image_tag }} - docker_image: - description: "docker image with tag" - value: ${{ steps.prep.outputs.docker_image }} - -runs: - using: "composite" - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ inputs.github_token }} - - - name: 🐳 Prepare Docker - id: prep - env: - DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository }} - shell: bash - run: | - BRANCH_NAME=$(\ - echo $GITHUB_REF_NAME | \ - sed 's|:|-|' | \ - tr '[:upper:]' '[:lower:]' | \ - sed 's/_/-/g' | \ - cut -c1-100 | \ - sed 's/-*$//'\ - ) - - GIT_HASH="c$(echo $GITHUB_SHA | head -c7)" - - # XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker - if [[ "$BRANCH_NAME" == *"/"* ]]; then - # XXX: Change the docker image package to -dev - DOCKER_IMAGE_NAME="$DOCKER_IMAGE_NAME-dev" - DOCKER_IMAGE_TAG="$(echo "$BRANCH_NAME" | sed 's|/|-|g').$GIT_HASH" - else - DOCKER_IMAGE_TAG="$BRANCH_NAME.$GIT_HASH" - fi - DOCKER_IMAGE_NAME=$(echo $DOCKER_IMAGE_NAME | tr '[:upper:]' '[:lower:]') - - echo "docker_image_name=${DOCKER_IMAGE_NAME}" >> $GITHUB_OUTPUT - echo "docker_image_tag=${DOCKER_IMAGE_TAG}" >> $GITHUB_OUTPUT - echo "docker_image=${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" >> $GITHUB_OUTPUT - - - name: 🐳 Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: 🐳 Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.ref }} - restore-keys: | - ${{ runner.os }}-buildx-refs/develop - ${{ runner.os }}-buildx- - - - name: 🐳 Docker build - uses: docker/build-push-action@v6 - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - file: Dockerfile - target: ${{ inputs.docker_target }} - push: false - load: true - provenance: false # XXX: Without this we have untagged images in ghcr.io - tags: ${{ steps.prep.outputs.docker_image }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: 🐳 Docker push - run: docker push $DOCKER_IMAGE - shell: bash - env: - DOCKER_IMAGE: ${{ steps.prep.outputs.docker_image }} - - - name: 🐳 Summary/Annotations generate - shell: bash - env: - DOCKER_IMAGE_NAME: ${{ steps.prep.outputs.docker_image_name }} - DOCKER_IMAGE_TAG: ${{ steps.prep.outputs.docker_image_tag }} - DOCKER_IMAGE: ${{ steps.prep.outputs.docker_image }} - run: | - echo "# Docker Build info" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "> [!Important]" >> $GITHUB_STEP_SUMMARY - echo "> Docker name: **$DOCKER_IMAGE_NAME**" >> $GITHUB_STEP_SUMMARY - echo "> Docker tag: **$DOCKER_IMAGE_TAG**" >> $GITHUB_STEP_SUMMARY - echo "> Docker image: **$DOCKER_IMAGE**" >> $GITHUB_STEP_SUMMARY - - # Add annotations as well (This is shown in reverse order) - echo "::notice::Docker image: $DOCKER_IMAGE" - echo "::notice::Docker tag: $DOCKER_IMAGE_TAG" - echo "::notice::Docker name: $DOCKER_IMAGE_NAME" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9835038..f0fdea9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -14,7 +14,30 @@ jobs: - name: 🐳 Prepare Docker id: prep - uses: ./.github/actions/prep + env: + REPO_NAME: ghcr.io/${{ github.repository }} + shell: bash + run: | + # If it's a pull request ref, fallback to 'pr-' format + if [[ "$GITHUB_REF_NAME" == *"pull/"* ]]; then + PR_NUMBER=$(echo "$GITHUB_REF_NAME" | grep -oP 'pull/\K[0-9]+') + TAG="pr-${PR_NUMBER}" + else + TAG=$(echo "$GITHUB_REF_NAME" | \ + sed 's|:|-|g' | \ + tr '[:upper:]' '[:lower:]' | \ + sed 's/[^a-z0-9_.-]/-/g' | \ + cut -c1-100 | \ + sed 's/^[.-]*//' | \ + sed 's/[-.]*$//') + fi + + REPO_NAME=$(echo $REPO_NAME | tr '[:upper:]' '[:lower:]') + + # Docker + echo "docker_image_name=${REPO_NAME}" >> $GITHUB_OUTPUT + echo "docker_image_tag=${TAG}" >> $GITHUB_OUTPUT + echo "docker_image=${REPO_NAME}:${TAG}" >> $GITHUB_OUTPUT - name: Login to GitHub Container Registry if: github.event_name != 'pull_request' @@ -43,6 +66,7 @@ jobs: context: . builder: ${{ steps.buildx.outputs.name }} file: Dockerfile + target: web-app-serve push: false load: true provenance: false # XXX: Without this we have untagged images in ghcr.io diff --git a/.github/workflows/helm-publish.yml b/.github/workflows/helm-publish.yml deleted file mode 100644 index 697c235..0000000 --- a/.github/workflows/helm-publish.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Helm publish - -on: - workflow_call: - outputs: - helm_repo_url: - description: "Helm repo URL" - value: ${{ jobs.helm-publish.outputs.helm_repo_url }} - helm_chart: - description: "Helm Chart" - value: ${{ jobs.helm-publish.outputs.helm_chart }} - helm_target_revision: - description: "Helm target revision" - value: ${{ jobs.helm-publish.outputs.helm_target_revision }} - pull_request: - -permissions: - packages: write - -jobs: - helm-publish: - name: Publish Helm - runs-on: ubuntu-latest - - outputs: - helm_repo_url: ${{ steps.push.outputs.helm_repo_url }} - helm_chart: ${{ steps.push.outputs.helm_chart }} - helm_target_revision: ${{ steps.push.outputs.helm_target_revision }} - - steps: - - uses: actions/checkout@v4 - - - name: Prepare - id: prep - uses: ./.github/actions/prep - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - if: github.event_name != 'pull_request' - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 🐳 Helm dependency - run: | - yq --indent 0 '.dependencies | map(select(.repository | test("^oci:") | not)) | map(["helm", "repo", "add", .name, .repository] | join(" ")) | .[]' ./helm/Chart.lock | sh -- - helm dependency build ./helm/ - - - name: Helm lint - run: helm lint ./helm --values ./helm/linter_values.yaml - - - name: Helm template - run: | - helm template ./helm --values ./helm/linter_values.yaml - - # Test using all test values - for values_file in ./helm/tests/values-*.yaml; do - helm template ./helm --values "$values_file" - done - - - name: Package Helm Chart - run: helm package ./helm/ -d ./helm/.helm-charts - - - name: Push Helm Chart - id: push - if: github.event_name != 'pull_request' - env: - OCI_REPO: ${{ steps.prep.outputs.helm_oci_repo }} - HELM_TARGET_REVISION: "${{ steps.prep.outputs.helm_target_revision }}" - HELM_CHART: "${{ steps.prep.outputs.helm_chart }}" - run: | - PACKAGE_FILE=$(ls ./helm/.helm-charts/*.tgz | head -n 1) - echo "# Helm Chart" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```yaml' >> $GITHUB_STEP_SUMMARY - helm push "$PACKAGE_FILE" $OCI_REPO 2>> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "> [!Important]" >> $GITHUB_STEP_SUMMARY - echo "> Helm Repo URL: **$OCI_REPO**" >> $GITHUB_STEP_SUMMARY - echo "> Helm Chart: **$HELM_CHART**" >> $GITHUB_STEP_SUMMARY - echo "> Helm Target Revision: **$HELM_TARGET_REVISION**" >> $GITHUB_STEP_SUMMARY - - # Add annotations as well (This is shown in reverse order) - echo "::notice::Helm Target Revision: $HELM_TARGET_REVISION" - echo "::notice::Helm Chart: $HELM_CHART" - echo "::notice::Helm Repo URL: $OCI_REPO" - # Add outputs as well (This is shown in reverse order) - echo "helm_target_revision=$HELM_TARGET_REVISION" >> $GITHUB_OUTPUT - echo "helm_chart=$HELM_CHART" >> $GITHUB_OUTPUT - echo "helm_repo_url=$OCI_REPO" >> $GITHUB_OUTPUT diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 229b09c..ff8c7e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,15 +16,10 @@ jobs: name: CI uses: ./.github/workflows/docker-publish.yml - helm-publish: - name: CI - uses: ./.github/workflows/helm-publish.yml - generate-release: name: Generate release runs-on: ubuntu-22.04 needs: - - helm-publish - docker-publish steps: @@ -32,10 +27,6 @@ jobs: with: fetch-depth: 0 - - name: Prepare - id: prep - uses: ./.github/actions/prep - - name: Set the release version id: release shell: bash @@ -59,30 +50,6 @@ jobs: config: cliff.toml args: -vv --latest --no-exec --github-repo ${{ github.repository }} --strip all - - name: Changelog content pre-processing - id: changelog-pre-process - env: - HELM_REPO_URL: "${{ steps.prep.outputs.helm_oci_repo }}" - HELM_CHART: "${{ steps.prep.outputs.helm_chart }}" - HELM_TARGET_REVISION: "${{ steps.prep.outputs.helm_target_revision }}" - DOCKER_IMAGE: "${{ steps.prep.outputs.docker_image }}" - shell: bash - run: | - EXTENDEND_CHANGELOG=/tmp/extendend_changelog.md - echo "# Changelog" >> $EXTENDEND_CHANGELOG - echo "" >> $EXTENDEND_CHANGELOG - - echo "> [!Important]" >> $EXTENDEND_CHANGELOG - echo "> Helm Repo URL: **$HELM_REPO_URL**" >> $EXTENDEND_CHANGELOG - echo "> Helm Chart: **$HELM_CHART**" >> $EXTENDEND_CHANGELOG - echo "> Helm Target Revision: **$HELM_TARGET_REVISION**" >> $EXTENDEND_CHANGELOG - echo "> Docker image: **$DOCKER_IMAGE**" >> $EXTENDEND_CHANGELOG - - echo "${{ steps.git-cliff.outputs.content }}" >> $EXTENDEND_CHANGELOG - - echo "extended_changelog_path=${EXTENDEND_CHANGELOG}" >> $GITHUB_OUTPUT - cat $EXTENDEND_CHANGELOG >> $GITHUB_STEP_SUMMARY - - name: Create Github Release uses: softprops/action-gh-release@v2 if: github.ref_type == 'tag' @@ -90,5 +57,4 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} name: "v${{ steps.release.outputs.release_version }}" prerelease: ${{ steps.release.outputs.is_prerelease }} - body: ${{ steps.changelog-pre-process.outputs.extended_changelog }} - body_path: ${{ steps.changelog-pre-process.outputs.extended_changelog_path }} + body: ${{ steps.git-cliff.outputs.content }} diff --git a/Dockerfile b/Dockerfile index 1b3eddb..94c5dce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,57 @@ -FROM nginx:1 +FROM nginx:1 AS web-app-serve -LABEL maintainer="Togglecorp" -LABEL org.opencontainers.image.source="https://github.com/toggle-corp/web-app-serve" +LABEL org.opencontainers.image.authors="dev@togglecorp.com" \ + org.opencontainers.image.source="https://github.com/toggle-corp/web-app-serve" \ + org.opencontainers.image.title="web-app-serve" ARG BIOME_VERSION=2.0.6 +ARG RIPGREP_VERSION=14.1.1 +ARG JQ_VERSION=1.8.1 -RUN curl -L "https://github.com/biomejs/biome/releases/download/@biomejs/biome@${BIOME_VERSION}/biome-linux-x64" -o /usr/local/bin/biome && \ - chmod +x /usr/local/bin/biome +RUN mkdir /build && \ + # Installing ripgrep + curl -L \ + -o /build/ripgrep.tar.gz \ + "https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz" && \ + mkdir "/build/ripgrep" && \ + tar xf "/build/ripgrep.tar.gz" --directory="/build/ripgrep" && \ + mv "$(find /build/ripgrep -type f -name rg)" "/usr/local/bin/rg" && \ + chmod +x "/usr/local/bin/rg" && \ + rg --version && \ + # Installing jq + curl -L \ + -o /usr/local/bin/jq \ + "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64" && \ + chmod +x "/usr/local/bin/jq" && \ + jq --version && \ + # Installing biome + curl -L \ + -o "/usr/local/bin/biome" \ + "https://github.com/biomejs/biome/releases/download/@biomejs/biome@${BIOME_VERSION}/biome-linux-x64" && \ + chmod +x "/usr/local/bin/biome" && \ + biome --version && \ + # Cleanup + rm -rf /build # NOTE: Used by apply-config.sh ENV APPLY_CONFIG__ENABLE_DEBUG=false ENV APPLY_CONFIG__DEBUG_USE_BIOME=true ENV APPLY_CONFIG__DESTINATION_DIRECTORY=/usr/share/nginx/html/ +ENV APPLY_CONFIG__APPLY_CONFIG_PATH=/web-app-serve/default-app-apply-config.sh COPY ./src/ /web-app-serve/ RUN ln -sf /web-app-serve/apply-config.sh /docker-entrypoint.d/apply-config.sh && \ mkdir /etc/nginx/templates && \ ln -sf /web-app-serve/nginx.conf.template /etc/nginx/templates/default.conf.template + + +FROM web-app-serve AS web-app-serve-example + +LABEL org.opencontainers.image.source="https://github.com/toggle-corp/web-app-serve" +LABEL org.opencontainers.image.authors="dev@togglecorp.com" + +# Env for apply-config script +ENV APPLY_CONFIG__SOURCE_DIRECTORY=/code/build/ + +COPY ./example/source "$APPLY_CONFIG__SOURCE_DIRECTORY" diff --git a/README.md b/README.md index e69de29..c3ae043 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,252 @@ +# web-app-serve + +## Related repositories +- https://github.com/toggle-corp/web-app-serve-helm +- https://github.com/toggle-corp/web-app-serve-action + +## Docker Guide + +This guide shows how to package a Web app (SPA and Vite) using Docker with a base image that allows configuration to be changed when the Nginx container starts. + +We use simple tools like `nginx`, `find`, `sed`, and `diff2html` to help package and debug SPA deployments. + +For actual examples, see: + +- https://github.com/IFRCGo/go-web-app/ + - https://github.com/IFRCGo/go-web-app/tree/develop/web-app-serve + - https://github.com/IFRCGo/go-web-app/blob/develop/.github/workflows/publish-web-app-serve.yml + - https://github.com/IFRCGo/go-web-app/blob/develop/Dockerfile + - https://github.com/IFRCGo/go-web-app/blob/develop/app/env.ts + +### Project Structure + +``` +├── .github/workflows +│ └── publish-web-app-serve.yml +├── web-app-serve +│ ├── .env +│ ├── .gitignore +│ └── docker-compose.yml +└── Dockerfile +```` + +### Setting up env variables placeholder using Vite + +In this guide, our example app uses these env variables: + +- `APP_TITLE` +- `APP_GRAPHQL_ENDPOINT` +- `APP_ANALYTIC_SRC` + +> [!IMPORTANT] +> Replace them with the env variables for your own app. + +Add `vite-plugin-validate-env` plugin as dependency (togglecorp fork) + +```diff + { + "devDependencies": { ++ "@julr/vite-plugin-validate-env": "git+https://github.com/toggle-corp/vite-plugin-validate-env#v2.2.0-tc.1", + } + } +``` + +> [!TIP] +> Check the latest version of `vite-plugin-validate-env` at https://github.com/toggle-corp/vite-plugin-validate-env/releases + +Update the `./env.ts` file and define `overrideDefine` config to enable env variable placeholder: + +```diff +- import { Schema, defineConfig } from '@julr/vite-plugin-validate-env'; ++ import { ++ defineConfig, ++ overrideDefineForWebAppServe, ++ Schema, ++ } from '@julr/vite-plugin-validate-env'; ++ ++ const webAppServeEnabled = process.env.WEB_APP_SERVE_ENABLED?.toLowerCase() === 'true'; ++ if (webAppServeEnabled) { ++ // eslint-disable-next-line no-console ++ console.warn('Building application for web-app-serve'); ++ } ++ const overrideDefine = webAppServeEnabled ++ ? overrideDefineForWebAppServe ++ : undefined; + + export default defineConfig({ ++ overrideDefine, + validator: 'builtin', + schema: { + // NOTE: These are the dynamic env variables + APP_TITLE: Schema.string(), + APP_GRAPHQL_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), + }, + }); +``` +> [!TIP] +> To learn more about what `overrideDefineForWebAppServe` does,\ +> visit: https://github.com/toggle-corp/vite-plugin-validate-env/blob/main/src/index.ts \ +> Look for `overrideDefineForWebAppServe` in the file. + + +### Setting up Dockerfile + +To package a Web app using `web-app-serve`, we'll define a Dockerfile that includes: + +1. A build step for your app with env variables placeholder. +2. A final image using `web-app-serve` that updates those placeholders at runtime. + +```dockerfile +# Builder stage +FROM node:18-bullseye AS dev + +# ... add your build steps here ... + +# --------------------------------------------------------------------- +# Build stage for web app +FROM dev AS web-app-serve-build + +# ... add your build steps here ... + +# NOTE: Dynamic env variables +# These env variables can be dynamically defined in web-app-serve container runtime. +# These variables are not included in the build files but the values should still be valid. +# See "schema" field in "./env.ts" +ENV APP_TITLE=My Best Dashboard +ENV APP_GRAPHQL_ENDPOINT=https://my-best-dashboard.com/graphql/ + +# NOTE: These are set directly in `vite.config.ts` +# We're using raw web-app-serve placeholder values here to treat them as dynamic values +ENV APP_ANALYTIC_SRC=WEB_APP_SERVE_PLACEHOLDER__APP_ANALYTIC_SRC + +# NOTE: Static env variables: +# These env variables are used during build +ENV APP_GRAPHQL_CODEGEN_ENDPOINT=./backend/schema.graphql + +# NOTE: WEB_APP_SERVE_ENABLED=true will skip defining the above dynamic env variables +# See "overrideDefine" field in "./env.ts" +RUN WEB_APP_SERVE_ENABLED=true pnpm build + +# --------------------------------------------------------------------- +# Final image using web-app-serve +FROM ghcr.io/toggle-corp/web-app-serve:v0.1.2 AS web-app-serve + +LABEL org.opencontainers.image.source="https://github.com/my-org/my-best-dashboard" +LABEL org.opencontainers.image.authors="my-email@company.com" + +# Env for apply-config script +ENV APPLY_CONFIG__SOURCE_DIRECTORY=/code/build/ + +COPY --from=web-app-serve-build /code/build "$APPLY_CONFIG__SOURCE_DIRECTORY" +```` + +> [!IMPORTANT] +> Make sure all the dynamic environment variables are prefixed with `APP_` + +> [!IMPORTANT] +> Make sure all the environment variables are defined (e.g. `APP_TITLE`, `APP_GRAPHQL_ENDPOINT`)\ +> These values will be replaced at runtime. + +### Setting up Docker Compose for debugging locally + +Create a `web-app-serve/docker-compose.yml` file + +```yaml +# NOTE: The name should is mandatory and should be unique +name: my-org-my-best-dashboard + +services: + web-app-serve: + build: + context: ../ + target: web-app-serve + environment: + # web-app-serve config + APPLY_CONFIG__ENABLE_DEBUG: true + # NOTE: See "Dockerfile" to get dynamic env variables for .env file + env_file: .env + ports: + - '8050:80' +``` + +Create `web-app-serve/.env` file + +```ini +APP_TITLE=My Good Dashboard +APP_GRAPHQL_ENDPOINT=https://my-good-dashboard.com/graphql/ +APP_ANALYTIC_SRC=https://my-good-analytic.com/script.js +``` + +> [!WARNING] +> Replace them with the env variables for your own app. + +Make sure `web-app-serve/.env` is ignored by git + +```bash +echo ".env" >> web-app-serve/.gitignore +``` + +Run + +```bash +docker compose -f web-app-serve/docker-compose.yml up --build +``` + +> [!TIP] +> Re-run the command if you make any change + +### Setting up GitHub Actions Workflow + +Create `.github/workflows/publish-web-app-serve.yml` file\ +This workflow builds and pushes your Docker image to the GitHub container registry. + +```yaml +name: Publish web app serve + +on: + workflow_dispatch: + push: + branches: + - develop + +permissions: + packages: write + +jobs: + publish_image: + name: Publish Docker Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Publish web-app-serve + uses: toggle-corp/web-app-serve-action@v0.1.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} +``` + +> [!IMPORTANT] +> If your app doesn't use submodules, remove `submodules: true` from the `actions/checkout` step. + +> [!IMPORTANT] +> To see what this action does internally, check: [.github/actions/publish-web-app-serve/action.yml](.github/actions/publish-web-app-serve/action.yml) + +> [!TIP] +> To run this workflow on a Pull Request, temporarily add your branch to the `on.push.branches` list. + +> [!TIP] +> To avoid redundant image builds, this workflow only runs on the `develop` (default) branch or when triggered manually. +> If you have [GitHub CLI (gh)](https://cli.github.com/) set up, you can run this from your terminal: +> +> ```bash +> gh workflow run .github/workflows/publish-web-app-serve.yml --ref $(git rev-parse --abbrev-ref HEAD) +> ``` +> +> This will only work after the workflow file exists on the `develop` (default) branch. + + +## K8s Guide + +See [./docs/kubernetes.md](./docs/kubernetes.md) diff --git a/docs/kubernetes.md b/docs/kubernetes.md new file mode 100644 index 0000000..157e5f3 --- /dev/null +++ b/docs/kubernetes.md @@ -0,0 +1,114 @@ +# K8s Guide + +In this guide, our example app uses these env variables: + +- `APP_TITLE` +- `APP_GRAPHQL_ENDPOINT` +- `APP_ANALYTIC_SRC` + +> [!IMPORTANT] +> Replace them with the env variables for your own app. + +## Using Helm (Direct) + +**Download the Helm chart** + +```bash +helm pull oci://ghcr.io/toggle-corp/web-app-serve-helm +``` + +**Create a config file** + +Save as `my-values.yaml`: + +```yaml +fullnameOverride: my-web-app +ingress: + ingressClassName: nginx + hostname: my-dashboard.togglecorp.com +image: + name: ghcr.io/toggle-corp/my-dashboard + tag: feat-web-app-serve.cXXXXXXX +resources: + requests: + cpu: "0.1" + memory: "100Mi" + limits: + memory: "300Mi" # For debug mode (biome needs more RAM at initial start) +env: + APPLY_CONFIG__ENABLE_DEBUG: true + APP_TITLE: "My Dashboard" + APP_GRAPHQL_ENDPOINT: https://my-dashboard-api.togglecorp.com/graphql/ + APP_ANALYTIC_SRC: https://my-good-analytic.com/script.js +``` + +**Deploy to Kubernetes** + +```bash +# Create namespace +kubectl create namespace test-my-dashboard + +# Install (or upgrade) the Helm release +helm upgrade --install \ + -n test-my-dashboard \ + my-dashboard \ + oci://ghcr.io/toggle-corp/web-app-serve-helm \ + --values ./my-values.yaml +``` + +--- + +## Using Helm with ArgoCD + +Example ArgoCD `Application` manifest: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: my-web-app + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + destination: + name: in-cluster + namespace: my-web-app + source: + chart: web-app-serve-helm + repoURL: ghcr.io/toggle-corp + targetRevision: 0.1.1 + helm: + valuesObject: + fullnameOverride: my-web-app + ingress: + ingressClassName: nginx + hostname: https://my-dashboard.togglecorp.com + image: + name: ghcr.io/toggle-corp/my-dashboard + tag: feat-web-app-serve.cXXXXXXX + resources: + requests: + cpu: "0.1" + memory: "100Mi" + env: + # web-app-serve config + APPLY_CONFIG__ENABLE_DEBUG: true + # Placeholder replacement variables + APP_TITLE: "My Dashboard" + APP_GRAPHQL_ENDPOINT: https://my-dashboard-api.togglecorp.com/graphql/ + APP_ANALYTIC_SRC: https://my-good-analytic.com/script.js + syncPolicy: + automated: + prune: true + selfHeal: true + managedNamespaceMetadata: + labels: + argocd.argoproj.io/instance: my-web-app + annotations: + argocd.argoproj.io/tracking-id: >- + my-web-app:apps/Namespace:my-web-app/my-web-app + syncOptions: + - CreateNamespace=true +``` diff --git a/example/.env b/example/.env new file mode 100644 index 0000000..0350684 --- /dev/null +++ b/example/.env @@ -0,0 +1,2 @@ +APP_TITLE=Example Dev +APP_GRAPHQL_ENDPOINT=https://example-dev@togglecorp.com/api/ diff --git a/example/docker-compose.yml b/example/docker-compose.yml new file mode 100644 index 0000000..277aa24 --- /dev/null +++ b/example/docker-compose.yml @@ -0,0 +1,23 @@ +# NOTE: The name should is mandatory and should be unique +name: togglecorp-web-app-serve-example + +services: + web-app-serve: + build: + context: ../ + target: web-app-serve-example + environment: + # web-app-serve config + APPLY_CONFIG__ENABLE_DEBUG: true + # NOTE: See "Dockerfile" to get dynamic env variables for .env file + env_file: .env + ports: + - '8050:80' + develop: + watch: + - action: sync+restart + path: ./source + target: /code/build + - action: sync+restart + path: ../src/ + target: /web-app-serve/ diff --git a/example/source/index.html b/example/source/index.html new file mode 100644 index 0000000..50b741a --- /dev/null +++ b/example/source/index.html @@ -0,0 +1,20 @@ + + + + WEB_APP_SERVE_PLACEHOLDER__APP_TITLE + + + + +

+ WEB_APP_SERVE_PLACEHOLDER__APP_TITLE +

+

+ This is a example HTML file for debugging. + Go to debug page +

+
+ Copyright WEB_APP_SERVE_PLACEHOLDER__APP_COPYRIGHT_YEAR +
+ + diff --git a/example/source/index.js b/example/source/index.js new file mode 100644 index 0000000..4fe6b88 --- /dev/null +++ b/example/source/index.js @@ -0,0 +1 @@ +console.log("Fetching data from ", "WEB_APP_SERVE_PLACEHOLDER__APP_GRAPHQL_ENDPOINT"); function init() { console.log("This is init script"); if ("import.meta.env.APP_TITLE".endsWith("dev")) { console.debug("Running on dev mode"); } console.log("Exiting!"); } diff --git a/helm/.gitignore b/helm/.gitignore deleted file mode 100644 index bbe7669..0000000 --- a/helm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -values-local.yaml diff --git a/helm/.helmignore b/helm/.helmignore deleted file mode 100644 index bbe7669..0000000 --- a/helm/.helmignore +++ /dev/null @@ -1 +0,0 @@ -values-local.yaml diff --git a/helm/Chart.yaml b/helm/Chart.yaml deleted file mode 100644 index 25ed9c3..0000000 --- a/helm/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: web-app-serve-helm -description: "Basic Helm Chart to deploy Web Apps" -type: application -version: 0.1.1 # managed by release.sh -sources: - - https://github.com/toggle-corp/web-app-serve diff --git a/helm/linter_values.yaml b/helm/linter_values.yaml deleted file mode 100644 index 530b7a8..0000000 --- a/helm/linter_values.yaml +++ /dev/null @@ -1,14 +0,0 @@ -fullnameOverride: test-123 - -image: - name: github.local/react/web-app - tag: latest - pullPolicy: Always - -ingress: - hostname: react.web-app.com - ingressClassName: nginx - -env: - APP_TITLE: "My APP" - APP_ENVIRONMENT: ALPHA diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl deleted file mode 100644 index 7063b97..0000000 --- a/helm/templates/_helpers.tpl +++ /dev/null @@ -1,80 +0,0 @@ -{{/* - Expand the name of the chart. -*/}} -{{- define "web-app-serve.name" -}} - {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* - Create a default fully qualified app name. - We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). - If release name contains chart name it will be used as a full name. - https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names -*/}} -{{- define "web-app-serve.fullname" -}} - {{- if .Values.fullnameOverride -}} - {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} - {{- else -}} - {{- $name := default .Chart.Name .Values.nameOverride -}} - {{- if contains $name .Release.Name -}} - {{- .Release.Name | trunc 63 | trimSuffix "-" -}} - {{- else -}} - {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} - {{- end -}} - {{- end -}} -{{- end -}} - -{{/* - Create chart name and version as used by the chart label. -*/}} -{{- define "web-app-serve.chart" -}} - {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Return the proper image name with tag -*/}} -{{- define "web-app-serve.container_image" -}} -{{- $imageName := required ".Values.image.name" .Values.image.name -}} -{{- $imageTag := required ".Values.image.tag" .Values.image.tag -}} -{{- printf "%s:%s" $imageName $imageTag -}} -{{- end -}} - -{{/* -Extract platform, repo, and image name from image repository and return as labels -Usage: {{ include "web-app-serve.ingress_project_labels" . }} -*/}} -{{- define "web-app-serve.ingress_project_labels" -}} -{{- $image := .Values.image.name | default "" -}} -{{- $parts := splitList "/" $image -}} - -{{- $registry := (index $parts 0) | default "" -}} -{{- $org := (index $parts 1) | default "" -}} - -{{- $imageNameParts := list -}} -{{- range $i, $val := $parts }} - {{- if ge (int $i) 2 }} - {{- $imageNameParts = append $imageNameParts $val }} - {{- end }} -{{- end }} - -{{- $imageName := join "/" $imageNameParts -}} - -{{- $tagRaw := .Values.image.tag -}} -{{- $tagRawList := splitList "" $tagRaw -}} -{{- $tagRawLength := len $tagRawList -}} - -{{- $tagTruncated := "" -}} -{{- if lt $tagRawLength 63 -}} - {{- $tagTruncated = $tagRaw -}} -{{- else -}} - {{- $tagRawStart := sub $tagRawLength 63 -}} - {{- $tagRawLast63Chars := slice $tagRawList $tagRawStart $tagRawLength -}} - {{- $tagTruncated = join "" $tagRawLast63Chars -}} -{{- end -}} - -web-app-serve/docker-registry: {{ $registry | quote }} -web-app-serve/docker-org: {{ $org | quote }} -web-app-serve/docker-name: {{ $imageName | quote }} -web-app-serve/docker-truncated-tag: {{ $tagTruncated | quote }} -{{- end }} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml deleted file mode 100644 index 47abcc5..0000000 --- a/helm/templates/configmap.yaml +++ /dev/null @@ -1,13 +0,0 @@ -kind: ConfigMap -apiVersion: v1 -metadata: - name: {{ template "web-app-serve.fullname" . }}-configmap - labels: - component: web-app-deployment - environment: {{ .Values.environment }} - release: {{ .Release.Name }} -data: - # Provided configs using env - {{- range $name, $value := .Values.env }} - {{ $name }}: {{ $value | quote }} - {{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml deleted file mode 100644 index 5d1d31a..0000000 --- a/helm/templates/deployment.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ template "web-app-serve.fullname" . }} - labels: - environment: {{ .Values.environment }} - release: {{ .Release.Name }} -spec: - replicas: 1 - selector: - matchLabels: - app: {{ template "web-app-serve.fullname" . }} - release: {{ .Release.Name }} - run: {{ .Release.Name }} - template: - metadata: - annotations: - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - labels: - app: {{ template "web-app-serve.fullname" . }} - release: {{ .Release.Name }} - run: {{ .Release.Name }} - spec: - {{- with .Values.image.pullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: {{ .Values.container.name }} - image: {{ include "web-app-serve.container_image" . }} - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.container.port }} - protocol: TCP - resources: - {{- toYaml .Values.resources | nindent 12 }} - envFrom: - - configMapRef: - name: {{ template "web-app-serve.fullname" . }}-configmap diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml deleted file mode 100644 index 3c7be8b..0000000 --- a/helm/templates/ingress.yaml +++ /dev/null @@ -1,41 +0,0 @@ -{{- if .Values.ingress.enabled }} - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ template "web-app-serve.fullname" . }}-ingress - labels: - app: {{ template "web-app-serve.name" . }} - environment: {{ .Values.environment }} - release: {{ .Release.Name }} - {{- if .Values.ingress.dockerMetadataEnabled }} - {{- include "web-app-serve.ingress_project_labels" . | nindent 4 -}} - {{- end }} - {{- with .Values.ingress.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.ingress.annotations }} - annotations: {{- toYaml . | nindent 4 }} - {{- end }} -spec: - ingressClassName: {{ required "ingress.ingressClassName" .Values.ingress.ingressClassName | quote }} - rules: - - host: {{ required "ingress.hostname" .Values.ingress.hostname | quote }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: {{ template "web-app-serve.fullname" . }}-svc - port: - number: 80 - - {{- if .Values.ingress.tls.enabled }} - tls: - - hosts: - - {{ .Values.ingress.host | quote }} - secretName: {{ required "ingress.tls.secretName" .Values.ingress.tls.secretName }} - {{- end }} - -{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml deleted file mode 100644 index 6176438..0000000 --- a/helm/templates/service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ template "web-app-serve.fullname" . }}-svc - labels: - app: {{ template "web-app-serve.fullname" . }} - environment: {{ .Values.environment }} - release: {{ .Release.Name }} -spec: - type: ClusterIP - selector: - app: {{ template "web-app-serve.fullname" . }} - release: {{ .Release.Name }} - run: {{ .Release.Name }} - ports: - - protocol: TCP - port: 80 - targetPort: {{ .Values.container.port }} diff --git a/helm/tests/values-01.yaml b/helm/tests/values-01.yaml deleted file mode 100644 index c4f6afc..0000000 --- a/helm/tests/values-01.yaml +++ /dev/null @@ -1,14 +0,0 @@ -fullnameOverride: test-123 - -image: - name: ghcr.io/toggle-corp/hi/some-etl-dashboard-dev - tag: my-very-very-very-very-long-good-good-good-feat-web-app-serve.2c59d93 - pullPolicy: Always - -ingress: - hostname: react.web-app.com - ingressClassName: nginx - -env: - APP_TITLE: "My APP" - APP_ENVIRONMENT: ALPHA diff --git a/helm/tests/values-02.yaml b/helm/tests/values-02.yaml deleted file mode 100644 index e2a024e..0000000 --- a/helm/tests/values-02.yaml +++ /dev/null @@ -1,15 +0,0 @@ -fullnameOverride: test-123 - -image: - name: ghcr.io/toggle-corp/hi/some-etl-dashboard-dev - tag: my-very-very-very-very-long-good-good-good-feat-web-app-serve.2c59d93 - pullPolicy: Always - -ingress: - hostname: react.web-app.com - ingressClassName: nginx - dockerMetadataEnabled: false - -env: - APP_TITLE: "My APP" - APP_ENVIRONMENT: ALPHA diff --git a/helm/values.yaml b/helm/values.yaml deleted file mode 100644 index 8507082..0000000 --- a/helm/values.yaml +++ /dev/null @@ -1,86 +0,0 @@ -environment: prod - -fullnameOverride: - -## React image (static servied by nginx) -## @param image.name Container image name -## @param image.tag Container image tag -## @param image.pullPolicy Container pull policy `Always|IfNotPresent|Never` -## @param image.pullSecrets Container image pull secrets -## -image: - name: - tag: - pullPolicy: IfNotPresent - pullSecrets: - -## React container -## @param container.name Container name -## @param container.port Container port -## -container: - name: web-app-serve - port: 80 - -## @param resources Set container requests and limits for different resources like CPU or memory -## Example: -## resources: -## requests: -## cpu: 2 -## memory: 512Mi -## limits: -## cpu: 3 -## memory: 1024Mi -## -resources: - requests: - cpu: "0.5" - memory: "100Mi" - limits: - cpu: "1" - memory: "100Mi" - -ingress: - ## @param ingress.enabled Enable an ingress resource - ## - enabled: true - ## @param ingress.dockerMetadataEnabled Enable an deployment docker image metadata in ingress labels - ## - dockerMetadataEnabled: true - ## @param ingress.ingressClassName Defines which ingress controller will implement the resource - ## - ingressClassName: - ## @param ingress.hostname Ingress hostname for the ingress - ## Hostname must be provided if Ingress is enabled. - ## - hostname: - ## @param ingress.labels to add additional ingress labels - ## e.g: - ## labels: - ## node-role.kubernetes.io/ingress: platform - ## - labels: {} - ## @param ingress.labels to add ingress annotations - ## e.g: - ## annotations: - ## kubernetes.io/ingress.class: nginx - ## - annotations: {} - ## @param ingress.tls Ingress TLS configuration - ## - tls: - ## @param ingress.tls.enabled Enable an tls for ingress resource - ## - enabled: false - ## @param ingress.tls.secretName Ingress TLS secrets name - ## secretName must be provided if Ingress TLS is enabled. - ## - secretName: - -## @param env with environment variables to pass to the container -## e.g: -## env: -## APP_TITLE: "My React APP" -## APP_ENVIRONMENT: "ALPHA" -## -env: diff --git a/release.sh b/release.sh index e44301e..9cff85d 100755 --- a/release.sh +++ b/release.sh @@ -22,11 +22,17 @@ if [ -z "$version_tag" ]; then exit fi + +if [[ $version_tag != v* ]]; then + echo "Make sure version tag starts with vX.Y.Z" + exit 1 +fi + if semver valid "$version_tag" > /dev/null; then echo "Valid SemVer: $version_tag" else echo "Invalid SemVer: \"$version_tag\"" >&2 - echo "Eg: 0.1.1 0.1.1-dev0" + echo "Eg: v0.1.1 v0.1.1-dev0" exit 1 fi @@ -44,11 +50,11 @@ BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$BASE_DIR" echo "Preparing $version_tag..." -# update the version -msg="# managed by release.sh" -sed -E -i "s/^version: .* $msg$/version: ${version_tag#v} $msg/" "./helm/Chart.yaml" -git add ./helm/Chart.yaml +# Update README.md +sed -E -i "/ghcr\.io\/toggle-corp\/web-app-serve:/ s/v[0-9]+\.[0-9]+\.[0-9]+/$version_tag/" README.md + +git add ./README.md # update the changelog git-cliff "$START_COMMIT..HEAD" --config cliff.toml --tag "$version_tag" > CHANGELOG.md diff --git a/src/apply-config.sh b/src/apply-config.sh index 9609097..a3a62e7 100755 --- a/src/apply-config.sh +++ b/src/apply-config.sh @@ -1,4 +1,6 @@ -#!/bin/bash -xe +#!/bin/env bash + +set -xe ENABLE_DEBUG=${APPLY_CONFIG__ENABLE_DEBUG:-false} APPLY_CONFIG_PATH=${APPLY_CONFIG__APPLY_CONFIG_PATH?Required} @@ -20,6 +22,16 @@ function post_debug { ln -sf /web-app-serve/debug.html "$DEBUG_DIRECTORY/index.html" set +xe + # Check for WEB_APP_SERVE_PLACEHOLDER__ and dump to file + rg \ + -A 2 -B 2 \ + -F "WEB_APP_SERVE_PLACEHOLDER__" \ + -g '!**/*.map' \ + -g '!**/*.gz' \ + --json \ + "$DESTINATION_DIRECTORY" \ + | jq -s . > "$DEBUG_DIRECTORY/missing_placeholders.json" + # Show diffs (Useful to debug issues) find "$SOURCE_DIRECTORY" -type f -printf '%P\n' | while IFS= read -r file; do diff -u \ diff --git a/src/debug.html b/src/debug.html index 2b6f309..15ed6d0 100644 --- a/src/debug.html +++ b/src/debug.html @@ -3,16 +3,19 @@ - - + + + + + + -

Nginx apply config changes:

-
+

Web Server App: Debugger

+

Missing env variable replacements

+ +

Env variable replacements

+
diff --git a/src/default-app-apply-config.sh b/src/default-app-apply-config.sh new file mode 100755 index 0000000..4d869fe --- /dev/null +++ b/src/default-app-apply-config.sh @@ -0,0 +1,8 @@ +#!/bin/env bash + +set -xe + +while IFS='=' read -r KEY VALUE; do + # FIXME: If js and value is empty string, replace it with undefined? + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$VALUE|g" {} + +done < <(env | grep '^APP_')