diff --git a/.github/actions/gcloud/deploy-app/action.yaml b/.github/actions/gcloud/deploy-app/action.yaml new file mode 100644 index 00000000..25310fd6 --- /dev/null +++ b/.github/actions/gcloud/deploy-app/action.yaml @@ -0,0 +1,23 @@ +name: "Code for Life - GCloud - Deploy App" +description: "Deploy an app to Google Cloud." +inputs: + gcp-credentials: + description: "The JSON credentials used to access GCP." + required: true + deploy-args: + description: "Arguments to pass to `gcloud app deploy`." + required: false +runs: + using: composite + steps: + - name: ๐Ÿ— Authenticate with GCloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ inputs.gcp-credentials }} + + - name: ๐Ÿค– Set up GCloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: ๐Ÿš€ Deploy App on GCloud + shell: bash + run: gcloud app deploy ${{ inputs.deploy-args }} diff --git a/.github/actions/javascript/setup-environment/action.yaml b/.github/actions/javascript/setup-environment/action.yaml new file mode 100644 index 00000000..da8b903c --- /dev/null +++ b/.github/actions/javascript/setup-environment/action.yaml @@ -0,0 +1,42 @@ +name: "Code for Life - JavaScript - Setup Environment" +description: "Set up a JavaScript environment." +inputs: + checkout: + description: "A flag to designate if the code should be checked out." + required: true + default: "true" + node-version: + description: "The Node.js version to set up." + required: true + default: "18" + working-directory: + description: "The current working directory." + required: true + default: "." + install-args: + description: "Arguments to pass to pipenv install." + required: false +runs: + using: composite + steps: + - name: ๐Ÿ›ซ Checkout + if: ${{ inputs.checkout == 'true' }} + uses: actions/checkout@v4 + + - name: ๐ŸŒ Set up Node + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: โฌ†๏ธ Upgrade npm + shell: bash + run: npm install --global npm + + - name: ๐Ÿ›  Install Yarn + shell: bash + run: npm install --global yarn + + - name: ๐Ÿ›  Install Dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: yarn install ${{ inputs.install-args }} diff --git a/.github/workflows/backend.yaml b/.github/workflows/backend.yaml new file mode 100644 index 00000000..566eff1a --- /dev/null +++ b/.github/workflows/backend.yaml @@ -0,0 +1,84 @@ +name: Backend + +on: + workflow_call: + inputs: + python-version: + description: "The Python version to set up." + type: number + required: false + default: 3.8 + secrets: + CODECOV_TOKEN: + description: "The token used to gain access to Codecov." + required: false + GCP_CREDENTIALS: + description: "The JSON credentials used to access GCP." + required: false + +jobs: + validate-pr-refs: + uses: ocadotechnology/codeforlife-workspace/.github/workflows/validate-pull-request-refs.yaml@main + + test: + uses: ocadotechnology/codeforlife-workspace/.github/workflows/test-python-code.yaml@main + secrets: inherit + with: + python-version: ${{ inputs.python-version }} + + deploy: + runs-on: ubuntu-latest + needs: [validate-pr-refs, test] + # Only deploy if the repo's owner is Ocado Tech. and a change is made to an environment's branch. + if: github.repository_owner_id == 2088731 && (github.ref_name == 'production' || github.ref_name == 'development' || github.ref_name == 'staging') + environment: ${{ github.ref_name }} + steps: + - name: ๐Ÿ Set up Python ${{ inputs.python-version }} Environment + uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + python-version: ${{ inputs.python-version }} + install-args: --dev + + - name: ๐Ÿ—๏ธ Generate requirements.txt + run: pipenv requirements > requirements.txt + + - name: ๐Ÿ—๏ธ Collect Static Files + run: pipenv run python ./manage.py collectstatic --noinput --clear + + # https://mikefarah.gitbook.io/yq/ + # TODO: clean up app.yaml environment variables + - name: ๐Ÿ–Š๏ธ Configure App Deployment + uses: mikefarah/yq@master + with: + cmd: | + # Set name with convention "{ENV_NAME}-{REPO_NAME}" + name=${{ github.repository }} + name=${name#"ocadotechnology/codeforlife-"} + name="${{ github.ref_name }}-${name}" + + # Check if service is the client-facing service. + is_root=$( + if [ ${{ github.ref_name }} == "production" ] + then echo "1" + else echo "0" + fi + ) + + # Set runtime with convention "python{PY_VERSION}". + # The version must have the dot removed: "python3.8" -> "python38". + runtime=python${{ inputs.python-version }} + runtime=${runtime//.} + + yq -i ' + .runtime = "'$runtime'" | + .service = "'$name'" | + .env_variables.SECRET_KEY = "${{ secrets.SECRET_KEY }}" | + .env_variables.SERVICE_NAME = "$name" | + .env_variables.SERVICE_IS_ROOT = "$is_root" | + .env_variables.MODULE_NAME = "${{ github.ref_name }}" + ' app.yaml + + - name: ๐Ÿš€ Deploy App on GCloud + uses: ocadotechnology/codeforlife-workspace/.github/actions/gcloud/deploy-app@main + with: + gcp-credentials: ${{ secrets.GCP_CREDENTIALS }} diff --git a/.github/workflows/cron.yaml b/.github/workflows/cron.yaml index 65daaf47..b3d2f712 100644 --- a/.github/workflows/cron.yaml +++ b/.github/workflows/cron.yaml @@ -5,7 +5,7 @@ on: branches: - main paths: - - 'cron.yaml' + - "cron.yaml" workflow_dispatch: jobs: @@ -13,15 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - name: ๐Ÿ›ซ Checkout - uses: actions/checkout@v3 - - - name: ๐Ÿ— Authenticate with GCloud - uses: google-github-actions/auth@v1 - with: - credentials_json: ${{ secrets.GCP_CREDENTIALS }} - - - name: ๐Ÿค– Set up GCloud SDK - uses: google-github-actions/setup-gcloud@v1 + uses: actions/checkout@v4 - name: ๐Ÿš€ Deploy Cron Jobs on GCloud - run: gcloud app deploy cron.yaml + uses: ocadotechnology/codeforlife-workspace/.github/actions/gcloud/deploy-app@main + with: + gcp-credentials: ${{ secrets.GCP_CREDENTIALS }} + deploy-args: cron.yaml diff --git a/.github/workflows/dispatch.yaml b/.github/workflows/dispatch.yaml index 4adf4f84..b867f47c 100644 --- a/.github/workflows/dispatch.yaml +++ b/.github/workflows/dispatch.yaml @@ -5,7 +5,7 @@ on: branches: - main paths: - - 'dispatch.yaml' + - "dispatch.yaml" workflow_dispatch: jobs: @@ -13,15 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - name: ๐Ÿ›ซ Checkout - uses: actions/checkout@v3 - - - name: ๐Ÿ— Authenticate with GCloud - uses: google-github-actions/auth@v1 - with: - credentials_json: ${{ secrets.GCP_CREDENTIALS }} - - - name: ๐Ÿค– Set up GCloud SDK - uses: google-github-actions/setup-gcloud@v1 + uses: actions/checkout@v4 - name: ๐Ÿš€ Deploy Routing Rules on GCloud - run: gcloud app deploy dispatch.yaml + uses: ocadotechnology/codeforlife-workspace/.github/actions/gcloud/deploy-app@main + with: + gcp-credentials: ${{ secrets.GCP_CREDENTIALS }} + deploy-args: dispatch.yaml diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml new file mode 100644 index 00000000..ae9d4314 --- /dev/null +++ b/.github/workflows/frontend.yaml @@ -0,0 +1,63 @@ +name: Frontend + +on: + workflow_call: + inputs: + node-version: + description: "The Node.js version to set up." + type: number + required: false + default: 18 + secrets: + CODECOV_TOKEN: + description: "The token used to gain access to Codecov." + required: false + GCP_CREDENTIALS: + description: "The JSON credentials used to access GCP." + required: false + +jobs: + validate-pr-refs: + uses: ocadotechnology/codeforlife-workspace/.github/workflows/validate-pull-request-refs.yaml@main + + test: + uses: ocadotechnology/codeforlife-workspace/.github/workflows/test-javascript-code.yaml@main + secrets: inherit + with: + node-version: ${{ inputs.node-version }} + + deploy: + runs-on: ubuntu-latest + needs: [validate-pr-refs, test] + # Only deploy if the repo's owner is Ocado Tech. and a change is made to an environment's branch. + if: github.repository_owner_id == 2088731 && (github.ref_name == 'production' || github.ref_name == 'development' || github.ref_name == 'staging') + environment: ${{ github.ref_name }} + steps: + - name: ๐ŸŒ Set up JavaScript ${{ inputs.node-version }} Environment + uses: ocadotechnology/codeforlife-workspace/.github/actions/javascript/setup-environment@main + with: + node-version: ${{ inputs.node-version }} + install-args: --production=false + + - name: ๐Ÿ—๏ธ Build App + run: yarn build + + # https://mikefarah.gitbook.io/yq/ + - name: ๐Ÿ–Š๏ธ Configure App Deployment + uses: mikefarah/yq@master + with: + cmd: | + # Set name with convention "{ENV_NAME}-{REPO_NAME}" + name=${{ github.repository }} + name=${name#"ocadotechnology/codeforlife-"} + name="${{ github.ref_name }}-${name}" + + yq -i ' + .runtime = "nodejs${{ inputs.node-version }}" | + .service = "'$name'" + ' app.yaml + + - name: ๐Ÿš€ Deploy App on GCloud + uses: ocadotechnology/codeforlife-workspace/.github/actions/gcloud/deploy-app@main + with: + gcp-credentials: ${{ secrets.GCP_CREDENTIALS }} diff --git a/.github/workflows/test-javascript-code.yaml b/.github/workflows/test-javascript-code.yaml new file mode 100644 index 00000000..f1e94331 --- /dev/null +++ b/.github/workflows/test-javascript-code.yaml @@ -0,0 +1,80 @@ +name: Test JavaScript Code + +on: + workflow_call: + inputs: + node-version: + description: "The Node.js version to set up." + type: number + required: false + default: 18 + working-directory: + description: "The current working directory." + type: string + required: false + default: "." + codecov-slug: + description: "The slug provided to Codecov for the coverage report." + type: string + required: false + default: ${{ github.repository }} + codecov-yml-path: + description: "The path of the Codecov YAML file." + type: string + required: false + default: "./codecov.yml" + codecov-file: + description: "The path to the coverage file to upload." + type: string + required: false + default: "coverage/cobertura-coverage.xml" + secrets: + CODECOV_TOKEN: + description: "The token used to gain access to Codecov." + required: false # Needs to be false to support contributors + +jobs: + test-js-code: + runs-on: ubuntu-latest + env: + OCADO_TECH_ORG_ID: 2088731 + steps: + - name: ๐ŸŒ Set up JavaScript ${{ inputs.node-version }} Environment + uses: ocadotechnology/codeforlife-workspace/.github/actions/javascript/setup-environment@main + with: + node-version: ${{ inputs.node-version }} + working-directory: ${{ inputs.working-directory }} + install-args: --production=false + + - name: ๐Ÿ”Ž Check Code Format + working-directory: ${{ inputs.working-directory }} + run: yarn format:check + + - name: ๐Ÿ”Ž Check Static Type Hints + working-directory: ${{ inputs.working-directory }} + run: yarn type-check + + - name: ๐Ÿ”Ž Check Static Code + working-directory: ${{ inputs.working-directory }} + run: yarn lint + + - name: ๐Ÿงช Test Code Units + working-directory: ${{ inputs.working-directory }} + run: | + if [ ${{ github.repository_owner_id }} = ${{ env.OCADO_TECH_ORG_ID }} ] + then + yarn test:coverage + else + yarn test:verbose + fi + + - name: ๐Ÿ“ˆ Upload Coverage Reports to Codecov + if: github.repository_owner_id == env.OCADO_TECH_ORG_ID + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + slug: ${{ inputs.codecov-slug }} + codecov_yml_path: ${{ inputs.codecov-yml-path }} + working-directory: ${{ inputs.working-directory }} + file: ${{ inputs.codecov-file }} diff --git a/.github/workflows/test-python-code.yaml b/.github/workflows/test-python-code.yaml index f763223e..65b0595a 100644 --- a/.github/workflows/test-python-code.yaml +++ b/.github/workflows/test-python-code.yaml @@ -41,7 +41,7 @@ on: secrets: CODECOV_TOKEN: description: "The token used to gain access to Codecov." - required: false + required: false # Needs to be false to support contributors jobs: test-py-code: diff --git a/.github/workflows/validate-pull-request-refs.yaml b/.github/workflows/validate-pull-request-refs.yaml index eba8077a..35799424 100644 --- a/.github/workflows/validate-pull-request-refs.yaml +++ b/.github/workflows/validate-pull-request-refs.yaml @@ -6,6 +6,8 @@ on: jobs: validate-pr-refs: runs-on: ubuntu-latest + # If the repo's owner is Ocado Tech. and the triggering event is a pull request. + if: github.repository_owner_id == 2088731 && github.event_name == 'pull_request' env: PROD_REF: production STAGING_REF: staging diff --git a/.gitmodules b/.gitmodules index e21ceb93..2de7c9ff 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "codeforlife-sso"] path = codeforlife-sso url = https://github.com/ocadotechnology/codeforlife-sso.git +[submodule "codeforlife-portal-frontend"] + path = codeforlife-portal-frontend + url = https://github.com/ocadotechnology/codeforlife-portal-frontend.git diff --git a/.submodules/config/configs.jsonc b/.submodules/config/configs.jsonc index c3ebab2a..b3057f23 100644 --- a/.submodules/config/configs.jsonc +++ b/.submodules/config/configs.jsonc @@ -74,14 +74,11 @@ } } }, - "javascript": { + "javascript.devcontainer": { "inherits": [ "base" ], "description": "A devcontainer with a javascript environment.", - "submodules": [ - "codeforlife-package-javascript" - ], "devcontainer": { "features": { "ghcr.io/devcontainers/features/node:1": { @@ -91,17 +88,26 @@ "customizations": { "vscode": { "extensions": [ - "dbaeumer.vscode-eslint" - // "dsznajder.es7-react-js-snippets", - // "ecmel.vscode-html-css", - // "jock.svg" + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" ] } } - }, + } + }, + "javascript.config": { + "inherits": [ + "base" + ], + "description": "A configured javascript environment.", "vscode": { "settings": { - "typescript.preferences.quoteStyle": "single" + "javascript.format.semicolons": "remove", + "typescript.format.semicolons": "remove", + "javascript.preferences.quoteStyle": "double", + "typescript.preferences.quoteStyle": "double", + "!prettier.configPath": ".prettierrc.json", + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "codeSnippets": { "javascript.module.doccomment": { @@ -121,17 +127,21 @@ } } }, - "python": { + "javascript": { "inherits": [ - "base" + "javascript.devcontainer", + "javascript.config" ], - "description": "A devcontainer with a python environment.", + "description": "A devcontainer with a configured javascript environment.", "submodules": [ - "codeforlife-deploy-appengine", - "codeforlife-package-python", - "codeforlife-portal", - "rapid-router" + "codeforlife-package-javascript" + ] + }, + "python.devcontainer": { + "inherits": [ + "base" ], + "description": "A devcontainer with a python environment.", "devcontainer": { "features": { "ghcr.io/devcontainers/features/python:1": { @@ -157,7 +167,13 @@ ] } } - }, + } + }, + "python.config": { + "inherits": [ + "base" + ], + "description": "A configured python environment.", "vscode": { "settings": { "python.defaultInterpreterPath": ".venv/bin/python", @@ -282,73 +298,71 @@ } } }, - "service": { + "python": { "inherits": [ - "javascript", - "python" + "python.devcontainer", + "python.config" ], - "description": "A devcontainer for a micro-service.", + "description": "A devcontainer with a configured python environment.", "submodules": [ - "codeforlife-service-template", - "codeforlife-portal-react", - "codeforlife-sso" + "codeforlife-deploy-appengine", + "codeforlife-package-python", + "codeforlife-portal", + "rapid-router" + ] + }, + "service.devcontainer": { + "inherits": [ + "python.devcontainer", + "javascript.devcontainer" ], + "description": "The devcontainer for a micro-service.", "devcontainer": { - "postCreateCommand": "./setup", + "postCreateCommand": "chmod u+x ./setup && ./setup", "mounts": [ "source=./codeforlife-package-javascript,target=/workspace/codeforlife-package-javascript,type=bind,consistency=cached", "source=./codeforlife-package-python,target=/workspace/codeforlife-package-python,type=bind,consistency=cached" ] }, + "workspace": { + "folders": [ + { + "path": "../codeforlife-package-python", + "name": "package-python" + }, + { + "path": "../codeforlife-package-javascript", + "name": "package-javascript" + } + ] + } + }, + "service.backend": { + "inherits": [ + "service.devcontainer", + "python.config" + ], + "description": "A devcontainer for a backend micro-service.", + "submodules": [ + "codeforlife-portal-react" + ], "vscode": { "settings": { - "python.defaultInterpreterPath": "backend/.venv/bin/python", "python.analysis.extraPaths": [ "../codeforlife-package-python" - ], - "!isort.path": [ - "backend/.venv/bin/python", - "-m", - "isort" - ], - "!isort.args": [ - "--settings-file=backend/pyproject.toml" - ], - "black-formatter.cwd": "${workspaceFolder}/backend", - "mypy-type-checker.cwd": "${workspaceFolder}/backend", - "pylint.cwd": "${workspaceFolder}/backend", - "python.testing.cwd": "${workspaceFolder}/backend" + ] }, "tasks": { "version": "2.0.0", "tasks": [ - { - "label": "start-react-dev-server", - "isBackground": true, - "type": "npm", - "script": "start", - "options": { - "env": { - "BROWSER": "none" - } - }, - "path": "frontend", - "problemMatcher": [] - }, { "label": "pipenv-install-dev", "type": "shell", - "options": { - "cwd": "${workspaceFolder}/backend" - }, "command": "pipenv install --dev" }, { "label": "migrate-db", "type": "shell", - "options": { - "cwd": "${workspaceFolder}/backend" - }, "dependsOn": [ "pipenv-install-dev" ], @@ -359,20 +373,13 @@ "launch": { "version": "0.2.0", "configurations": [ - { - "name": "React Dev Server", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000", - "preLaunchTask": "start-react-dev-server" - }, { "name": "Django Server", "type": "debugpy", "request": "launch", "django": true, "justMyCode": false, - "program": "${workspaceFolder}/backend/manage.py", + "program": "${workspaceFolder}/manage.py", "?args": [ "runserver", "localhost:8000" @@ -381,23 +388,52 @@ } ] } - }, - "workspace": { - "folders": [ - { - "path": "../codeforlife-package-python", - "name": "package-python" - }, - { - "path": "../codeforlife-package-javascript", - "name": "package-javascript" - } - ] + } + }, + "service.frontend": { + "inherits": [ + "service.devcontainer", + "javascript.config" + ], + "description": "A devcontainer for a frontend micro-service.", + "submodules": [ + "codeforlife-portal-frontend" + ], + "vscode": { + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "start-react-dev-server", + "isBackground": true, + "type": "npm", + "script": "start", + "options": { + "env": { + "BROWSER": "none" + } + }, + "problemMatcher": [] + } + ] + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "React Dev Server", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5173", + "preLaunchTask": "start-react-dev-server" + } + ] + } } }, "service+sso": { "inherits": [ - "service" + "service.devcontainer" ], "description": "A service that also runs the SSO service in the background.", // TODO: set submodules after testing how this would work. @@ -416,9 +452,9 @@ "type": "shell", "isBackground": true, "options": { - "cwd": "${workspaceFolder}/../codeforlife-sso/backend", + "cwd": "${workspaceFolder}/../codeforlife-sso", "env": { - "DB_NAME": "${fileWorkspaceFolder}/backend/db.sqlite3", + "DB_NAME": "${fileWorkspaceFolder}/db.sqlite3", "SERVICE_NAME": "sso", "SERVICE_PORT": "8001" } @@ -439,7 +475,7 @@ "request": "launch", "django": true, "justMyCode": false, - "program": "${fileWorkspaceFolder}/backend/manage.py", + "program": "${fileWorkspaceFolder}/manage.py", "args": [ "runserver", "localhost:8000" diff --git a/codeforlife-portal-frontend b/codeforlife-portal-frontend new file mode 160000 index 00000000..bc3a3c5d --- /dev/null +++ b/codeforlife-portal-frontend @@ -0,0 +1 @@ +Subproject commit bc3a3c5d439329503bbed8c5c3e24125c5d608dc