diff --git a/.dockerignore b/.dockerignore index b332862629d..09d3362e7f5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,8 @@ client/public .git .gitignore .dockerignore -*Dockerfile* -*docker-compose* +docker/web/Dockerfile +docker/api/Dockerfile +**/*docker-compose* **/node_modules +.eslintcache diff --git a/.github/workflows/temporary-container-checks.yml b/.github/workflows/temporary-container-checks.yml new file mode 100644 index 00000000000..6a59b117ba0 --- /dev/null +++ b/.github/workflows/temporary-container-checks.yml @@ -0,0 +1,170 @@ +# TODO: remove this workflow once we use containers in the other workflows. This +# workflow is intended to prevent regressions until that has been achieved. +name: CI - E2E - Containers +on: + workflow_dispatch: + workflow_run: + workflows: ['CI - Node.js'] + types: + - completed + # TODO: refactor with a workflow_call + pull_request: + paths-ignore: + - 'docs/**' + branches: + - 'main' + - 'next-**' + - 'e2e-**' + +jobs: + build-client: + name: Build Client (Container) + runs-on: ubuntu-20.04 + strategy: + matrix: + node-version: [18.x] + + steps: + - name: Checkout Source Files + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + + - name: Checkout client-config + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + with: + repository: freeCodeCamp/client-config + path: client-config + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Create Image + run: | + docker build \ + --build-arg HOME_LOCATION=http://localhost:8000 \ + --build-arg API_LOCATION=http://localhost:3000 \ + --build-arg FORUM_LOCATION=https://forum.freecodecamp.org \ + --build-arg NEWS_LOCATION=https://www.freecodecamp.org/news \ + --build-arg RADIO_LOCATION=https://coderadio.freecodecamp.org \ + --build-arg CLIENT_LOCALE=english \ + --build-arg CURRICULUM_LOCALE=english \ + --build-arg SHOW_LOCALE_DROPDOWN_MENU=false \ + --build-arg ALGOLIA_APP_ID=app_id_from_algolia_dashboard \ + --build-arg ALGOLIA_API_KEY=api_key_from_algolia_dashboard \ + --build-arg STRIPE_PUBLIC_KEY=pk_from_stripe_dashboard \ + --build-arg PAYPAL_CLIENT_ID=id_from_paypal_dashboard \ + --build-arg PATREON_CLIENT_ID=id_from_patreon_dashboard \ + --build-arg DEPLOYMENT_ENV=staging \ + --build-arg SHOW_UPCOMING_CHANGES=false \ + --build-arg SHOW_NEW_CURRICULUM=true \ + --build-arg GROWTHBOOK_URI=api_URI_from_Growthbook_dashboard \ + --build-arg FREECODECAMP_NODE_ENV=development \ + -t fcc-client \ + -f docker/web/Dockerfile . + + - name: Save Image + run: docker save fcc-client > client-artifact.tar + + - name: Upload Client Artifact + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 + with: + name: client-artifact + path: client-artifact.tar + + # TODO: Figure out what to do with the webpack stats file. Create it + # during the build via a mounted host directory? i.e. docker build -t + # myimage -v /path/on/host:/output/dir -f Dockerfile . It's important to + # ensure it doesn't end up in the final image, as it's a large file. + # - name: Upload Webpack Stats + # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 + # with: + # name: webpack-stats + # path: client/public/stats.json + + cypress-run: + name: Test + runs-on: ubuntu-20.04 + needs: build-client + strategy: + fail-fast: false + matrix: + # To avoid wasting resources we're only using chrome for now. + browsers: [chrome] + node-version: [18.x] + + services: + mongodb: + image: mongo:4.4 + ports: + - 27017:27017 + # We need mailhog to catch any emails the api tries to send. + mailhog: + image: mailhog/mailhog + ports: + - 1025:1025 + + steps: + - name: Set Action Environment Variables + run: | + echo "CYPRESS_RECORD_KEY=${{ secrets.CYPRESS_RECORD_KEY }}" >> $GITHUB_ENV + echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + + - name: Checkout Source Files + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 + with: + name: client-artifact + + - name: Load Client Image + run: | + docker load < client-artifact.tar + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3 + with: + node-version: ${{ matrix.node-version }} + # cypress-io/github-action caches the store, so we should not cache it + # here. + + - name: Set freeCodeCamp Environment Variables + run: cp sample.env .env + + - name: Install and Build + run: | + pnpm install + pnpm run create:config + pnpm run build:curriculum + pnpm run build:server + + - name: Seed Database + run: pnpm run seed + + # start-ci uses pm2, so it needs to be installed globally + - name: Install pm2 + run: npm i -g pm2 + + - name: Cypress run + uses: cypress-io/github-action@v4 + with: + record: ${{ env.CYPRESS_RECORD_KEY != 0 }} + start: | + pnpm start:server + docker compose up -d + wait-on: http://localhost:8000 + wait-on-timeout: 1200 + config: baseUrl=http://localhost:8000 + browser: ${{ matrix.browsers }} + spec: ${{ matrix.spec }} diff --git a/client/package.json b/client/package.json index 8da5c07bcf1..64d19accb67 100644 --- a/client/package.json +++ b/client/package.json @@ -23,7 +23,7 @@ "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=7168\" gatsby build --prefix-paths", "build:scripts": "pnpm run -F=browser-scripts build", "clean": "gatsby clean", - "common-setup": "pnpm -w run create:config && pnpm run create:env && pnpm run create:trending && pnpm run build:components-library", + "common-setup": "pnpm -w run create:config && pnpm -w run create:utils && pnpm run create:env && pnpm run create:trending && pnpm run build:components-library", "create:env": "cross-env DEBUG=fcc:* ts-node ./tools/create-env.ts", "create:trending": "ts-node ./tools/download-trending.ts", "predevelop": "pnpm run common-setup && pnpm run build:scripts --env development", diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..7399a780558 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + client: + image: fcc-client + ports: + # PORT is used by the new api, so we use the less generic HOST_PORT to avoid + # conflicts. + - '${HOST_PORT:-8000}:8000' diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index 58bf440ce70..7bc67130cdb 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -1,28 +1,51 @@ -FROM node:16-buster AS builder -# Install doppler CLI -RUN (curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh) | sh -s -- --verify-signature +# bookworm was only released on 10-6-2023, so is a little too new. +FROM node:18-bullseye AS builder +# global installs need root permissions, so have to happen before we switch to +# the node user +RUN npm i -g pnpm@8 # node images create a non-root user that we can use USER node WORKDIR /home/node/build + COPY --chown=node:node . . -# Pass `DOPPLER_TOKEN` at build time to create an encrypted snapshot for high-availability -ARG DOPPLER_TOKEN -RUN \ - doppler secrets download doppler.encrypted.json &&\ - # Install and donot ignore the scripts for sharp - pnpm install --no-progress --ignore-scripts=false &&\ - doppler run --fallback=doppler.encrypted.json --command="npm run create:config" &&\ - doppler run --fallback=doppler.encrypted.json --command="npm run build:curriculum" &&\ - doppler run --fallback=doppler.encrypted.json --command="npm run build:client" -# Use a lightweight image for the serving the files -FROM node:16-alpine -RUN npm i -g serve@13 +ARG HOME_LOCATION +ARG API_LOCATION +ARG FORUM_LOCATION +ARG NEWS_LOCATION +ARG RADIO_LOCATION +ARG CLIENT_LOCALE +ARG CURRICULUM_LOCALE +ARG SHOW_LOCALE_DROPDOWN_MENU +ARG ALGOLIA_APP_ID +ARG ALGOLIA_API_KEY +ARG STRIPE_PUBLIC_KEY +ARG PAYPAL_CLIENT_ID +ARG PATREON_CLIENT_ID +ARG DEPLOYMENT_ENV +ARG SHOW_UPCOMING_CHANGES +ARG SHOW_NEW_CURRICULUM +ARG GROWTHBOOK_URI +ARG FREECODECAMP_NODE_ENV +# We're installing specific packages even though it is not strictly necessary - +# pnpm install would work. The idea is to make the dependencies explicit and +# keep them under our control. +RUN pnpm config set dedupe-peer-dependents false +# Scripts need to be run at this stage (--ignore-scripts cannot be used) because +# without them, Gatsby will not install sharp. +RUN pnpm install -w -F=client -F=ui -F=browser-scripts -F=challenge-parser \ + --frozen-lockfile +RUN pnpm build:client + +FROM node:18-alpine +RUN npm i -g serve@13 pm2@4 USER node -WORKDIR /home/node -COPY --from=builder /home/node/build/client/public/ client/public -COPY --from=builder /home/node/build/docker/web/serve.json client/serve.json +WORKDIR /home/node/client +COPY --from=builder /home/node/build/client/public/ public +COPY --from=builder /home/node/build/docker/web/serve.sh serve.sh +COPY --from=builder /home/node/build/docker/web/pm2-start.sh pm2-start.sh +COPY --from=builder /home/node/build/docker/web/serve.json serve.json -EXPOSE 8000 -CMD ["serve", "--config", "client/serve.json", "--cors", "--no-clipboard", "--no-port-switching", "-p", "8000", "client/public"] +ENTRYPOINT [ "./pm2-start.sh" ] +CMD [ "8000" ] diff --git a/docker/web/pm2-start.sh b/docker/web/pm2-start.sh new file mode 100755 index 00000000000..44914f1de08 --- /dev/null +++ b/docker/web/pm2-start.sh @@ -0,0 +1,2 @@ +#!/bin/sh +pm2-runtime start -i 0 --interpreter=sh ./serve.sh --name client-primary -- $1 diff --git a/docker/web/serve.json b/docker/web/serve.json index fb5c32a25cd..66c25ffdb32 100644 --- a/docker/web/serve.json +++ b/docker/web/serve.json @@ -1,5 +1,43 @@ { "directoryListing": false, + "headers": [ + { + "source": "{**/*.html,**/app-data.json,**/page-data.json}", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=0, must-revalidate" + } + ] + }, + { + "source": "**/*-@(????????????????????????????????|????????????????????).@(js|woff|ttf|svg|png)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=172800, immutable" + } + ] + }, + { + "source": "{misc/*.js,sw.js}", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=0, must-revalidate" + } + ] + }, + { + "source": "{js/sass.sync.js,css/bootstrap.min.css}", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=14400, stale-while-revalidate=172800, must-revalidate" + } + ] + } + ], "trailingSlash": false, "rewrites": [ { diff --git a/docker/web/serve.sh b/docker/web/serve.sh new file mode 100755 index 00000000000..d324fa67212 --- /dev/null +++ b/docker/web/serve.sh @@ -0,0 +1,2 @@ +#!/bin/sh +serve -c ../serve.json -p $1 public