diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000000..47d7e038d2e --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,31 @@ +# To start developing: + +Wait for the container to build and start. You will see "Done. Press any key to close the terminal." in the terminal when it's ready. + +Once it's running, you can start the development server: + +**Option 1:** Press `Ctrl+Shift+P`, type "Run Task", select "Start Development" +**Option 2:** Open a terminal and run: + +```bash +pnpm run develop +``` + +## Optional setup + +For E2E tests: + +```bash +npx playwright install chromium +``` + +For curriculum tests: + +```bash +pnpm -F=curriculum install-puppeteer +``` + +## More information + +For detailed setup instructions and contribution guidelines, visit: +https://contribute.freecodecamp.org/how-to-setup-freecodecamp-locally diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4533b9f1567..f373492c453 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,11 @@ { - "name": "freeCodeCampDC", + "name": "freeCodeCamp", "dockerComposeFile": "docker-compose.yml", "service": "devcontainer", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "workspaceFolder": "/workspaces/freeCodeCamp", + "mounts": [ + "source=fcc-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume" + ], "forwardPorts": [3000, 8000], "portsAttributes": { "3000": { @@ -17,9 +20,64 @@ "otherPortsAttributes": { "onAutoForward": "silent" }, - "onCreateCommand": "[ ! -f .env ] && cp sample.env .env || true", - "updateContentCommand": "pnpm install && pnpm seed", - "postAttachCommand": { - "instructions": "bash -c 'echo \"\n\n\n Start a new terminal and run \\`pnpm run develop\\` when you are ready.\n\n\n\"'" + "onCreateCommand": "sudo chown node:node node_modules && ([ ! -f .env ] && cp sample.env .env || true)", + "updateContentCommand": "pnpm install --prefer-offline", + "postCreateCommand": "rsync -a --include='*/' --include='.turbo/***' --exclude='*' /home/node/.cache/fcc/ ./ && set -a && . ./.env && set +a && until mongosh --eval 'rs.status().ok' 2>/dev/null; do sleep 1; done && pnpm seed", + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ], + "settings": { + "task.allowAutomaticTasks": "on", + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "Start API Server", + "type": "shell", + "command": "pnpm run develop:api", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "develop" + } + }, + { + "label": "Start Client Server", + "type": "shell", + "command": "pnpm run develop:client", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "develop" + } + }, + { + "label": "Start Development", + "dependsOn": ["Start API Server", "Start Client Server"], + "problemMatcher": [] + }, + { + "label": "Open README", + "type": "shell", + "command": "code .devcontainer/README.md", + "presentation": { + "reveal": "silent", + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] + } + } + } } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 6b772f45e36..a2d83835a8b 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,17 +1,16 @@ services: devcontainer: + image: ghcr.io/freecodecamp/devcontainer:latest depends_on: - db - setup - image: mcr.microsoft.com/devcontainers/typescript-node:22 volumes: - - ../..:/workspaces:cached + - ..:/workspaces/freeCodeCamp:cached network_mode: service:db command: sleep infinity db: image: mongo:8.0 - container_name: mongodb command: mongod --replSet rs0 restart: unless-stopped hostname: mongodb @@ -21,27 +20,20 @@ services: test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"] interval: 2s retries: 5 + start_period: 10s setup: image: mongo:8.0 depends_on: db: condition: service_healthy - restart: on-failure - # This will try to initiate the replica set, until it succeeds twice (i.e. until the replica set is already initialized) + restart: on-failure:5 command: > mongosh --host mongodb:27017 --eval ' - var cfg = { + rs.initiate({ _id: "rs0", - members: [ - { _id: 0, host: "mongodb:27017" } - ] - }; - try { - rs.initiate(cfg); - } catch (err) { - if(err.codeName !== "AlreadyInitialized") throw err; - } + members: [{ _id: 0, host: "mongodb:27017" }] + }).ok || rs.status().ok ' volumes: diff --git a/.github/workflows/devcontainer-ci.yml b/.github/workflows/devcontainer-ci.yml new file mode 100644 index 00000000000..94144364fb3 --- /dev/null +++ b/.github/workflows/devcontainer-ci.yml @@ -0,0 +1,63 @@ +name: CI - Devcontainer + +on: + pull_request: + paths: + - '.devcontainer/**' + - 'docker/devcontainer/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: read + +jobs: + validate: + name: Validate + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Login to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Install devcontainer CLI + # renovate: datasource=npm depName=@devcontainers/cli + run: npm install -g @devcontainers/cli@0.83.0 + + - name: Build devcontainer + run: devcontainer build --workspace-folder . + + - name: Start devcontainer + run: devcontainer up --workspace-folder . + + - name: Validate required tools + run: | + devcontainer exec --workspace-folder . pnpm --version + devcontainer exec --workspace-folder . rsync --version + devcontainer exec --workspace-folder . mongosh --version + devcontainer exec --workspace-folder . node --version + devcontainer exec --workspace-folder . git --version + + - name: Validate MongoDB replica set + run: | + for i in $(seq 1 30); do + if devcontainer exec --workspace-folder . mongosh --eval "rs.status().ok" 2>/dev/null; then + echo "Replica set is ready" + exit 0 + fi + echo "Waiting for replica set... (attempt $i/30)" + sleep 2 + done + echo "Replica set failed to initialize" + exit 1 diff --git a/.github/workflows/docker-ghcr.yml b/.github/workflows/docker-ghcr.yml index 9005cb39915..09f03a4b668 100644 --- a/.github/workflows/docker-ghcr.yml +++ b/.github/workflows/docker-ghcr.yml @@ -1,4 +1,4 @@ -name: CD - Docker - GHCR (Gitpod) +name: CD - Docker - GHCR Images on: workflow_dispatch: @@ -6,55 +6,44 @@ on: branches: - main paths: - - 'docker/gitpod/*' + - 'pnpm-lock.yaml' + - 'docker/devcontainer/**' + - '.github/workflows/docker-ghcr.yml' + +permissions: + contents: read + packages: write jobs: - build-and-push-image: + build-and-push: + name: Build and Push Images runs-on: ubuntu-24.04 - permissions: - contents: read - packages: write - - strategy: - fail-fast: false - matrix: - images: - - gitpod steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + submodules: 'recursive' + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - name: Log in to the GHCR + - name: Log in to GHCR uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Cache Docker layers - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + - name: Build and push images + uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ matrix.images }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-${{ matrix.images }}- - - - name: Build and push Docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - with: - context: ./docker/${{ matrix.images }} + files: docker/devcontainer/docker-bake.hcl + targets: devcontainer push: true - tags: | - ghcr.io/freecodecamp/${{ matrix.images }}:${{ github.sha }} - ghcr.io/freecodecamp/${{ matrix.images }}:latest - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache + env: + TAG: ${{ github.sha }} + TAG_LATEST: ${{ github.ref_name == 'main' }} diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index bf61e9b5259..00000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,78 +0,0 @@ -image: ghcr.io/freecodecamp/gitpod:latest -ports: - - port: 27017 # mongodb - onOpen: ignore - - port: 8000 # client - onOpen: notify - visibility: public - - port: 9228 # node debug - onOpen: ignore - - port: 3000 # api - onOpen: ignore - visibility: public - - port: 9229 # node debug - onOpen: ignore - - port: 9230 # client node debug - onOpen: ignore - - port: 3200 # challenge editor api - visibility: public - - port: 3300 # challenge editor client - visibility: public - - port: 8025 # Mailpit - visibility: public - onOpen: ignore - - port: 1025 # Mailpit - onOpen: ignore - - port: 9323 # Playwright - visibility: public - onOpen: ignore - -tasks: - - before: | - echo ' - export COOKIE_DOMAIN=.gitpod.io - export HOME_LOCATION=$(gp url 8000) - export API_LOCATION=$(gp url 3000) - export CHALLENGE_EDITOR_API_LOCATION=$(gp url 3200) - export CHALLENGE_EDITOR_CLIENT_LOCATION=$(gp url 3300) - export CHALLENGE_EDITOR_LEARN_CLIENT_LOCATION=$(gp url 8000) - ' >> ~/.bashrc; - exit; - - - name: db - # starting mongod in background, so it doesn't block prebuilds - before: > - docker compose -f docker/docker-compose.yml up -d - - - name: server - before: export COOKIE_DOMAIN=.gitpod.io && export HOME_LOCATION=$(gp url 8000) && export API_LOCATION=$(gp url 3000) - # init is not executed for prebuilt workspaces and restarts, - # so we should put all the heavy initialization here - init: > - cp sample.env .env && - pnpm install && - gp sync-done pnpm-install && - pnpm run build:curriculum && - gp ports await 27017 - command: > - pnpm run seed && - mongosh --eval "db.fsyncLock(); db.fsyncUnlock()" && - gp ports await 27017 && - cd api && - pnpm run develop - - - name: client - before: export HOME_LOCATION=$(gp url 8000) && export API_LOCATION=$(gp url 3000) - init: > - cd ./client && - gp sync-await pnpm-install && - cd .. - command: > - gp ports await 3000 && - pnpm run develop:client -- -H '0.0.0.0' - openMode: split-right - -vscode: - extensions: - - dbaeumer.vscode-eslint - - esbenp.prettier-vscode diff --git a/curriculum/i18n-curriculum b/curriculum/i18n-curriculum index 5e4b2ee159f..1bb234bbc44 160000 --- a/curriculum/i18n-curriculum +++ b/curriculum/i18n-curriculum @@ -1 +1 @@ -Subproject commit 5e4b2ee159fe404f182184147af996b241dc5c9f +Subproject commit 1bb234bbc445e5a59f7f0d706130fe25612197cd diff --git a/docker/devcontainer/Dockerfile b/docker/devcontainer/Dockerfile new file mode 100644 index 00000000000..19513316966 --- /dev/null +++ b/docker/devcontainer/Dockerfile @@ -0,0 +1,97 @@ +# ============================================================================= +# BASE - System dependencies, pnpm, mongosh +# ============================================================================= +FROM node:24-bookworm AS base + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + jq \ + gnupg \ + sudo \ + rsync \ + && rm -rf /var/lib/apt/lists/* \ + && echo "node ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/node + +RUN curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" > /etc/apt/sources.list.d/mongodb-org-8.0.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends mongodb-mongosh \ + && rm -rf /var/lib/apt/lists/* + +# renovate: datasource=npm depName=pnpm +RUN npm install -g pnpm@10.28.1 + +ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright +ENV npm_config_store_dir=/home/node/.local/share/pnpm/store + +# ============================================================================= +# BUILDER - Install dependencies and build to populate caches (disposable) +# ============================================================================= +FROM base AS builder + +USER node +WORKDIR /home/node/.cache/fcc + +# Package manifests only (for pnpm install caching) +COPY --chown=node:node pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json tsconfig-base.json ./ +COPY --chown=node:node api/package.json api/ +COPY --chown=node:node client/package.json client/turbo.json client/ +COPY --chown=node:node curriculum/package.json curriculum/turbo.json curriculum/ +COPY --chown=node:node e2e/package.json e2e/ + +COPY --chown=node:node tools/challenge-helper-scripts/package.json tools/challenge-helper-scripts/ +COPY --chown=node:node tools/challenge-parser/package.json tools/challenge-parser/ +COPY --chown=node:node tools/client-plugins/browser-scripts/package.json tools/client-plugins/browser-scripts/ +COPY --chown=node:node tools/client-plugins/gatsby-remark-node-identity/package.json tools/client-plugins/gatsby-remark-node-identity/ +COPY --chown=node:node tools/client-plugins/gatsby-source-challenges/package.json tools/client-plugins/gatsby-source-challenges/ +COPY --chown=node:node tools/daily-challenges/package.json tools/daily-challenges/ +COPY --chown=node:node tools/scripts/seed/package.json tools/scripts/seed/ +COPY --chown=node:node tools/scripts/seed-exams/package.json tools/scripts/seed-exams/ + +COPY --chown=node:node packages/challenge-builder/package.json packages/challenge-builder/ +COPY --chown=node:node packages/challenge-linter/package.json packages/challenge-linter/ +COPY --chown=node:node packages/eslint-config/package.json packages/eslint-config/ +COPY --chown=node:node packages/shared/package.json packages/shared/ + +# Prisma schema needed for api postinstall script (prisma generate) +COPY --chown=node:node api/prisma/ api/prisma/ +COPY --chown=node:node api/prisma.config.ts api/ + +# Install dependencies (populates pnpm store) +ENV PUPPETEER_SKIP_DOWNLOAD=true +RUN pnpm install --frozen-lockfile + +# Source files for builds +COPY --chown=node:node packages/ packages/ +COPY --chown=node:node tools/ tools/ +COPY --chown=node:node curriculum/ curriculum/ + +# Build shared packages and curriculum (populates Turbo cache) +# Source .env so turbo hashes env vars (e.g. FCC_*) identically to runtime +COPY --chown=node:node sample.env .env +RUN set -a && . ./.env && set +a && \ + pnpm turbo build --filter=@freecodecamp/shared && \ + pnpm turbo build --filter=@freecodecamp/curriculum + +# ============================================================================= +# DEVCONTAINER - Clean image with only pre-populated caches +# Used by: GitHub Codespaces, local VS Code devcontainers +# ============================================================================= +FROM base AS devcontainer + +LABEL org.opencontainers.image.source=https://github.com/freeCodeCamp/freeCodeCamp \ + org.opencontainers.image.description="Development container for freeCodeCamp with pre-populated caches" \ + org.opencontainers.image.licenses=BSD-3-Clause + +USER node + +# Copy pre-populated pnpm store (for fast pnpm install --prefer-offline) +COPY --from=builder --chown=node:node /home/node/.local/share/pnpm/store /home/node/.local/share/pnpm/store + +# Copy turbo cache only (preserving directory structure for rsync at runtime) +WORKDIR /home/node/.cache/fcc +RUN --mount=type=bind,from=builder,source=/home/node/.cache/fcc,target=/tmp/fcc-build \ + rsync -a --include='*/' --include='.turbo/***' --exclude='*' /tmp/fcc-build/ ./ + +WORKDIR /workspaces/freeCodeCamp diff --git a/docker/devcontainer/docker-bake.hcl b/docker/devcontainer/docker-bake.hcl new file mode 100644 index 00000000000..68eba1f8c95 --- /dev/null +++ b/docker/devcontainer/docker-bake.hcl @@ -0,0 +1,50 @@ +// Docker Bake configuration for freeCodeCamp devcontainer images +// +// Usage (from repo root): +// docker buildx bake -f docker/devcontainer/docker-bake.hcl # Local build (native platform) +// docker buildx bake -f docker/devcontainer/docker-bake.hcl devcontainer --push # Push multi-arch to GHCR +// +// With custom tag: +// TAG=v1.0.0 docker buildx bake -f docker/devcontainer/docker-bake.hcl devcontainer --push + +variable "REGISTRY" { + default = "ghcr.io/freecodecamp" +} + +variable "TAG" { + default = "latest" +} + +variable "TAG_LATEST" { + default = "true" +} + +group "default" { + targets = ["local-devcontainer"] +} + +target "_common" { + context = "." + dockerfile = "docker/devcontainer/Dockerfile" +} + +// Multi-arch image for pushing to registry (CI and local --push) +target "devcontainer" { + inherits = ["_common"] + target = "devcontainer" + platforms = ["linux/amd64", "linux/arm64"] + cache-from = ["type=gha"] + cache-to = ["type=gha,mode=max"] + tags = concat( + ["${REGISTRY}/devcontainer:${TAG}"], + TAG_LATEST == "true" ? ["${REGISTRY}/devcontainer:latest"] : [] + ) +} + +// Native platform only (fast local builds) +target "local-devcontainer" { + inherits = ["_common"] + target = "devcontainer" + output = ["type=docker"] + tags = ["ghcr.io/freecodecamp/devcontainer:latest"] +} diff --git a/docker/gitpod/Dockerfile b/docker/gitpod/Dockerfile deleted file mode 100644 index 1095fe50ca6..00000000000 --- a/docker/gitpod/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM gitpod/workspace-mongodb:latest - -LABEL org.opencontainers.image.source=https://github.com/freecodecamp/freecodecamp \ - org.opencontainers.image.description="A Gitpod image for the main freeCodeCamp repository" \ - org.opencontainers.image.licenses=BSD-3-Clause - -# from https://www.gitpod.io/docs/introduction/languages/javascript#node-versions -RUN bash -c 'VERSION="24" \ - && source $HOME/.nvm/nvm.sh && nvm install $VERSION \ - && nvm use $VERSION && nvm alias default $VERSION \ - && npm i -g pnpm@10 \ - && echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix \ - && pnpm dlx playwright@1.47.1 install --with-deps chromium'